Skip to content

Commit 9773d41

Browse files
Eilodonclaudevsajan
authored
Claude/durability verification gaps jwav0c (#58)
* Fix P0 findings from durability/verification audit: OOM cap, process-tree kill, reference_impact undercounting, release version-tag CI guard - verify.rs::run_cargo_check: bound stdout/stderr capture to 8 MiB/stream (read_capped) so a pathological build.rs/proc-macro can't exhaust host memory before the wall-clock timeout fires; spawn cargo in its own process group and kill the whole tree (kill_process_tree) on timeout/output-overflow instead of only the direct cargo PID, which left descendants (rustc, build scripts, proc-macro servers) running. Function signature unchanged -- caps are internal constants, not config-plumbed, to avoid a signature-change risk escalation on this hub symbol. - reference_impact (trace.rs): fixed two undercounting bugs. (1) a textual grep hit in a file that already had a call-edge hit at a DIFFERENT line was wrongly suppressed because the skip-set was built from the whole `seen` set instead of just import-edge file-level hits. (2) must_change_count/likely_change_count/review_count/ textual_only_count were computed after hits.truncate(), silently under-reporting once the real match count exceeded REFERENCE_IMPACT_LIMIT. - release.yml: qualify-release now fails a tag push if Cargo.toml's committed version doesn't match the tag, preventing the exact recurring class of bug documented in Cargo.toml's own version-field comment (stuck at 0.1.1 through v0.1.2-v0.1.4, defeating scripts/mcp-launcher.sh's verified-download fast path for a whole release cycle). Deliberately does NOT bump Cargo.toml's version here -- both release binaries and npm packages are versioned straight from the git tag (sed-stamp / stage-release.sh), not from this file, so bumping it prematurely without a corresponding tag/publish would just pin `calm setup --npx` at a phantom, unpublished npm version. Test coverage: read_capped unit tests (under/over cap), 2 new reference_impact regression tests (verified to fail against the pre-fix code, pass against the fix), existing run_cargo_check tests unchanged and still passing through the new kill path. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013ZhKa2oNHjgX9fbuJCM7Sq * Fail closed the audit ledger's HMAC key instead of silently downgrading to an unkeyed chain (P1) - ledger.rs: ledger_key_for_conn/compute_event_hash now return Result. A real on-disk connection whose audit.key can't be read or created (e.g. a read-only .calm/) propagates LedgerError::KeyUnavailable instead of collapsing into the same None as a legitimate path-less (:memory:) connection -- previously that failure silently produced a plain SHA-256 row indistinguishable at a glance from a genuine HMAC-signed one, forgeable by anyone with plain SQLite write access. - txn.rs::append_ledger_in_savepoint: P0-4 (a ledger failure must never block the write it's auditing) is unchanged -- the existing savepoint rollback already does the right thing once append() actually signals failure instead of quietly succeeding with a weak hash. Added a warn-level log on failure so an operator has a signal that the ledger is accumulating gaps, instead of finding out only via a manual verify_chain/completeness check. - New regression test forces the I/O failure by pre-creating audit.key as a directory (deterministic, works under root -- a permission-bit trick doesn't, root bypasses those checks) and asserts append() returns KeyUnavailable with zero rows written, never a forged-looking unkeyed row. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013ZhKa2oNHjgX9fbuJCM7Sq * Add per-connection preset handshake so calm connect --preset works when attaching, not just spawning (P1) - daemon.rs::relay (forwarder side) now writes a one-line preamble (the requested preset, or empty) to the socket before starting its pure byte relay -- the forwarder still does zero MCP/JSON-RPC parsing, ADR-0005's design is unchanged. - daemon.rs::read_connection_preset_preamble (daemon side) reads that line byte-by-byte directly off the raw socket (no BufReader, so nothing gets over-consumed from the MCP stream that immediately follows), bounded by both a max length and a 2s timeout so a misbehaving connector can't hang or balloon memory for its own connection's task -- either bound just falls back to "no preset requested", since this handshake is a UX affordance, not a security boundary. - CalmServer::narrow_connection_preset applies the requested preset to THIS connection's own ceiling via the same resolve_preset/ current_visible_tool_names/call_tool machinery set_toolset already uses -- can only ever narrow, never widen past the daemon's real tool_router (built once at spawn time and still the hard ceiling ToolRouter::call itself enforces), so a too-wide request is a no-op, never a privilege escalation. - New end-to-end test daemon_respects_per_connection_preset: spawns connection A with no --preset (wide), keeps it alive, then connects B with --preset orient to the SAME already-live daemon and confirms B's tools/list is actually narrowed -- confirmed to fail against the pre-fix code (edit_context leaks into B's list) and pass against the fix. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013ZhKa2oNHjgX9fbuJCM7Sq * Add batch_status tool for multi-file edit observability (P2) New read-only batch_status tool (txn.rs): takes a caller-supplied list of tx_ids -- the ones a set of edit_lines/edit_symbol/format_files calls already returned for one multi-file change -- and reports one aggregate view (counts by state, which are missing, whether any failed) instead of requiring a separate edit_transaction_status call per file. Deliberately scoped down from a full multi-file transaction/change-set (new column, schema migration, threading through the core write path): this is pure read-side aggregation over the existing per-file EditTransaction journal, so it adds zero risk to edit_lines/edit_symbol or txn::begin. KNOWN_LIMITATIONS.md's "No multi-file change-set" entry is updated to describe exactly what this does and does not cover -- the atomicity/rollback gap it describes is unchanged. Also: regenerated docs/status.generated.md and bumped every doc-truth-checked "36 tools" reference to 37 (scripts/check-doc-truth.sh, scripts/check-claims-registry.sh both clean). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013ZhKa2oNHjgX9fbuJCM7Sq * Add calm guard CLI command: Git/CI-native gate on the staged diff (P2) New `calm guard --project-root .` command wraps the exact `diff_impact` tool an MCP agent's own Stage-7 pre-commit gate already runs, against `git diff --cached`, and exits non-zero when the resulting aggregate_risk is at or above `--fail-on` (default "high"). Usable directly as a pre-commit hook or CI step for a change made outside any MCP session (a teammate's native editor, a bot PR) -- previously invisible to CALM entirely (KNOWN_LIMITATIONS.md "No Git/CI-native integration path"). Implementation is pure reuse, not a second risk-analysis path: CalmServer::diff_impact_json (guardrails.rs) is a thin `pub` wrapper that constructs a CalmServer, calls the existing pub(crate) diff_impact tool method directly (in-process, no MCP transport needed -- same CalmServer::new(...) constructor every test already uses), and returns serde_json::Value so calm-cli never needs to name any of this crate's internal tool param/output types. 4 new integration tests spawn the real calm binary against a real git repo + index: blocks (exit 1) on a signature change to a 3-caller function, passes (exit 0) on a body-only change under the default threshold, --json emits parseable diff_impact output, and an invalid --fail-on value fails loudly instead of silently doing something else. Also updates KNOWN_LIMITATIONS.md's "No Git/CI-native integration path" entry to describe what calm guard covers and what's still missing (a commit-range/PR-diff mode, a publishable GitHub Action). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013ZhKa2oNHjgX9fbuJCM7Sq * Lay storage foundation for splitting durable state out of index.db index.db currently holds both the rebuildable symbol/call-graph index (synchronous=NORMAL) and durable state -- project memory, the edit transaction journal, the audit ledger, the maintenance outbox -- none of which are rebuildable from source. All of it shares one file and one durability posture. This adds the storage-layer pieces for a real split, without wiring any production call site to use them yet: - db::schema::STATE_SCHEMA_SQL / init_state_db: the durable tables as a schema separate from SCHEMA_SQL / init_db, always created fresh at their current full shape (no incremental ALTER migrations to carry forward, unlike the index schema). - db::conn::open_state_writer: opens a connection at PRAGMA synchronous=FULL for state.db. - db::schema::migrate_legacy_durable_tables: one-time, idempotent, copy-only migration of durable rows out of a pre-split index.db (never DROPs/DELETEs the legacy rows; INSERT OR IGNORE keyed on each table's real primary key makes repeat calls safe; audit_ledger seq is preserved verbatim so the ledger's hash chain survives the copy). txn.rs/ledger.rs/maintenance.rs/memory.rs test setup now builds its connection via init_state_db instead of init_db, since these modules operate on tables that now live in the state schema. Rewiring calm-server's actual runtime call sites (edit.rs, txn.rs, memory.rs, common.rs, lib.rs, ...) to open and write through state.db is a deliberately separate follow-up -- open_writer's own blast radius (~320 transitively-affected files per diff_impact) makes it too large to fold into this groundwork. KNOWN_LIMITATIONS.md and CHANGELOG.md spell out exactly what's done vs. still pending. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013ZhKa2oNHjgX9fbuJCM7Sq --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: t <t@t.com>
1 parent 60ff9c9 commit 9773d41

29 files changed

Lines changed: 1635 additions & 209 deletions

.claude-plugin/marketplace.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"plugins": [
99
{
1010
"name": "calm",
11-
"description": "Adds the CALM MCP server: call-graph-aware locate/source/edit_symbol/diff_impact and 36 tools total for navigating and safely editing a codebase, backed by a multi-tier resolver (syntactic -> Stack Graphs -> SCIP) instead of grep.",
11+
"description": "Adds the CALM MCP server: call-graph-aware locate/source/edit_symbol/diff_impact and 37 tools total for navigating and safely editing a codebase, backed by a multi-tier resolver (syntactic -> Stack Graphs -> SCIP) instead of grep.",
1212
"author": {
1313
"name": "Eilodon"
1414
},

.github/workflows/release.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,29 @@ jobs:
2929
steps:
3030
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
3131

32+
# Cargo.toml's own version field is cosmetic for the actual release
33+
# artifacts -- the `build` job below stamps release binaries from the
34+
# tag directly (sed, "Set crate version from tag"), and npm-publish's
35+
# stage-release.sh takes the tag as its own argument -- so a stale
36+
# Cargo.toml can't ship a wrong-versioned binary or npm package. What
37+
# it DOES still break: `scripts/mcp-launcher.sh`'s verified-download
38+
# fast path, which compares a checked-out `workspace_version` against
39+
# the downloaded release binary's real `--version` and treats any
40+
# mismatch as a corrupted download, falling back to a from-source
41+
# rebuild on every single invocation for the whole release cycle --
42+
# exactly what happened for real, stuck at 0.1.1 through
43+
# v0.1.2/v0.1.3/v0.1.4 (see Cargo.toml's own version-field comment).
44+
# Catch that here, before any publish job runs, not after.
45+
- name: Verify Cargo.toml version matches the release tag
46+
run: |
47+
tag_version="${GITHUB_REF_NAME#v}"
48+
cargo_version=$(grep -m1 '^version = ' Cargo.toml | sed -E 's/version = "(.*)"/\1/')
49+
if [ "$cargo_version" != "$tag_version" ]; then
50+
echo "::error::Cargo.toml's [workspace.package].version ('${cargo_version}') does not match release tag ${GITHUB_REF_NAME} (expected '${tag_version}'). Bump it in the same commit as cutting this tag -- see Cargo.toml's own comment for what breaks otherwise (scripts/mcp-launcher.sh's fast path, defeated for this whole release cycle)."
51+
exit 1
52+
fi
53+
echo "Cargo.toml version ${cargo_version} matches release tag ${GITHUB_REF_NAME}"
54+
3255
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
3356
with:
3457
components: clippy, rustfmt

AGENTS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Full stage-by-stage guide, all 8 Mandatory Rules, the Preset table, and Tool Qui
1212

1313
---
1414

15-
> 36 tools. 8 stages. Every response carries `suggested_next` — follow it.
15+
> 37 tools. 8 stages. Every response carries `suggested_next` — follow it.
1616
---
1717

1818
## Core Principles
@@ -291,7 +291,7 @@ remember("auth-flow", "OAuth callback must validate state param — see incident
291291
| `trace` | `repo_overview`, `search`, `locate`, `symbol_info`, `source`, `callers`, `callees`, `path`, `dependencies`, `reference_impact`, `indexing_status` | Call graph traversal |
292292
| `edit` | `repo_overview`, `search`, `locate`, `symbol_info`, `source`, `callers`, `callees`, `edit_context`, `edit_lines`, `edit_symbol`, `diff_impact`, `indexing_status`, `edit_transaction_status`, `maintenance_status`, `retry_maintenance`, `repair_consistency`, `verify_change` | Code modification workflow |
293293
| `compound` | `repo_overview`, `locate`, `hotspots`, `fitness_report`, `source`, `understand`, `edit_context`, `diff_impact`, `session_context`, `indexing_status`, `remember`, `recall` | Full workflow, no raw graph traversal |
294-
| `full` | All 36 tools | Default; use when workflow spans multiple stages |
294+
| `full` | All 37 tools | Default; use when workflow spans multiple stages |
295295
`--preset` is set once at server startup and cannot change mid-session. Use `full` (default) when the workflow spans multiple stages. Use specific presets only when scope is locked to one stage.
296296

297297
Beyond the 5 named presets above, `--preset`/`config.json`'s `preset` field also accept a **composable toolset spec**: a comma-separated list of toolset (module-domain) names — `trace`, `locate`, `orient`, `memory`, `guardrails`, `recover`, `scip`, `lsp`, `security`, `testgap`, `inspect`, `edit`, `patterndebt` — optionally prefixed with `-` to subtract that toolset instead of adding it. E.g. `--preset "trace,security"` unions two toolsets; `--preset "full,-edit"` is every tool except the edit toolset's (`edit_symbol`/`edit_lines`/`format_files`). This is a different, finer-grained axis than the 5 named presets (which are hand-curated cross-cutting workflow bundles, not toolset unions) — an unrecognized token in either syntax is a hard startup error, never a silent full-access fallback.

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ git tags in [Releases](https://github.com/Eilodon/CALM/releases).
2323
- `calm serve --http` now caps request body size (16 MiB, `axum::extract::DefaultBodyLimit`) and concurrent in-flight requests (64, `tower::limit::ConcurrencyLimitLayer`) as defense-in-depth against the unbounded-resource gap a bare `axum::Router` had; still not a substitute for a reverse proxy's real rate limiting
2424
- New `reference_impact` tool: merges call edges, import edges naming a symbol, and a repo-wide textual grep into one classified reference list (`must_change`/`likely_change`/`review`/`textual_only`) for rename/removal planning -- closes the exact gap behind two real `benchmarks/b7_task_correctness` misses (a bare re-export statement invisible to the call graph alone)
2525
- `edit_lines`/`edit_symbol` gained an optional `cites` param: the EXACT `qualified_name` of a caller `edit_context` returned this session, checked by equality rather than the existing `reason` field's word-boundary substring search -- closes the "paste a real caller name into an unrelated sentence" gaming path for callers that opt in; the free-text `reason` path remains for backward compatibility
26+
- A real on-disk audit ledger connection whose `audit.key` can't be read or created (e.g. a read-only `.calm/`) now fails the write closed (`LedgerError::KeyUnavailable`) instead of silently falling back to the old unkeyed, forgeable SHA-256 chain -- the existing `append_ledger_in_savepoint` savepoint rollback (P0-4: a ledger failure must never block the write it's auditing) already does the right thing once `append` actually signals failure, now with a warn-level log so the gap is observable
27+
- `calm connect --preset` now takes effect even when attaching to an already-live daemon, not just when this connection is the one that spawns it: a one-line handshake preamble ahead of the raw MCP byte stream lets each connection narrow its own effective tool ceiling (`CalmServer::narrow_connection_preset`), reusing the same `resolve_preset`/`current_visible_tool_names` machinery `set_toolset` already enforces -- a too-wide request is a no-op, never a privilege escalation, since the daemon's own `tool_router` (built once at spawn time) stays the hard ceiling
28+
- New `batch_status` tool: takes a caller-supplied list of `tx_id`s (the ones a set of `edit_lines`/`edit_symbol`/`format_files` calls already returned) and reports one aggregate view -- counts by state, which are missing, whether any failed -- instead of requiring a separate `edit_transaction_status` call per file for a multi-file change. Observability only: doesn't group transactions server-side or change what those write tools do (see `KNOWN_LIMITATIONS.md` "No multi-file change-set / transaction")
29+
- New `calm guard` CLI command: runs the exact `diff_impact` tool an MCP agent's own Stage-7 pre-commit gate uses against the staged diff (`git diff --cached`) and exits non-zero when `aggregate_risk` is at or above `--fail-on` (default `high`) -- a first Git/CI-native integration point for changes made outside any MCP session (a teammate's native editor, a bot PR), usable directly as a pre-commit hook or CI step
30+
- Storage-layer foundation for splitting durable state out of the rebuildable index (`KNOWN_LIMITATIONS.md` "Durable state and the rebuildable index share one SQLite file at runtime"): `db::schema::STATE_SCHEMA_SQL`/`init_state_db` define `project_memory`, `project_memory_refs`, `edit_transactions`, `tx_events`, `maintenance_jobs`, and `audit_ledger` as a schema separate from the rebuildable index's `SCHEMA_SQL`/`init_db`; `db::conn::open_state_writer` opens a `PRAGMA synchronous=FULL` connection for it; `db::schema::migrate_legacy_durable_tables` does a one-time, idempotent, copy-only migration of a pre-split `index.db`'s durable rows into a `state.db`. Not yet wired into any real `calm-server` call site -- see `KNOWN_LIMITATIONS.md` for what's left
2631

2732
## [0.5.0] - 2026-08-03
2833

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

KNOWN_LIMITATIONS.md

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ allowlist, resource limits) applied uniformly first — bolting each new
2929
language's runner directly onto today's bare `Command::new(...)` would
3030
just multiply the unsandboxed surface instead of closing it. Not started.
3131

32-
## Durable state and the rebuildable index share one SQLite file
32+
## Durable state and the rebuildable index share one SQLite file at runtime
3333

3434
`.calm/index.db` holds the symbol/call-graph index (rebuildable from
3535
source, `PRAGMA synchronous=NORMAL` is a deliberate tradeoff for it) *and*
@@ -40,20 +40,43 @@ power-loss can only lose the last few committed rows under WAL+NORMAL,
4040
never corrupt the file — an acceptable cost for a cache, less obviously
4141
so for a journal used as evidence.
4242

43-
Splitting into two files (`index.db` at NORMAL, a `state.db` at FULL for
44-
transactions/ledger/memory) is a real architecture change — new
45-
migration path, two DB handles instead of one, cross-file consistency to
46-
reason about — not attempted here.
43+
The storage-layer foundation for a split now exists in `calm-core`:
44+
`db::schema::STATE_SCHEMA_SQL`/`init_state_db` define the durable tables
45+
(`project_memory`, `project_memory_refs`, `edit_transactions`, `tx_events`,
46+
`maintenance_jobs`, `audit_ledger`) as a schema separate from the
47+
rebuildable `SCHEMA_SQL`/`init_db`; `db::conn::open_state_writer` opens a
48+
connection at `PRAGMA synchronous=FULL`; and
49+
`db::schema::migrate_legacy_durable_tables` does a one-time, idempotent,
50+
copy-only migration of any pre-split `index.db`'s durable rows into a
51+
`state.db`. All of this is unit-tested but **not yet wired up** — every
52+
real call site in `calm-server` (`edit.rs`, `txn.rs`, `memory.rs`,
53+
`common.rs`, `lib.rs`, and others) still opens and writes through the one
54+
shared `index.db`/`open_writer` connection. Rewiring those call sites to
55+
actually use `state.db` for durable writes is a separate follow-up, scoped
56+
by `open_writer`'s own blast radius (~320 transitively-affected files per
57+
`diff_impact`) rather than attempted alongside the schema/migration
58+
groundwork.
4759

4860
## No multi-file change-set / transaction
4961

5062
Every `edit_lines`/`edit_symbol`/`format_files` call gets its own,
5163
independent `EditTransaction` (`crates/calm-core/src/txn.rs`), scoped to
5264
one file. A multi-file rename or refactor is N independent transactions
5365
that can each land in a different state (one committed, one gated, one
54-
failed) with no aggregate "did the whole refactor succeed" view. There is
55-
no `prepare → validate-all → atomic-ish commit → verify` pipeline across
56-
files, no `PARTIALLY_APPLIED` status, no rollback plan spanning a set.
66+
failed). There is no `prepare → validate-all → atomic-ish commit →
67+
verify` pipeline across files, no `PARTIALLY_APPLIED` status, no rollback
68+
plan spanning a set -- and none of that is what's described below.
69+
70+
What now exists is strictly narrower: a new `batch_status` tool
71+
(`crates/calm-server/src/tools/txn.rs`) takes a caller-supplied list of
72+
`tx_id`s (the ones each independent write already returned) and reports
73+
one aggregate view -- counts by state, which are still missing, whether
74+
any failed -- instead of requiring a separate `edit_transaction_status`
75+
call per file. Purely observability: it doesn't group transactions
76+
server-side (there's no stored notion of "these N tx_ids are one
77+
change-set"), doesn't gate anything, and doesn't change what `edit_lines`/
78+
`edit_symbol`/`format_files` themselves do. The atomicity/rollback gap
79+
above is unchanged.
5780

5881
## Risk classification's change-kind signal covers signatures only
5982

@@ -137,10 +160,17 @@ transition period), not a silent rename — not done here.
137160

138161
## No Git/CI-native integration path
139162

140-
Everything above assumes an MCP client calling CALM's tools directly. A
141-
native editor `Edit`/`Bash` call, or a change made outside any MCP
142-
session entirely (a teammate's local edit, a bot PR), is invisible to
143-
CALM. There's no `calm guard --staged` / `calm review-diff --base
144-
origin/main` CLI surface and no publishable GitHub Action — the
145-
integration points where a team would see CALM's value without depending
146-
on every contributor's agent calling the right tool. Not started.
163+
Everything above mostly assumes an MCP client calling CALM's tools
164+
directly. A first step now exists: `calm guard --project-root .`
165+
(`crates/calm-cli/src/main.rs`) runs the exact same `diff_impact` tool
166+
an MCP agent's own Stage-7 pre-commit gate uses, against the staged diff
167+
(`git diff --cached`), and exits non-zero when the resulting
168+
`aggregate_risk` is at or above `--fail-on` (default `high`) — usable
169+
directly as a pre-commit hook or CI step for a change made outside any
170+
MCP session (a teammate's native editor, a bot PR), which was previously
171+
invisible to CALM entirely. What's still missing: a `calm review-diff
172+
--base origin/main`-style PR-range analysis (`calm guard` only looks at
173+
the staged diff, not an arbitrary commit range — though `diff_impact`
174+
itself already supports `commits`, so this is mostly CLI plumbing away)
175+
and no publishable GitHub Action wrapping `calm guard` for one-line CI
176+
adoption.

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,8 @@ Full technical detail lives in [`docs/architecture.md`](docs/architecture.md)
175175
## Crate layout
176176

177177
- `crates/calm-core/` — the index engine: `tree-sitter` parsing, SQLite schema, the multi-tier resolver (conservative → inferred → formal/Stack-Graphs, SCIP, or LSP), graph algorithms (coreness, hub detection), FTS5/semantic search, analysis (hotspots, coverage, codeowners, diff-impact, dead-code), fitness metrics, gitignore management.
178-
- `crates/calm-server/` — the MCP server (`rmcp` over stdio or a unix-socket daemon), exposing 36 tools plus the incremental file watcher.
179-
- `crates/calm-cli/` — the CLI: `calm init`, `calm index`, `calm serve`, `calm connect`, `calm setup`, `calm fitness-check`, `calm doctor`.
178+
- `crates/calm-server/` — the MCP server (`rmcp` over stdio or a unix-socket daemon), exposing 37 tools plus the incremental file watcher.
179+
- `crates/calm-cli/` — the CLI: `calm init`, `calm index`, `calm serve`, `calm connect`, `calm setup`, `calm fitness-check`, `calm guard`, `calm doctor`.
180180

181181
## CLI reference
182182

@@ -194,12 +194,14 @@ calm setup --project-root . # writes/merges MCP config (.mcp.json/.cursor/
194194
calm fitness-check --project-root . # CI gate, exits 1 on failure
195195
calm fitness-check --project-root . --json # JSON output
196196
calm fitness-check --project-root . --config thresholds.toml # custom thresholds
197+
calm guard --project-root . # pre-commit/CI gate on the staged diff, exits 1 if aggregate risk >= --fail-on (default: high)
198+
calm guard --project-root . --fail-on medium --json # stricter threshold, machine-readable output
197199
calm scip-run --project-root . --lang go # force one SCIP provider to run now, bypassing refresh policy
198200
calm scip-run --project-root . # --lang omitted = run every provider ("rust,go,python,javascript,java,csharp,php,ruby,c")
199201
calm index --project-root . --scip-file build/index.scip --sub-root services/api # ingest a pre-built SCIP index (CI/sandboxed, no external indexer install needed)
200202
```
201203

202-
## 36 MCP tools for AI agents
204+
## 37 MCP tools for AI agents
203205
CLI presets filter tools by workflow phase: `orient`, `trace`, `edit`, `compound`, `full` (default) via `calm serve --preset` or the `preset` field in `config.json` — or compose a custom set from toolset (module) names, e.g. `--preset "trace,security"` or `--preset "full,-edit"` (see AGENTS.md for the full toolset list). Every response carries `suggested_next` to point at the next step — full detail on each tool and the complete workflow lives in [AGENTS.md](AGENTS.md).
204206

205207
| Group | Tools |

0 commit comments

Comments
 (0)