@holmes-lab/holmes-kit 0.8.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/README.md +5 -3
  3. package/dist/.build-id +1 -1
  4. package/dist/holmes/cli/agents.js +17 -16
  5. package/dist/holmes/cli/doctor.js +19 -7
  6. package/dist/holmes/cli/mcp-schema-cost.d.ts +18 -0
  7. package/dist/holmes/cli/mcp-schema-cost.js +28 -0
  8. package/dist/holmes/config/config.d.ts +8 -0
  9. package/dist/holmes/config/config.js +1 -1
  10. package/dist/holmes/governance/constitution.d.ts +26 -0
  11. package/dist/holmes/governance/constitution.js +33 -0
  12. package/dist/holmes/governance/ledger-timeline.d.ts +21 -0
  13. package/dist/holmes/governance/ledger-timeline.js +21 -0
  14. package/dist/holmes/hooks/stop.d.ts +24 -0
  15. package/dist/holmes/hooks/stop.js +83 -3
  16. package/dist/holmes/mcp/elicit-approval.d.ts +9 -0
  17. package/dist/holmes/mcp/elicit-approval.js +15 -0
  18. package/dist/holmes/mcp/handlers.d.ts +108 -0
  19. package/dist/holmes/mcp/handlers.js +239 -3
  20. package/dist/holmes/mcp/tool-schemas.js +33 -0
  21. package/dist/holmes/review/mutate.d.ts +17 -0
  22. package/dist/holmes/review/mutate.js +66 -0
  23. package/dist/holmes/review/test-outcomes.d.ts +35 -0
  24. package/dist/holmes/review/test-outcomes.js +108 -0
  25. package/dist/holmes/review/test-runner.d.ts +30 -0
  26. package/dist/holmes/review/test-runner.js +71 -5
  27. package/dist/holmes/spec/approval-status.d.ts +29 -0
  28. package/dist/holmes/spec/approval-status.js +33 -0
  29. package/dist/holmes/spec/kills.d.ts +14 -0
  30. package/dist/holmes/spec/kills.js +28 -0
  31. package/dist/holmes/spec/spec-store.d.ts +9 -0
  32. package/dist/holmes/spec/spec-store.js +17 -0
  33. package/dist/holmes/spec/validator.js +18 -0
  34. package/dist/holmes/spec/version-conflict.d.ts +21 -0
  35. package/dist/holmes/spec/version-conflict.js +21 -0
  36. package/package.json +1 -1
  37. package/playbooks/tdd-slice/PLAYBOOK.md +82 -0
package/CHANGELOG.md CHANGED
@@ -5,6 +5,87 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
  <!-- @implements A-SPEC-209 -->
8
+ ## [0.10.0] - 2026-09-04
9
+
10
+ Governance UX tools for multi-agent round-trips and observability, a cross-harness approval fix, and
11
+ an Antigravity portability fix. All additive and backward-compatible; existing behaviour is unchanged
12
+ at the default settings.
13
+
14
+ ### Added
15
+
16
+ - **Governance UX tools** (REQ-538): three new read/act MCP tools plus a conflict-report enrichment.
17
+ - **`spec_unseal`** — the inverse of `spec_approve`: return a sealed (approved) spec to an editable
18
+ `draft` in one act, clearing `approved_digest` and `parent_digests`, and record `spec-unsealed`
19
+ in the provenance ledger. Un-sealing withdraws a seal, so it requires the same out-of-band
20
+ `HOLMES_APPROVAL` as retiring a sealed doc (fail-closed), and it refuses when an **approved** spec
21
+ depends on the target (naming the blockers — their `parent_digests` would go stale). Idempotent on
22
+ an already-draft spec. Distinct from `spec_retire` (which withdraws authority to `outdated`);
23
+ un-seal keeps the spec alive and editable.
24
+ - **`approval_status`** *(read-only)* — report a spec's approval/seal state without parsing files:
25
+ `{ sealed, approvedDigest?, status, parents: [{ id, status, sealed, resolved }], blockers }`.
26
+ `sealed` is the same predicate the code gate reads and `blockers` is the exact list `spec_approve`
27
+ would refuse on, so the report cannot drift from the acts it describes.
28
+ - **`ledger_timeline`** *(read-only)* — return the provenance ledger's events in time order
29
+ (`{ ts, kind, actor, summary, inputs }`), optionally narrowed to one spec, so the governance
30
+ history (approved / unsealed / retired / review-needed …) is legible at a glance. Chain-integrity
31
+ fields are projected away.
32
+ - **`spec_approve` conflict detail** — the optimistic-concurrency refusal now also carries a
33
+ structured `conflict: { id, expectedVersion, currentVersion, retry }` (the version this act read,
34
+ the version now on disk, and what to retry). The refusal itself — refuse, write nothing, the edit
35
+ wins — is unchanged; this only exposes what happened.
36
+ - **Cross-harness approval opt-out** (REQ-540): a new server-env switch **`HOLMES_ELICIT=off`** makes
37
+ the server skip the in-client `elicitInput` prompt entirely and fold to the byte-identical
38
+ refuse+enqueue path — the same path clients that do not advertise elicitation already take. With it
39
+ on, **claude/agy/codex converge on one out-of-band decision surface** (`holmes-kit approve`, whose
40
+ default readline input is robust over SSH) instead of each client's own prompt rendering. Env-only
41
+ (a session cannot set it); unset = byte-identical to before. Not an approval bypass — it routes the
42
+ prompt to the queue; approval still comes only from a grant or `HOLMES_APPROVAL`.
43
+
44
+ ### Fixed
45
+
46
+ - **Antigravity hooks survive a package path with spaces** (REQ-539): the `.agents/hooks.json`
47
+ wiring now emits `command: "node"` + `args: ["<abs path>"]` instead of a single
48
+ `node "<quoted path>"` string. agy's hook launcher tokenizes the command on whitespace **without
49
+ honoring quotes**, so a Windows default path (`C:\Program Files\nodejs\…`) split at the space and
50
+ the PreToolUse hook never loaded (the gate silently off). Passing the path as an `args` element —
51
+ the array form `mcp_config.json` already used — sidesteps tokenization on every platform. `doctor`'s
52
+ Antigravity wiring check reads `args[0]` too (older string wirings still handled). *Real Windows/agy
53
+ execution remains unverified in CI; validated by the emitted JSON shape on the dev environment.*
54
+
55
+ Internalized TDD discipline — RED-first evidence enforced by a new constitution article (ART-8),
56
+ not a prompt — plus a doctor transparency check and a BUG-1 authoring fix. All additive and
57
+ backward-compatible; existing behaviour is unchanged at the default settings.
58
+
59
+ ### Added
60
+
61
+ - **Inbuilt TDD, made enforceable** (REQ-534): the superpowers TDD discipline is now a holmes-installed
62
+ skill AND deterministically enforced, not merely advised.
63
+ - **ART-8 RED-first evidence** (new constitution article): a changed A-SPEC must show a recorded
64
+ `red-assertion → green` sequence in the ledger. A `red-error` (a test that could not run — a
65
+ load/import/collection failure) is **not** a valid RED, so "the covering test failed *correctly*"
66
+ is judged mechanically. `test_run` classifies each covered file as `red-assertion | red-error |
67
+ green` and appends per-A-SPEC outcomes; the Stop hook reads them. Ships at
68
+ `guardrail.redFirstEvidence: track` (observe-first, non-blocking) — promote to `strict` per repo,
69
+ `off` disables. Evidence-gated (inert until outcomes are recorded) and jest-only for now.
70
+ - **`holmes-tdd-slice` skill**: restates the Iron Law and Red-Green-Refactor in holmes terms and
71
+ tags each rule with the article that enforces it (`[ART-1]`/`[ART-4]`/`[ART-8]`/honor-system).
72
+ Installed alongside the other recovery skills.
73
+ - **Discriminating power**: a T-SPEC may declare `kills:` (named mutations), and
74
+ `test_run --mutate <tspec>` applies each against the A-SPEC's source, reporting which SURVIVED
75
+ (a coverage gap). Selective and opt-in — never a blanket gate.
76
+ - **doctor `mcp schema cost`** (REQ-535): reports holmes-kit's own advertised MCP schema cost
77
+ (computed live from the tool set, no hardcoded number) and WARNs when `HOLMES_MCP_PROFILE=full`
78
+ re-advertises the hook-enforced gate-duplicate tools; advises client-side deferred loading.
79
+ Advisory only (never FAIL), and it never claims to have detected a client's resident behaviour.
80
+
81
+ ### Fixed
82
+
83
+ - **BUG-1 remainder** (REQ-536): `spec_slice_init` now safe-quotes the `slice` value with `yamlScalar`
84
+ — it was the one raw interpolation among the slice builders, so a `sliceName` containing a colon or
85
+ quote could break the generated A-SPEC's frontmatter YAML. And `spec_approve` on a missing id now
86
+ surfaces any unparseable spec file (with a YAML hint) instead of a bare "not found", closing the
87
+ silent-loss that hid a broken file behind a "not found".
88
+
8
89
  ## [0.8.1] - 2026-09-03
9
90
 
10
91
  Codex hard-enforcement completed and verified on real codex-cli 0.152.1 (GOAL-codex-enforcement),
package/README.md CHANGED
@@ -14,17 +14,19 @@
14
14
 
15
15
  ---
16
16
 
17
- ### 🛡️ Currently Supported Features (v0.8.x Production Features)
17
+ ### 🛡️ Currently Supported Features (v0.9.x Production Features)
18
18
 
19
19
  - 📋 **Requirements & Specification Governance**: Strict **"No Spec, No Code"** enforcement with 4-tier spec chain traceability (`REQ ➔ H-SPEC ➔ A-SPEC ➔ T-SPEC`) and `// @implements A-SPEC-XXX` code anchors (comma-lists and every anchor in a file participate in the gate).
20
- - 🤖 **Autonomous Approval** *(new in 0.8.0)*: with the out-of-band `HOLMES_AUTONOMOUS_APPROVAL` switch on, an agent seals **low/mid-risk** specs itself (ledgered under an `autonomous:<client>` actor) while `gate-behavior` changes, architecture/gate/taint files, and every upstream `REQ`/`H-SPEC`/`C-SPEC` still ask a human through the in-session TUI. The switch is env-only; a session cannot set it (blocked like `HOLMES_ROLE`). Off = byte-identical to before.
20
+ - 🔴 **Inbuilt TDD — RED-first, enforced not asked** *(new in 0.9.0)*: the test-first discipline is a holmes-installed `holmes-tdd-slice` skill **and** a new constitution article **ART-8**. A changed A-SPEC must show a recorded `red-assertion green` sequence in the ledger; a `red-error` (a test that could not run) is not a valid RED, so "the covering test failed *correctly*" is judged mechanically, not on trust. `test_run` classifies each covered file (`red-assertion`/`red-error`/`green`) and records per-A-SPEC outcomes the Stop hook reads. Ships at `redFirstEvidence: track` (observe-first, non-blocking; `strict`/`off` per repo), evidence-gated and jest-only for now. A T-SPEC may also declare `kills:` mutations and `test_run --mutate` reports which SURVIVED (a coverage gap). Where superpowers *asks* for RED-first and discriminating power, holmes-kit *proves* them.
21
+ - 🧰 **Governance UX tools** *(new in 0.10.0)*: `spec_unseal` (the inverse of `spec_approve` — return a sealed spec to editable `draft` in one act, out-of-band approval required, refuses approved dependents), `approval_status` and `ledger_timeline` (read-only observability into a spec's seal state and the governance history), and a structured `conflict` on `spec_approve`'s optimistic-concurrency refusal (read vs. current version + retry). See CHANGELOG for details.
22
+ - 🤖 **Autonomous Approval** *(new in 0.8.0)*: with the out-of-band `HOLMES_AUTONOMOUS_APPROVAL` switch on, an agent seals **low/mid-risk** specs itself (ledgered under an `autonomous:<client>` actor) — while `gate-behavior` changes, architecture/gate/taint files, and every upstream `REQ`/`H-SPEC`/`C-SPEC` still ask a human through the in-session TUI. The switch is env-only; a session cannot set it (blocked like `HOLMES_ROLE`). Off = byte-identical to before. *(new in 0.10.0)* Set **`HOLMES_ELICIT=off`** to skip the in-session prompt entirely and route every decision to the out-of-band `holmes-kit approve` queue instead — one decision surface that behaves identically across Claude Code / Antigravity / Codex.
21
23
  - 🪧 **Session Banner + Update Notice** *(new in 0.8.0)*: every session start emits an English intro (version + governance rule + npm URL) to both the human transcript and the agent context (SessionStart hook + MCP `instructions`); when a newer published version is cached, an install-mode-aware update command is appended. Registry check is detached, fail-silent, and opts out via `HOLMES_NO_UPDATE_CHECK`/`CI`.
22
24
  - 🧱 **Deterministic Gate, Hardened** *(new in 0.8.0)*: shell writes are judged at the segment's **effective working directory** (`cd sub && cat > ../src/x.ts` is sealed, legitimate out-of-tree scratch writes are freed); the governing anchor is the **whole set**, not the first match. Every gate change ships with two consecutive clean adversarial rounds.
23
25
  - 🧠 **3-Tier Semantic Layer** *(new in 0.3.0)*: knowledge-graph semantic search with an explicit consent ladder — `none` (default, **zero egress**), `local` (bge-m3, no egress, optional module), `cloud` (gemini-embedding-001, opt-in via `GEMINI_API_KEY`). Measured on 305 traceability cases: recall 0.486 (lexical) → 0.667 (local) → **0.887 (cloud)**; on lexical-zero requests: 0% → 52% → **92%**. Surfaced only additively — rerank, evidence (`semCos`), and `semanticAlternates` — never as a hard filter.
24
26
  - 🎯 **Graded Impact Surface** *(new in 0.3.0)*: `rankedImpact` (personalized-PageRank over the spec/code graph) beat its pre-registered naive baseline on **both recall and precision across 3 corpora (×1.6–×17)** — the necessary condition for any better-than-a-person phrasing, measured before claimed.
25
27
  - 🐞 **Causal Defect Localization & CPG** *(equalized in 0.5–0.7)*: AST Code Property Graph (CFG/DDG/CDG) & Dataflow Taint reachability across 7 languages (TS/JS, Python, Go, Rust, Java, C/C++, C#) — **42 language×layer cells graded on measured evidence** (11 corpora, 39,344 functions, zero invariant violations; C++ conditional on 67.9% parse coverage, disclosed in the matrix).
26
28
  - 📏 **Measured, Not Claimed** *(new in 0.3.x)*: performance is judged against a pre-registered modeled-human band (R 0.67–0.78 / P ≈0.9±). Current official grade: **band entry on recall; division-of-labor precision 0.727 = 81% of the modeled human — reproduced by an independent context-free judge on a fresh blind window.** No superhuman claims until both metrics exceed the band.
27
- - 🧪 **Self-Healing & Diagnostic Doctor**: Automated integrity checks and self-healing auto-fix remediation (`holmes-kit doctor --fix` & `spec_remediate`) — wiring-handshake checks run on Windows natively as of 0.3.2.
29
+ - 🧪 **Self-Healing & Diagnostic Doctor**: Automated integrity checks and self-healing auto-fix remediation (`holmes-kit doctor --fix` & `spec_remediate`) — wiring-handshake checks run on Windows natively as of 0.3.2. As of 0.9.0, doctor also reports holmes-kit's own advertised **MCP schema token cost** (computed live) and warns when `HOLMES_MCP_PROFILE=full` needlessly re-advertises the hook-enforced gate-duplicate tools.
28
30
  - 🔔 **Approval UX** *(new in 0.3.1)*: in-session approval dialogs forewarn their 120s deadline and, on expiry, the refusal says exactly where the decision went (`npx holmes-kit approve` out-of-band queue) — no more silently dead dialogs.
29
31
  - 🚦 **Push & Server-Side Re-Validation** *(hardened in 0.8.0)*: a local `pre-push` evidence gate (test-run ledger head == push HEAD, green, executed > 0) plus a **server-side CI workflow** that re-runs `npm ci → build → full suite → tarball install probe`, so a `--no-verify` push or a hook-less clone is still caught.
30
32
  - 📊 **Automated RTM & Taint Heatmap**: Interactive standalone HTML/SVG report generation (`generateRtmHeatmap`) for spec coverage and security dataflow reachability.
package/dist/.build-id CHANGED
@@ -1 +1 @@
1
- a14dfa1-mtkqq7jw
1
+ 0d70dec-mtn1jk14
@@ -107,12 +107,21 @@ const mcpConfig = (packageRoot, specsDir, launcher) => {
107
107
  * (Claude 배선이 `guardrail` 모드에서 좁은 매처를 쓰는 것과 다른 선택인데, 그쪽은 그 좁힘이
108
108
  * 무엇을 뜻하는지 문서화된 모드 선택이고 여기는 기본 배선이기 때문이다.)
109
109
  */
110
- // A hook `command` is ONE shell string, so the path must be quoted — `buildHookPlan` in init.ts has
111
- // always done this for the Claude wiring, and this one did not. Measured 2026-08-23 on a machine
112
- // whose home directory contains a space: the shell split the path, the hook never ran, and the
113
- // Antigravity gate was silently off. (The MCP config above is different: it passes `args` as an
114
- // array, where quoting would put literal quote characters into the path.)
115
- const hookCommand = (packageRoot, script) => `node "${path.join(packageRoot, 'bin', script)}"`;
110
+ // @implements A-SPEC-539.1
111
+ // agy's hook launcher tokenizes the `command` string on whitespace and does NOT honor quotes, so an
112
+ // earlier quoted `node "<path>"` (A-SPEC-193 §2b-2) still broke on a path with spaces: measured on a
113
+ // Windows machine whose Node lives at `C:\Program Files\nodejs`, reported from agy 2026-09-04 the
114
+ // launcher split `node "C:\Program Files\...` at the space, node took `"C:\Program` for a module
115
+ // (MODULE_NOT_FOUND), and the gate loaded not at all (silently off). Passing the script path as an
116
+ // `args` ELEMENT — the same array form mcp_config already uses — sidesteps tokenization entirely, on
117
+ // every platform. This supersedes the quoted-string form for the antigravity wiring; quoting only
118
+ // ever helped launchers that honor quotes, which agy's does not.
119
+ const antigravityHookEntry = (packageRoot, script) => ({
120
+ type: 'command',
121
+ command: 'node',
122
+ args: [path.join(packageRoot, 'bin', script)],
123
+ timeout: 30,
124
+ });
116
125
  /**
117
126
  * @implements A-SPEC-442
118
127
  * Codex 훅이 부를 것: **Claude 와 같은 정책 커널**.
@@ -128,17 +137,9 @@ const hooksJson = (packageRoot) => `${JSON.stringify({
128
137
  'holmes-kit': {
129
138
  PreToolUse: [{
130
139
  matcher: '*',
131
- hooks: [{
132
- type: 'command',
133
- command: hookCommand(packageRoot, 'holmes-hook-antigravity.js'),
134
- timeout: 30,
135
- }],
136
- }],
137
- Stop: [{
138
- type: 'command',
139
- command: hookCommand(packageRoot, 'holmes-stop-antigravity.js'),
140
- timeout: 30,
140
+ hooks: [antigravityHookEntry(packageRoot, 'holmes-hook-antigravity.js')],
141
141
  }],
142
+ Stop: [antigravityHookEntry(packageRoot, 'holmes-stop-antigravity.js')],
142
143
  },
143
144
  }, null, 2)}\n`;
144
145
  const AGENTS_MD = (enforced) => `# Holmes-Kit — Workspace Operational Discipline
@@ -63,6 +63,8 @@ const mcp_version_1 = require("./mcp-version");
63
63
  const codex_toml_1 = require("./codex-toml");
64
64
  const agents_1 = require("./agents");
65
65
  const mcp_launcher_1 = require("./mcp-launcher");
66
+ const tool_schemas_1 = require("../mcp/tool-schemas");
67
+ const mcp_schema_cost_1 = require("./mcp-schema-cost");
66
68
  /**
67
69
  * Checks if a script path belongs to the packageRoot, supporting symlinked global installs.
68
70
  */
@@ -443,6 +445,16 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
443
445
  else {
444
446
  add('ledger signing', 'WARN', '원장이 서명되지 않습니다(HOLMES_LEDGER_KEY 없음) — 체인이 평문 해시라 파일을 쓸 수 있는 자가 승인 기록도 만들 수 있고, ART-5 의 원장 대조는 장벽이 아니라 비용이 됩니다', 'HOLMES_LEDGER_KEY 를 대역외(에이전트를 기동하는 사람의 환경)에서 설정한 뒤 서버를 다시 시작하십시오. 값은 이 보고서에 절대 출력되지 않습니다.');
445
447
  }
448
+ // @implements A-SPEC-535.1
449
+ // Advisory: holmes-kit's OWN advertised MCP schema cost, and the one holmes-controlled
450
+ // misconfiguration (HOLMES_MCP_PROFILE=full re-advertising hook-enforced gate-duplicates). Never
451
+ // FAIL — efficiency, not correctness. The number is derived from TOOL_SCHEMAS at runtime (no drift),
452
+ // and the resident-client cost is stated conditionally: a static checker cannot observe whether the
453
+ // client keeps schemas resident, so it never claims to have detected it.
454
+ {
455
+ const sc = (0, mcp_schema_cost_1.mcpSchemaCost)(tool_schemas_1.TOOL_SCHEMAS, tool_schemas_1.HOOK_ENFORCED_TOOLS, process.env.HOLMES_MCP_PROFILE);
456
+ add('mcp schema cost', sc.level, sc.detail, sc.fix);
457
+ }
446
458
  // @implements A-SPEC-457
447
459
  // Whether the ART-2 truncation backstop (A-SPEC-455) is even armed. That backstop compares the
448
460
  // committed ledger to the working copy and enumerates targets with `git ls-files .ax/ledger`; an
@@ -798,17 +810,17 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
798
810
  const h = JSON.parse(fs.readFileSync(agyHooks, 'utf8'));
799
811
  const entry = h['holmes-kit'];
800
812
  const pre = entry?.PreToolUse?.[0];
801
- const preCmd = pre?.hooks?.[0]?.command ?? '';
802
- const stopCmd = entry?.Stop?.[0]?.command ?? '';
803
- const stale = [preCmd, stopCmd].filter((c) => {
804
- const p2 = (0, settings_merge_1.hookScriptPath)(c);
805
- return !resolvesToPackage(p2, packageRoot);
806
- });
813
+ // @implements A-SPEC-539.1 — the hook script path now lives in args[0] (command is bare
814
+ // 'node'); older wirings joined it into the command string. Read args first, fall back to
815
+ // parsing the string so a pre-539 hooks.json is still judged correctly.
816
+ const scriptOf = (e) => (Array.isArray(e?.args) && typeof e?.args[0] === 'string') ? e.args[0] : (0, settings_merge_1.hookScriptPath)(e?.command ?? '');
817
+ const stale = [scriptOf(pre?.hooks?.[0]), scriptOf(entry?.Stop?.[0])]
818
+ .filter((p2) => !resolvesToPackage(p2, packageRoot));
807
819
  if (entry === undefined) {
808
820
  add('antigravity wiring', 'WARN', `${agyHooks} 에 holmes-kit 항목이 없습니다`, 'holmes-kit init --target <dir> --agent antigravity');
809
821
  }
810
822
  else if (stale.length > 0) {
811
- add('antigravity wiring', 'FAIL', `이 설치본을 가리키지 않는 명령: ${stale.join(' | ')}`, 'holmes-kit init --target <dir> --agent antigravity --force 로 절대 경로를 갱신하십시오.');
823
+ add('antigravity wiring', 'FAIL', `이 설치본을 가리키지 않는 경로: ${stale.join(' | ')}`, 'holmes-kit init --target <dir> --agent antigravity --force 로 절대 경로를 갱신하십시오.');
812
824
  }
813
825
  else if ((pre?.matcher ?? '') !== '*') {
814
826
  // 매처가 좁으면 그 밖의 도구가 게이트를 지나간다 — 조용한 구멍이므로 말한다.
@@ -0,0 +1,18 @@
1
+ /**
2
+ * doctor advisory (REQ-535): report holmes-kit's OWN advertised MCP schema cost and flag the one
3
+ * holmes-controlled misconfiguration (HOLMES_MCP_PROFILE=full re-advertising hook-enforced
4
+ * gate-duplicates). Pure — no I/O. Never FAIL (efficiency, not correctness). Never claims to have
5
+ * detected that the client keeps schemas resident (a static checker cannot observe that); the
6
+ * resident-client cost is stated conditionally.
7
+ */
8
+ export interface SchemaCostResult {
9
+ level: 'PASS' | 'WARN';
10
+ detail: string;
11
+ fix?: string;
12
+ advertised: number;
13
+ tokens: number;
14
+ }
15
+ export declare function mcpSchemaCost(schemas: Record<string, {
16
+ description?: string;
17
+ inputSchema?: unknown;
18
+ }>, hidden: Set<string>, profileEnv: string | undefined): SchemaCostResult;
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mcpSchemaCost = mcpSchemaCost;
4
+ function mcpSchemaCost(schemas, hidden, profileEnv) {
5
+ const jsonLen = (n) => JSON.stringify({ name: n, description: schemas[n]?.description, inputSchema: schemas[n]?.inputSchema }).length;
6
+ const names = Object.keys(schemas);
7
+ const advertisedNames = names.filter((n) => !hidden.has(n));
8
+ const hiddenNames = names.filter((n) => hidden.has(n));
9
+ const est = (ns) => Math.round(ns.reduce((s, n) => s + jsonLen(n), 0) / 4);
10
+ const tokens = est(advertisedNames);
11
+ const advertised = advertisedNames.length;
12
+ if (profileEnv === 'full' && hiddenNames.length > 0) {
13
+ return {
14
+ level: 'WARN',
15
+ detail: `HOLMES_MCP_PROFILE=full re-advertises ${hiddenNames.length} hook-enforced gate-duplicate tool(s) (${hiddenNames.join(', ')}) — about +${est(hiddenNames)} tokens/turn on a client that keeps MCP schemas resident. They are already enforced deterministically by the Stop/PreToolUse hooks and stay callable by name.`,
16
+ fix: 'Unset HOLMES_MCP_PROFILE unless you specifically need to invoke these tools by name.',
17
+ advertised, tokens,
18
+ };
19
+ }
20
+ return {
21
+ level: 'PASS',
22
+ detail: `holmes-kit advertises ${advertised} MCP tool(s) ≈ ${tokens} tokens in the default profile`
23
+ + `${hiddenNames.length ? ` (${hiddenNames.length} gate-duplicate tool(s) already hidden)` : ''}. `
24
+ + `If your MCP client keeps tool schemas resident, this is re-sent each turn — a fixed cost that shrinks as the session grows.`,
25
+ fix: 'If your MCP client supports on-demand/deferred tool loading, enabling it removes this overhead with no loss (tools load when searched).',
26
+ advertised, tokens,
27
+ };
28
+ }
@@ -12,6 +12,14 @@ export interface AxConfig {
12
12
  * silently impose a new one.
13
13
  */
14
14
  preEditEvidence: 'off' | 'warn' | 'block';
15
+ /**
16
+ * @implements A-SPEC-534.3
17
+ * RED-first evidence enforcement (ART-8). `off` skips the check; `track` records violations
18
+ * without blocking a turn (observe-first, the ship default); `strict` blocks like any other
19
+ * article. Default `track` — unlike `preEditEvidence`, `track` never blocks, so observing
20
+ * RED-first across the repo is safe before an owner promotes it to `strict`.
21
+ */
22
+ redFirstEvidence: 'off' | 'track' | 'strict';
15
23
  };
16
24
  highRiskDomains: string[];
17
25
  storage: {
@@ -47,7 +47,7 @@ exports.DEFAULT_CONFIG = {
47
47
  // first refusal is `no-analysis`. Warn reports the missing evidence without stopping work, which
48
48
  // is also the only path that accumulates the data a later block decision would need. Raised as an
49
49
  // authority question rather than decided by measurement — the user chose this level.
50
- guardrail: { enforcement: 'block', enforceHighRisk: true, overrideRequiresAdr: true, preEditEvidence: 'warn' },
50
+ guardrail: { enforcement: 'block', enforceHighRisk: true, overrideRequiresAdr: true, preEditEvidence: 'warn', redFirstEvidence: 'track' },
51
51
  highRiskDomains: ['@auth', '@payment'],
52
52
  storage: { specStore: 'local-markdown' },
53
53
  };
@@ -1,4 +1,5 @@
1
1
  import { Spec } from '../spec/spec-parser';
2
+ import type { TestOutcome } from '../review/test-runner';
2
3
  /**
3
4
  * L1 — Governance CONSTITUTION (target-architecture §7 L1).
4
5
  *
@@ -56,6 +57,31 @@ export interface ConstitutionContext {
56
57
  * record cannot hide a missing test suite because the syntactic bound is still enforced.
57
58
  */
58
59
  executedByAspec?: Record<string, number>;
60
+ /**
61
+ * @implements A-SPEC-534.2
62
+ * RED-first evidence (ART-8). `changedAspecs` are the A-SPECs whose source changed in this work
63
+ * unit; `outcomesByAspec` are the recorded per-A-SPEC test outcomes (from the ledger). ART-8 fires
64
+ * only in `strict` mode here — `track`/`off` are the caller's report-only / ignore concerns (the
65
+ * config read and the track-mode recording are I/O, done by the Stop hook). Evidence-gated like
66
+ * ART-4: an A-SPEC with no recorded outcomes is not a violation.
67
+ */
68
+ redFirstMode?: 'strict' | 'track' | 'off';
69
+ changedAspecs?: string[];
70
+ outcomesByAspec?: Record<string, Array<{
71
+ outcome: TestOutcome;
72
+ ts: string;
73
+ }>>;
59
74
  }
75
+ /**
76
+ * @implements A-SPEC-534.2
77
+ * ART-8 RED-first check (pure). For each changed A-SPEC that HAS recorded outcomes, require a
78
+ * `red-assertion` at ts_red followed by a `green` at ts_green > ts_red. A `red-error` is NOT a valid
79
+ * RED (a file that could not run its cases proves nothing). An A-SPEC with no outcomes is skipped
80
+ * (evidence-gated).
81
+ */
82
+ export declare function redFirstViolations(changedAspecs: string[], outcomesByAspec: Record<string, Array<{
83
+ outcome: TestOutcome;
84
+ ts: string;
85
+ }>>): ConstitutionViolation[];
60
86
  export declare const ARTICLES: Record<string, string>;
61
87
  export declare function verifyConstitution(ctx: ConstitutionContext): ConstitutionViolation[];
@@ -1,11 +1,36 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ARTICLES = void 0;
4
+ exports.redFirstViolations = redFirstViolations;
4
5
  exports.verifyConstitution = verifyConstitution;
5
6
  const basis_1 = require("../mcp/basis");
6
7
  const spec_types_1 = require("../spec/spec-types");
7
8
  const rtm_check_1 = require("../rtm/rtm-check");
8
9
  const validator_1 = require("../spec/validator");
10
+ /**
11
+ * @implements A-SPEC-534.2
12
+ * ART-8 RED-first check (pure). For each changed A-SPEC that HAS recorded outcomes, require a
13
+ * `red-assertion` at ts_red followed by a `green` at ts_green > ts_red. A `red-error` is NOT a valid
14
+ * RED (a file that could not run its cases proves nothing). An A-SPEC with no outcomes is skipped
15
+ * (evidence-gated).
16
+ */
17
+ function redFirstViolations(changedAspecs, outcomesByAspec) {
18
+ const v = [];
19
+ for (const aspec of changedAspecs) {
20
+ const outcomes = outcomesByAspec[aspec];
21
+ if (!outcomes || outcomes.length === 0)
22
+ continue; // evidence-gated: no record → no violation
23
+ // The EARLIEST red-assertion anchors the ordering; a green must come after it. A red-error is
24
+ // deliberately excluded — it means the cases never ran, so it cannot stand in for a real RED.
25
+ const reds = outcomes.filter((o) => o.outcome === 'red-assertion').map((o) => o.ts).sort();
26
+ const tsRed = reds[0];
27
+ const greenAfterRed = tsRed !== undefined && outcomes.some((o) => o.outcome === 'green' && o.ts > tsRed);
28
+ if (!greenAfterRed) {
29
+ v.push({ article: 'ART-8', detail: `${aspec}: source changed but no recorded red-assertion→green sequence — a covering test that never failed first proves nothing; make it fail (assertion, not error) before the code (ART-8)` });
30
+ }
31
+ }
32
+ return v;
33
+ }
9
34
  exports.ARTICLES = {
10
35
  'ART-7': '열린 치명 발견은 미완이다 — severity critical 이 open 인 한 완료가 없다 (important/minor 는 review_status 보고에만 남는다: 기록을 피하게 만드는 차단은 원장을 죽인다)',
11
36
  'ART-6': '판단은 현재 빌드에서 내려야 한다 — 설치된 코드와 갈라진 서버에서 봉인된 리뷰 결과는 이미 사라진 세계를 기술한다',
@@ -14,6 +39,7 @@ exports.ARTICLES = {
14
39
  'ART-3': 'Spec validity — every governed spec satisfies its type rules, including honest 4-quadrant GWT coverage',
15
40
  'ART-4': 'Coverage honesty — declared coverage must be backed by real anchored test cases, not prose',
16
41
  'ART-5': 'Approval is out-of-band — a spec cannot self-approve; governance config cannot be self-written',
42
+ 'ART-8': 'Test-first is observed — a changed A-SPEC must show a recorded red-assertion→green sequence before it is done (a red-error is not a valid RED)',
17
43
  };
18
44
  function verifyConstitution(ctx) {
19
45
  const governed = (0, spec_types_1.filterGoverned)(ctx.specs);
@@ -67,6 +93,13 @@ function verifyConstitution(ctx) {
67
93
  }
68
94
  }
69
95
  }
96
+ // @implements A-SPEC-534.2
97
+ // ART-8: RED-first evidence. Only `strict` mode emits a BLOCKING violation here; `track` (report
98
+ // only) and `off` are the caller's I/O concern (the config read + track-mode ledger recording live
99
+ // in the Stop hook, 534.3). Evidence-gated inside redFirstViolations.
100
+ if (ctx.redFirstMode === 'strict' && ctx.changedAspecs && ctx.outcomesByAspec) {
101
+ v.push(...redFirstViolations(ctx.changedAspecs, ctx.outcomesByAspec));
102
+ }
70
103
  // @implements A-SPEC-160
71
104
  // ART-6: a judgement sealed while the answering server ran code that no longer matched what was
72
105
  // installed. Measured 2026-08-08: such a server reported `impactedSpecs: []` for a commit touching
@@ -0,0 +1,21 @@
1
+ import { ProvenanceEvent } from './provenance-chain';
2
+ /** One governance event, projected to what a timeline shows — chain-integrity fields dropped. */
3
+ export interface TimelineEntry {
4
+ ts: string;
5
+ kind: string;
6
+ actor: string;
7
+ summary: string;
8
+ inputs: string[];
9
+ }
10
+ /** The subset of a ledger event this projection reads (structurally typed so callers can pass
11
+ * FileLedgerStore.loadAll() results directly). */
12
+ type TimelineSource = Pick<ProvenanceEvent, 'ts' | 'kind' | 'actor' | 'summary'> & Partial<Pick<ProvenanceEvent, 'inputs' | 'seq'>>;
13
+ /**
14
+ * @implements A-SPEC-538.3
15
+ * Pure: order ledger events into a timeline, optionally narrowed to one spec. Ascending by `ts`
16
+ * (ISO strings sort lexically), tiebroken by `seq` so same-timestamp events keep chain order. When
17
+ * `id` is given, only events whose `inputs` reference it survive. Chain-integrity fields
18
+ * (hash/prevHash/seq/replicaId) are plumbing, not timeline content, and are projected away.
19
+ */
20
+ export declare function timelineFrom(events: TimelineSource[], id?: string): TimelineEntry[];
21
+ export {};
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.timelineFrom = timelineFrom;
4
+ /**
5
+ * @implements A-SPEC-538.3
6
+ * Pure: order ledger events into a timeline, optionally narrowed to one spec. Ascending by `ts`
7
+ * (ISO strings sort lexically), tiebroken by `seq` so same-timestamp events keep chain order. When
8
+ * `id` is given, only events whose `inputs` reference it survive. Chain-integrity fields
9
+ * (hash/prevHash/seq/replicaId) are plumbing, not timeline content, and are projected away.
10
+ */
11
+ function timelineFrom(events, id) {
12
+ const selected = id ? events.filter((e) => (e.inputs ?? []).includes(id)) : events.slice();
13
+ selected.sort((a, b) => (a.ts < b.ts ? -1 : a.ts > b.ts ? 1 : (a.seq ?? 0) - (b.seq ?? 0)));
14
+ return selected.map((e) => ({
15
+ ts: e.ts,
16
+ kind: e.kind,
17
+ actor: e.actor,
18
+ summary: e.summary,
19
+ inputs: e.inputs ?? [],
20
+ }));
21
+ }
@@ -1,5 +1,6 @@
1
1
  import { PendingRequest } from '../governance/approval-queue';
2
2
  import { Spec } from '../spec/spec-parser';
3
+ import type { TestOutcome } from '../review/test-runner';
3
4
  /**
4
5
  * @implements A-SPEC-100.2
5
6
  * Stop-hook governance gate (Phase-2 #1: push, not pull).
@@ -81,7 +82,26 @@ export interface StopEvidence {
81
82
  * catches it already existed inside `rechainLedger` and ran from the CLI only.
82
83
  */
83
84
  rolledBackLedgers?: string[];
85
+ /**
86
+ * @implements A-SPEC-534.4
87
+ * ART-8 RED-first evidence. `changedAspecs` are the A-SPECs whose source is dirty this turn;
88
+ * `outcomesByAspec` are their recorded outcomes at the current baseline HEAD; `redFirstMode` is the
89
+ * config posture. `strict` blocks via the constitution; `track` records to `tracked` without
90
+ * blocking; `off`/absent does nothing.
91
+ */
92
+ changedAspecs?: string[];
93
+ outcomesByAspec?: Record<string, Array<{
94
+ outcome: TestOutcome;
95
+ ts: string;
96
+ }>>;
97
+ redFirstMode?: 'strict' | 'track' | 'off';
84
98
  }
99
+ /**
100
+ * @implements A-SPEC-534.4
101
+ * ART-8 evidence (I/O half): the A-SPECs whose DIRTY source files carry an @implements anchor. git is
102
+ * a refinement — no repository means `undefined` (no signal), never a false clean.
103
+ */
104
+ export declare function changedAnchoredAspecs(root: string): string[] | undefined;
85
105
  /**
86
106
  * @implements A-SPEC-452
87
107
  * ART-1 evidence: which changed source files claim nothing.
@@ -127,6 +147,10 @@ export declare function evaluateStop(specs: Spec[], evidence?: StopEvidence): {
127
147
  article: string;
128
148
  detail: string;
129
149
  }[];
150
+ tracked?: {
151
+ article: string;
152
+ detail: string;
153
+ }[];
130
154
  };
131
155
  /**
132
156
  * @implements A-SPEC-134
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.MAX_CONSECUTIVE_BLOCKS = void 0;
37
+ exports.changedAnchoredAspecs = changedAnchoredAspecs;
37
38
  exports.unanchoredChangedSources = unanchoredChangedSources;
38
39
  exports.unrecordedApprovals = unrecordedApprovals;
39
40
  exports.rolledBackLedgers = rolledBackLedgers;
@@ -56,10 +57,50 @@ const test_scope_1 = require("../rtm/test-scope");
56
57
  const constitution_1 = require("../governance/constitution");
57
58
  const provenance_chain_1 = require("../governance/provenance-chain");
58
59
  const test_evidence_1 = require("../review/test-evidence");
60
+ const test_outcomes_1 = require("../review/test-outcomes");
61
+ const config_1 = require("../config/config");
59
62
  const pre_tool_use_1 = require("./pre-tool-use");
60
63
  const governance_history_1 = require("../guardrail/governance-history");
61
64
  const constitution_debt_1 = require("../governance/constitution-debt");
62
65
  const root_1 = require("../project/root");
66
+ /**
67
+ * @implements A-SPEC-534.4
68
+ * ART-8 evidence (I/O half): the A-SPECs whose DIRTY source files carry an @implements anchor. git is
69
+ * a refinement — no repository means `undefined` (no signal), never a false clean.
70
+ */
71
+ function changedAnchoredAspecs(root) {
72
+ const SOURCE = /\.(?:ts|tsx|mts|cts|js|jsx|mjs|cjs|py|go|rs|java|kt|cs|rb|php|swift)$/;
73
+ const VENDORED = /^(?:reference|node_modules|dist|build|vendor|third_party)\//;
74
+ let raw;
75
+ try {
76
+ raw = (0, node_child_process_1.execFileSync)('git', ['status', '--porcelain', '-uall'], {
77
+ cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], env: (0, root_1.cleanSubprocessEnv)(),
78
+ });
79
+ }
80
+ catch {
81
+ return undefined;
82
+ }
83
+ const ids = new Set();
84
+ for (const line of raw.split('\n')) {
85
+ if (line.trim() === '')
86
+ continue;
87
+ let rel = line.slice(3).trim().replace(/^"|"$/g, '');
88
+ if (rel.includes(' -> '))
89
+ rel = rel.split(' -> ')[1]; // renames name the destination
90
+ if (!SOURCE.test(rel) || VENDORED.test(rel))
91
+ continue;
92
+ let text;
93
+ try {
94
+ text = fs.readFileSync(path.join(root, rel), 'utf8');
95
+ }
96
+ catch {
97
+ continue;
98
+ } // deleted/unreadable
99
+ for (const m of text.matchAll(/@implements\s+(A-SPEC-\d+(?:\.\d+)?)/g))
100
+ ids.add(m[1]);
101
+ }
102
+ return [...ids].sort();
103
+ }
63
104
  /**
64
105
  * @implements A-SPEC-452
65
106
  * ART-1 evidence: which changed source files claim nothing.
@@ -248,7 +289,21 @@ function evaluateStop(specs, evidence) {
248
289
  // L1: the Stop gate IS the constitution's re-verification point — every turn boundary re-runs the
249
290
  // inviolable articles (ART-2 RTM, ART-3 validity incl. 4-quadrant GWT, ART-4 coverage evidence).
250
291
  // The articles live in ONE place (governance/constitution.ts); this gate merely executes them.
251
- const violations = (0, constitution_1.verifyConstitution)({ specs, testCasesByAspec: evidence?.testCasesByAspec, executedByAspec: evidence?.executedByAspec, findings: evidence?.findings });
292
+ // @implements A-SPEC-534.4 ART-8 evidence rides through to the constitution, which emits a
293
+ // BLOCKING ART-8 violation only in `strict` mode. `track`/`off` produce none here.
294
+ const violations = (0, constitution_1.verifyConstitution)({
295
+ specs, testCasesByAspec: evidence?.testCasesByAspec, executedByAspec: evidence?.executedByAspec, findings: evidence?.findings,
296
+ redFirstMode: evidence?.redFirstMode, changedAspecs: evidence?.changedAspecs, outcomesByAspec: evidence?.outcomesByAspec,
297
+ });
298
+ // @implements A-SPEC-534.4 — `track` records ART-8 findings without blocking the turn. Computed
299
+ // separately (the constitution stays silent on ART-8 outside strict) and returned in `tracked` for
300
+ // the CLI to record; it never enters `problems`/the block decision.
301
+ let tracked;
302
+ if (evidence?.redFirstMode === 'track' && evidence.changedAspecs && evidence.outcomesByAspec) {
303
+ const t = (0, constitution_1.redFirstViolations)(evidence.changedAspecs, evidence.outcomesByAspec);
304
+ if (t.length)
305
+ tracked = t;
306
+ }
252
307
  const problems = violations.map((x) => `[${x.article}] ${x.detail}`);
253
308
  // @implements A-SPEC-247 — structured list so the caller can ask acknowledgeStop which of these
254
309
  // are waiting on an owner. Mirrors `problems` exactly, including the two synthesized below.
@@ -287,7 +342,7 @@ function evaluateStop(specs, evidence) {
287
342
  structured.push({ article: 'ART-2', detail });
288
343
  }
289
344
  if (problems.length === 0)
290
- return { block: false };
345
+ return { block: false, ...(tracked ? { tracked } : {}) };
291
346
  const shown = problems.slice(0, 20);
292
347
  const more = problems.length > shown.length ? `\n…and ${problems.length - shown.length} more` : '';
293
348
  // @implements A-SPEC-134 — the distinct articles feed the constitution-debt state on a cap-yield.
@@ -301,6 +356,7 @@ function evaluateStop(specs, evidence) {
301
356
  block: true,
302
357
  articles,
303
358
  violations: structured,
359
+ ...(tracked ? { tracked } : {}),
304
360
  reason: `[Holmes-Kit] constitution gate: ${problems.length} article violation(s) must be ` +
305
361
  `fixed before finishing:\n${shown.join('\n')}${more}`,
306
362
  };
@@ -656,6 +712,25 @@ if (require.main === module) {
656
712
  catch {
657
713
  executedByAspec = undefined;
658
714
  }
715
+ // @implements A-SPEC-534.4 — ART-8 RED-first evidence (I/O half). Outcomes recorded at the
716
+ // current baseline HEAD (where the dirty work's red+green ran); changed anchored A-SPECs from
717
+ // the working tree; the posture from config. Fail-open: any error leaves ART-8 inert this turn.
718
+ let redFirstMode;
719
+ let changedAspecs;
720
+ let outcomesByAspec;
721
+ try {
722
+ redFirstMode = (0, config_1.loadConfig)(stopProjectRoot()).guardrail.redFirstEvidence;
723
+ if (redFirstMode !== 'off') {
724
+ const head = (0, node_child_process_1.execFileSync)('git', ['rev-parse', 'HEAD'], { cwd: stopProjectRoot(), stdio: ['ignore', 'pipe', 'ignore'], env: (0, root_1.cleanSubprocessEnv)() }).toString().trim();
725
+ changedAspecs = changedAnchoredAspecs(stopProjectRoot());
726
+ outcomesByAspec = (0, test_outcomes_1.groupOutcomesByAspec)((0, test_outcomes_1.readOutcomes)(stopProjectRoot()), head);
727
+ }
728
+ }
729
+ catch {
730
+ redFirstMode = undefined;
731
+ changedAspecs = undefined;
732
+ outcomesByAspec = undefined;
733
+ }
659
734
  // Provenance-chain verification (fail-open: a verify error skips the check, never crashes).
660
735
  let provenance;
661
736
  // @implements A-SPEC-148
@@ -690,7 +765,12 @@ if (require.main === module) {
690
765
  const unrecorded = unrecordedApprovals(stopProjectRoot());
691
766
  // @implements A-SPEC-455
692
767
  const rolledBack = rolledBackLedgers(stopProjectRoot());
693
- let out = evaluateStop(specs, { testCasesByAspec, provenance, executedByAspec, findings, findingsUnreadable, unanchoredChangedSources: unanchored, unrecordedApprovals: unrecorded, rolledBackLedgers: rolledBack });
768
+ let out = evaluateStop(specs, { testCasesByAspec, provenance, executedByAspec, findings, findingsUnreadable, unanchoredChangedSources: unanchored, unrecordedApprovals: unrecorded, rolledBackLedgers: rolledBack, redFirstMode, changedAspecs, outcomesByAspec });
769
+ // @implements A-SPEC-534.4 — track mode records ART-8 findings without blocking: surface them so
770
+ // the operator observes RED-first gaps before an owner promotes the posture to strict.
771
+ if (out.tracked && out.tracked.length > 0) {
772
+ process.stderr.write(`[Holmes-Kit] ART-8 RED-first (track): ${out.tracked.map((t) => t.detail).join(' | ')}\n`);
773
+ }
694
774
  // @implements A-SPEC-247 — before deciding to re-block, ask whether every unresolved debt is
695
775
  // already queued for the owner. If so, tell the user ONCE and let the turn finish; a single
696
776
  // non-waiting violation and we block exactly as before.