Skip to content

feat(cli): add guard as first-class resource type with CRUD subcommands#4

Merged
lucifer1004 merged 1 commit into
mainfrom
guard
Mar 27, 2026
Merged

feat(cli): add guard as first-class resource type with CRUD subcommands#4
lucifer1004 merged 1 commit into
mainfrom
guard

Conversation

@lucifer1004
Copy link
Copy Markdown
Contributor

@lucifer1004 lucifer1004 commented Mar 27, 2026

Amend RFC-0002 to v0.5.0 adding guard to C-RESOURCES, C-CRUD-VERBS, and C-RESOURCE-MODEL. Implement govctl guard new/list/show/get/set/ add/remove/delete following the standard resource model pattern.

Includes guard-writer skill, edit-ops rules, GuardTomlAdapter, safety checks on delete (blocks when referenced by config or work items), and 10 integration tests.

Closes WI-2026-03-27-001.

Summary by CodeRabbit

  • New Features

    • Added verification "guard" resource and CLI: create, list, show/get, set/add/remove, delete (with safety checks).
    • Guards include command, timeout and pattern checks; show supports JSON/TOML and list presents id/title/command.
  • Behavior

    • Creation scaffolds a guard and hints to add it to verification defaults; deletion is blocked if referenced by defaults, work items, or waivers.
  • Documentation

    • RFC, changelog, and authoring guide updated.
  • Tests

    • Integration tests cover lifecycle, edits, and deletion safety.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 27, 2026

📝 Walkthrough

Walkthrough

Adds a new "guard" resource with full govctl CLI support (new/list/get/set/add/remove/delete/show), RFC and schema updates, TOML adapters and I/O, edit-op/runtime support, deletion safety checks, tests, and documentation including a guard-writer skill.

Changes

Cohort / File(s) Summary
Documentation & Skills
CHANGELOG.md, CLAUDE.md, .claude/skills/guard-writer/SKILL.md
New docs: changelog/CLI references and a guard-writer skill page describing guard TOML structure, CLI quick-reference, and config integration.
RFC & Resource Model
docs/rfc/RFC-0002.md, gov/rfc/RFC-0002/rfc.toml, gov/rfc/RFC-0002/clauses/C-RESOURCE-MODEL.toml, gov/rfc/RFC-0002/clauses/C-RESOURCES.toml, gov/rfc/RFC-0002/clauses/C-CRUD-VERBS.toml
Bumped RFC to v0.5.0 and introduced guard as a first-class resource with ID/storage rules, new CRUD applicability, scaffold semantics, and delete-safety constraints.
Schema & Build
gov/schema/edit-ops.json, gov/schema/edit-ops.schema.json, build.rs
Added "guard" to edit-op schemas and runtime fields; added integer set-mode; mapped SSOT "guard" to ArtifactType::Guard in build.
CLI surface & Routing
src/cli.rs, src/command_router.rs
Added Guard CLI subcommand types and corresponding CanonicalCommand variants and routing for all guard operations.
Command implementations
src/cmd/guard.rs, src/cmd/list.rs, src/cmd/mod.rs
Implemented guard handlers: new_guard, delete_guard (with safety checks), show_guard; integrated guard listing and exported guard module.
Edit adapters & runtime
src/cmd/edit/mod.rs, src/cmd/edit/adapter.rs, src/cmd/edit/runtime.rs
Added ArtifactType::Guard, GuardTomlAdapter, TOML editable entry for guards, runtime Integer set mode, field routing for get/set/add/remove, and unknown-field diagnostics.
Parsing & I/O
src/parse.rs, src/cmd/new.rs
Added write_guard for serializing guard TOML and included guard-writer SKILL.md in embedded templates.
Schema-driven edit ops
gov/schema/edit-ops.json, gov/schema/edit-ops.schema.json
Declared guard fields (title, refs, command, timeout_secs, pattern) and runtime mappings; allowed artifact: "guard" in schema enums and integer runtime set-mode.
Tests & Work item
tests/test_guard.rs, gov/work/2026-03-27-add-guard-resource-subcommands-new-list-show-set-delete.toml
New integration tests covering guard lifecycle, mutation, listing, and deletion-safety; added work item documenting implementation and acceptance criteria.

Sequence Diagram(s)

sequenceDiagram
  participant CLI as Client CLI
  participant Router as Command Router
  participant Cmd as Guard Command Impl
  participant FS as Filesystem (gov/guard/)
  participant CFG as Config (gov/config.toml)
  participant WI as Work Items (gov/work/...)

  CLI->>Router: parse `govctl guard new|delete|show ...`
  Router->>Cmd: dispatch to new_guard / delete_guard / show_guard
  alt new_guard
    Cmd->>FS: read existing guard files
    FS-->>Cmd: file list / existence
    Cmd->>FS: write guard TOML
    FS-->>Cmd: write result
    Cmd->>CLI: print created path & hint
  else delete_guard
    Cmd->>CFG: read verification.default_guards
    Cmd->>WI: scan work items for required_guards / waivers
    CFG-->>Cmd: default_guards
    WI-->>Cmd: referencing work items (if any)
    alt references found
      Cmd->>CLI: error listing blockers
    else
      Cmd->>FS: delete guard TOML
      FS-->>Cmd: deletion result
      Cmd->>CLI: confirmation
    end
  else show_guard
    Cmd->>FS: read guard TOML
    FS-->>Cmd: guard spec
    Cmd->>CLI: render TOML/JSON output
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I nibble TOML and craft a guard,

IDs hop out, commands on guard.
New lists, safe deletes, checks in tune,
A tiny rabbit hums a dev-time tune.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 63.83% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically summarizes the main change: adding guard as a first-class resource type with CRUD subcommands to the CLI, which is the core objective of this PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch guard

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 27, 2026

Welcome to Codecov 🎉

Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests.

ℹ️ You can also turn on project coverage checks and project coverage reporting on Pull Request comment

Thanks for integrating Codecov - We've got you covered ☂️

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (2)
src/parse.rs (1)

313-346: Consider extracting a generic TOML write helper.

The write_guard, write_adr, and write_work_item functions share identical structure. A generic helper could reduce duplication:

♻️ Optional refactor
fn write_toml_artifact<T: Serialize>(
    path: &Path,
    spec: &T,
    schema: ArtifactSchema,
    op: WriteOp,
    display_path: Option<&Path>,
) -> Result<(), Diagnostic> {
    let body = toml::to_string_pretty(spec).map_err(|e| {
        Diagnostic::new(
            DiagnosticCode::E0901IoError,
            format!("Failed to serialize TOML: {e}"),
            path.display().to_string(),
        )
    })?;
    let content = with_schema_header(schema, &body);
    
    match op {
        WriteOp::Execute => {
            std::fs::write(path, &content).map_err(|e| {
                Diagnostic::new(DiagnosticCode::E0901IoError, e.to_string(), path.display().to_string())
            })?;
        }
        WriteOp::Preview => {
            ui::dry_run_file_preview(display_path.unwrap_or(path), &content);
        }
    }
    Ok(())
}

Then write_guard becomes:

pub fn write_guard(path: &Path, spec: &GuardSpec, op: WriteOp, display_path: Option<&Path>) -> Result<(), Diagnostic> {
    write_toml_artifact(path, spec, ArtifactSchema::Guard, op, display_path)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/parse.rs` around lines 313 - 346, The three functions write_guard,
write_adr, and write_work_item duplicate TOML serialization and
file/write/preview logic; extract a generic helper fn write_toml_artifact<T:
Serialize>(path: &Path, spec: &T, schema: ArtifactSchema, op: WriteOp,
display_path: Option<&Path>) -> Result<(), Diagnostic> that performs
toml::to_string_pretty, wraps with with_schema_header(schema, &body), maps
serialization and IO errors to Diagnostic::new(DiagnosticCode::E0901IoError, ...
, path.display().to_string()), and handles WriteOp::Execute with std::fs::write
and WriteOp::Preview with ui::dry_run_file_preview(display_path.unwrap_or(path),
&content); then simplify write_guard (and likewise write_adr/write_work_item) to
delegate to write_toml_artifact with the appropriate specific spec type (e.g.,
GuardSpec) and ArtifactSchema::Guard.
tests/test_guard.rs (1)

68-97: Add one round-trip test for timeout_secs.

Current guard field coverage only hits string fields (title / command), so the new numeric edit path can regress unnoticed. A guard set ... timeout_secs 30 followed by guard get ... timeout_secs would cover the non-string runtime mapping.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_guard.rs` around lines 68 - 97, Add a new round-trip test in
tests/test_guard.rs that mirrors the string-field tests but exercises the
numeric path: create a temp project with init_project(), write a guard via
write_guard(...) (e.g., "GUARD-ECHO" with some initial timeout), call
run_commands(...) to run ["guard","set","GUARD-ECHO","timeout_secs","30"]
followed by ["guard","get","GUARD-ECHO","timeout_secs"], and assert the output
contains "30"; name the test function something like test_guard_set_timeout_secs
and reuse the existing helpers (init_project, write_guard, run_commands).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@CHANGELOG.md`:
- Around line 10-16: Add the missing "govctl guard get retrieves a specific
guard field value (WI-2026-03-27-001)" entry under the "### Added" section in
CHANGELOG.md so it matches the other resource commands; ensure the text and WI
identifier format follow the existing lines for "govctl guard
new/list/show/set/delete" to keep the changelog consistent with the tests that
use guard get in test_guard.rs.

In `@gov/schema/edit-ops.json`:
- Around line 559-566: The runtime schema for the "guard" artifact exposes
"timeout_secs" with mode type "string", but GuardCheck.timeout_secs (in
src/model.rs / GuardSpec) is a u64, so set_toml_field will fail deserializing
string values; change the runtime field mode for the entry with name
"timeout_secs" (path ["check","timeout_secs"]) from a string type to a
numeric/integer mode (use an integer/number mode appropriate for u64) so the
runtime writes numeric values that round-trip into GuardSpec.

In `@src/cli.rs`:
- Around line 1086-1092: The Delete command's force flag (Delete { id: String,
force: bool }) is currently skipping mandatory reference checks; modify the
delete handler in src/cmd/guard.rs (the guard deletion function handling lines
~76–113) so that reference/safety checks always run regardless of force, and
only the interactive confirmation prompt is bypassed when force is true; if
references (e.g., entries in verification.default_guards or required_guards) are
found, abort deletion and return an error/log, otherwise proceed to delete, but
do not tie the check logic to the force flag—keep force solely for skipping the
confirmation prompt.

In `@src/cmd/guard.rs`:
- Around line 76-116: The current blocker scanning is gated by if !force &&
!op.is_preview(), which causes --force (and preview) to skip reference checks
and allow deletion with dangling references; change the logic so blockers are
always computed (use the existing checks: config.verification.default_guards,
crate::parse::load_work_items and scanning wi.spec.verification.required_guards
and waivers) regardless of force/preview, and only use force to bypass the
interactive confirmation step before calling crate::write::delete_file (use
guard.path and op as before); mirror the delete_clause/delete_work_item flow by
moving the blocker collection out of the force/preview guard and keeping force
solely for skipping the prompt.
- Around line 41-57: The scaffolded GuardCheck uses a successful echo command
allowing a no-op guard to always pass; change the default check.command in the
GuardCheck construction (the field set where GuardCheck is instantiated before
write_guard) to a failing placeholder command (e.g., a command that prints an
explanatory message and exits non‑zero) so newly generated guards fail until
user replaces it; keep the rest of the flow (write_guard, ui hints) but ensure
the preview/creation path still shows the file while the default command is
non‑passing.

In `@src/cmd/list.rs`:
- Around line 57-61: The guard branch in list() converts
load_guards_with_warnings errors into an anyhow instead of the standard
Vec<Diagnostic> used by other targets; change the error handling so
load_guards_with_warnings returns/propagates the same Vec<Diagnostic> path
(i.e., remove the map_err(|d| anyhow::anyhow!("{}", d.message)) conversion) and
return the diagnostics directly on error, while keeping
list_guards(&result.items, filter, limit, output) and returning
Ok(result.warnings) on success; reference: ListTarget::Guard,
load_guards_with_warnings, list_guards, and list().
- Around line 360-378: The current mapping truncates guard commands with a
byte-slice (&cmd[..50]) which can panic on non-ASCII and also loses the full
command for JSON/plain outputs; change the mapping in the creation of summaries
(where GuardSummary is constructed from items and cmd is read from
g.spec.check.command) to store the full cmd (e.g., cmd.clone()) without any
slicing, and move any display truncation into the table/plain renderer used by
output_list (or the renderer callback passed to output_list) where you apply a
char-boundary-safe truncation helper (e.g., truncate_str_by_chars) so table
output shows a shortened preview while JSON/plain output contains the full
command.

---

Nitpick comments:
In `@src/parse.rs`:
- Around line 313-346: The three functions write_guard, write_adr, and
write_work_item duplicate TOML serialization and file/write/preview logic;
extract a generic helper fn write_toml_artifact<T: Serialize>(path: &Path, spec:
&T, schema: ArtifactSchema, op: WriteOp, display_path: Option<&Path>) ->
Result<(), Diagnostic> that performs toml::to_string_pretty, wraps with
with_schema_header(schema, &body), maps serialization and IO errors to
Diagnostic::new(DiagnosticCode::E0901IoError, ... , path.display().to_string()),
and handles WriteOp::Execute with std::fs::write and WriteOp::Preview with
ui::dry_run_file_preview(display_path.unwrap_or(path), &content); then simplify
write_guard (and likewise write_adr/write_work_item) to delegate to
write_toml_artifact with the appropriate specific spec type (e.g., GuardSpec)
and ArtifactSchema::Guard.

In `@tests/test_guard.rs`:
- Around line 68-97: Add a new round-trip test in tests/test_guard.rs that
mirrors the string-field tests but exercises the numeric path: create a temp
project with init_project(), write a guard via write_guard(...) (e.g.,
"GUARD-ECHO" with some initial timeout), call run_commands(...) to run
["guard","set","GUARD-ECHO","timeout_secs","30"] followed by
["guard","get","GUARD-ECHO","timeout_secs"], and assert the output contains
"30"; name the test function something like test_guard_set_timeout_secs and
reuse the existing helpers (init_project, write_guard, run_commands).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 63367a76-ce87-472b-9d8e-b7b69151ec46

📥 Commits

Reviewing files that changed from the base of the PR and between 5af92fc and 81f8953.

📒 Files selected for processing (22)
  • .claude/skills/guard-writer/SKILL.md
  • CHANGELOG.md
  • CLAUDE.md
  • build.rs
  • docs/rfc/RFC-0002.md
  • gov/rfc/RFC-0002/clauses/C-CRUD-VERBS.toml
  • gov/rfc/RFC-0002/clauses/C-RESOURCE-MODEL.toml
  • gov/rfc/RFC-0002/clauses/C-RESOURCES.toml
  • gov/rfc/RFC-0002/rfc.toml
  • gov/schema/edit-ops.json
  • gov/schema/edit-ops.schema.json
  • gov/work/2026-03-27-add-guard-resource-subcommands-new-list-show-set-delete.toml
  • src/cli.rs
  • src/cmd/edit/adapter.rs
  • src/cmd/edit/mod.rs
  • src/cmd/edit/runtime.rs
  • src/cmd/guard.rs
  • src/cmd/list.rs
  • src/cmd/mod.rs
  • src/command_router.rs
  • src/parse.rs
  • tests/test_guard.rs

Comment thread CHANGELOG.md
Comment on lines +10 to +16
### Added

- govctl guard new scaffolds guard TOML with hint about default_guards (WI-2026-03-27-001)
- govctl guard list shows all guards in tabular format (WI-2026-03-27-001)
- govctl guard show displays a guard (WI-2026-03-27-001)
- govctl guard set edits guard fields (WI-2026-03-27-001)
- govctl guard delete removes a guard with safety checks (WI-2026-03-27-001)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if guard get/add/remove commands are implemented
rg -n "guard.*get|guard.*add|guard.*remove" src/cli.rs src/command_router.rs 2>/dev/null || echo "No matches in CLI files"
ast-grep --pattern 'fn $FUNC($_) { $$$ }' | rg -i "guard" | head -20

Repository: govctl-org/govctl

Length of output: 84


🏁 Script executed:

# Find all Rust source files and explore structure
git ls-files | grep -E '\.(rs|toml)$' | head -20

Repository: govctl-org/govctl

Length of output: 1288


🏁 Script executed:

# Search for where CLI commands are registered/defined
rg -n "guard" --type rust | grep -i "command\|subcommand\|arg\|cmd" | head -30

Repository: govctl-org/govctl

Length of output: 2950


🏁 Script executed:

# Look for the actual command implementations
rg -n "\"get\"|\"add\"|\"remove\"" --type rust | grep -i "guard\|sub" | head -20

Repository: govctl-org/govctl

Length of output: 295


Add changelog entry for guard get command.

Tests in test_guard.rs show that guard get is implemented and used to retrieve individual guard field values (lines 76–77, 93), but this command is missing from the changelog. The other standard resource model commands (new, list, show, set, delete) are documented, so get should be added for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CHANGELOG.md` around lines 10 - 16, Add the missing "govctl guard get
retrieves a specific guard field value (WI-2026-03-27-001)" entry under the "###
Added" section in CHANGELOG.md so it matches the other resource commands; ensure
the text and WI identifier format follow the existing lines for "govctl guard
new/list/show/set/delete" to keep the changelog consistent with the tests that
use guard get in test_guard.rs.

Comment thread gov/schema/edit-ops.json
Comment thread src/cli.rs
Comment thread src/cmd/guard.rs
Comment thread src/cmd/guard.rs Outdated
Comment thread src/cmd/list.rs
Comment on lines +57 to +61
if target == ListTarget::Guard {
let result =
load_guards_with_warnings(config).map_err(|d| anyhow::anyhow!("{}", d.message))?;
list_guards(&result.items, filter, limit, output);
return Ok(result.warnings);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Keep guard list on the same diagnostic path as the other list targets.

This branch converts the top-level guard load failure into anyhow, while the rest of list() returns Vec<Diagnostic>. A read/permission error under gov/guard will therefore bypass the normal diagnostic renderer just for guards.

Suggested fix
-        let result =
-            load_guards_with_warnings(config).map_err(|d| anyhow::anyhow!("{}", d.message))?;
+        let result = match load_guards_with_warnings(config) {
+            Ok(result) => result,
+            Err(d) => return Ok(vec![d]),
+        };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cmd/list.rs` around lines 57 - 61, The guard branch in list() converts
load_guards_with_warnings errors into an anyhow instead of the standard
Vec<Diagnostic> used by other targets; change the error handling so
load_guards_with_warnings returns/propagates the same Vec<Diagnostic> path
(i.e., remove the map_err(|d| anyhow::anyhow!("{}", d.message)) conversion) and
return the diagnostics directly on error, while keeping
list_guards(&result.items, filter, limit, output) and returning
Ok(result.warnings) on success; reference: ListTarget::Guard,
load_guards_with_warnings, list_guards, and list().

Comment thread src/cmd/list.rs Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (4)
src/cmd/guard.rs (2)

41-45: ⚠️ Potential issue | 🟠 Major

Scaffolded guards should fail until they're configured.

The default check.command is an echo that exits 0, so a brand-new guard can be added to verification.default_guards and immediately report success without doing any verification. Use a failing placeholder until the command is replaced.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cmd/guard.rs` around lines 41 - 45, The scaffolded GuardCheck uses a
success-returning placeholder command; change the default check.command in the
GuardCheck initializer to a failing placeholder (e.g., "false" or "sh -c 'exit
1'") so new guards fail until configured; update the initializer where
GuardCheck is constructed (the check field in the Guard struct) to use the
failing command string and leave timeout_secs/pattern as-is.

76-113: ⚠️ Potential issue | 🔴 Critical

Keep blocker checks mandatory for guard delete.

Blocker scanning is gated behind !force && !op.is_preview(), so guard delete --force and preview mode both skip the very checks RFC-0002 requires. --force should only skip confirmation after blockers are known, never bypass config/work-item reference detection. Downstream, the CLI help text and tests/test_guard.rs::test_guard_delete_force_bypasses_checks will need to be updated with the same contract.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cmd/guard.rs` around lines 76 - 113, The blocker detection (building
blockers via config.verification.default_guards and scanning work items with
crate::parse::load_work_items into the blockers Vec) must run unconditionally;
remove the surrounding gate "if !force && !op.is_preview() { ... }" so those
checks always execute, and only use force/preview to skip the subsequent
confirmation step (not the detection). Keep the existing code that returns an
error when blockers is_non_empty, and adjust caller logic or add a separate
confirmation branch that is conditional on force or op.is_preview(); also update
the CLI help text and the test
tests/test_guard.rs::test_guard_delete_force_bypasses_checks to reflect that
--force does not bypass blocker checks but only skips confirmation.
src/cmd/list.rs (2)

57-61: ⚠️ Potential issue | 🟡 Minor

Keep guard list on the standard diagnostic path.

This branch turns a Diagnostic into anyhow, so guard-specific load/read failures skip the normal renderer used by every other list target. Return the diagnostic directly here instead of erroring out.

Suggested fix
     if target == ListTarget::Guard {
-        let result =
-            load_guards_with_warnings(config).map_err(|d| anyhow::anyhow!("{}", d.message))?;
+        let result = match load_guards_with_warnings(config) {
+            Ok(result) => result,
+            Err(d) => return Ok(vec![d]),
+        };
         list_guards(&result.items, filter, limit, output);
         return Ok(result.warnings);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cmd/list.rs` around lines 57 - 61, The branch for ListTarget::Guard
converts a Diagnostic into an anyhow error, bypassing the standard diagnostic
renderer; instead propagate the Diagnostic directly: call
load_guards_with_warnings(config) without map_err (e.g. let result =
load_guards_with_warnings(config)?), keep calling list_guards(&result.items,
filter, limit, output), and return result.warnings; ensure the surrounding
function's Result type propagates the Diagnostic so diagnostics follow the
normal rendering path.

360-378: ⚠️ Potential issue | 🟠 Major

Don't truncate guard commands before serialization.

&cmd[..50] can panic on non-ASCII commands, and truncating here also means JSON/plain output loses the real command. Keep the full value in GuardSummary and only shorten it in the table renderer with a char-boundary-safe helper.

Suggested fix
 struct GuardSummary {
     id: String,
     title: String,
     command: String,
 }
 
+fn truncate_for_display(value: &str, max_chars: usize) -> String {
+    if value.chars().count() <= max_chars {
+        return value.to_string();
+    }
+    let end = value
+        .char_indices()
+        .nth(max_chars)
+        .map(|(idx, _)| idx)
+        .unwrap_or(value.len());
+    format!("{}…", &value[..end])
+}
+
 fn list_guards(
     guards: &[crate::model::GuardEntry],
     filter: Option<&str>,
     limit: Option<usize>,
@@
     let summaries: Vec<GuardSummary> = items
         .iter()
-        .map(|g| {
-            let cmd = &g.spec.check.command;
-            let truncated = if cmd.len() > 50 {
-                format!("{}…", &cmd[..50])
-            } else {
-                cmd.clone()
-            };
-            GuardSummary {
-                id: g.meta().id.clone(),
-                title: g.meta().title.clone(),
-                command: truncated,
-            }
-        })
+        .map(|g| GuardSummary {
+            id: g.meta().id.clone(),
+            title: g.meta().title.clone(),
+            command: g.spec.check.command.clone(),
+        })
         .collect();
 
     output_list(&summaries, &["Guard", "Title", "Command"], output, |s| {
-        vec![s.id.clone(), s.title.clone(), s.command.clone()]
+        vec![
+            s.id.clone(),
+            s.title.clone(),
+            match output {
+                OutputFormat::Table => truncate_for_display(&s.command, 50),
+                OutputFormat::Json | OutputFormat::Plain => s.command.clone(),
+            },
+        ]
     });
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cmd/list.rs` around lines 360 - 378, The code currently truncates command
strings inside the map that builds GuardSummary (in the items.iter() ->
GuardSummary creation), using &cmd[..50] which can panic on non-ASCII and also
loses the full command for JSON/plain output; instead store the full command in
GuardSummary.command (remove the truncation logic in the iterator) and perform
any visual shortening only in the table renderer path passed to output_list
(implement a char-boundary-safe truncation helper, e.g. using
chars().take(n).collect::<String>() or a unicode-segmentation helper) so
JSON/plain serializers still receive the complete command while the table view
shows a shortened version.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/rfc/RFC-0002.md`:
- Around line 588-590: The v0.5.0 changelog line incorrectly calls guard
“resource type 6”; update the changelog so it matches C-RESOURCES (which inserts
guard as item 5 and renumbers release to 6) by changing any mention of guard
being type 6 to guard being type 5 and ensuring release is documented as type 6;
verify related references in the v0.5.0 entry that mention C-CRUD-VERBS or
C-RESOURCE-MODEL to reflect the corrected resource numbering.
- Around line 214-217: The RFC's guard-delete contract must also require
checking work-item waivers for references: update the normative text to include
verification.waivers[*].guard alongside verification.required_guards and
verification.default_guards so deletion MUST verify no work items reference the
guard ID in verification.required_guards or in verification.waivers[*].guard and
MUST verify the guard is not listed in verification.default_guards in
gov/config.toml, and MUST error with a list of referencing artifacts/config if
any references exist; reference these exact symbols
(verification.waivers[*].guard, verification.required_guards,
verification.default_guards, gov/config.toml) when editing the prose.

In `@src/cmd/guard.rs`:
- Around line 19-21: Slug produced by slugify(title) can be empty or start with
a digit, which yields invalid guard IDs; after computing let slug =
slugify(title).to_uppercase().replace('_', "-"), validate slug and reject the
input if slug.is_empty() or slug.chars().next().map_or(false, |c|
c.is_ascii_digit()); in that case return an explicit error/result indicating the
title is invalid instead of proceeding to let id = format!("GUARD-{slug}"), so
the invalid title fails fast (refer to the slug and id variables and the slugify
call to locate the change).

---

Duplicate comments:
In `@src/cmd/guard.rs`:
- Around line 41-45: The scaffolded GuardCheck uses a success-returning
placeholder command; change the default check.command in the GuardCheck
initializer to a failing placeholder (e.g., "false" or "sh -c 'exit 1'") so new
guards fail until configured; update the initializer where GuardCheck is
constructed (the check field in the Guard struct) to use the failing command
string and leave timeout_secs/pattern as-is.
- Around line 76-113: The blocker detection (building blockers via
config.verification.default_guards and scanning work items with
crate::parse::load_work_items into the blockers Vec) must run unconditionally;
remove the surrounding gate "if !force && !op.is_preview() { ... }" so those
checks always execute, and only use force/preview to skip the subsequent
confirmation step (not the detection). Keep the existing code that returns an
error when blockers is_non_empty, and adjust caller logic or add a separate
confirmation branch that is conditional on force or op.is_preview(); also update
the CLI help text and the test
tests/test_guard.rs::test_guard_delete_force_bypasses_checks to reflect that
--force does not bypass blocker checks but only skips confirmation.

In `@src/cmd/list.rs`:
- Around line 57-61: The branch for ListTarget::Guard converts a Diagnostic into
an anyhow error, bypassing the standard diagnostic renderer; instead propagate
the Diagnostic directly: call load_guards_with_warnings(config) without map_err
(e.g. let result = load_guards_with_warnings(config)?), keep calling
list_guards(&result.items, filter, limit, output), and return result.warnings;
ensure the surrounding function's Result type propagates the Diagnostic so
diagnostics follow the normal rendering path.
- Around line 360-378: The code currently truncates command strings inside the
map that builds GuardSummary (in the items.iter() -> GuardSummary creation),
using &cmd[..50] which can panic on non-ASCII and also loses the full command
for JSON/plain output; instead store the full command in GuardSummary.command
(remove the truncation logic in the iterator) and perform any visual shortening
only in the table renderer path passed to output_list (implement a
char-boundary-safe truncation helper, e.g. using
chars().take(n).collect::<String>() or a unicode-segmentation helper) so
JSON/plain serializers still receive the complete command while the table view
shows a shortened version.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: af61318b-73b5-4b13-afeb-bf244b214f70

📥 Commits

Reviewing files that changed from the base of the PR and between 81f8953 and 2116944.

📒 Files selected for processing (23)
  • .claude/skills/guard-writer/SKILL.md
  • CHANGELOG.md
  • CLAUDE.md
  • build.rs
  • docs/rfc/RFC-0002.md
  • gov/rfc/RFC-0002/clauses/C-CRUD-VERBS.toml
  • gov/rfc/RFC-0002/clauses/C-RESOURCE-MODEL.toml
  • gov/rfc/RFC-0002/clauses/C-RESOURCES.toml
  • gov/rfc/RFC-0002/rfc.toml
  • gov/schema/edit-ops.json
  • gov/schema/edit-ops.schema.json
  • gov/work/2026-03-27-add-guard-resource-subcommands-new-list-show-set-delete.toml
  • src/cli.rs
  • src/cmd/edit/adapter.rs
  • src/cmd/edit/mod.rs
  • src/cmd/edit/runtime.rs
  • src/cmd/guard.rs
  • src/cmd/list.rs
  • src/cmd/mod.rs
  • src/cmd/new.rs
  • src/command_router.rs
  • src/parse.rs
  • tests/test_guard.rs
✅ Files skipped from review due to trivial changes (7)
  • src/cmd/edit/runtime.rs
  • gov/rfc/RFC-0002/clauses/C-RESOURCE-MODEL.toml
  • CHANGELOG.md
  • .claude/skills/guard-writer/SKILL.md
  • gov/schema/edit-ops.json
  • CLAUDE.md
  • gov/work/2026-03-27-add-guard-resource-subcommands-new-list-show-set-delete.toml
🚧 Files skipped from review as they are similar to previous changes (9)
  • src/cmd/mod.rs
  • build.rs
  • gov/schema/edit-ops.schema.json
  • src/parse.rs
  • src/cmd/edit/adapter.rs
  • gov/rfc/RFC-0002/clauses/C-RESOURCES.toml
  • src/cmd/edit/mod.rs
  • gov/rfc/RFC-0002/clauses/C-CRUD-VERBS.toml
  • gov/rfc/RFC-0002/rfc.toml

Comment thread docs/rfc/RFC-0002.md Outdated
Comment thread docs/rfc/RFC-0002.md Outdated
Comment thread src/cmd/guard.rs
Amend RFC-0002 to v0.5.0 adding guard to C-RESOURCES, C-CRUD-VERBS,
and C-RESOURCE-MODEL. Implement govctl guard new/list/show/get/set/
add/remove/delete following the standard resource model pattern.

Includes guard-writer skill, edit-ops rules, GuardTomlAdapter,
safety checks on delete (blocks when referenced by config or work
items), and 10 integration tests.

Closes WI-2026-03-27-001.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
src/cmd/list.rs (1)

67-72: ⚠️ Potential issue | 🟡 Minor

Guard error path diverges from the established diagnostic pattern.

Other list targets propagate load failures as Ok(vec![d]), but the guard branch converts them to anyhow. This inconsistency means guard-related I/O errors will bypass the normal diagnostic renderer.

Suggested fix
     if target == ListTarget::Guard {
-        let result =
-            load_guards_with_warnings(config).map_err(|d| anyhow::anyhow!("{}", d.message))?;
+        let result = match load_guards_with_warnings(config) {
+            Ok(r) => r,
+            Err(d) => return Ok(vec![d]),
+        };
         list_guards(&result.items, filter, limit, output);
         return Ok(result.warnings);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cmd/list.rs` around lines 67 - 72, The guard branch diverges by turning
load_guards_with_warnings errors into anyhow errors; instead change the error
path to return the diagnostics Vec like the other targets. In the
ListTarget::Guard case, call load_guards_with_warnings(config) and, on Err(d),
return Ok(vec![d]) (or otherwise propagate the diagnostics container the same
way as the other branches) before calling list_guards(&result.items, filter,
limit, output) so guard I/O failures are rendered by the normal diagnostic
renderer.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/cmd/list.rs`:
- Around line 67-72: The guard branch diverges by turning
load_guards_with_warnings errors into anyhow errors; instead change the error
path to return the diagnostics Vec like the other targets. In the
ListTarget::Guard case, call load_guards_with_warnings(config) and, on Err(d),
return Ok(vec![d]) (or otherwise propagate the diagnostics container the same
way as the other branches) before calling list_guards(&result.items, filter,
limit, output) so guard I/O failures are rendered by the normal diagnostic
renderer.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 10ebb892-9d14-47a6-a9f0-57ba9a14013d

📥 Commits

Reviewing files that changed from the base of the PR and between 2116944 and 38c46d6.

📒 Files selected for processing (23)
  • .claude/skills/guard-writer/SKILL.md
  • CHANGELOG.md
  • CLAUDE.md
  • build.rs
  • docs/rfc/RFC-0002.md
  • gov/rfc/RFC-0002/clauses/C-CRUD-VERBS.toml
  • gov/rfc/RFC-0002/clauses/C-RESOURCE-MODEL.toml
  • gov/rfc/RFC-0002/clauses/C-RESOURCES.toml
  • gov/rfc/RFC-0002/rfc.toml
  • gov/schema/edit-ops.json
  • gov/schema/edit-ops.schema.json
  • gov/work/2026-03-27-add-guard-resource-subcommands-new-list-show-set-delete.toml
  • src/cli.rs
  • src/cmd/edit/adapter.rs
  • src/cmd/edit/mod.rs
  • src/cmd/edit/runtime.rs
  • src/cmd/guard.rs
  • src/cmd/list.rs
  • src/cmd/mod.rs
  • src/cmd/new.rs
  • src/command_router.rs
  • src/parse.rs
  • tests/test_guard.rs
✅ Files skipped from review due to trivial changes (6)
  • gov/rfc/RFC-0002/clauses/C-RESOURCE-MODEL.toml
  • src/cmd/mod.rs
  • CHANGELOG.md
  • .claude/skills/guard-writer/SKILL.md
  • src/cmd/edit/runtime.rs
  • CLAUDE.md
🚧 Files skipped from review as they are similar to previous changes (9)
  • src/cmd/new.rs
  • gov/work/2026-03-27-add-guard-resource-subcommands-new-list-show-set-delete.toml
  • gov/schema/edit-ops.schema.json
  • gov/rfc/RFC-0002/rfc.toml
  • src/cmd/guard.rs
  • gov/rfc/RFC-0002/clauses/C-CRUD-VERBS.toml
  • src/cli.rs
  • docs/rfc/RFC-0002.md
  • gov/schema/edit-ops.json

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant