Skip to content

feat(cli): stable JSON for accept, check, onboard + stdout fix - #119

Merged
George-RD merged 3 commits into
devfrom
fix/json-accept-rebase
May 16, 2026
Merged

feat(cli): stable JSON for accept, check, onboard + stdout fix#119
George-RD merged 3 commits into
devfrom
fix/json-accept-rebase

Conversation

@George-RD

Copy link
Copy Markdown
Collaborator

Summary

Replaces PR #114 (rebased onto current dev, review fixes applied).

  • Add --json support to accept, check, and onboard commands
  • Standard JSON envelope: {"command": "<name>", "status": "ok|error", "data": {...}}
  • Fix: suppress cargo subprocess stdout/stderr when --json is set (prevents output bleed into machine-readable stream)
  • Document exit code scheme (0/1/2) in src/error.rs and src/cli/format.rs

Bead: cairn-048 | GH #98 (partial)
Supersedes: #114

Test plan

  • cargo build (zero warnings)
  • cargo clippy --all-targets --all-features -D warnings
  • cargo test (113+ tests pass)
  • JSON output clean (no cargo bleed when --json set)

🤖 Generated with Claude Code

Add --json support to accept, check, and onboard commands with
standard JSON envelope. Fix stdout bleed: suppress cargo subprocess
output when --json is set so machine-readable output is clean.
Document exit code scheme in error.rs and format.rs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 15, 2026 20:03
@coderabbitai

coderabbitai Bot commented May 15, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1ee9dd3c-91f3-4b17-a771-b3b098dae0e8

📥 Commits

Reviewing files that changed from the base of the PR and between 446cc3f and b262a59.

📒 Files selected for processing (4)
  • src/cli/accept.rs
  • src/cli/format.rs
  • src/cli/mod.rs
  • tests/phase_7_7_ux_foundation.rs

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Acceptance gate and other commands emit structured JSON envelope (command/status/data) and a JSON output mode for per-step results.
  • Improvements

    • check and onboard support JSON output; check --json returns non‑zero on errors. Per-step failure details captured for JSON reports. String escaping expanded for safe JSON.
  • Tests

    • CLI tests updated to validate JSON envelopes and per-step fields.
  • Documentation

    • CLI exit codes documented (0/1/2).

Walkthrough

CLI commands now emit standardized JSON envelopes {command,status,data} when --json is used. Accept gate accepts a json flag and can silence subcommand output; onboard and check return structured JSON on success/error; exit codes (0,1,2) are documented. Tests updated to assert JSON envelopes.

Changes

Structured JSON output across CLI commands

Layer / File(s) Summary
Exit code documentation
src/error.rs, src/cli/format.rs
Document CLI process exit code meanings (0 success, 1 advisory/operational error, 2 argument/usage error).
Accept gate JSON output
src/cli/accept.rs, src/cli/mod.rs
Accept gate API now accepts json: bool, threads it into verification steps via run_command(..., quiet), refactors state-to-string mapping, adds format_json for structured output (escaped details), and includes unit tests.
Onboard command JSON wrapping
src/cli/commands.rs
Onboard success now wraps inner report JSON in {command:"onboard",status:"ok",data:...}; on load failure, JSON mode returns an error envelope with escaped message, non-JSON continues to call err(1, ...).
Check command JSON support
src/cli/mod.rs
check now supports --json, handles missing blueprint with JSON-aware output, computes has_errors to set exit code to 1 when findings contain errors, and emits {command:"check",status,data:{findings}} in JSON mode.
Tests and minor CLI tweaks
tests/*, src/cli/mod.rs
Tests updated/added to validate JSON envelopes for accept, check --json, and onboard --json; minor test comment/format adjustments.
Beads issue additions
.beads/issues.jsonl
Added several new issue records to the issues JSONL stream.

Sequence Diagram

sequenceDiagram
  participant User as User/CLI
  participant Run as CLI dispatch
  participant Check as run_check
  participant Accept as run_accept_gate
  participant Cmd as run_command
  participant Formatter as format_json/format_findings
  User->>Run: cairn check --json
  Run->>Check: load project / detect blueprint
  Check->>Check: compute findings, has_errors?
  Check->>Formatter: format_json/findings -> envelope {command:"check",status,data}
  Formatter-->>User: stdout JSON + exit code
  User->>Run: cairn accept --json
  Run->>Accept: run_accept_gate(change_id, json:true)
  Accept->>Cmd: run_command("cargo build", quiet:true)
  Cmd-->>Accept: GateStep
  Accept->>Cmd: run_command("cargo clippy", quiet:true)
  Cmd-->>Accept: GateStep
  Accept->>Cmd: run_command("cargo fmt --check", quiet:true)
  Cmd-->>Accept: GateStep
  Accept->>Cmd: run_command("cargo test --workspace --locked", quiet:true)
  Cmd-->>Accept: GateStep
  Accept->>Formatter: format_json(findings,has_failed,has_blocked)
  Formatter-->>User: stdout JSON + exit code
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

🐰 A rabbit writes a JSON tune,
envelopes snug beneath the moon,
Commands now whisper, neat and bright,
status, data, wrapped just right—✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(cli): stable JSON for accept, check, onboard + stdout fix' clearly summarizes the main changes: adding stable JSON support to multiple CLI commands and fixing stdout suppression.
Description check ✅ Passed The description is directly related to the changeset, providing context on adding JSON support to CLI commands, fixing stdout bleed with cargo, and documenting exit codes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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 fix/json-accept-rebase

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with 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.

Inline comments:
In `@src/cli/mod.rs`:
- Around line 100-124: The current "no blueprint found" branch returns plain
text via ok(...) which breaks the --json contract; change it to emit a JSON
envelope when parsed.json is true. Implement a conditional around the final
return: if parsed.json { return ok_json(parsed.json, "No cairn.blueprint found.
...") } else { return ok(...) }, or alternatively construct and return the same
JSON envelope shape used by error_output(parsed.json, ...) so callers always get
machine-readable output when parsed.json is set; update references around ok and
error_output to use parsed.json and the same message text.

In `@tests/phase_7_7_ux_foundation.rs`:
- Around line 55-73: The test currently branches on stdout.starts_with('{') and
allows non-JSON output; change it to always parse and validate JSON by removing
the conditional and calling serde_json::from_str on result.stdout.trim()
unconditionally (e.g., bind stdout = result.stdout.trim(), then let parsed:
serde_json::Value = serde_json::from_str(stdout).expect("cairn check --json must
produce valid JSON");), then keep the assertions against parsed["command"] and
parsed["data"]["findings"] as-is and retain the stderr assert that it does not
contain "cairn lint --json".
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3493c50b-2acb-4d87-9950-a66427d49da6

📥 Commits

Reviewing files that changed from the base of the PR and between dea5a4f and 0917fe0.

📒 Files selected for processing (7)
  • src/cli/accept.rs
  • src/cli/commands.rs
  • src/cli/format.rs
  • src/cli/mod.rs
  • src/error.rs
  • tests/kernel.rs
  • tests/phase_7_7_ux_foundation.rs

Comment thread src/cli/mod.rs
Comment thread tests/phase_7_7_ux_foundation.rs Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds/standardizes --json output for additional CLI commands (accept, check, onboard) and documents the project’s exit-code scheme, with a key fix to prevent subprocess output from corrupting JSON streams.

Changes:

  • Implement JSON output for check and onboard using a {command,status,data} envelope.
  • Add --json support to accept, including suppressing cargo subprocess stdout/stderr to keep JSON output clean.
  • Update tests and inline documentation for the new JSON behavior and exit-code taxonomy.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/phase_7_7_ux_foundation.rs Updates the UX test to reflect that check now supports --json.
tests/kernel.rs Adjusts onboard JSON assertions to validate the new envelope (command/status/data).
src/error.rs Documents the CLI exit code scheme in crate-level docs.
src/cli/mod.rs Wires accept JSON flag through and adds JSON envelope output for check.
src/cli/format.rs Documents exit code semantics on err(...).
src/cli/commands.rs Wraps onboard JSON payload in the standard {command,status,data} envelope.
src/cli/accept.rs Adds JSON output for accept and suppresses subprocess output when JSON is enabled.
Comments suppressed due to low confidence (1)

src/cli/mod.rs:123

  • When --json is set, this check early-return still emits a human guidance string via ok(...), so cairn check --json is not guaranteed to produce machine-readable JSON (violates the stated stable JSON envelope). Consider returning the standard {command,status,data} envelope here as well (e.g., status:"error" with a data.message or data.findings payload) when parsed.json is true.
        return ok(
            "No cairn.blueprint found. Inspection has nothing to look at.\n\
             Run `cairn init` to scaffold a blueprint, then re-run `cairn check`.\n"
                .to_owned(),
        );

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/cli/mod.rs
Comment on lines +112 to 117
if parsed.file.ends_with("cairn.blueprint") && root.join("cairn.dsl").exists() {
return error_output(
parsed.json,
"CAIRN_COMMAND_FAILED",
"no blueprint file was found; rename `cairn.dsl` to `cairn.blueprint`",
);
Comment thread src/cli/accept.rs
Comment on lines +156 to +163
fn format_json(findings: &[VerificationFinding], has_failed: bool, has_blocked: bool) -> String {
let status = if has_failed {
"failed"
} else if has_blocked {
"blocked"
} else {
"passed"
};
Comment thread tests/phase_7_7_ux_foundation.rs Outdated
Comment on lines 55 to 70
// When a blueprint exists in the CWD (the repo root), check
// produces a JSON envelope. When it does not, it falls through
// to a human-friendly guidance message.
let stdout = result.stdout.trim();
if stdout.starts_with('{') {
let parsed: serde_json::Value =
serde_json::from_str(stdout).expect("cairn check --json must produce valid JSON");
assert_eq!(parsed["command"], "check", "envelope must name the command");
assert!(
parsed["data"]["findings"].is_array(),
"envelope must contain findings array"
);
}
// Either way, check --json must not be rejected with an error
// message pointing at `cairn lint --json`.
assert!(
- Wrap "no blueprint found" check path in JSON envelope when --json set
- Normalize accept --json status to ok/error with gate_outcome in data
- Enforce JSON unconditionally in check --json acceptance test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Wrap DSL migration check path in command envelope when --json set
- Remove ad-hoc message field from no-blueprint JSON (empty findings suffice)
- Capture subprocess stderr in JSON mode instead of discarding to /dev/null
- Add \r, \t, \b, \f escapes to esc() per RFC 8259
- Report blocked gate as status "error" (incomplete verification is not ok)
- Strengthen acceptance test: assert exit code and status field shape

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@George-RD
George-RD merged commit 346bacc into dev May 16, 2026
2 of 3 checks passed
Comment thread src/cli/mod.rs
Comment on lines +114 to +125
return ok(format!(
"{{\"command\":\"check\",\"status\":\"error\",\"data\":{{\"findings\":[{}]}}}}\n",
finding_json(&Finding {
code: "CAIRN_COMMAND_FAILED".to_owned(),
severity: FindingSeverity::Error,
message:
"no blueprint file was found; rename `cairn.dsl` to `cairn.blueprint`"
.to_owned(),
node: None,
path: None,
})
));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Exit code mismatch with JSON status. This code path returns exit code 0 via ok(), but the JSON envelope contains "status":"error" with an Error-severity finding. According to the documented exit code scheme (line 357-359 in format.rs and lines 7-11 in error.rs), errors should return exit code 1.

if parsed.json {
    return CliResult {
        code: 1,  // Error status requires non-zero exit code
        stdout: format!(
            "{{\"command\":\"check\",\"status\":\"error\",\"data\":{{\"findings\":[{}]}}}}}\n",
            finding_json(&Finding {
                code: "CAIRN_COMMAND_FAILED".to_owned(),
                severity: FindingSeverity::Error,
                message:
                    "no blueprint file was found; rename `cairn.dsl` to `cairn.blueprint`"
                        .to_owned(),
                node: None,
                path: None,
            })
        ),
        stderr: String::new(),
    };
}

This inconsistency will break orchestration tools relying on exit codes to detect failures.

Suggested change
return ok(format!(
"{{\"command\":\"check\",\"status\":\"error\",\"data\":{{\"findings\":[{}]}}}}\n",
finding_json(&Finding {
code: "CAIRN_COMMAND_FAILED".to_owned(),
severity: FindingSeverity::Error,
message:
"no blueprint file was found; rename `cairn.dsl` to `cairn.blueprint`"
.to_owned(),
node: None,
path: None,
})
));
if parsed.json {
return CliResult {
code: 1,
stdout: format!(
"{{\"command\":\"check\",\"status\":\"error\",\"data\":{{\"findings\":[{}]}}}}\n",
finding_json(&Finding {
code: "CAIRN_COMMAND_FAILED".to_owned(),
severity: FindingSeverity::Error,
message:
"no blueprint file was found; rename `cairn.dsl` to `cairn.blueprint`"
.to_owned(),
node: None,
path: None,
})
),
stderr: String::new(),
};
}

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

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.

2 participants