From af76e676fed7bddf136758e8bf12df3a61f8ec8c Mon Sep 17 00:00:00 2001 From: Gabriel Wu <13583761+lucifer1004@users.noreply.github.com> Date: Sun, 2 Aug 2026 14:15:35 +0800 Subject: [PATCH 1/2] feat(edit): support acceptance criterion replacement --- .claude/skills/wi-writer/SKILL.md | 6 + AGENTS.md | 1 + CHANGELOG.md | 8 + build.rs | 27 ++- build_support/edit_ops_spec.rs | 30 ++-- docs/guide/getting-started.md | 4 + docs/guide/work-items.md | 18 ++ docs/rfc/RFC-0002.md | 33 +++- .../clauses/C-EDIT-FIELD-CONTRACT.toml | 21 ++- gov/rfc/RFC-0002/rfc.toml | 12 +- gov/schema/edit-ops.json | 5 +- gov/schema/edit-ops.schema.json | 14 ++ ...evel-acceptance-criterion-replacement.toml | 39 +++++ src/cli/help.rs | 5 +- src/cli/resources/work.rs | 1 + src/cmd/edit/engine/mod.rs | 3 +- src/cmd/edit/engine/resolve.rs | 2 + src/cmd/edit/engine/tests.rs | 21 ++- src/cmd/edit/rules.rs | 7 + src/cmd/edit/runtime/nested/mod.rs | 10 ++ src/cmd/edit/toml_target/set.rs | 62 ++++++- src/model/changelog.rs | 24 ++- src/write/changelog/mod.rs | 20 ++- src/write/mod.rs | 4 + tests/edit_tests/work_tests/acceptance.rs | 154 ++++++++++++++++++ .../snapshots/test_help__work_edit_help.snap | 1 + .../snapshots/test_help__work_root_help.snap | 5 +- 27 files changed, 494 insertions(+), 43 deletions(-) create mode 100644 gov/work/2026-08-02-support-item-level-acceptance-criterion-replacement.toml diff --git a/.claude/skills/wi-writer/SKILL.md b/.claude/skills/wi-writer/SKILL.md index d1a81482..681ed4bd 100644 --- a/.claude/skills/wi-writer/SKILL.md +++ b/.claude/skills/wi-writer/SKILL.md @@ -62,6 +62,12 @@ decide done/not-done without prescribing incidental private structure. Use `chore` for internal validation or documentation outcomes that should not enter the release changelog. +Correct an existing criterion with +`govctl work edit acceptance_criteria[N] --set `. A recognized +category prefix updates both text and category; other input updates only text. +The operation preserves checklist status. Use the `.text` child path when a +recognized prefix must remain literal text, and use `--tick` for status changes. + ### Notes Use notes sparingly for closure-worthy constraints, durable implementation facts, diff --git a/AGENTS.md b/AGENTS.md index 2a206ffc..edccdeab 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -217,6 +217,7 @@ govctl rfc advance RFC-0001 impl # Nested field editing (path-based per ADR-0029) govctl adr edit ADR-0001 alternatives[0].text --set "Updated option" govctl adr edit ADR-0001 alternatives[0].pros --add "New advantage" +govctl work edit WI-001 acceptance_criteria[0] --set "fix: Correct criterion" govctl work edit WI-001 acceptance_criteria[0].category --set fixed ``` diff --git a/CHANGELOG.md b/CHANGELOG.md index 503084df..cf033fdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,14 @@ Release entries are curated summaries for readers. Work item traceability remain ## [Unreleased] +### Added + +- Item-level acceptance-criterion set updates text and recognized category prefixes while preserving checklist status (WI-2026-08-02-001) + +### Fixed + +- Plain or unrecognized-prefix input replaces literal criterion text without changing its category or status (WI-2026-08-02-001) + ## [0.18.0] - 2026-07-31 0.18.0 makes govctl's agent integration a first-class, user-scoped feature. diff --git a/build.rs b/build.rs index b0c7420b..1f0e2787 100644 --- a/build.rs +++ b/build.rs @@ -42,6 +42,7 @@ fn main() { // Edit rules SSOT + schema (ADR-0030) println!("cargo:rerun-if-changed=gov/schema/edit-ops.schema.json"); println!("cargo:rerun-if-changed=gov/schema/edit-ops.json"); + println!("cargo:rerun-if-changed=build_support/edit_ops_spec.rs"); generate_skill_assets().expect("failed to generate skill asset manifest"); generate_plugin_assets().expect("failed to generate agent plugin asset manifest"); @@ -270,11 +271,16 @@ fn render_nested_node_defs( " set_mode: {},\n", render_nested_scalar_mode_expr(set_mode.as_ref())? )); + out.push_str(" object_set_mode: None,\n"); out.push_str(" item: None,\n"); out.push_str(" fields: &[],\n"); out.push_str("};\n\n"); } - NestedNodeRule::Object { verbs, fields } => { + NestedNodeRule::Object { + verbs, + set_mode, + fields, + } => { for field in fields { let child_const = format!("{const_name}_{}", sanitize_const_fragment(&field.name)); render_nested_node_defs(out, &child_const, &field.node)?; @@ -297,6 +303,18 @@ fn render_nested_node_defs( out.push_str(" text_key: None,\n"); out.push_str(" value_codec: None,\n"); out.push_str(" set_mode: None,\n"); + out.push_str(&format!( + " object_set_mode: {},\n", + match set_mode.as_deref() { + Some("acceptance_criterion") => { + "Some(NestedObjectSetMode::AcceptanceCriterion)" + } + Some(other) => { + return Err(format!("unknown nested object set mode: {other}").into()); + } + None => "None", + } + )); out.push_str(" item: None,\n"); out.push_str(&format!(" fields: {},\n", fields_const)); out.push_str("};\n\n"); @@ -334,6 +352,7 @@ fn render_nested_node_defs( } )); out.push_str(" set_mode: None,\n"); + out.push_str(" object_set_mode: None,\n"); out.push_str(&format!(" item: Some(&{}),\n", item_const)); out.push_str(" fields: &[],\n"); out.push_str("};\n\n"); @@ -358,6 +377,9 @@ fn render_nested_scalar_mode_expr(mode: Option<&RuntimeSetMode>) -> Result Ok("None".to_string()), Some(RuntimeSetMode::String) => Ok("Some(NestedScalarMode::String)".to_string()), + Some(RuntimeSetMode::NonEmptyString) => { + Ok("Some(NestedScalarMode::NonEmptyString)".to_string()) + } Some(RuntimeSetMode::Semver) => Ok("Some(NestedScalarMode::Semver)".to_string()), Some(RuntimeSetMode::Integer) => { Err("integer set mode is not supported for nested edit paths".into()) @@ -498,6 +520,9 @@ fn runtime_set_expr(set: Option<&RuntimeSetRule>) -> Result "SetMode::String".to_string(), + RuntimeSetMode::NonEmptyString => { + return Err("non-empty string set mode is only supported for nested edit paths".into()); + } RuntimeSetMode::Integer => "SetMode::Integer".to_string(), RuntimeSetMode::Semver => { return Err("semver set mode is only supported for nested edit paths".into()); diff --git a/build_support/edit_ops_spec.rs b/build_support/edit_ops_spec.rs index f345701d..741ede5f 100644 --- a/build_support/edit_ops_spec.rs +++ b/build_support/edit_ops_spec.rs @@ -37,6 +37,7 @@ pub(super) enum NestedNodeRule { }, Object { verbs: Vec, + set_mode: Option, fields: Vec, }, List { @@ -82,6 +83,7 @@ pub(super) struct RuntimeSetRule { #[serde(tag = "type", rename_all = "snake_case")] pub(super) enum RuntimeSetMode { String, + NonEmptyString, Integer, Semver, Enum { @@ -110,7 +112,7 @@ pub(super) fn load_edit_ops_spec( validate_spec_against_schema(&schema_value, &spec_value)?; let spec: EditOpsSpec = serde_json::from_value(spec_value)?; validate_runtime_fields(&spec)?; - validate_nested_scalar_list_items(&spec)?; + validate_nested_rules(&spec)?; Ok(spec) } @@ -172,22 +174,30 @@ fn validate_runtime_fields(spec: &EditOpsSpec) -> Result<(), Box> { Ok(()) } -fn validate_nested_scalar_list_items(spec: &EditOpsSpec) -> Result<(), Box> { +fn validate_nested_rules(spec: &EditOpsSpec) -> Result<(), Box> { for root in &spec.nested_rules { - validate_nested_scalar_list_node(&root.node, &format!("{}:{}", root.artifact, root.root))?; + validate_nested_node(&root.node, &format!("{}:{}", root.artifact, root.root))?; } Ok(()) } -fn validate_nested_scalar_list_node( - node: &NestedNodeRule, - path: &str, -) -> Result<(), Box> { +fn validate_nested_node(node: &NestedNodeRule, path: &str) -> Result<(), Box> { match node { NestedNodeRule::Scalar { .. } => {} - NestedNodeRule::Object { fields, .. } => { + NestedNodeRule::Object { + verbs, + set_mode, + fields, + } => { + let settable = verbs.iter().any(|verb| verb == "set"); + if settable != set_mode.is_some() { + return Err(format!( + "object set capability and set_mode must be declared together: {path}" + ) + .into()); + } for field in fields { - validate_nested_scalar_list_node(&field.node, &format!("{path}.{}", field.name))?; + validate_nested_node(&field.node, &format!("{path}.{}", field.name))?; } } NestedNodeRule::List { verbs, item, .. } => { @@ -203,7 +213,7 @@ fn validate_nested_scalar_list_node( .into()); } } - validate_nested_scalar_list_node(item, &format!("{path}[]"))?; + validate_nested_node(item, &format!("{path}[]"))?; } } Ok(()) diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md index b3362f56..81c84f1f 100644 --- a/docs/guide/getting-started.md +++ b/docs/guide/getting-started.md @@ -281,6 +281,9 @@ govctl adr edit ADR-0003 refs --add RFC-0010 # Replace a scalar list item in place govctl rfc edit RFC-0010 owners[0] --set "@new-owner" +# Correct criterion text and optionally its category +govctl work edit WI-2026-01-17-001 acceptance_criteria[0] --set "fix: Handle edge case" + # Remove by index govctl work edit WI-2026-01-17-001 acceptance_criteria[0] --remove @@ -294,6 +297,7 @@ Nested object fields use dot-delimited paths: ```bash govctl adr edit ADR-0003 decision --set "We will use Redis" govctl adr edit ADR-0003 "alternatives[0].pros" --add "Low latency" +govctl work edit WI-2026-01-17-001 "acceptance_criteria[0].text" --set "Literal criterion text" govctl work edit WI-2026-01-17-001 "acceptance_criteria[0].category" --set fixed ``` diff --git a/docs/guide/work-items.md b/docs/guide/work-items.md index b25d178b..6cec0178 100644 --- a/docs/guide/work-items.md +++ b/docs/guide/work-items.md @@ -88,6 +88,23 @@ Category prefixes (`add:`, `fix:`, `change:`, `chore:`, etc.) are required and d Canonical changelog categories are still the preferred form in stored artifacts. The conventional-commit aliases are accepted as input sugar and normalized into the changelog model. +### Correct Criteria + +Set an indexed criterion directly to correct its text. A recognized category +prefix updates the category at the same time; input without one preserves the +existing category. Both forms preserve checklist status. + +```bash +govctl work edit WI-2026-01-17-001 "acceptance_criteria[0]" --set "fix: Handle empty input" +govctl work edit WI-2026-01-17-001 "acceptance_criteria[0]" --set "Handle empty input" +``` + +Use the child path when a recognized prefix must remain literal text: + +```bash +govctl work edit WI-2026-01-17-001 "acceptance_criteria[0].text" --set "fix: shown to the user" +``` + ### Mark Criteria Complete ```bash @@ -242,6 +259,7 @@ Do not use notes for progress updates, commands run, validation output, current Nested path edits are also available for structured fields: ```bash +govctl work edit WI-2026-01-17-001 "acceptance_criteria[0]" --set "fix: Handle edge case" govctl work edit WI-2026-01-17-001 "acceptance_criteria[0].category" --set fixed ``` diff --git a/docs/rfc/RFC-0002.md b/docs/rfc/RFC-0002.md index 180cd5a8..52034832 100644 --- a/docs/rfc/RFC-0002.md +++ b/docs/rfc/RFC-0002.md @@ -1,9 +1,9 @@ - + # RFC-0002: CLI Resource Model and Command Architecture -> **Version:** 3.5.0 | **Status:** normative | **Phase:** impl +> **Version:** 3.6.0 | **Status:** normative | **Phase:** impl > **Owners:** @govctl-org > **Tags:** `cli`, `editing`, `lifecycle`, `validation`, `release` @@ -880,7 +880,7 @@ Each resource MUST support these paths and operations: | Work Item | `title`, `description` | `--set` | | Work Item | `refs`, `depends_on`, `tags`, `notes` | `--add`, `--remove` | | Work Item | `acceptance_criteria` | `--add`, `--remove`, `--tick` | -| Work Item | `acceptance_criteria[i].text`, `acceptance_criteria[i].category` | `--set` | +| Work Item | `acceptance_criteria[i]`, `acceptance_criteria[i].text`, `acceptance_criteria[i].category` | `--set` | | Work Item | `verification.required_guards` | `--add`, `--remove` | | Work Item | `verification.waivers` | `--remove` | | Work Item | `verification.waivers[i].guard`, `verification.waivers[i].reason` | `--set` | @@ -893,7 +893,21 @@ Each resource MUST support these paths and operations: | Conformance Case | `requirements[i]` | `--remove` | | Conformance Case | `requirements[i].version` | `--set` | -Every listed list path whose items are scalar values also defines an indexed `[i]` path supporting `--set` and `--remove`. Indexed scalar replacement MUST replace exactly one item in place, preserve the list length and item position, and apply before persistence the same value and reference validation as `--add` on the owning list. Structured list items MUST NOT inherit direct `--set`; their listed child paths remain the only scalar replacement surface. +Every listed list path whose items are scalar values also defines an indexed `[i]` path supporting `--set` and `--remove`. Indexed scalar replacement MUST replace exactly one item in place, preserve the list length and item position, and apply before persistence the same value and reference validation as `--add` on the owning list. Structured list items MUST NOT inherit direct `--set` unless the indexed item path is explicitly listed above; their listed child paths remain the scalar replacement surface. + +Acceptance-criterion category input MUST recognize these ASCII case-insensitive prefix tokens when the trimmed token precedes the first colon: + +- `add`, `added`, `feat`, and `feature` map to `added`; +- `change`, `changed`, `refactor`, and `perf` map to `changed`; +- `deprecate` and `deprecated` map to `deprecated`; +- `remove` and `removed` map to `removed`; +- `fix` and `fixed` map to `fixed`; +- `security` and `sec` map to `security`; and +- `chore`, `internal`, `test`, `tests`, `doc`, `docs`, `ci`, and `build` map to `chore`. + +The text following a recognized prefix MUST be trimmed and non-empty. `acceptance_criteria --add` MUST require a recognized prefix and reject an absent or unrecognized prefix. + +For Work Item `acceptance_criteria[i]`, `--set ` MUST update the existing criterion without changing its status. If `` begins with a recognized acceptance-criterion category prefix, the operation MUST update both the criterion text and category using the mapping above. Otherwise, it MUST treat the complete trimmed value as criterion text and preserve the existing category. The direct `acceptance_criteria[i].text` path MUST treat its value as literal text and preserve both status and category. An empty resulting text MUST be rejected. For Conformance Case `requirements`, `--remove` MUST select either one exact `@` value or one indexed path. `--regex` and `--all` MUST be rejected for this object-valued, non-empty list. @@ -901,6 +915,7 @@ List selection MUST use only these canonical forms: ``` govctl edit [] --set +govctl work edit acceptance_criteria[] --set govctl edit [] --remove govctl edit --remove govctl edit --remove --regex @@ -916,7 +931,7 @@ The logical Guard paths `command`, `timeout_secs`, and `pattern` map to persiste **Rationale:** -A closed list gives scripts and agents one discoverable interface and makes removal of aliases testable. Uniform indexed replacement gives scalar lists one predictable correction operation without exposing structured item replacement. Logical paths keep the command contract independent of TOML table layout while lifecycle and verification commands retain ownership of constrained transitions. +A closed list gives scripts and agents one discoverable interface and makes removal of aliases testable. Uniform indexed replacement gives scalar lists one predictable correction operation. Acceptance criteria expose a concise item-level text update while retaining explicit child paths for precise field edits. Logical paths keep the command contract independent of TOML table layout while lifecycle and verification commands retain ownership of constrained transitions. *Since: v1.0.0* @@ -1090,6 +1105,14 @@ The existing `govctl init-skills` command remains the project-local and custom-d ## Changelog +### v3.6.0 (2026-08-02) + +Make acceptance-criterion text correction ergonomic + +#### Added + +- Define item-level acceptance-criterion replacement with category-prefix parsing + ### v3.5.0 (2026-07-31) Define the internal agent hook adapter command contract diff --git a/gov/rfc/RFC-0002/clauses/C-EDIT-FIELD-CONTRACT.toml b/gov/rfc/RFC-0002/clauses/C-EDIT-FIELD-CONTRACT.toml index f389b43e..f6cbf82d 100644 --- a/gov/rfc/RFC-0002/clauses/C-EDIT-FIELD-CONTRACT.toml +++ b/gov/rfc/RFC-0002/clauses/C-EDIT-FIELD-CONTRACT.toml @@ -27,7 +27,7 @@ Each resource MUST support these paths and operations: | Work Item | `title`, `description` | `--set` | | Work Item | `refs`, `depends_on`, `tags`, `notes` | `--add`, `--remove` | | Work Item | `acceptance_criteria` | `--add`, `--remove`, `--tick` | -| Work Item | `acceptance_criteria[i].text`, `acceptance_criteria[i].category` | `--set` | +| Work Item | `acceptance_criteria[i]`, `acceptance_criteria[i].text`, `acceptance_criteria[i].category` | `--set` | | Work Item | `verification.required_guards` | `--add`, `--remove` | | Work Item | `verification.waivers` | `--remove` | | Work Item | `verification.waivers[i].guard`, `verification.waivers[i].reason` | `--set` | @@ -40,7 +40,21 @@ Each resource MUST support these paths and operations: | Conformance Case | `requirements[i]` | `--remove` | | Conformance Case | `requirements[i].version` | `--set` | -Every listed list path whose items are scalar values also defines an indexed `[i]` path supporting `--set` and `--remove`. Indexed scalar replacement MUST replace exactly one item in place, preserve the list length and item position, and apply before persistence the same value and reference validation as `--add` on the owning list. Structured list items MUST NOT inherit direct `--set`; their listed child paths remain the only scalar replacement surface. +Every listed list path whose items are scalar values also defines an indexed `[i]` path supporting `--set` and `--remove`. Indexed scalar replacement MUST replace exactly one item in place, preserve the list length and item position, and apply before persistence the same value and reference validation as `--add` on the owning list. Structured list items MUST NOT inherit direct `--set` unless the indexed item path is explicitly listed above; their listed child paths remain the scalar replacement surface. + +Acceptance-criterion category input MUST recognize these ASCII case-insensitive prefix tokens when the trimmed token precedes the first colon: + +- `add`, `added`, `feat`, and `feature` map to `added`; +- `change`, `changed`, `refactor`, and `perf` map to `changed`; +- `deprecate` and `deprecated` map to `deprecated`; +- `remove` and `removed` map to `removed`; +- `fix` and `fixed` map to `fixed`; +- `security` and `sec` map to `security`; and +- `chore`, `internal`, `test`, `tests`, `doc`, `docs`, `ci`, and `build` map to `chore`. + +The text following a recognized prefix MUST be trimmed and non-empty. `acceptance_criteria --add` MUST require a recognized prefix and reject an absent or unrecognized prefix. + +For Work Item `acceptance_criteria[i]`, `--set ` MUST update the existing criterion without changing its status. If `` begins with a recognized acceptance-criterion category prefix, the operation MUST update both the criterion text and category using the mapping above. Otherwise, it MUST treat the complete trimmed value as criterion text and preserve the existing category. The direct `acceptance_criteria[i].text` path MUST treat its value as literal text and preserve both status and category. An empty resulting text MUST be rejected. For Conformance Case `requirements`, `--remove` MUST select either one exact `@` value or one indexed path. `--regex` and `--all` MUST be rejected for this object-valued, non-empty list. @@ -48,6 +62,7 @@ List selection MUST use only these canonical forms: ``` govctl edit [] --set +govctl work edit acceptance_criteria[] --set govctl edit [] --remove govctl edit --remove govctl edit --remove --regex @@ -63,4 +78,4 @@ The logical Guard paths `command`, `timeout_secs`, and `pattern` map to persiste **Rationale:** -A closed list gives scripts and agents one discoverable interface and makes removal of aliases testable. Uniform indexed replacement gives scalar lists one predictable correction operation without exposing structured item replacement. Logical paths keep the command contract independent of TOML table layout while lifecycle and verification commands retain ownership of constrained transitions.""" +A closed list gives scripts and agents one discoverable interface and makes removal of aliases testable. Uniform indexed replacement gives scalar lists one predictable correction operation. Acceptance criteria expose a concise item-level text update while retaining explicit child paths for precise field edits. Logical paths keep the command contract independent of TOML table layout while lifecycle and verification commands retain ownership of constrained transitions.""" diff --git a/gov/rfc/RFC-0002/rfc.toml b/gov/rfc/RFC-0002/rfc.toml index 24ec65c6..63cde523 100644 --- a/gov/rfc/RFC-0002/rfc.toml +++ b/gov/rfc/RFC-0002/rfc.toml @@ -3,12 +3,12 @@ [govctl] id = "RFC-0002" title = "CLI Resource Model and Command Architecture" -version = "3.5.0" +version = "3.6.0" status = "normative" phase = "impl" owners = ["@govctl-org"] created = "2026-01-19" -updated = "2026-07-31" +updated = "2026-08-02" tags = [ "cli", "editing", @@ -16,7 +16,7 @@ tags = [ "validation", "release", ] -signature = "d55103e00864241a0c95b47251aff7573c382342b3797b7c3fc0df2340da3e64" +signature = "d0dba4f59762cac471b5c2a0396d5d7ed754d4dc3b190bbf08b0bae89313ffd7" [[sections]] title = "Summary" @@ -48,6 +48,12 @@ clauses = [ "clauses/C-AGENT-INTEGRATION.toml", ] +[[changelog]] +version = "3.6.0" +date = "2026-08-02" +notes = "Make acceptance-criterion text correction ergonomic" +added = ["Define item-level acceptance-criterion replacement with category-prefix parsing"] + [[changelog]] version = "3.5.0" date = "2026-07-31" diff --git a/gov/schema/edit-ops.json b/gov/schema/edit-ops.json index e0399e52..f87961ba 100644 --- a/gov/schema/edit-ops.json +++ b/gov/schema/edit-ops.json @@ -833,14 +833,15 @@ "text_key": "text", "item": { "kind": "object", - "verbs": ["get"], + "verbs": ["get", "set"], + "set_mode": "acceptance_criterion", "fields": [ { "name": "text", "node": { "kind": "scalar", "verbs": ["get", "set"], - "set_mode": { "type": "string" } + "set_mode": { "type": "non_empty_string" } } }, { diff --git a/gov/schema/edit-ops.schema.json b/gov/schema/edit-ops.schema.json index f292c359..89b13776 100644 --- a/gov/schema/edit-ops.schema.json +++ b/gov/schema/edit-ops.schema.json @@ -110,6 +110,12 @@ }, "minItems": 1 }, + "set_mode": { + "oneOf": [ + { "type": "string", "enum": ["acceptance_criterion"] }, + { "type": "null" } + ] + }, "fields": { "type": "array", "items": { "$ref": "#/definitions/nestedFieldRule" } @@ -276,6 +282,14 @@ }, "additionalProperties": false }, + { + "type": "object", + "required": ["type"], + "properties": { + "type": { "type": "string", "enum": ["non_empty_string"] } + }, + "additionalProperties": false + }, { "type": "object", "required": ["type"], diff --git a/gov/work/2026-08-02-support-item-level-acceptance-criterion-replacement.toml b/gov/work/2026-08-02-support-item-level-acceptance-criterion-replacement.toml new file mode 100644 index 00000000..80ca3a00 --- /dev/null +++ b/gov/work/2026-08-02-support-item-level-acceptance-criterion-replacement.toml @@ -0,0 +1,39 @@ +#:schema ../schema/work.schema.json + +[govctl] +id = "WI-2026-08-02-001" +title = "Support item-level acceptance criterion replacement" +status = "done" +created = "2026-08-02" +started = "2026-08-02" +completed = "2026-08-02" +refs = ["RFC-0002:C-EDIT-FIELD-CONTRACT"] + +[content] +description = "Implement and document the [[RFC-0002]] item-level acceptance-criterion replacement surface so agents can correct criteria without reconstructing checklist state." + +[[content.acceptance_criteria]] +text = "Item-level acceptance-criterion set updates text and recognized category prefixes while preserving checklist status" +status = "done" +category = "added" + +[[content.acceptance_criteria]] +text = "Plain or unrecognized-prefix input replaces literal criterion text without changing its category or status" +status = "done" +category = "fixed" + +[[content.acceptance_criteria]] +text = "Focused tests cover prefixed, fallback, literal child-path, empty-value, and invalid-index behavior" +status = "done" +category = "chore" + +[[content.acceptance_criteria]] +text = "CLI help, user guidance, and bundled agent guidance show the supported correction paths" +status = "done" +category = "chore" + +[verification] +required_guards = [ + "GUARD-CARGO-TEST", + "GUARD-GOVCTL-CHECK", +] diff --git a/src/cli/help.rs b/src/cli/help.rs index e2172026..61c4f836 100644 --- a/src/cli/help.rs +++ b/src/cli/help.rs @@ -155,8 +155,9 @@ pub(super) const WORK: &str = r#"COMMON WORKFLOW: 1. `govctl work list` to discover work items 2. `govctl work get ...` for metadata/fields 3. `govctl work edit` to define scope and acceptance criteria - 4. `govctl work edit acceptance_criteria[N] --tick ` to update criterion status - 5. `govctl work move` to change lifecycle state + 4. `govctl work edit acceptance_criteria[N] --set ` to correct a criterion + 5. `govctl work edit acceptance_criteria[N] --tick ` to update criterion status + 6. `govctl work move` to change lifecycle state START HERE: - New work item: `govctl work new "Title"` diff --git a/src/cli/resources/work.rs b/src/cli/resources/work.rs index 9a6ad6e1..76272285 100644 --- a/src/cli/resources/work.rs +++ b/src/cli/resources/work.rs @@ -71,6 +71,7 @@ EXAMPLES: govctl work edit WI-2026-04-06-001 description --set \"Scope and why\" govctl work edit WI-2026-04-06-001 depends_on --add WI-2026-04-06-002 govctl work edit WI-2026-04-06-001 acceptance_criteria --add \"add: Implement feature X\" + govctl work edit WI-2026-04-06-001 acceptance_criteria[0] --set \"fix: Correct edge case\" govctl work edit WI-2026-04-06-001 acceptance_criteria[0] --tick done govctl work edit WI-2026-04-06-001 verification.required_guards --add GUARD-CARGO-TEST ")] diff --git a/src/cmd/edit/engine/mod.rs b/src/cmd/edit/engine/mod.rs index 1eca70fb..33286e7a 100644 --- a/src/cmd/edit/engine/mod.rs +++ b/src/cmd/edit/engine/mod.rs @@ -10,7 +10,7 @@ mod resolve; use self::resolve::resolve_target; use super::ArtifactType; use super::path::{self, FieldPath}; -use super::rules::Verb; +use super::rules::{NestedObjectSetMode, Verb}; use crate::diagnostic::{Diagnostic, DiagnosticCode, DiagnosticResult}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -41,6 +41,7 @@ pub enum ResolvedTarget { container_path: FieldPath, index: i32, item_kind: TargetKind, + object_set_mode: Option, status_list: bool, container_verbs: &'static [&'static str], item_verbs: &'static [&'static str], diff --git a/src/cmd/edit/engine/resolve.rs b/src/cmd/edit/engine/resolve.rs index a852b722..092c4e17 100644 --- a/src/cmd/edit/engine/resolve.rs +++ b/src/cmd/edit/engine/resolve.rs @@ -64,6 +64,7 @@ pub(super) fn resolve_target( }, index, item_kind: TargetKind::Scalar, + object_set_mode: None, status_list: edit_rules::simple_field_supports_verb( artifact.rule_key(), &seg.name, @@ -135,6 +136,7 @@ fn resolve_nested_target( }, index, item_kind: map_nested_kind(item_node.kind), + object_set_mode: item_node.object_set_mode, status_list: edit_rules::nested_status_list_spec(current_node).is_some(), container_verbs: current_node.verbs, item_verbs: item_node.verbs, diff --git a/src/cmd/edit/engine/tests.rs b/src/cmd/edit/engine/tests.rs index 93d9351a..7912a74d 100644 --- a/src/cmd/edit/engine/tests.rs +++ b/src/cmd/edit/engine/tests.rs @@ -158,6 +158,7 @@ fn test_plan_mutation_request_classifies_nested_root_item_target() }, index: 0, item_kind: TargetKind::Object, + object_set_mode: None, status_list: true, container_verbs: &["get", "add", "remove", "tick"], item_verbs: &["get"], @@ -200,6 +201,7 @@ fn test_plan_mutation_request_classifies_nested_list_item_target() }, index: 1, item_kind: TargetKind::Scalar, + object_set_mode: None, status_list: false, container_verbs: &["get", "add", "remove"], item_verbs: &["get", "set"], @@ -302,6 +304,7 @@ fn test_indexed_set_support_follows_list_item_shape() -> Result<(), Box Result<(), Box Result<(), Box Result<(), Box> +{ + let target = plan_mutation_request("WI-2026-01-01-001", "acceptance_criteria[0]", Verb::Set)? + .target + .ok_or("acceptance criterion target missing")?; + + assert!(matches!( + target, + ResolvedTarget::IndexedItem { + object_set_mode: Some(NestedObjectSetMode::AcceptanceCriterion), + .. + } + )); + Ok(()) +} diff --git a/src/cmd/edit/rules.rs b/src/cmd/edit/rules.rs index f792cd71..83c39b3a 100644 --- a/src/cmd/edit/rules.rs +++ b/src/cmd/edit/rules.rs @@ -18,6 +18,7 @@ pub enum NestedNodeKind { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum NestedScalarMode { String, + NonEmptyString, Semver, Enum { allowed: &'static [&'static str], @@ -26,6 +27,11 @@ pub enum NestedScalarMode { }, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NestedObjectSetMode { + AcceptanceCriterion, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum NestedListValueCodec { RequirementBinding, @@ -44,6 +50,7 @@ pub struct NestedNodeRule { pub text_key: Option<&'static str>, pub value_codec: Option, pub set_mode: Option, + pub object_set_mode: Option, pub item: Option<&'static NestedNodeRule>, pub fields: &'static [NestedChildRule], } diff --git a/src/cmd/edit/runtime/nested/mod.rs b/src/cmd/edit/runtime/nested/mod.rs index 08ea7074..20fb9f45 100644 --- a/src/cmd/edit/runtime/nested/mod.rs +++ b/src/cmd/edit/runtime/nested/mod.rs @@ -115,6 +115,16 @@ fn apply_nested_scalar_set( ) -> DiagnosticResult<()> { match mode.unwrap_or(NestedScalarMode::String) { NestedScalarMode::String => *slot = Value::String(value.to_string()), + NestedScalarMode::NonEmptyString => { + if value.is_empty() { + return Err(Diagnostic::new( + DiagnosticCode::E0805EmptyValue, + "Value cannot be empty", + id, + )); + } + *slot = Value::String(value.to_string()); + } NestedScalarMode::Semver => { semver::Version::parse(value).map_err(|_| { Diagnostic::new( diff --git a/src/cmd/edit/toml_target/set.rs b/src/cmd/edit/toml_target/set.rs index 8533166a..fb38c7d8 100644 --- a/src/cmd/edit/toml_target/set.rs +++ b/src/cmd/edit/toml_target/set.rs @@ -3,12 +3,14 @@ use super::work_dependencies::{is_work_dependency_target, validate_work_dependen use crate::cmd::edit::adapter::{TomlAdapter, WorkTomlAdapter}; use crate::cmd::edit::engine as edit_engine; use crate::cmd::edit::refs::{is_refs_target, validate_ref_edit}; +use crate::cmd::edit::rules::NestedObjectSetMode; use crate::cmd::edit::runtime as edit_runtime; use crate::cmd::edit::{ ArtifactType, deserialize_edit_doc, serialize_edit_doc, unexpected_edit_state, }; use crate::config::Config; use crate::diagnostic::{Diagnostic, DiagnosticCode, DiagnosticResult}; +use crate::model::WorkItemEntry; use crate::write::WriteOp; pub(in crate::cmd::edit) fn set_toml_field( @@ -49,14 +51,18 @@ pub(in crate::cmd::edit) fn set_work_toml_field( allow_forced_simple_set: bool, ) -> DiagnosticResult<()> { let mut entry = WorkTomlAdapter::load(config, id)?; - apply_toml_target_to_entry( - &mut entry, - target, - value, - ArtifactType::WorkItem, - allow_forced_simple_set, - id, - )?; + if let Some(index) = acceptance_criterion_index(target) { + set_acceptance_criterion(&mut entry, index, value, id)?; + } else { + apply_toml_target_to_entry( + &mut entry, + target, + value, + ArtifactType::WorkItem, + allow_forced_simple_set, + id, + )?; + } if is_refs_target(target) { validate_ref_edit(config, ArtifactType::WorkItem, id, value)?; } @@ -67,6 +73,46 @@ pub(in crate::cmd::edit) fn set_work_toml_field( Ok(()) } +fn acceptance_criterion_index(target: &edit_engine::ResolvedTarget) -> Option { + match target { + edit_engine::ResolvedTarget::IndexedItem { + index, + item_kind: edit_engine::TargetKind::Object, + object_set_mode: Some(NestedObjectSetMode::AcceptanceCriterion), + .. + } => Some(*index), + _ => None, + } +} + +fn set_acceptance_criterion( + entry: &mut WorkItemEntry, + index: i32, + value: &str, + id: &str, +) -> DiagnosticResult<()> { + let parsed = crate::write::parse_changelog_change_with_fallback(value)?; + if parsed.message.is_empty() { + return Err(empty_acceptance_criterion(id)); + } + let criteria = &mut entry.spec.content.acceptance_criteria; + let resolved = crate::cmd::edit::path::resolve_index(index, criteria.len())?; + let criterion = &mut criteria[resolved]; + criterion.text = parsed.message; + if parsed.explicit { + criterion.category = parsed.category; + } + Ok(()) +} + +fn empty_acceptance_criterion(id: &str) -> Diagnostic { + Diagnostic::new( + DiagnosticCode::E0805EmptyValue, + "Acceptance criterion text cannot be empty", + id, + ) +} + fn apply_toml_target_to_entry( entry: &mut E, target: &edit_engine::ResolvedTarget, diff --git a/src/model/changelog.rs b/src/model/changelog.rs index cf7d4a33..2ac46d5d 100644 --- a/src/model/changelog.rs +++ b/src/model/changelog.rs @@ -51,16 +51,32 @@ pub enum ChangelogCategory { } impl ChangelogCategory { - /// Canonical category prefixes shown in error messages and docs. - /// All verb forms for consistency with imperative acceptance criteria. + /// Accepted category prefixes shown in error messages and docs. pub const VALID_PREFIXES: &'static [&'static str] = &[ "add", - "fix", + "added", + "feat", + "feature", "change", - "remove", + "changed", + "refactor", + "perf", "deprecate", + "deprecated", + "remove", + "removed", + "fix", + "fixed", "security", + "sec", "chore", + "internal", + "test", + "tests", + "doc", + "docs", + "ci", + "build", ]; pub const RELEASE_CHANGELOG_SECTIONS: &'static [(Self, &'static str)] = &[ diff --git a/src/write/changelog/mod.rs b/src/write/changelog/mod.rs index 54973dcc..fe4eb943 100644 --- a/src/write/changelog/mod.rs +++ b/src/write/changelog/mod.rs @@ -19,6 +19,12 @@ pub struct ParsedChange { pub explicit: bool, } +#[derive(Debug, Clone, Copy)] +enum UnknownPrefix { + Reject, + TreatAsText, +} + /// Parse a change string with optional prefix (per ADR-0012). /// /// Format: `[prefix:] message` @@ -28,6 +34,18 @@ pub struct ParsedChange { /// /// Returns error if prefix is present but invalid. pub fn parse_changelog_change(change: &str) -> DiagnosticResult { + parse_changelog_change_with_policy(change, UnknownPrefix::Reject) +} + +/// Parse a recognized changelog prefix, treating other input as literal text. +pub fn parse_changelog_change_with_fallback(change: &str) -> DiagnosticResult { + parse_changelog_change_with_policy(change, UnknownPrefix::TreatAsText) +} + +fn parse_changelog_change_with_policy( + change: &str, + unknown_prefix: UnknownPrefix, +) -> DiagnosticResult { if let Some(colon_pos) = change.find(':') { let prefix = change[..colon_pos].trim(); let message = change[colon_pos + 1..].trim(); @@ -46,7 +64,7 @@ pub fn parse_changelog_change(change: &str) -> DiagnosticResult { message: message.to_string(), explicit: true, }); - } else { + } else if matches!(unknown_prefix, UnknownPrefix::Reject) { return Err(Diagnostic::new( DiagnosticCode::E0808InvalidPrefix, format!( diff --git a/src/write/mod.rs b/src/write/mod.rs index 01afbad1..bf4c9c61 100644 --- a/src/write/mod.rs +++ b/src/write/mod.rs @@ -24,6 +24,10 @@ pub fn parse_changelog_change(change: &str) -> DiagnosticResult { changelog::parse_changelog_change(change) } +pub fn parse_changelog_change_with_fallback(change: &str) -> DiagnosticResult { + changelog::parse_changelog_change_with_fallback(change) +} + /// Write operation mode. /// /// Controls whether write operations execute or just preview. diff --git a/tests/edit_tests/work_tests/acceptance.rs b/tests/edit_tests/work_tests/acceptance.rs index 1405fab8..e46e18f9 100644 --- a/tests/edit_tests/work_tests/acceptance.rs +++ b/tests/edit_tests/work_tests/acceptance.rs @@ -7,6 +7,23 @@ fn work_edit_tick_acceptance_criteria_index(id: &str, index: usize, status: &str command(&["work", "edit", id, &field, "--tick", status]) } +fn read_first_criterion( + project: &std::path::Path, +) -> Result> { + let work_path = std::fs::read_dir(project.join("gov/work"))? + .next() + .ok_or("work item file missing")?? + .path(); + let work: toml::Value = toml::from_str(&std::fs::read_to_string(work_path)?)?; + work.get("content") + .and_then(|value| value.get("acceptance_criteria")) + .and_then(toml::Value::as_array) + .and_then(|criteria| criteria.first()) + .and_then(toml::Value::as_table) + .cloned() + .ok_or_else(|| "first acceptance criterion missing".into()) +} + #[test] fn test_work_add_acceptance_criteria() -> common::TestResult { let (temp_dir, date) = init_project_with_date()?; @@ -53,6 +70,143 @@ fn test_work_acceptance_criteria_use_category_prefixes() -> common::TestResult { Ok(()) } +#[test] +fn test_work_set_acceptance_criterion_parses_prefix_and_preserves_status() -> common::TestResult { + let (temp_dir, date) = init_project_with_date()?; + let id = first_work_id(&date); + + common::run_dynamic_commands( + temp_dir.path(), + &[ + work_new("Correct Criterion"), + work_add_acceptance(&id, "add: Original text"), + work_tick_acceptance(&id, 0, "done"), + work_set_field(&id, "acceptance_criteria[0]", "fix: Corrected text"), + ], + )?; + + let criterion = read_first_criterion(temp_dir.path())?; + assert_eq!( + criterion.get("text").and_then(toml::Value::as_str), + Some("Corrected text") + ); + assert_eq!( + criterion.get("category").and_then(toml::Value::as_str), + Some("fixed") + ); + assert_eq!( + criterion.get("status").and_then(toml::Value::as_str), + Some("done") + ); + Ok(()) +} + +#[test] +fn test_work_set_acceptance_criterion_falls_back_to_text_and_preserves_metadata() +-> common::TestResult { + let (temp_dir, date) = init_project_with_date()?; + let id = first_work_id(&date); + + common::run_dynamic_commands( + temp_dir.path(), + &[ + work_new("Correct Criterion"), + work_add_acceptance(&id, "change: Original text"), + work_tick_acceptance(&id, 0, "cancelled"), + work_set_field( + &id, + "acceptance_criteria[0]", + "API: Preserve response shape", + ), + ], + )?; + + let criterion = read_first_criterion(temp_dir.path())?; + assert_eq!( + criterion.get("text").and_then(toml::Value::as_str), + Some("API: Preserve response shape") + ); + assert_eq!( + criterion.get("category").and_then(toml::Value::as_str), + Some("changed") + ); + assert_eq!( + criterion.get("status").and_then(toml::Value::as_str), + Some("cancelled") + ); + Ok(()) +} + +#[test] +fn test_work_set_acceptance_criterion_text_path_is_literal() -> common::TestResult { + let (temp_dir, date) = init_project_with_date()?; + let id = first_work_id(&date); + + common::run_dynamic_commands( + temp_dir.path(), + &[ + work_new("Correct Criterion"), + work_add_acceptance(&id, "add: Original text"), + work_tick_acceptance(&id, 0, "done"), + work_set_field(&id, "acceptance_criteria[0].text", "fix: Literal text"), + ], + )?; + + let criterion = read_first_criterion(temp_dir.path())?; + assert_eq!( + criterion.get("text").and_then(toml::Value::as_str), + Some("fix: Literal text") + ); + assert_eq!( + criterion.get("category").and_then(toml::Value::as_str), + Some("added") + ); + assert_eq!( + criterion.get("status").and_then(toml::Value::as_str), + Some("done") + ); + Ok(()) +} + +#[test] +fn test_work_set_acceptance_criterion_rejects_invalid_value_and_index_atomically() +-> common::TestResult { + let (temp_dir, date) = init_project_with_date()?; + let id = first_work_id(&date); + + let output = common::run_dynamic_commands( + temp_dir.path(), + &[ + work_new("Correct Criterion"), + work_add_acceptance(&id, "add: Original text"), + work_set_field(&id, "acceptance_criteria[0]", ""), + work_set_field(&id, "acceptance_criteria[0].text", ""), + work_set_field(&id, "acceptance_criteria[4]", "fix: Missing item"), + ], + )?; + + assert_eq!( + output.matches("error[E0805]").count(), + 2, + "output: {output}" + ); + assert!(output.contains("error[E0816]"), "output: {output}"); + let criterion = read_first_criterion(temp_dir.path())?; + assert_eq!( + criterion.get("text").and_then(toml::Value::as_str), + Some("Original text") + ); + assert_eq!( + criterion.get("category").and_then(toml::Value::as_str), + Some("added") + ); + assert_eq!( + criterion.get("status").and_then(toml::Value::as_str), + Some("pending") + ); + Ok(()) +} + #[test] fn test_work_tick_acceptance_criteria() -> common::TestResult { let (temp_dir, date) = init_project_with_date()?; diff --git a/tests/snapshots/test_help__work_edit_help.snap b/tests/snapshots/test_help__work_edit_help.snap index efb91cd3..645a3674 100644 --- a/tests/snapshots/test_help__work_edit_help.snap +++ b/tests/snapshots/test_help__work_edit_help.snap @@ -57,6 +57,7 @@ EXAMPLES: govctl work edit WI--001 description --set "Scope and why" govctl work edit WI--001 depends_on --add WI--002 govctl work edit WI--001 acceptance_criteria --add "add: Implement feature X" + govctl work edit WI--001 acceptance_criteria[0] --set "fix: Correct edge case" govctl work edit WI--001 acceptance_criteria[0] --tick done govctl work edit WI--001 verification.required_guards --add GUARD-CARGO-TEST exit: 0 diff --git a/tests/snapshots/test_help__work_root_help.snap b/tests/snapshots/test_help__work_root_help.snap index 44cf5817..cd520b48 100644 --- a/tests/snapshots/test_help__work_root_help.snap +++ b/tests/snapshots/test_help__work_root_help.snap @@ -27,8 +27,9 @@ COMMON WORKFLOW: 1. `govctl work list` to discover work items 2. `govctl work get ...` for metadata/fields 3. `govctl work edit` to define scope and acceptance criteria - 4. `govctl work edit acceptance_criteria[N] --tick ` to update criterion status - 5. `govctl work move` to change lifecycle state + 4. `govctl work edit acceptance_criteria[N] --set ` to correct a criterion + 5. `govctl work edit acceptance_criteria[N] --tick ` to update criterion status + 6. `govctl work move` to change lifecycle state START HERE: - New work item: `govctl work new "Title"` From 32cbd49fe05e0d9f98e900df96988e3b214f9fb5 Mon Sep 17 00:00:00 2001 From: Gabriel Wu <13583761+lucifer1004@users.noreply.github.com> Date: Sun, 2 Aug 2026 16:55:17 +0800 Subject: [PATCH 2/2] fix(docs): make CLI examples shell-safe --- .claude/skills/wi-writer/SKILL.md | 4 +++- AGENTS.md | 11 +++++++---- README.md | 2 +- docs/guide/adrs.md | 10 +++++----- docs/guide/getting-started.md | 14 +++++++++----- docs/guide/recommended-workflows.md | 4 ++-- docs/guide/rfcs.md | 4 ++-- docs/guide/work-items.md | 12 ++++++------ docs/rfc/RFC-0002.md | 4 ++-- gov/rfc/RFC-0002/rfc.toml | 2 +- src/cli/help.rs | 6 +++--- src/cli/resources/adr.rs | 8 ++++---- src/cli/resources/guard.rs | 2 +- src/cli/resources/rfc.rs | 2 +- src/cli/resources/work.rs | 8 ++++---- tests/snapshots/test_help__adr_edit_help.snap | 4 ++-- tests/snapshots/test_help__adr_get_help.snap | 2 +- tests/snapshots/test_help__adr_reject_help.snap | 2 +- tests/snapshots/test_help__adr_root_help.snap | 2 +- tests/snapshots/test_help__rfc_edit_help.snap | 2 +- tests/snapshots/test_help__work_edit_help.snap | 4 ++-- tests/snapshots/test_help__work_get_help.snap | 2 +- tests/snapshots/test_help__work_root_help.snap | 4 ++-- 23 files changed, 62 insertions(+), 53 deletions(-) diff --git a/.claude/skills/wi-writer/SKILL.md b/.claude/skills/wi-writer/SKILL.md index 681ed4bd..1422be29 100644 --- a/.claude/skills/wi-writer/SKILL.md +++ b/.claude/skills/wi-writer/SKILL.md @@ -63,10 +63,12 @@ decide done/not-done without prescribing incidental private structure. Use the release changelog. Correct an existing criterion with -`govctl work edit acceptance_criteria[N] --set `. A recognized +`govctl work edit "acceptance_criteria[N]" --set `. A recognized category prefix updates both text and category; other input updates only text. The operation preserves checklist status. Use the `.text` child path when a recognized prefix must remain literal text, and use `--tick` for status changes. +Quote every path containing brackets. Use `--stdin` for rich text containing +backticks, `$()`, or other shell syntax. ### Notes diff --git a/AGENTS.md b/AGENTS.md index edccdeab..3c043686 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -215,12 +215,15 @@ govctl rfc finalize RFC-0001 normative govctl rfc advance RFC-0001 impl # Nested field editing (path-based per ADR-0029) -govctl adr edit ADR-0001 alternatives[0].text --set "Updated option" -govctl adr edit ADR-0001 alternatives[0].pros --add "New advantage" -govctl work edit WI-001 acceptance_criteria[0] --set "fix: Correct criterion" -govctl work edit WI-001 acceptance_criteria[0].category --set fixed +govctl adr edit ADR-0001 "alternatives[0].text" --set "Updated option" +govctl adr edit ADR-0001 "alternatives[0].pros" --add "New advantage" +govctl work edit WI-001 "acceptance_criteria[0]" --set "fix: Correct criterion" +govctl work edit WI-001 "acceptance_criteria[0].category" --set fixed ``` +Quote every CLI path containing brackets. Pass rich text containing backticks, +`$()`, or other shell syntax through `--stdin` so the shell cannot interpret it. + Clauses are first-class CLI resources. Use the root `govctl clause` namespace for every Clause operation, even though Clause IDs and storage are scoped by an RFC. diff --git a/README.md b/README.md index 0056616f..11b63ab9 100644 --- a/README.md +++ b/README.md @@ -187,7 +187,7 @@ For precise artifact mutation, `govctl` provides a canonical path-first interfac ```bash govctl adr edit ADR-0038 decision --set --stdin -govctl work edit WI-2026-01-17-001 acceptance_criteria[0] --tick done +govctl work edit WI-2026-01-17-001 "acceptance_criteria[0]" --tick done govctl clause edit RFC-0002:C-CRUD-VERBS text --set --stdin ``` diff --git a/docs/guide/adrs.md b/docs/guide/adrs.md index eb551a25..657021cd 100644 --- a/docs/guide/adrs.md +++ b/docs/guide/adrs.md @@ -73,17 +73,17 @@ govctl adr edit ADR-0003 context --set --stdin < context.md # Array fields — add, remove, tick govctl adr edit ADR-0003 refs --add RFC-0010 -govctl adr edit ADR-0003 refs[0] --remove +govctl adr edit ADR-0003 "refs[0]" --remove # Nested alternatives govctl adr edit ADR-0003 alternatives --add "Option C: Use etcd" govctl adr edit ADR-0003 "alternatives[0].pros" --add "Fast reads" govctl adr edit ADR-0003 "alternatives[0].cons" --add "Operational cost" -govctl adr edit ADR-0003 alternatives[0] --tick accepted +govctl adr edit ADR-0003 "alternatives[0]" --tick accepted govctl adr edit ADR-0003 "alternatives[0].rejection_reason" --set "Too complex" # Tick alternative status -govctl adr edit ADR-0003 alternatives[0] --tick accepted +govctl adr edit ADR-0003 "alternatives[0]" --tick accepted ``` Edit paths use logical field names: @@ -126,8 +126,8 @@ proposed → accepted → superseded Before accepting, the ADR must have at least 2 alternatives with 1 accepted and 1 rejected per [[ADR-0042]]: ```bash -govctl adr edit ADR-0003 alternatives[0] --tick accepted -govctl adr edit ADR-0003 alternatives[1] --tick rejected +govctl adr edit ADR-0003 "alternatives[0]" --tick accepted +govctl adr edit ADR-0003 "alternatives[1]" --tick rejected govctl adr accept ADR-0003 ``` diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md index 81c84f1f..e8b0c7a9 100644 --- a/docs/guide/getting-started.md +++ b/docs/guide/getting-started.md @@ -271,6 +271,10 @@ Run `govctl migrate` for supported schema upgrades and bundled-file synchronizat Editable artifact fields use a unified path-based edit interface: +Quote every path containing brackets so shells such as zsh do not expand it as +a glob. For rich text containing backticks, `$()`, or other shell syntax, use +`--stdin` so the shell does not interpret the value. + ```bash # Set a scalar value govctl rfc edit RFC-0010 title --set "Updated title" @@ -279,17 +283,17 @@ govctl rfc edit RFC-0010 title --set "Updated title" govctl adr edit ADR-0003 refs --add RFC-0010 # Replace a scalar list item in place -govctl rfc edit RFC-0010 owners[0] --set "@new-owner" +govctl rfc edit RFC-0010 "owners[0]" --set "@new-owner" # Correct criterion text and optionally its category -govctl work edit WI-2026-01-17-001 acceptance_criteria[0] --set "fix: Handle edge case" +govctl work edit WI-2026-01-17-001 "acceptance_criteria[0]" --set "fix: Handle edge case" # Remove by index -govctl work edit WI-2026-01-17-001 acceptance_criteria[0] --remove +govctl work edit WI-2026-01-17-001 "acceptance_criteria[0]" --remove # Tick checklist items -govctl adr edit ADR-0003 alternatives[0] --tick accepted -govctl work edit WI-2026-01-17-001 acceptance_criteria[0] --tick done +govctl adr edit ADR-0003 "alternatives[0]" --tick accepted +govctl work edit WI-2026-01-17-001 "acceptance_criteria[0]" --tick done ``` Nested object fields use dot-delimited paths: diff --git a/docs/guide/recommended-workflows.md b/docs/guide/recommended-workflows.md index 0d894ace..2418af24 100644 --- a/docs/guide/recommended-workflows.md +++ b/docs/guide/recommended-workflows.md @@ -142,8 +142,8 @@ Confirm the intended work item appears in the active list. During execution: After execution: ```bash -govctl work edit WI-YYYY-MM-DD-NNN acceptance_criteria[0] --tick done -govctl work edit WI-YYYY-MM-DD-NNN acceptance_criteria[1] --tick done +govctl work edit WI-YYYY-MM-DD-NNN "acceptance_criteria[0]" --tick done +govctl work edit WI-YYYY-MM-DD-NNN "acceptance_criteria[1]" --tick done govctl work move WI-YYYY-MM-DD-NNN done ``` diff --git a/docs/guide/rfcs.md b/docs/guide/rfcs.md index f2984d27..990253d4 100644 --- a/docs/guide/rfcs.md +++ b/docs/guide/rfcs.md @@ -69,14 +69,14 @@ govctl rfc bump RFC-0010 --minor -m "Add new clause for edge case" govctl rfc get RFC-0010 changelog govctl rfc edit RFC-0010 changelog.summary --set "Clarify edge-case behavior" govctl rfc edit RFC-0010 changelog.fixed --add "Correct timeout wording" -govctl rfc edit RFC-0010 changelog.fixed[0] --remove +govctl rfc edit RFC-0010 "changelog.fixed[0]" --remove # Add to array fields govctl rfc edit RFC-0010 refs --add RFC-0001 govctl rfc edit RFC-0010 owners --add "@co-maintainer" # Remove by index or pattern -govctl rfc edit RFC-0010 refs[0] --remove +govctl rfc edit RFC-0010 "refs[0]" --remove govctl rfc edit RFC-0010 owners --remove "@old-owner" # Edit clause text diff --git a/docs/guide/work-items.md b/docs/guide/work-items.md index 6cec0178..9bce61f6 100644 --- a/docs/guide/work-items.md +++ b/docs/guide/work-items.md @@ -108,7 +108,7 @@ govctl work edit WI-2026-01-17-001 "acceptance_criteria[0].text" --set "fix: sho ### Mark Criteria Complete ```bash -govctl work edit WI-2026-01-17-001 acceptance_criteria[0] --tick done +govctl work edit WI-2026-01-17-001 "acceptance_criteria[0]" --tick done ``` Checklist state changes address one item by index. @@ -129,11 +129,11 @@ govctl work edit WI-2026-01-17-001 depends_on --add WI-2026-01-16-001 govctl work edit WI-2026-01-17-001 acceptance_criteria --add "fix: Handle edge case" # Remove by index -govctl work edit WI-2026-01-17-001 acceptance_criteria[0] --remove +govctl work edit WI-2026-01-17-001 "acceptance_criteria[0]" --remove # Tick checklist items -govctl work edit WI-2026-01-17-001 acceptance_criteria[0] --tick done -govctl work edit WI-2026-01-17-001 acceptance_criteria[1] --tick cancelled +govctl work edit WI-2026-01-17-001 "acceptance_criteria[0]" --tick done +govctl work edit WI-2026-01-17-001 "acceptance_criteria[1]" --tick cancelled ``` Edit paths use logical field names: @@ -251,7 +251,7 @@ Add closure-worthy durable notes for constraints or retry rules that should rema ```bash govctl work edit WI-2026-01-17-001 notes --add "Do not retry the old validation path; it fails on missing refs" -govctl work edit WI-2026-01-17-001 notes[0] --set "Retry only after the referenced RFC is normative" +govctl work edit WI-2026-01-17-001 "notes[0]" --set "Retry only after the referenced RFC is normative" ``` Do not use notes for progress updates, commands run, validation output, current plans, next actions, temporary blockers, or TODOs. Put transient execution trace in local loop state and round artifacts instead. @@ -275,7 +275,7 @@ govctl work edit WI-2026-01-17-001 notes --remove "edge case" govctl work edit WI-2026-01-17-001 notes --remove "Discovered edge case in validation" # By index (0-based) -govctl work edit WI-2026-01-17-001 notes[0] --remove +govctl work edit WI-2026-01-17-001 "notes[0]" --remove # Regex pattern govctl work edit WI-2026-01-17-001 refs --remove "RFC-.*" --regex diff --git a/docs/rfc/RFC-0002.md b/docs/rfc/RFC-0002.md index 52034832..dae7e9e9 100644 --- a/docs/rfc/RFC-0002.md +++ b/docs/rfc/RFC-0002.md @@ -1,9 +1,9 @@ - + # RFC-0002: CLI Resource Model and Command Architecture -> **Version:** 3.6.0 | **Status:** normative | **Phase:** impl +> **Version:** 3.6.0 | **Status:** normative | **Phase:** test > **Owners:** @govctl-org > **Tags:** `cli`, `editing`, `lifecycle`, `validation`, `release` diff --git a/gov/rfc/RFC-0002/rfc.toml b/gov/rfc/RFC-0002/rfc.toml index 63cde523..dd2ae03f 100644 --- a/gov/rfc/RFC-0002/rfc.toml +++ b/gov/rfc/RFC-0002/rfc.toml @@ -5,7 +5,7 @@ id = "RFC-0002" title = "CLI Resource Model and Command Architecture" version = "3.6.0" status = "normative" -phase = "impl" +phase = "test" owners = ["@govctl-org"] created = "2026-01-19" updated = "2026-08-02" diff --git a/src/cli/help.rs b/src/cli/help.rs index 61c4f836..ec7315db 100644 --- a/src/cli/help.rs +++ b/src/cli/help.rs @@ -148,15 +148,15 @@ pub(super) const ADR: &str = r#"COMMON WORKFLOW: START HERE: - New ADR: `govctl adr new "Title"` - Inspect one ADR: `govctl adr get ADR-0001` - - Move an alternative to accepted: `govctl adr edit ADR-0001 alternatives[0] --tick accepted` + - Move an alternative to accepted: `govctl adr edit ADR-0001 "alternatives[0]" --tick accepted` "#; pub(super) const WORK: &str = r#"COMMON WORKFLOW: 1. `govctl work list` to discover work items 2. `govctl work get ...` for metadata/fields 3. `govctl work edit` to define scope and acceptance criteria - 4. `govctl work edit acceptance_criteria[N] --set ` to correct a criterion - 5. `govctl work edit acceptance_criteria[N] --tick ` to update criterion status + 4. `govctl work edit "acceptance_criteria[N]" --set ` to correct a criterion + 5. `govctl work edit "acceptance_criteria[N]" --tick ` to update criterion status 6. `govctl work move` to change lifecycle state START HERE: diff --git a/src/cli/resources/adr.rs b/src/cli/resources/adr.rs index 872733cc..391df7e9 100644 --- a/src/cli/resources/adr.rs +++ b/src/cli/resources/adr.rs @@ -28,7 +28,7 @@ VALID FIELDS: EXAMPLES: govctl adr get ADR-0001 govctl adr get ADR-0001 decision - govctl adr get ADR-0001 alternatives[0].status + govctl adr get ADR-0001 \"alternatives[0].status\" ")] Get(CommonGetArgs), /// Show rendered ADR content @@ -62,8 +62,8 @@ EXAMPLES: govctl adr edit ADR-0001 decision --set \"We will ...\" govctl adr edit ADR-0001 consequences --set \"Trade-off summary\" govctl adr edit ADR-0001 alternatives --add \"Option A\" - govctl adr edit ADR-0001 alternatives[0].pros --add \"Readable\" - govctl adr edit ADR-0001 alternatives[0] --tick accepted + govctl adr edit ADR-0001 \"alternatives[0].pros\" --add \"Readable\" + govctl adr edit ADR-0001 \"alternatives[0]\" --tick accepted ")] Edit(CommonEditArgs), /// Accept ADR (proposed → accepted) @@ -92,7 +92,7 @@ EXAMPLES: NOTES: - Reject the ADR itself when the proposal should not proceed. - - Use `govctl adr edit ADR-0001 alternatives[N] --tick rejected` to reject a specific alternative instead. + - Use `govctl adr edit ADR-0001 \"alternatives[N]\" --tick rejected` to reject a specific alternative instead. ")] Reject(CommonIdArgs), /// Supersede ADR diff --git a/src/cli/resources/guard.rs b/src/cli/resources/guard.rs index 37b8ce63..90f608cd 100644 --- a/src/cli/resources/guard.rs +++ b/src/cli/resources/guard.rs @@ -57,7 +57,7 @@ NOTES: EXAMPLES: govctl guard edit GUARD-0001 command --set \"cargo test\" govctl guard edit GUARD-0001 refs --add RFC-0001 - govctl guard edit GUARD-0001 refs[0] --remove + govctl guard edit GUARD-0001 \"refs[0]\" --remove ")] Edit(CommonEditArgs), /// Delete a verification guard diff --git a/src/cli/resources/rfc.rs b/src/cli/resources/rfc.rs index b67dfbc0..28207b64 100644 --- a/src/cli/resources/rfc.rs +++ b/src/cli/resources/rfc.rs @@ -69,7 +69,7 @@ NOTES: EXAMPLES: govctl rfc edit RFC-0001 changelog.summary --set \"Clarify retry behavior\" govctl rfc edit RFC-0001 changelog.fixed --add \"Correct timeout wording\" - govctl rfc edit RFC-0001 changelog.fixed[0] --remove + govctl rfc edit RFC-0001 \"changelog.fixed[0]\" --remove govctl rfc edit RFC-0001 refs --add RFC-0002 NOTES: diff --git a/src/cli/resources/work.rs b/src/cli/resources/work.rs index 76272285..7d1d2f91 100644 --- a/src/cli/resources/work.rs +++ b/src/cli/resources/work.rs @@ -32,7 +32,7 @@ VALID FIELDS: EXAMPLES: govctl work get WI-2026-04-06-001 govctl work get WI-2026-04-06-001 description - govctl work get WI-2026-04-06-001 acceptance_criteria[0].status + govctl work get WI-2026-04-06-001 \"acceptance_criteria[0].status\" govctl work get WI-2026-04-06-001 verification.required_guards ")] Get(CommonGetArgs), @@ -71,8 +71,8 @@ EXAMPLES: govctl work edit WI-2026-04-06-001 description --set \"Scope and why\" govctl work edit WI-2026-04-06-001 depends_on --add WI-2026-04-06-002 govctl work edit WI-2026-04-06-001 acceptance_criteria --add \"add: Implement feature X\" - govctl work edit WI-2026-04-06-001 acceptance_criteria[0] --set \"fix: Correct edge case\" - govctl work edit WI-2026-04-06-001 acceptance_criteria[0] --tick done + govctl work edit WI-2026-04-06-001 \"acceptance_criteria[0]\" --set \"fix: Correct edge case\" + govctl work edit WI-2026-04-06-001 \"acceptance_criteria[0]\" --tick done govctl work edit WI-2026-04-06-001 verification.required_guards --add GUARD-CARGO-TEST ")] Edit(CommonEditArgs), @@ -84,7 +84,7 @@ EXAMPLES: NOTES: - `done` requires acceptance criteria and effective guards to pass. - - Use `work edit ... acceptance_criteria[N] --tick` to update criterion status. + - Use `work edit ... \"acceptance_criteria[N]\" --tick` to update criterion status. ")] Move { /// Work item file path or ID diff --git a/tests/snapshots/test_help__adr_edit_help.snap b/tests/snapshots/test_help__adr_edit_help.snap index 97f9b597..d0b873d3 100644 --- a/tests/snapshots/test_help__adr_edit_help.snap +++ b/tests/snapshots/test_help__adr_edit_help.snap @@ -57,6 +57,6 @@ EXAMPLES: govctl adr edit ADR-0001 decision --set "We will ..." govctl adr edit ADR-0001 consequences --set "Trade-off summary" govctl adr edit ADR-0001 alternatives --add "Option A" - govctl adr edit ADR-0001 alternatives[0].pros --add "Readable" - govctl adr edit ADR-0001 alternatives[0] --tick accepted + govctl adr edit ADR-0001 "alternatives[0].pros" --add "Readable" + govctl adr edit ADR-0001 "alternatives[0]" --tick accepted exit: 0 diff --git a/tests/snapshots/test_help__adr_get_help.snap b/tests/snapshots/test_help__adr_get_help.snap index 62e09e4a..9351b433 100644 --- a/tests/snapshots/test_help__adr_get_help.snap +++ b/tests/snapshots/test_help__adr_get_help.snap @@ -41,5 +41,5 @@ VALID FIELDS: EXAMPLES: govctl adr get ADR-0001 govctl adr get ADR-0001 decision - govctl adr get ADR-0001 alternatives[0].status + govctl adr get ADR-0001 "alternatives[0].status" exit: 0 diff --git a/tests/snapshots/test_help__adr_reject_help.snap b/tests/snapshots/test_help__adr_reject_help.snap index 21a35a8d..34ffb55c 100644 --- a/tests/snapshots/test_help__adr_reject_help.snap +++ b/tests/snapshots/test_help__adr_reject_help.snap @@ -20,5 +20,5 @@ EXAMPLES: NOTES: - Reject the ADR itself when the proposal should not proceed. - - Use `govctl adr edit ADR-0001 alternatives[N] --tick rejected` to reject a specific alternative instead. + - Use `govctl adr edit ADR-0001 "alternatives[N]" --tick rejected` to reject a specific alternative instead. exit: 0 diff --git a/tests/snapshots/test_help__adr_root_help.snap b/tests/snapshots/test_help__adr_root_help.snap index 817ddbb9..b2dacf23 100644 --- a/tests/snapshots/test_help__adr_root_help.snap +++ b/tests/snapshots/test_help__adr_root_help.snap @@ -34,5 +34,5 @@ COMMON WORKFLOW: START HERE: - New ADR: `govctl adr new "Title"` - Inspect one ADR: `govctl adr get ADR-0001` - - Move an alternative to accepted: `govctl adr edit ADR-0001 alternatives[0] --tick accepted` + - Move an alternative to accepted: `govctl adr edit ADR-0001 "alternatives[0]" --tick accepted` exit: 0 diff --git a/tests/snapshots/test_help__rfc_edit_help.snap b/tests/snapshots/test_help__rfc_edit_help.snap index 823e81c0..71b992d8 100644 --- a/tests/snapshots/test_help__rfc_edit_help.snap +++ b/tests/snapshots/test_help__rfc_edit_help.snap @@ -56,7 +56,7 @@ Options: EXAMPLES: govctl rfc edit RFC-0001 changelog.summary --set "Clarify retry behavior" govctl rfc edit RFC-0001 changelog.fixed --add "Correct timeout wording" - govctl rfc edit RFC-0001 changelog.fixed[0] --remove + govctl rfc edit RFC-0001 "changelog.fixed[0]" --remove govctl rfc edit RFC-0001 refs --add RFC-0002 NOTES: diff --git a/tests/snapshots/test_help__work_edit_help.snap b/tests/snapshots/test_help__work_edit_help.snap index 645a3674..1c37d3b7 100644 --- a/tests/snapshots/test_help__work_edit_help.snap +++ b/tests/snapshots/test_help__work_edit_help.snap @@ -57,7 +57,7 @@ EXAMPLES: govctl work edit WI--001 description --set "Scope and why" govctl work edit WI--001 depends_on --add WI--002 govctl work edit WI--001 acceptance_criteria --add "add: Implement feature X" - govctl work edit WI--001 acceptance_criteria[0] --set "fix: Correct edge case" - govctl work edit WI--001 acceptance_criteria[0] --tick done + govctl work edit WI--001 "acceptance_criteria[0]" --set "fix: Correct edge case" + govctl work edit WI--001 "acceptance_criteria[0]" --tick done govctl work edit WI--001 verification.required_guards --add GUARD-CARGO-TEST exit: 0 diff --git a/tests/snapshots/test_help__work_get_help.snap b/tests/snapshots/test_help__work_get_help.snap index d810eb1a..3545dd5a 100644 --- a/tests/snapshots/test_help__work_get_help.snap +++ b/tests/snapshots/test_help__work_get_help.snap @@ -42,6 +42,6 @@ VALID FIELDS: EXAMPLES: govctl work get WI--001 govctl work get WI--001 description - govctl work get WI--001 acceptance_criteria[0].status + govctl work get WI--001 "acceptance_criteria[0].status" govctl work get WI--001 verification.required_guards exit: 0 diff --git a/tests/snapshots/test_help__work_root_help.snap b/tests/snapshots/test_help__work_root_help.snap index cd520b48..3f4c003b 100644 --- a/tests/snapshots/test_help__work_root_help.snap +++ b/tests/snapshots/test_help__work_root_help.snap @@ -27,8 +27,8 @@ COMMON WORKFLOW: 1. `govctl work list` to discover work items 2. `govctl work get ...` for metadata/fields 3. `govctl work edit` to define scope and acceptance criteria - 4. `govctl work edit acceptance_criteria[N] --set ` to correct a criterion - 5. `govctl work edit acceptance_criteria[N] --tick ` to update criterion status + 4. `govctl work edit "acceptance_criteria[N]" --set ` to correct a criterion + 5. `govctl work edit "acceptance_criteria[N]" --tick ` to update criterion status 6. `govctl work move` to change lifecycle state START HERE: