@holmes-lab/holmes-kit 0.7.1 → 0.9.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 +102 -0
- package/README.md +10 -6
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/agents.js +21 -11
- package/dist/holmes/cli/doctor.js +25 -2
- package/dist/holmes/cli/init.js +3 -0
- package/dist/holmes/cli/mcp-schema-cost.d.ts +18 -0
- package/dist/holmes/cli/mcp-schema-cost.js +28 -0
- package/dist/holmes/cli/settings-merge.d.ts +1 -0
- package/dist/holmes/cli/settings-merge.js +6 -1
- package/dist/holmes/config/config.d.ts +8 -0
- package/dist/holmes/config/config.js +1 -1
- package/dist/holmes/governance/autonomy.d.ts +14 -0
- package/dist/holmes/governance/autonomy.js +75 -0
- package/dist/holmes/governance/constitution.d.ts +26 -0
- package/dist/holmes/governance/constitution.js +33 -0
- package/dist/holmes/guardrail/write-target.d.ts +25 -0
- package/dist/holmes/guardrail/write-target.js +143 -0
- package/dist/holmes/hooks/pre-tool-use.js +131 -48
- package/dist/holmes/hooks/session-start.d.ts +23 -0
- package/dist/holmes/hooks/session-start.js +111 -0
- package/dist/holmes/hooks/stop.d.ts +24 -0
- package/dist/holmes/hooks/stop.js +83 -3
- package/dist/holmes/mcp/handlers.d.ts +19 -0
- package/dist/holmes/mcp/handlers.js +126 -21
- package/dist/holmes/mcp/server-instructions.d.ts +8 -0
- package/dist/holmes/mcp/server-instructions.js +13 -0
- package/dist/holmes/mcp/server.js +21 -1
- package/dist/holmes/mcp/tool-schemas.js +1 -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/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/update/update-notice.d.ts +28 -0
- package/dist/holmes/update/update-notice.js +131 -0
- package/package.json +1 -1
- package/playbooks/tdd-slice/PLAYBOOK.md +82 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,108 @@ 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.9.0] - 2026-09-04
|
|
9
|
+
|
|
10
|
+
Internalized TDD discipline — RED-first evidence enforced by a new constitution article (ART-8),
|
|
11
|
+
not a prompt — plus a doctor transparency check and a BUG-1 authoring fix. All additive and
|
|
12
|
+
backward-compatible; existing behaviour is unchanged at the default settings.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **Inbuilt TDD, made enforceable** (REQ-534): the superpowers TDD discipline is now a holmes-installed
|
|
17
|
+
skill AND deterministically enforced, not merely advised.
|
|
18
|
+
- **ART-8 RED-first evidence** (new constitution article): a changed A-SPEC must show a recorded
|
|
19
|
+
`red-assertion → green` sequence in the ledger. A `red-error` (a test that could not run — a
|
|
20
|
+
load/import/collection failure) is **not** a valid RED, so "the covering test failed *correctly*"
|
|
21
|
+
is judged mechanically. `test_run` classifies each covered file as `red-assertion | red-error |
|
|
22
|
+
green` and appends per-A-SPEC outcomes; the Stop hook reads them. Ships at
|
|
23
|
+
`guardrail.redFirstEvidence: track` (observe-first, non-blocking) — promote to `strict` per repo,
|
|
24
|
+
`off` disables. Evidence-gated (inert until outcomes are recorded) and jest-only for now.
|
|
25
|
+
- **`holmes-tdd-slice` skill**: restates the Iron Law and Red-Green-Refactor in holmes terms and
|
|
26
|
+
tags each rule with the article that enforces it (`[ART-1]`/`[ART-4]`/`[ART-8]`/honor-system).
|
|
27
|
+
Installed alongside the other recovery skills.
|
|
28
|
+
- **Discriminating power**: a T-SPEC may declare `kills:` (named mutations), and
|
|
29
|
+
`test_run --mutate <tspec>` applies each against the A-SPEC's source, reporting which SURVIVED
|
|
30
|
+
(a coverage gap). Selective and opt-in — never a blanket gate.
|
|
31
|
+
- **doctor `mcp schema cost`** (REQ-535): reports holmes-kit's own advertised MCP schema cost
|
|
32
|
+
(computed live from the tool set, no hardcoded number) and WARNs when `HOLMES_MCP_PROFILE=full`
|
|
33
|
+
re-advertises the hook-enforced gate-duplicate tools; advises client-side deferred loading.
|
|
34
|
+
Advisory only (never FAIL), and it never claims to have detected a client's resident behaviour.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- **BUG-1 remainder** (REQ-536): `spec_slice_init` now safe-quotes the `slice` value with `yamlScalar`
|
|
39
|
+
— it was the one raw interpolation among the slice builders, so a `sliceName` containing a colon or
|
|
40
|
+
quote could break the generated A-SPEC's frontmatter YAML. And `spec_approve` on a missing id now
|
|
41
|
+
surfaces any unparseable spec file (with a YAML hint) instead of a bare "not found", closing the
|
|
42
|
+
silent-loss that hid a broken file behind a "not found".
|
|
43
|
+
|
|
44
|
+
## [0.8.1] - 2026-09-03
|
|
45
|
+
|
|
46
|
+
Codex hard-enforcement completed and verified on real codex-cli 0.152.1 (GOAL-codex-enforcement),
|
|
47
|
+
plus the README refreshed to the 0.8.x feature set.
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
51
|
+
- **Codex marketplace manifest location** (REQ-533): `init --agent codex` now writes the manifest
|
|
52
|
+
where codex-cli 0.152.x actually reads it (`.claude-plugin/marketplace.json`), keeping the old
|
|
53
|
+
`.agents/plugins/` location for 0.151.x. Measured: the shipped wiring installs with no manual copy
|
|
54
|
+
— `codex plugin marketplace add` → `codex plugin add` → `installed, enabled`. `doctor` now flags a
|
|
55
|
+
wiring that has only the old location.
|
|
56
|
+
|
|
57
|
+
### Verified
|
|
58
|
+
|
|
59
|
+
- **Codex hard gate, model-independently**: the codex PreToolUse payload was captured (Claude-shaped:
|
|
60
|
+
`tool_name` + `tool_input`) and replayed through the gate — an unauthorized shell code-write is
|
|
61
|
+
denied, a harmless read passes. A session cannot self-grant `HOLMES_AUTONOMOUS_APPROVAL` (blocked
|
|
62
|
+
like `HOLMES_ROLE`).
|
|
63
|
+
|
|
64
|
+
## [0.8.0] - 2026-09-03
|
|
65
|
+
|
|
66
|
+
A hardening campaign (GOAL-hardening-2026-09) closing measured gate gaps, adding server-side CI
|
|
67
|
+
re-validation, a session banner with update notice, and — new this release — **autonomous spec
|
|
68
|
+
approval** so an agent in an explicitly-enabled autonomous mode can seal low/mid-risk specs
|
|
69
|
+
without the human elicitation TUI, while gate-behavior, architecture, taint, and upstream specs
|
|
70
|
+
still ask a human. Every slice was TDD'd (RED verified first) with two consecutive clean
|
|
71
|
+
adversarial rounds where a gate was changed.
|
|
72
|
+
|
|
73
|
+
### Added
|
|
74
|
+
|
|
75
|
+
- **Autonomous spec approval** (REQ-532): with the out-of-band `HOLMES_AUTONOMOUS_APPROVAL`
|
|
76
|
+
switch set (an agent cannot set it — the pre-tool-use gate blocks that, like `HOLMES_ROLE`),
|
|
77
|
+
`spec_approve` seals a low/mid-risk spec itself, ledgered under an `autonomous:<client>` actor
|
|
78
|
+
so audits tell the channel apart from human (`elicitation:`) and operator (`env`) approvals.
|
|
79
|
+
The bound is conservative: `gate-behavior` breaking changes, architecture/gate/governance/taint
|
|
80
|
+
files, and every REQ/H-SPEC/C-SPEC stay on the human channel. Autonomy OFF is byte-identical to
|
|
81
|
+
before.
|
|
82
|
+
- **Session banner + update notice** (REQ-531): every session start emits an English intro line
|
|
83
|
+
(version + governance rule + npm page URL) to both the human transcript and the agent context
|
|
84
|
+
via a SessionStart hook, and the MCP server carries the same banner in its `instructions` for
|
|
85
|
+
harnesses without that hook. When the `~/.holmes` cache knows a newer published version, an
|
|
86
|
+
install-mode-aware update command is appended. The registry refresh is detached and fail-silent;
|
|
87
|
+
`HOLMES_NO_UPDATE_CHECK` or `CI` opts out.
|
|
88
|
+
- **Server-side CI re-validation** (REQ-530): a Gitea Actions workflow re-runs the pre-push
|
|
89
|
+
evidence check (npm ci → build → full suite → tarball install probe) so a `--no-verify` push or
|
|
90
|
+
a hook-less clone is still caught. The lockfile is now tracked (`npm ci` reproducible). NOTE:
|
|
91
|
+
wiring only until a runner is registered — an operator step.
|
|
92
|
+
|
|
93
|
+
### Fixed
|
|
94
|
+
|
|
95
|
+
- **cd-relative shell writes are judged at the segment's effective directory** (REQ-528): a write
|
|
96
|
+
spelled from a subdirectory (`cd sub && cat > ../src/x.ts`) is judged at the real location, not
|
|
97
|
+
the project root — five bypasses (subshell, `sh -c`, heredoc-program, …) are sealed and three
|
|
98
|
+
legitimate out-of-tree writes are un-blocked. A quote-aware split closes a data-injection route
|
|
99
|
+
a transparent split would open.
|
|
100
|
+
- **A file's whole governing anchor SET is judged** (REQ-529): comma-list anchors past the first
|
|
101
|
+
id used to be absent from the stale gate and phaseCheck; every governing id now participates and
|
|
102
|
+
the refusal names the one that failed. String-value anchors in JSON/config files
|
|
103
|
+
(`"//": "@implements …"` in package.json) are honored; code-file fixture strings stay excluded.
|
|
104
|
+
|
|
105
|
+
### Guardrail
|
|
106
|
+
|
|
107
|
+
- **`HOLMES_AUTONOMOUS_APPROVAL` joins the self-disarm family**: a session cannot read or set the
|
|
108
|
+
autonomy switch (same protection as `HOLMES_ROLE`/`HOLMES_GATE_BYPASS`).
|
|
109
|
+
|
|
8
110
|
## [0.7.1] - 2026-09-02
|
|
9
111
|
|
|
10
112
|
An adversarial self-review of the seven-language work — 42 probes the test suite never pinned
|
package/README.md
CHANGED
|
@@ -14,16 +14,20 @@
|
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
17
|
-
### 🛡️ Currently Supported Features (v0.
|
|
17
|
+
### 🛡️ Currently Supported Features (v0.9.x Production Features)
|
|
18
18
|
|
|
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`
|
|
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
|
+
- 🔴 **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
|
+
- 🤖 **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.
|
|
22
|
+
- 🪧 **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`.
|
|
23
|
+
- 🧱 **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.
|
|
20
24
|
- 🧠 **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.
|
|
21
25
|
- 🎯 **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.
|
|
22
|
-
- 🐞 **Causal Defect Localization & CPG
|
|
26
|
+
- 🐞 **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).
|
|
23
27
|
- 📏 **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.
|
|
24
|
-
- 🧪 **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.
|
|
28
|
+
- 🧪 **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.
|
|
25
29
|
- 🔔 **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.
|
|
26
|
-
- 🚦 **
|
|
30
|
+
- 🚦 **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.
|
|
27
31
|
- 📊 **Automated RTM & Taint Heatmap**: Interactive standalone HTML/SVG report generation (`generateRtmHeatmap`) for spec coverage and security dataflow reachability.
|
|
28
32
|
- 🤖 **CLI-First AI Harness Matrix**: Native process hook gating for Claude Code, Antigravity CLI (AGY), Codex CLI, and Google Antigravity SDK.
|
|
29
33
|
|
|
@@ -46,7 +50,7 @@ Holmes-Kit prioritizes **CLI-based AI Coding Agents** where OS-level process hoo
|
|
|
46
50
|
| :--- | :--- | :--- |
|
|
47
51
|
| 🤖 **Claude Code CLI** | 🥇 Tier 1 (Native) | OS PreToolUse & Stop hooks (`.claude/settings.local.json`), MCP server (`.mcp.json`) |
|
|
48
52
|
| 🚀 **Antigravity CLI (AGY)** | 🥇 Tier 1 (Native) | AGY Hooks (`hooks.json`), MCP config (`.agents/mcp_config.json`), Governance Skills |
|
|
49
|
-
| 💻 **Codex CLI / Agentic Shell** | 🥇 Tier 1 (Native) | Codex MCP integration (`.codex/config.toml`), plugin-packaged gate hooks
|
|
53
|
+
| 💻 **Codex CLI / Agentic Shell** | 🥇 Tier 1 (Native) | Codex MCP integration (`.codex/config.toml`), plugin-packaged gate hooks at the marketplace path Codex reads (`.claude-plugin/marketplace.json`, codex-cli 0.152.x). Hard-gate enforcement verified on real codex-cli: the captured PreToolUse payload is judged and an unauthorized code-write is denied. |
|
|
50
54
|
| 🧩 **Google Antigravity SDK** | 🥇 Tier 1 (Native) | Autonomous Agent SDK bindings and cryptographic provenance verification |
|
|
51
55
|
|
|
52
56
|
> **Note**: Holmes-Kit focuses strictly on CLI-based autonomous agents to guarantee 100% deterministic OS hook gating (`deny` enforcement) before file modifications occur.
|
package/dist/.build-id
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
2a5de17-mtm0zp6n
|
|
@@ -204,19 +204,29 @@ function agentFiles(agent, opts) {
|
|
|
204
204
|
// The MCP wiring stays in `.codex/config.toml` (merged by init.ts).
|
|
205
205
|
// Wiring is not installation: the user still runs `codex plugin marketplace add` and
|
|
206
206
|
// `codex plugin add`, which is why HARNESS_ENFORCES.codex is false.
|
|
207
|
+
// @implements A-SPEC-533.1 — the marketplace manifest, built ONCE and emitted at BOTH
|
|
208
|
+
// locations. codex-cli 0.152.x reads `<root>/.claude-plugin/marketplace.json` (measured
|
|
209
|
+
// 2026-09-03: the `.agents/plugins/` path A-SPEC-442 wrote for 0.151.0 fails with "does not
|
|
210
|
+
// contain a supported manifest"); the old path stays for 0.151.x backward compatibility.
|
|
211
|
+
// One source so the two files cannot drift in format or source.path.
|
|
212
|
+
const marketplaceJson = `${JSON.stringify({
|
|
213
|
+
name: exports.CODEX_MARKETPLACE,
|
|
214
|
+
interface: { displayName: 'Holmes-Kit (local)' },
|
|
215
|
+
plugins: [{
|
|
216
|
+
name: 'holmes-kit',
|
|
217
|
+
source: { source: 'local', path: `./${exports.CODEX_PLUGIN_DIR.split(path.sep).join('/')}` },
|
|
218
|
+
policy: { installation: 'AVAILABLE', authentication: 'ON_INSTALL' },
|
|
219
|
+
category: 'Developer Tools',
|
|
220
|
+
}],
|
|
221
|
+
}, null, 2)}\n`;
|
|
207
222
|
return [
|
|
208
223
|
{
|
|
209
|
-
path: path.join(target, '.
|
|
210
|
-
content:
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
source: { source: 'local', path: `./${exports.CODEX_PLUGIN_DIR.split(path.sep).join('/')}` },
|
|
216
|
-
policy: { installation: 'AVAILABLE', authentication: 'ON_INSTALL' },
|
|
217
|
-
category: 'Developer Tools',
|
|
218
|
-
}],
|
|
219
|
-
}, null, 2)}\n`,
|
|
224
|
+
path: path.join(target, '.claude-plugin', 'marketplace.json'), // codex 0.152.x reads here
|
|
225
|
+
content: marketplaceJson,
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
path: path.join(target, '.agents', 'plugins', 'marketplace.json'), // 0.151.x (backward compat)
|
|
229
|
+
content: marketplaceJson,
|
|
220
230
|
},
|
|
221
231
|
{
|
|
222
232
|
path: path.join(target, exports.CODEX_PLUGIN_DIR, '.codex-plugin', 'plugin.json'),
|
|
@@ -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
|
|
@@ -876,9 +888,14 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
|
|
|
876
888
|
// it made the tool agree with the mistake.
|
|
877
889
|
const cdxPluginJson = path.join(target, agents_1.CODEX_PLUGIN_DIR, '.codex-plugin', 'plugin.json');
|
|
878
890
|
const cdxHooksJson = path.join(target, agents_1.CODEX_PLUGIN_DIR, 'hooks.json');
|
|
879
|
-
|
|
891
|
+
// @implements A-SPEC-533.3 — codex-cli 0.152.x reads the marketplace manifest at
|
|
892
|
+
// `.claude-plugin/marketplace.json` (measured 2026-09-03); 0.151.x read `.agents/plugins/`.
|
|
893
|
+
// Judge the gate against the location codex ACTUALLY reads, so a pre-A-SPEC-533.1 wiring (old
|
|
894
|
+
// location only) is caught instead of passing as "wiring is correct" when it cannot install.
|
|
895
|
+
const cdxMarketplaceNew = path.join(target, '.claude-plugin', 'marketplace.json');
|
|
896
|
+
const cdxMarketplaceOld = path.join(target, '.agents', 'plugins', 'marketplace.json');
|
|
880
897
|
const installCmds = `codex plugin marketplace add "${target}" 후 codex plugin add holmes-kit@${agents_1.CODEX_MARKETPLACE}`;
|
|
881
|
-
if (fs.existsSync(cdxPluginJson) && fs.existsSync(cdxHooksJson) && fs.existsSync(
|
|
898
|
+
if (fs.existsSync(cdxPluginJson) && fs.existsSync(cdxHooksJson) && (fs.existsSync(cdxMarketplaceNew) || fs.existsSync(cdxMarketplaceOld))) {
|
|
882
899
|
try {
|
|
883
900
|
const h = JSON.parse(fs.readFileSync(cdxHooksJson, 'utf8'));
|
|
884
901
|
const manifest = JSON.parse(fs.readFileSync(cdxPluginJson, 'utf8'));
|
|
@@ -895,6 +912,12 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
|
|
|
895
912
|
// The one path measured to load. `./hooks/hooks.json` parses fine and never fires.
|
|
896
913
|
add('codex gate', 'FAIL', `플러그인 매니페스트의 hooks 가 './hooks.json' 이 아닙니다: ${String(manifest.hooks)} — Codex 가 적재하지 않습니다`, 'holmes-kit init --target <dir> --agent codex --force 로 다시 배선하십시오.');
|
|
897
914
|
}
|
|
915
|
+
else if (!fs.existsSync(cdxMarketplaceNew)) {
|
|
916
|
+
// @implements A-SPEC-533.3 — hooks are correct, but the marketplace manifest is only at
|
|
917
|
+
// the old `.agents/plugins/` location; codex 0.152.x reads `.claude-plugin/` and cannot
|
|
918
|
+
// install this. Judged AFTER the stale-hook / manifest FAILs so those still win.
|
|
919
|
+
add('codex gate', 'WARN', `codex 0.152.x 가 읽는 .claude-plugin/marketplace.json 이 없습니다 — 구 위치(.agents/plugins/marketplace.json)만 있어 codex plugin marketplace add 가 "manifest 없음" 으로 실패합니다 (0.151.x 는 .agents/plugins/, 0.152.x 는 .claude-plugin/)`, 'holmes-kit init --target <dir> --agent codex --force 로 다시 배선하면 두 위치에 매니페스트가 놓입니다.');
|
|
920
|
+
}
|
|
898
921
|
else {
|
|
899
922
|
add('codex gate', 'WARN', '플러그인 배선은 옳습니다 — 그러나 설치되기 전까지 게이트는 집행되지 않습니다(Codex 는 설치된 마켓플레이스에서만 플러그인을 적재하며, doctor 는 설치 여부를 오프라인에서 확인할 수 없습니다)', `${installCmds} 를 실행한 뒤 새 세션에서 사용하십시오.`);
|
|
900
923
|
}
|
package/dist/holmes/cli/init.js
CHANGED
|
@@ -97,6 +97,9 @@ function buildHookPlan(packageRoot, matcher, specsDir = '.ax/specs') {
|
|
|
97
97
|
preToolUseMatcher: matcher,
|
|
98
98
|
preToolUseCommand: q(path.join(packageRoot, 'dist', 'holmes', 'hooks', 'pre-tool-use.js')),
|
|
99
99
|
stopCommand: q(path.join(packageRoot, 'dist', 'holmes', 'hooks', 'stop.js')),
|
|
100
|
+
// @implements A-SPEC-531.2 — the SessionStart banner runs the same node entry; it takes no
|
|
101
|
+
// `--specs` arg (the banner reads only the package version and the ~/.holmes cache).
|
|
102
|
+
sessionStartCommand: `node "${path.join(packageRoot, 'dist', 'holmes', 'hooks', 'session-start.js')}"`,
|
|
100
103
|
};
|
|
101
104
|
}
|
|
102
105
|
/**
|
|
@@ -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
|
+
}
|
|
@@ -32,6 +32,7 @@ export interface HookPlan {
|
|
|
32
32
|
preToolUseMatcher: string;
|
|
33
33
|
preToolUseCommand: string;
|
|
34
34
|
stopCommand: string;
|
|
35
|
+
sessionStartCommand?: string;
|
|
35
36
|
}
|
|
36
37
|
/** A hook entry belongs to holmes-kit iff its command references our hook scripts. */
|
|
37
38
|
export declare function isHolmesCommand(command: string): boolean;
|
|
@@ -12,7 +12,7 @@ exports.mergeMcpServers = mergeMcpServers;
|
|
|
12
12
|
exports.removeMcpServer = removeMcpServer;
|
|
13
13
|
/** A hook entry belongs to holmes-kit iff its command references our hook scripts. */
|
|
14
14
|
function isHolmesCommand(command) {
|
|
15
|
-
return /dist[/\\]holmes[/\\]hooks[/\\](pre-tool-use|stop)\.js/.test(command) || /\bholmes-(kit|mcp|pre-tool-use|stop)\b/.test(command);
|
|
15
|
+
return /dist[/\\]holmes[/\\]hooks[/\\](pre-tool-use|stop|session-start)\.js/.test(command) || /\bholmes-(kit|mcp|pre-tool-use|stop)\b/.test(command);
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
18
|
* The script path out of a hook command line, e.g. `node "/pkg/dist/.../stop.js"` -> `/pkg/dist/.../stop.js`.
|
|
@@ -43,6 +43,11 @@ function mergeHooks(existing, plan) {
|
|
|
43
43
|
};
|
|
44
44
|
upsert('PreToolUse', { matcher: plan.preToolUseMatcher, hooks: [{ type: 'command', command: plan.preToolUseCommand }] });
|
|
45
45
|
upsert('Stop', { hooks: [{ type: 'command', command: plan.stopCommand }] }); // Stop takes NO matcher
|
|
46
|
+
// @implements A-SPEC-531.2 — the banner hook, wired like Stop (no matcher). Guarded so an older
|
|
47
|
+
// caller that builds a plan without it is unchanged (no empty SessionStart group appears).
|
|
48
|
+
if (plan.sessionStartCommand) {
|
|
49
|
+
upsert('SessionStart', { hooks: [{ type: 'command', command: plan.sessionStartCommand }] });
|
|
50
|
+
}
|
|
46
51
|
out.hooks = hooks;
|
|
47
52
|
return out;
|
|
48
53
|
}
|
|
@@ -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
|
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Spec } from '../spec/spec-parser';
|
|
2
|
+
/** The out-of-band switch. An agent cannot read or set it — the pre-tool-use gate blocks that
|
|
3
|
+
* (A-SPEC-532.2), the same self-disarm protection HOLMES_APPROVAL and HOLMES_GATE_BYPASS have. */
|
|
4
|
+
export declare const AUTONOMY_ENV = "HOLMES_AUTONOMOUS_APPROVAL";
|
|
5
|
+
export type ApprovalAutonomy = 'auto' | 'hitl';
|
|
6
|
+
export declare function isHighRiskPath(p: string): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* The autonomy verdict for approving THIS spec. `resolveParent` is accepted for future
|
|
9
|
+
* parent-aware rules (kept in the signature so callers wire it once); the current bound is decided
|
|
10
|
+
* from the spec itself.
|
|
11
|
+
*/
|
|
12
|
+
export declare function specApprovalAutonomy(spec: Spec, _resolveParent: (id: string) => Spec | null): ApprovalAutonomy;
|
|
13
|
+
/** Whether autonomous approval is enabled at all — the out-of-band switch, read at the wiring layer. */
|
|
14
|
+
export declare function autonomousApprovalEnabled(env: NodeJS.ProcessEnv): boolean;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AUTONOMY_ENV = void 0;
|
|
4
|
+
exports.isHighRiskPath = isHighRiskPath;
|
|
5
|
+
exports.specApprovalAutonomy = specApprovalAutonomy;
|
|
6
|
+
exports.autonomousApprovalEnabled = autonomousApprovalEnabled;
|
|
7
|
+
const scope_judgment_1 = require("../guardrail/scope-judgment");
|
|
8
|
+
/** The out-of-band switch. An agent cannot read or set it — the pre-tool-use gate blocks that
|
|
9
|
+
* (A-SPEC-532.2), the same self-disarm protection HOLMES_APPROVAL and HOLMES_GATE_BYPASS have. */
|
|
10
|
+
exports.AUTONOMY_ENV = 'HOLMES_AUTONOMOUS_APPROVAL';
|
|
11
|
+
/**
|
|
12
|
+
* Path prefixes whose files govern the gate, the ledger, taint boundaries, or this session's own
|
|
13
|
+
* wiring. A spec touching one of these must not self-approve even at a mid grade — changing them is
|
|
14
|
+
* exactly the "structural / irreversible" class the owner drew the line at.
|
|
15
|
+
*/
|
|
16
|
+
const HIGH_RISK_PREFIXES = [
|
|
17
|
+
'src/holmes/hooks/',
|
|
18
|
+
'src/holmes/guardrail/',
|
|
19
|
+
'src/holmes/governance/',
|
|
20
|
+
'.ax/roles',
|
|
21
|
+
'.claude',
|
|
22
|
+
];
|
|
23
|
+
const TAINT_MARKERS = ['taint', 'dataflow-taint', 'flow-sensitive'];
|
|
24
|
+
function isHighRiskPath(p) {
|
|
25
|
+
const s = p.replace(/^\.\//, '').replace(/^["'`]|["'`]$/g, '');
|
|
26
|
+
if (HIGH_RISK_PREFIXES.some((pre) => s.startsWith(pre)))
|
|
27
|
+
return true;
|
|
28
|
+
if (s === '.mcp.json' || s.endsWith('/.mcp.json'))
|
|
29
|
+
return true;
|
|
30
|
+
// A taint/security boundary file anywhere under rtm/ — the flow engine and its vocabulary.
|
|
31
|
+
if (s.startsWith('src/holmes/rtm/') && TAINT_MARKERS.some((m) => s.includes(m)))
|
|
32
|
+
return true;
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
/** The declared breaking-change grade of an A-SPEC, or null when absent/blank. */
|
|
36
|
+
function breakingGrade(spec) {
|
|
37
|
+
const raw = spec.frontmatter?.breaking_change;
|
|
38
|
+
if (typeof raw !== 'string' || raw.trim() === '')
|
|
39
|
+
return null;
|
|
40
|
+
// `<grade>: <reason>` or a bare `none` — the grade is the token before the first colon.
|
|
41
|
+
return raw.trim().split(':')[0].trim();
|
|
42
|
+
}
|
|
43
|
+
// Grades at or below code-interface are auto; gate-behavior (and anything unrecognised) is not.
|
|
44
|
+
const AUTO_GRADES = new Set(['none', 'persisted-artifact', 'derived-artifact', 'code-interface']);
|
|
45
|
+
/**
|
|
46
|
+
* The autonomy verdict for approving THIS spec. `resolveParent` is accepted for future
|
|
47
|
+
* parent-aware rules (kept in the signature so callers wire it once); the current bound is decided
|
|
48
|
+
* from the spec itself.
|
|
49
|
+
*/
|
|
50
|
+
function specApprovalAutonomy(spec, _resolveParent) {
|
|
51
|
+
switch (spec.type) {
|
|
52
|
+
case 'T-SPEC':
|
|
53
|
+
return 'auto'; // tests are low risk
|
|
54
|
+
case 'REQ':
|
|
55
|
+
case 'H-SPEC':
|
|
56
|
+
case 'C-SPEC':
|
|
57
|
+
return 'hitl'; // wide blast radius / structural constraint
|
|
58
|
+
case 'A-SPEC': {
|
|
59
|
+
const grade = breakingGrade(spec);
|
|
60
|
+
if (grade === null || !AUTO_GRADES.has(grade))
|
|
61
|
+
return 'hitl'; // undeclared or gate-behavior
|
|
62
|
+
const paths = (0, scope_judgment_1.fttPathTokens)(spec.sections['Files to Touch'] ?? '');
|
|
63
|
+
if (paths.some(isHighRiskPath))
|
|
64
|
+
return 'hitl'; // gate/governance/taint file
|
|
65
|
+
return 'auto';
|
|
66
|
+
}
|
|
67
|
+
default:
|
|
68
|
+
return 'hitl'; // unknown kind: fail-safe to human
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** Whether autonomous approval is enabled at all — the out-of-band switch, read at the wiring layer. */
|
|
72
|
+
function autonomousApprovalEnabled(env) {
|
|
73
|
+
const v = env[exports.AUTONOMY_ENV];
|
|
74
|
+
return typeof v === 'string' && v !== '';
|
|
75
|
+
}
|
|
@@ -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
|
|
@@ -153,4 +153,29 @@ export interface PathImpl {
|
|
|
153
153
|
resolve: (...parts: string[]) => string;
|
|
154
154
|
sep: string;
|
|
155
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* @implements A-SPEC-528.1
|
|
158
|
+
* One shell command, cut at its separators, with the EFFECTIVE working directory each piece runs
|
|
159
|
+
* in. The shell-write rules used to hand every relative candidate to `resolvesInside` with the
|
|
160
|
+
* project root as the base; a `cd` earlier in the command moves that base, and the mismatch was
|
|
161
|
+
* measured both ways (2026-09-03 probe): five in-project writes spelled from a subdirectory were
|
|
162
|
+
* allowed, three legitimate out-of-tree writes were denied.
|
|
163
|
+
*
|
|
164
|
+
* `base: null` means the gate cannot know where the piece runs (dynamic cd argument, `cd -`,
|
|
165
|
+
* `popd`, ambiguous multi-token target). The consumer fails CLOSED on a governed relative write
|
|
166
|
+
* candidate in such a piece — and stays silent when no candidate follows.
|
|
167
|
+
*
|
|
168
|
+
* The split is QUOTE-AWARE on purpose: a separator inside quotes is data, and a transparent split
|
|
169
|
+
* would let `printf 'cd /\n'` place a fake `cd` at a segment start and push the base out of the
|
|
170
|
+
* project (injection found while designing this — the honest-revision note in H-SPEC-528).
|
|
171
|
+
* Command strings are re-opened explicitly instead: `sh -c '…'`/`eval '…'` programs and
|
|
172
|
+
* shell-stdin heredoc bodies recurse (their cds stay LOCAL to the child shell, matching real
|
|
173
|
+
* semantics); data heredoc bodies keep their text for candidate matching but never move the base.
|
|
174
|
+
* Pure: no fs, no env reads beyond `os.homedir()` for `~`, no `process.platform`.
|
|
175
|
+
*/
|
|
176
|
+
export interface ShellSegment {
|
|
177
|
+
text: string;
|
|
178
|
+
base: string | null;
|
|
179
|
+
}
|
|
180
|
+
export declare function shellSegments(command: string, startBase: string, depth?: number): ShellSegment[];
|
|
156
181
|
export declare function resolvesInside(raw: string, roots: readonly string[], impl?: PathImpl): boolean;
|