@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.
- package/CHANGELOG.md +81 -0
- package/README.md +5 -3
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/agents.js +17 -16
- package/dist/holmes/cli/doctor.js +19 -7
- package/dist/holmes/cli/mcp-schema-cost.d.ts +18 -0
- package/dist/holmes/cli/mcp-schema-cost.js +28 -0
- package/dist/holmes/config/config.d.ts +8 -0
- package/dist/holmes/config/config.js +1 -1
- package/dist/holmes/governance/constitution.d.ts +26 -0
- package/dist/holmes/governance/constitution.js +33 -0
- package/dist/holmes/governance/ledger-timeline.d.ts +21 -0
- package/dist/holmes/governance/ledger-timeline.js +21 -0
- package/dist/holmes/hooks/stop.d.ts +24 -0
- package/dist/holmes/hooks/stop.js +83 -3
- package/dist/holmes/mcp/elicit-approval.d.ts +9 -0
- package/dist/holmes/mcp/elicit-approval.js +15 -0
- package/dist/holmes/mcp/handlers.d.ts +108 -0
- package/dist/holmes/mcp/handlers.js +239 -3
- package/dist/holmes/mcp/tool-schemas.js +33 -0
- package/dist/holmes/review/mutate.d.ts +17 -0
- package/dist/holmes/review/mutate.js +66 -0
- package/dist/holmes/review/test-outcomes.d.ts +35 -0
- package/dist/holmes/review/test-outcomes.js +108 -0
- package/dist/holmes/review/test-runner.d.ts +30 -0
- package/dist/holmes/review/test-runner.js +71 -5
- package/dist/holmes/spec/approval-status.d.ts +29 -0
- package/dist/holmes/spec/approval-status.js +33 -0
- package/dist/holmes/spec/kills.d.ts +14 -0
- package/dist/holmes/spec/kills.js +28 -0
- package/dist/holmes/spec/spec-store.d.ts +9 -0
- package/dist/holmes/spec/spec-store.js +17 -0
- package/dist/holmes/spec/validator.js +18 -0
- package/dist/holmes/spec/version-conflict.d.ts +21 -0
- package/dist/holmes/spec/version-conflict.js +21 -0
- package/package.json +1 -1
- 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.
|
|
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
|
-
-
|
|
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
|
-
|
|
1
|
+
0d70dec-mtn1jk14
|
|
@@ -107,12 +107,21 @@ const mcpConfig = (packageRoot, specsDir, launcher) => {
|
|
|
107
107
|
* (Claude 배선이 `guardrail` 모드에서 좁은 매처를 쓰는 것과 다른 선택인데, 그쪽은 그 좁힘이
|
|
108
108
|
* 무엇을 뜻하는지 문서화된 모드 선택이고 여기는 기본 배선이기 때문이다.)
|
|
109
109
|
*/
|
|
110
|
-
// A
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
|
|
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
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
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', `이 설치본을 가리키지 않는
|
|
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
|
-
|
|
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.
|