Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .claude/skills/wi-writer/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <ID> acceptance_criteria[N] --set <value>`. 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,
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use the repository command form.

Line 220 assumes that govctl is installed on PATH. Use cargo run --quiet -- so agents can run this command from a repository checkout.

Based on learnings: invoke the tool with cargo run --quiet -- instead of govctl when developing in this repository.

Proposed fix
-govctl work edit WI-001 acceptance_criteria[0] --set "fix: Correct criterion"
+cargo run --quiet -- work edit WI-001 acceptance_criteria[0] --set "fix: Correct criterion"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
govctl work edit WI-001 acceptance_criteria[0] --set "fix: Correct criterion"
cargo run --quiet -- work edit WI-001 acceptance_criteria[0] --set "fix: Correct criterion"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@AGENTS.md` at line 220, Update the acceptance-criteria example around the
command `govctl work edit` to invoke the repository tool through `cargo run
--quiet --` instead of relying on a globally installed `govctl`, while
preserving the existing arguments and behavior.

Sources: Coding guidelines, Learnings

govctl work edit WI-001 acceptance_criteria[0].category --set fixed
```

Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
27 changes: 26 additions & 1 deletion build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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)?;
Expand All @@ -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");
Expand Down Expand Up @@ -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");
Expand All @@ -358,6 +377,9 @@ fn render_nested_scalar_mode_expr(mode: Option<&RuntimeSetMode>) -> Result<Strin
match mode {
None => 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())
Expand Down Expand Up @@ -498,6 +520,9 @@ fn runtime_set_expr(set: Option<&RuntimeSetRule>) -> Result<String, Box<dyn Erro
};
let mode = match &set.mode {
RuntimeSetMode::String => "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());
Expand Down
30 changes: 20 additions & 10 deletions build_support/edit_ops_spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub(super) enum NestedNodeRule {
},
Object {
verbs: Vec<String>,
set_mode: Option<String>,
fields: Vec<NestedFieldRule>,
},
List {
Expand Down Expand Up @@ -82,6 +83,7 @@ pub(super) struct RuntimeSetRule {
#[serde(tag = "type", rename_all = "snake_case")]
pub(super) enum RuntimeSetMode {
String,
NonEmptyString,
Integer,
Semver,
Enum {
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -172,22 +174,30 @@ fn validate_runtime_fields(spec: &EditOpsSpec) -> Result<(), Box<dyn Error>> {
Ok(())
}

fn validate_nested_scalar_list_items(spec: &EditOpsSpec) -> Result<(), Box<dyn Error>> {
fn validate_nested_rules(spec: &EditOpsSpec) -> Result<(), Box<dyn Error>> {
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<dyn Error>> {
fn validate_nested_node(node: &NestedNodeRule, path: &str) -> Result<(), Box<dyn Error>> {
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, .. } => {
Expand All @@ -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(())
Expand Down
4 changes: 4 additions & 0 deletions docs/guide/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
```

Expand Down
18 changes: 18 additions & 0 deletions docs/guide/work-items.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
```

Expand Down
33 changes: 28 additions & 5 deletions docs/rfc/RFC-0002.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<!-- GENERATED: do not edit. Source: RFC-0002 -->
<!-- SIGNATURE: sha256:4ef0839a950da230f19a24847db4431c9715c7730b702df00fe3131ca157f8e7 -->
<!-- SIGNATURE: sha256:2dd43638eb23c8152d4db680e485f59cf4ba8ac6869cd2c5c64d979a85819b33 -->

# 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`

Expand Down Expand Up @@ -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` |
Expand All @@ -893,14 +893,29 @@ 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 `<list-path>[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 `<list-path>[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 <value>` MUST update the existing criterion without changing its status. If `<value>` 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 `<CLAUSE-ID>@<VERSION>` value or one indexed path. `--regex` and `--all` MUST be rejected for this object-valued, non-empty list.

List selection MUST use only these canonical forms:

```
govctl <resource> edit <id> <scalar-list-path>[<index>] --set <value>
govctl work edit <id> acceptance_criteria[<index>] --set <value>
govctl <resource> edit <id> <list-path>[<index>] --remove
govctl <resource> edit <id> <list-path> --remove <exact-value>
govctl <resource> edit <id> <list-path> --remove <pattern> --regex
Expand All @@ -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*

Expand Down Expand Up @@ -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
Expand Down
21 changes: 18 additions & 3 deletions gov/rfc/RFC-0002/clauses/C-EDIT-FIELD-CONTRACT.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand All @@ -40,14 +40,29 @@ 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 `<list-path>[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 `<list-path>[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 <value>` MUST update the existing criterion without changing its status. If `<value>` 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 `<CLAUSE-ID>@<VERSION>` value or one indexed path. `--regex` and `--all` MUST be rejected for this object-valued, non-empty list.

List selection MUST use only these canonical forms:

```
govctl <resource> edit <id> <scalar-list-path>[<index>] --set <value>
govctl work edit <id> acceptance_criteria[<index>] --set <value>
govctl <resource> edit <id> <list-path>[<index>] --remove
govctl <resource> edit <id> <list-path> --remove <exact-value>
govctl <resource> edit <id> <list-path> --remove <pattern> --regex
Expand All @@ -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."""
Loading
Loading