@holmes-lab/holmes-kit 0.1.7 → 0.1.8

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 (114) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +11 -7
  3. package/bin/holmes-mcp.js +0 -0
  4. package/dist/.build-id +1 -1
  5. package/dist/holmes/cli/agents.d.ts +50 -0
  6. package/dist/holmes/cli/ci-gate.d.ts +18 -0
  7. package/dist/holmes/cli/doctor.d.ts +44 -0
  8. package/dist/holmes/cli/gitignore-merge.d.ts +18 -0
  9. package/dist/holmes/cli/governed-precondition.d.ts +27 -0
  10. package/dist/holmes/cli/index.d.ts +14 -0
  11. package/dist/holmes/cli/index.js +23 -1
  12. package/dist/holmes/cli/init.d.ts +69 -0
  13. package/dist/holmes/cli/interactive-prompt.d.ts +9 -0
  14. package/dist/holmes/cli/playbook-skills.d.ts +129 -0
  15. package/dist/holmes/cli/roles-readme.d.ts +12 -0
  16. package/dist/holmes/cli/serve.d.ts +14 -0
  17. package/dist/holmes/cli/serve.js +70 -0
  18. package/dist/holmes/cli/settings-merge.d.ts +66 -0
  19. package/dist/holmes/config/config.d.ts +13 -0
  20. package/dist/holmes/context/bundler.d.ts +40 -0
  21. package/dist/holmes/context/render.d.ts +9 -0
  22. package/dist/holmes/context/tiers.d.ts +54 -0
  23. package/dist/holmes/context/tokens.d.ts +2 -0
  24. package/dist/holmes/cpg/ast-mutation.d.ts +31 -0
  25. package/dist/holmes/cpg/ast-mutation.js +126 -0
  26. package/dist/holmes/cpg/cpg-scanner.d.ts +69 -0
  27. package/dist/holmes/cpg/dynamic-cpg.d.ts +18 -0
  28. package/dist/holmes/cpg/dynamic-cpg.js +82 -0
  29. package/dist/holmes/cpg/hash-cache.d.ts +21 -0
  30. package/dist/holmes/cpg/language-parser-walk.d.ts +31 -0
  31. package/dist/holmes/cpg/language-parser-worker.d.ts +1 -0
  32. package/dist/holmes/cpg/language-parser.d.ts +116 -0
  33. package/dist/holmes/cpg/program-slicing.d.ts +21 -0
  34. package/dist/holmes/cpg/program-slicing.js +123 -0
  35. package/dist/holmes/cpg/scan-cache.d.ts +21 -0
  36. package/dist/holmes/cpg/source-path.d.ts +2 -0
  37. package/dist/holmes/cpg/test-files.d.ts +36 -0
  38. package/dist/holmes/governance/constitution-debt.d.ts +3 -0
  39. package/dist/holmes/governance/constitution-report.d.ts +23 -0
  40. package/dist/holmes/governance/constitution.d.ts +61 -0
  41. package/dist/holmes/governance/identity.d.ts +28 -0
  42. package/dist/holmes/governance/ledger-lock.d.ts +55 -0
  43. package/dist/holmes/governance/ledger-store.conformance.d.ts +11 -0
  44. package/dist/holmes/governance/ledger-store.d.ts +114 -0
  45. package/dist/holmes/governance/progress-ledger.d.ts +25 -0
  46. package/dist/holmes/governance/provenance-chain.d.ts +154 -0
  47. package/dist/holmes/governance/provenance-ledger.d.ts +102 -0
  48. package/dist/holmes/governance/provenance-schema.d.ts +21 -0
  49. package/dist/holmes/governance/replica-id.d.ts +20 -0
  50. package/dist/holmes/governance/role-policy.d.ts +52 -0
  51. package/dist/holmes/governance/trust-score.d.ts +32 -0
  52. package/dist/holmes/guardrail/anchors.d.ts +8 -0
  53. package/dist/holmes/guardrail/blind-spots.d.ts +32 -0
  54. package/dist/holmes/guardrail/decision-ledger.d.ts +29 -0
  55. package/dist/holmes/guardrail/executable-artifact.d.ts +1 -0
  56. package/dist/holmes/guardrail/governance-history.d.ts +15 -0
  57. package/dist/holmes/guardrail/phase.d.ts +27 -0
  58. package/dist/holmes/guardrail/risk-classifier.d.ts +52 -0
  59. package/dist/holmes/guardrail/risk-gate.d.ts +77 -0
  60. package/dist/holmes/guardrail/risk-types.d.ts +48 -0
  61. package/dist/holmes/guardrail/tspec-state.d.ts +71 -0
  62. package/dist/holmes/guardrail/write-target.d.ts +77 -0
  63. package/dist/holmes/hooks/adapters/antigravity.d.ts +42 -0
  64. package/dist/holmes/hooks/pre-tool-use.d.ts +144 -0
  65. package/dist/holmes/hooks/stop.d.ts +91 -0
  66. package/dist/holmes/mcp/basis.d.ts +100 -0
  67. package/dist/holmes/mcp/handlers.d.ts +482 -0
  68. package/dist/holmes/mcp/server.d.ts +2 -0
  69. package/dist/holmes/mcp/stdio-client.d.ts +51 -0
  70. package/dist/holmes/mcp/supervisor.d.ts +48 -0
  71. package/dist/holmes/mcp/tool-schemas.d.ts +12 -0
  72. package/dist/holmes/mcp/validate-args.d.ts +27 -0
  73. package/dist/holmes/messages/registry.d.ts +23 -0
  74. package/dist/holmes/project/baseline.d.ts +66 -0
  75. package/dist/holmes/project/change-source.d.ts +89 -0
  76. package/dist/holmes/project/ignore.d.ts +33 -0
  77. package/dist/holmes/project/root.d.ts +34 -0
  78. package/dist/holmes/reverse/anchor.d.ts +47 -0
  79. package/dist/holmes/reverse/cluster.d.ts +45 -0
  80. package/dist/holmes/reverse/draft.d.ts +33 -0
  81. package/dist/holmes/reverse/dynamic-wiring.d.ts +18 -0
  82. package/dist/holmes/reverse/scan.d.ts +103 -0
  83. package/dist/holmes/reverse/surface.d.ts +91 -0
  84. package/dist/holmes/reverse/test-map.d.ts +91 -0
  85. package/dist/holmes/review/coverage.d.ts +21 -0
  86. package/dist/holmes/review/findings.d.ts +49 -0
  87. package/dist/holmes/review/package.d.ts +26 -0
  88. package/dist/holmes/review/review-targets.d.ts +28 -0
  89. package/dist/holmes/review/scope.d.ts +13 -0
  90. package/dist/holmes/review/test-evidence.d.ts +31 -0
  91. package/dist/holmes/review/test-runner.d.ts +141 -0
  92. package/dist/holmes/rtm/dataflow-taint.d.ts +81 -0
  93. package/dist/holmes/rtm/gap-analyzer.d.ts +28 -0
  94. package/dist/holmes/rtm/git-changes.d.ts +33 -0
  95. package/dist/holmes/rtm/heatmap.d.ts +21 -0
  96. package/dist/holmes/rtm/incremental.d.ts +38 -0
  97. package/dist/holmes/rtm/localize.d.ts +36 -0
  98. package/dist/holmes/rtm/rtm-builder.d.ts +101 -0
  99. package/dist/holmes/rtm/rtm-check.d.ts +13 -0
  100. package/dist/holmes/rtm/rtm-graph.d.ts +88 -0
  101. package/dist/holmes/rtm/taint.d.ts +35 -0
  102. package/dist/holmes/rtm/test-scope.d.ts +85 -0
  103. package/dist/holmes/server/dashboard.d.ts +30 -0
  104. package/dist/holmes/server/dashboard.js +1758 -0
  105. package/dist/holmes/spec/approval-blockers.d.ts +62 -0
  106. package/dist/holmes/spec/breaking-change.d.ts +42 -0
  107. package/dist/holmes/spec/legacy-format.d.ts +93 -0
  108. package/dist/holmes/spec/spec-digest.d.ts +29 -0
  109. package/dist/holmes/spec/spec-parser.d.ts +12 -0
  110. package/dist/holmes/spec/spec-store.conformance.d.ts +10 -0
  111. package/dist/holmes/spec/spec-store.d.ts +130 -0
  112. package/dist/holmes/spec/spec-types.d.ts +150 -0
  113. package/dist/holmes/spec/validator.d.ts +65 -0
  114. package/package.json +2 -1
@@ -0,0 +1,48 @@
1
+ export type RiskLevel = 'auto' | 'notify' | 'confirm' | 'hard-hitl';
2
+ export declare const RISK_ORDER: RiskLevel[];
3
+ export type RiskAxis = 'reversibility' | 'architecture' | 'secret' | 'blast-radius';
4
+ export interface AxisVerdict {
5
+ axis: RiskAxis;
6
+ level: RiskLevel;
7
+ reasons: string[];
8
+ }
9
+ export interface RmSignal {
10
+ gitIgnored?: boolean;
11
+ tracked?: boolean;
12
+ inProject?: boolean;
13
+ }
14
+ export interface RiskAction {
15
+ kind: string;
16
+ target: string;
17
+ command?: string;
18
+ tracked?: boolean;
19
+ isDirectory?: boolean;
20
+ adrSupersedes?: string[];
21
+ cspecConstraintChange?: boolean;
22
+ stagedContent?: string;
23
+ blastRadius?: {
24
+ impactedSpecCount: number;
25
+ hitsFoundational: boolean;
26
+ };
27
+ rmTargetSignals?: Record<string, RmSignal>;
28
+ /**
29
+ * Absolute path of the project this command runs against, supplied by the hook (the classifier
30
+ * itself stays pure and does no I/O).
31
+ *
32
+ * Used to scope the protected-`.ax` rules to THIS project's governance directory. Without it the
33
+ * substring match gated `.ax` paths belonging to any other repository on the machine — real
34
+ * friction for anyone working across projects. Absent means unknown scope, which stays
35
+ * conservative: every `.ax` path is treated as protected.
36
+ */
37
+ projectRoot?: string;
38
+ }
39
+ export interface RiskThresholds {
40
+ blastRadiusConfirm?: number;
41
+ blastRadiusHardHitl?: number;
42
+ }
43
+ export interface RiskAssessment {
44
+ level: RiskLevel;
45
+ verdicts: AxisVerdict[];
46
+ reasons: string[];
47
+ }
48
+ export declare function maxLevel(a: RiskLevel, b: RiskLevel): RiskLevel;
@@ -0,0 +1,71 @@
1
+ import { Spec } from '../spec/spec-parser';
2
+ /**
3
+ * The T-SPEC situation for one A-SPEC, kept as a state rather than the boolean the gate used to
4
+ * compute.
5
+ *
6
+ * The defect this exists for, measured 2026-08-13: four different situations produced the identical
7
+ * sentence `A-SPEC A-SPEC-129의 approved T-SPEC이 없습니다(테스트 먼저).` while needing different
8
+ * actions. The harness already knew — `promote-slice`'s 「침묵의 실패 모드」 section said outright
9
+ * that the refusal offered no way to tell them apart. A document was compensating for what the gate
10
+ * could have said.
11
+ *
12
+ * THE PARTITION LOOKS ONLY AT THE TARGET. An earlier draft split on whether the PROJECT had any
13
+ * T-SPEC at all, which made "none written yet" unreachable in any repo past its first slice —
14
+ * measured: 0 of this repository's 73 A-SPECs could reach it, and a new A-SPEC was instead handed
15
+ * three unrelated documents with "add depends_on to one of them". That was worse than the sentence
16
+ * it replaced, which at least said 테스트 먼저.
17
+ *
18
+ * So there are two deny states. "Not written yet" and "written but the edge is missing" are NOT
19
+ * distinguishable by the gate — only the author knows which — so they are one state whose message
20
+ * offers both moves. Guessing between them (e.g. from the `T-SPEC-<number>` naming convention,
21
+ * which measures 71/73 here) would be the gate judging rather than reporting.
22
+ */
23
+ export type TspecState = {
24
+ kind: 'approved';
25
+ spec: Spec;
26
+ } | {
27
+ kind: 'linked-not-approved';
28
+ specs: Spec[];
29
+ } | {
30
+ kind: 'unlinked';
31
+ orphans: Spec[];
32
+ };
33
+ /**
34
+ * Classify the T-SPEC situation for `aspecId`. Array lookups only — the gate calls this from a
35
+ * PreToolUse hook on every tool call, so it never touches the filesystem.
36
+ */
37
+ export declare function tspecStateFor(aspecId: string, specs: Spec[]): TspecState;
38
+ export declare const ID_BUDGET = 200;
39
+ export declare const REASON_BUDGET = 1200;
40
+ export declare const ID_MAX = 80;
41
+ /**
42
+ * `a, b 외 N개` — or, when nothing fits, `N개` (rule 2). The count always agrees with what is shown.
43
+ */
44
+ export declare const listOrCount: (items: string[], budget: number, unit: string, cap?: number) => string;
45
+ /**
46
+ * What the author should read, and what they should do, for a state that denies.
47
+ *
48
+ * `null` for `approved` — there is nothing to say when nothing is wrong.
49
+ */
50
+ export declare function tspecRemediation(rawAspecId: string, state: TspecState, why: (spec: Spec) => string[]): {
51
+ message: string;
52
+ next_action: string;
53
+ } | null;
54
+ /**
55
+ * @implements A-SPEC-192 §10R (round 10, 4th)
56
+ * ONE spec's clause, budgeted — the shared discipline both refusal surfaces now use.
57
+ *
58
+ * The twin in `phase.ts` compared the whole joined summary (framing + id + every blocker) against
59
+ * REASON_BUDGET and dropped it ALL when it did not fit. Two harms followed, both measured. A 93-char
60
+ * parent blocker — the one action the author could take right now — vanished because a sibling
61
+ * blocker happened to be 1,154 characters. And because the id sits inside what was being measured,
62
+ * the effective budget slid with the id's LENGTH: growing an id by one character past ID_MAX made
63
+ * the §7R scrub fold it, freed 63 characters, and turned a 236-char refusal that named nothing into
64
+ * a 1,209-char refusal that named everything. Worse input, better answer, decided by something the
65
+ * author cannot control.
66
+ *
67
+ * Here the framing is priced first and every judgement about a blocker is made against what remains
68
+ * for bodies; blockers are dropped one at a time and counted, and the label distinguishes "no reason
69
+ * fits at all" from "the budget was spent elsewhere" — the same words the sibling uses.
70
+ */
71
+ export declare function budgetedClause(labelId: string, fallbackLabel: string, blockers: string[], spentElsewhere?: boolean): string;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Tools known to only READ. Everything else that carries a path is treated as a write.
3
+ *
4
+ * The list is inverted on purpose. The gate used to ask `['Write','Edit'].includes(name)`, and
5
+ * measured 2026-08-08 that meant `MultiEdit`, `NotebookEdit`, and a lower-case `write` reached the
6
+ * same protected files `Write` was denied — No-Spec-No-Code, the role gate, the ledger ban and the
7
+ * approval-config ban all bypassed by a different name. Adding those three names would postpone the
8
+ * problem to the fourth tool; inverting it means an unrecognised tool fails CLOSED.
9
+ */
10
+ export declare const READ_ONLY_TOOLS: readonly ["read", "grep", "glob", "ls", "notebookread", "webfetch", "websearch", "todowrite", "task", "bashoutput", "killshell"];
11
+ /**
12
+ * Payload keys that name a file. Shared with normalization so the two cannot drift: a field the
13
+ * gate does not know about is a path the gate cannot see, which is how `notebook_path` slipped
14
+ * through.
15
+ */
16
+ export declare const PATH_FIELDS: readonly ["file_path", "notebook_path"];
17
+ /**
18
+ * Whether this tool call writes a file.
19
+ *
20
+ * A missing or non-string name counts as a write. `normalizeHookInput` turns both into `''`, and the
21
+ * old membership test read that as "not a write tool" — REQ-144 closed exactly this direction for
22
+ * the path field while leaving the name field open.
23
+ */
24
+ export declare function writesFiles(toolName: string | undefined, input: unknown): boolean;
25
+ /**
26
+ * The file a raw path actually names.
27
+ *
28
+ * `path.resolve` folds `//`, `.` and `..`. Then the longest EXISTING ancestor is realpath'd and the
29
+ * remainder rejoined lexically — a whole-path `realpath` throws on a file that does not exist yet,
30
+ * which is precisely when the gate has to answer.
31
+ */
32
+ export declare function resolveTarget(root: string, raw: string, depth?: number): string;
33
+ /**
34
+ * Whether a write lands inside a governance directory.
35
+ *
36
+ * Comparison folds case because that is what the filesystem does: measured 2026-08-08 on macOS,
37
+ * writing `.AX/roles/policy.yaml` changed the very file the gate protects. On a case-sensitive
38
+ * filesystem folding over-protects, which is the safe direction and only collides for a project
39
+ * that deliberately keeps both `.ax` and `.AX`.
40
+ *
41
+ * The separator suffix matters: without it `.axolotl/` would inherit `.ax/`'s protection by prefix.
42
+ */
43
+ export declare function isProtectedTarget(root: string, raw: string): boolean;
44
+ /**
45
+ * Which governance directory a write lands in, or `null`.
46
+ *
47
+ * Callers that phrase a refusal need to name the right thing — REQ-155 measured how much a message
48
+ * that points at the wrong file costs — so the matched directory is returned rather than a bare
49
+ * boolean re-derived from a second regex.
50
+ */
51
+ /**
52
+ * The spec FILE a write lands on, resolved — or `null` when the target is not a spec.
53
+ *
54
+ * @implements A-SPEC-191 §23 (round 12) — §22 moved `.mcp.json`, `.env`, `.ax/roles` and
55
+ * `.ax/ledger` onto identity and left the spec store itself on a case-sensitive regex over the
56
+ * caller's string. Measured on one fixture: `.AX/specs/…`, `.Ax/specs/…`, `.ax/SPECS/…`,
57
+ * `.ax//specs/…`, `.ax/./specs/…`, a link named `alias.md` and a link named `specslink` ALL
58
+ * reached the same files with the pattern looking elsewhere — so a session could self-approve an
59
+ * A-SPEC and a T-SPEC and then write the code they authorise, and could rewrite the body of an
60
+ * already-approved spec. The two rules the spec chain rests on (no self-approval, no seal-breaking
61
+ * edit) were both spelling-deep. The RESOLVED path is returned so callers read the disk at the
62
+ * same file they judged — asking `existsSync` about the caller's spelling was its own bypass.
63
+ */
64
+ export declare function specTargetOf(root: string, specsDir: string, raw: string): string | null;
65
+ /**
66
+ * Which governance FILE a write lands on, or `null`.
67
+ *
68
+ * @implements A-SPEC-191 §22 (round 11) — `.ax/*` and `.claude` were judged by identity here while
69
+ * `.mcp.json` and `.env` were judged by a regex over the caller's string, one function away. So the
70
+ * same sentence that A-SPEC-163 wrote ("identity, not spelling") was true of half its subject:
71
+ * a link inside the project under any other name reached `.mcp.json` with the pattern looking at a
72
+ * different string, and on a case-insensitive volume `.MCP.json` was the same file too. The
73
+ * resolution is `resolveTarget`'s — it follows links, including dangling ones, because a write
74
+ * through a dangling link creates exactly the file it names.
75
+ */
76
+ export declare function protectedFileKindOf(root: string, raw: string): string | null;
77
+ export declare function protectedKindOf(root: string, raw: string): string | null;
@@ -0,0 +1,42 @@
1
+ export interface AgyPreDecision {
2
+ decision: 'allow' | 'deny';
3
+ reason?: string;
4
+ }
5
+ export interface AgyStopDecision {
6
+ decision?: 'continue';
7
+ reason?: string;
8
+ }
9
+ /**
10
+ * 실측한 도구만 등재한다.
11
+ *
12
+ * 이름과 인자 키는 이 기계에 설치된 Antigravity CLI 의 실제 대화 기록에서 뽑았다
13
+ * (run_command{CommandLine,Cwd} 101건, view_file{AbsolutePath} 99건,
14
+ * replace_file_content{TargetFile,ReplacementContent,TargetContent} 46건, grep_search, list_dir).
15
+ * **추측으로 이름을 더하지 않는다** — 읽기로 잘못 등재하면 쓰기가 게이트를 지나가고, 쓰기로
16
+ * 잘못 등재하면 읽기가 막힌다. 등재되지 않은 이름은 그대로 게이트에 넘겨 fail-closed 로 받는다
17
+ * (A-SPEC-163: 읽기 전용이라고 알려지지 않은 도구가 경로를 들고 오면 그것은 쓰기다).
18
+ */
19
+ export declare const TOOL_MAP: Record<string, {
20
+ as: string;
21
+ pathArg?: string;
22
+ contentArg?: string;
23
+ commandArg?: string;
24
+ }>;
25
+ /** 훅을 실행해 그 표준 출력을 돌려준다. 실행 자체가 실패하면 `null`(호출자가 닫는 쪽으로 받는다). */
26
+ export type HookRunner = (hookPath: string, payload: string, cwd: string) => Record<string, unknown> | null;
27
+ /**
28
+ * PreToolUse: Antigravity 의 말 → 게이트 → Antigravity 의 말.
29
+ *
30
+ * 번역할 수 없는 입력은 **닫는 쪽**으로 답한다. 어댑터가 조용히 `allow` 를 돌려주면 게이트가
31
+ * 없는 것과 같고, 그 사실은 아무 데도 나타나지 않는다 — REQ-144 가 같은 방향으로 세운 규율이다.
32
+ */
33
+ export declare function adaptPreToolUse(stdin: string, cwd?: string, run?: HookRunner): AgyPreDecision;
34
+ /**
35
+ * Stop: 게이트가 "막아라"라고 하면, 이 하네스에서 막는 말은 `continue` 다.
36
+ *
37
+ * 이름이 반대라서 헷갈리기 쉽다 — Claude Code 의 `block` 은 "정지를 막아라"이고 Antigravity 의
38
+ * `continue` 도 "정지하지 말고 계속하라"이다. 같은 뜻, 다른 낱말이다.
39
+ *
40
+ * 여기서는 실행 실패를 **열어 준다**. Stop 훅은 이미 fail-open 계약이다(턴을 죽이지 않는다).
41
+ */
42
+ export declare function adaptStop(stdin: string, cwd?: string, run?: HookRunner): AgyStopDecision;
@@ -0,0 +1,144 @@
1
+ import { Spec } from '../spec/spec-parser';
2
+ import { Approval } from '../guardrail/risk-gate';
3
+ import { RiskAction, RiskAssessment, RmSignal } from '../guardrail/risk-types';
4
+ export declare function resolveRmSignals(command: string, cwd: string): Record<string, RmSignal> | undefined;
5
+ /**
6
+ * @implements A-SPEC-149
7
+ * The out-of-band approval channel, read at the ENTRY only. `evaluateHook` no longer calls this on
8
+ * its own — an exported HOLMES_APPROVAL used to reach the decision directly and turn a denial into
9
+ * permission with nothing recording that it had. Exported so the entry's contract (parse, and treat
10
+ * malformed input as absent) is testable at the layer that owns it.
11
+ */
12
+ export declare function readApprovalFromEnv(): Approval | undefined;
13
+ /**
14
+ * True when `env`/`printenv` runs as an actual COMMAND (an environment dump) rather than appearing as
15
+ * a mere word — `grep -rn env src/` and `git commit -m "set the env"` must stay allowed, while
16
+ * `env`, `/usr/bin/env`, `X=1 env`, `command env`, `(env)`, `sh -c 'env'` must not. Each segment is
17
+ * stripped of leading assignments/wrappers and, for a nested shell, its `-c` body is re-examined.
18
+ * `env FOO=1 cmd` is a RUNNER (assignment then command), not a dump.
19
+ */
20
+ export declare function dumpsEnvInCommandPosition(command: string, depth?: number): boolean;
21
+ /**
22
+ * Coerce a hook envelope into known shapes so a type surprise cannot become an exception.
23
+ *
24
+ * @implements A-SPEC-144
25
+ * Measured against a real installed package, in a governed project where the control payload was
26
+ * correctly DENIED: `file_path: ["src/x.ts"]` and `file_path: {"a":1}` both threw
27
+ * `p.replace is not a function`, and an absent `tool_input` threw on property access — and the
28
+ * entry point's catch turned each of those into `allow`. No-Spec-No-Code was bypassed by a wrong type.
29
+ *
30
+ * A non-string target is DROPPED rather than coerced. `String(['a'])` is `'a'` and `String({})` is
31
+ * `'[object Object]'`; either would hand the classifier a path that does not exist and let it decide
32
+ * confidently about nothing. An absent target is honest, and the gate already knows how to treat one.
33
+ */
34
+ export interface HookInput {
35
+ tool_name: string;
36
+ tool_input: {
37
+ file_path?: string;
38
+ notebook_path?: string;
39
+ content?: string;
40
+ new_string?: string;
41
+ command?: string;
42
+ };
43
+ /**
44
+ * @implements A-SPEC-163
45
+ * The raw payload carried a path field that was NOT a string, or `tool_input` was not an object.
46
+ *
47
+ * REQ-144's guarantee — a malformed payload denies rather than slips through — used to hold only
48
+ * by accident: `Write` happened to be in the hard-coded write list, so a dropped `file_path` fell
49
+ * into the write branch and failed there. Inverting the tool predicate removed that accident and
50
+ * three of its tests went red, which is what a guarantee resting on a coincidence looks like.
51
+ * Recording the fact makes it independent of which tool asked.
52
+ */
53
+ malformed?: boolean;
54
+ }
55
+ export declare function normalizeHookInput(raw: unknown): HookInput;
56
+ /**
57
+ * Which way to fail when the gate itself throws.
58
+ *
59
+ * @implements A-SPEC-144
60
+ * The entry point used to answer `allow` unconditionally, with the comment "to avoid blocking the
61
+ * workflow". The tension is real — an unrelated bug that denies every tool call bricks a session —
62
+ * but resolving it toward a silent grant makes every future bug in the gate a bypass instead of a
63
+ * visible failure, which is the opposite of the posture the rest of this codebase takes.
64
+ *
65
+ * So: a governed project DENIES, because a gate that cannot evaluate must not grant (the same rule
66
+ * REQ-141 applied to the ledger). An ungoverned project ALLOWS, because there was no policy to apply
67
+ * and inventing a denial there is its own failure mode. And an operator — never the session — can
68
+ * escape via `HOLMES_GATE_BYPASS`.
69
+ */
70
+ export declare function decideOnGateError(opts: {
71
+ governed: boolean;
72
+ bypass: boolean;
73
+ message: string;
74
+ }): {
75
+ permissionDecision: 'allow' | 'deny';
76
+ permissionDecisionReason?: string;
77
+ };
78
+ /**
79
+ * Is this project governed? Computed on the ERROR path, so it must not re-run the logic that just
80
+ * failed: presence of a configured spec root or a non-empty spec directory, nothing more. Any
81
+ * problem reading it means NOT governed — the conservative direction here is the one that keeps an
82
+ * unrelated failure from bricking a project that never asked for gating.
83
+ */
84
+ export declare function isGovernedProject(specsDir: string, env?: NodeJS.ProcessEnv): boolean;
85
+ /**
86
+ * The whole error-path decision, assembled from an environment.
87
+ *
88
+ * @implements A-SPEC-144
89
+ * Exported and env-injected so the WIRING is tested rather than trusted. Mutation testing found the
90
+ * gap: hardcoding `bypass: true` and inverting the governance check both left every test green,
91
+ * because the tests exercised the pure helpers directly and nothing exercised the code that reads
92
+ * the environment. An untested wire is where a fix quietly stops applying.
93
+ */
94
+ export declare function gateErrorDecision(message: string, env?: NodeJS.ProcessEnv, argv?: readonly string[], projectRoot?: string): {
95
+ permissionDecision: 'allow' | 'deny';
96
+ permissionDecisionReason?: string;
97
+ };
98
+ /**
99
+ * @implements A-SPEC-190 (round 8)
100
+ * Which spec directory this hook enforces against. The wiring writes it on the hook's own command
101
+ * line because the hook entry in settings carries no environment: `init --specs-dir docs/specs`
102
+ * reached the MCP server (HOLMES_SPECS in .mcp.json) and NOT the gate, so a project could be wired
103
+ * `governed` while the gate read an empty `.ax/specs` and refused nothing. Precedence is
104
+ * argv > env > default: the wiring is more specific than an ambient variable.
105
+ */
106
+ export declare function wiredSpecsDir(argv: readonly string[], env: NodeJS.ProcessEnv): string;
107
+ /** Default spec root. Used to tell "never governed" apart from an explicitly configured HOLMES_SPECS. */
108
+ export declare const DEFAULT_SPECS_DIR = ".ax/specs";
109
+ export declare function evaluateHook(input: {
110
+ tool_name: string;
111
+ tool_input: {
112
+ file_path?: string;
113
+ notebook_path?: string;
114
+ content?: string;
115
+ new_string?: string;
116
+ command?: string;
117
+ };
118
+ }, specsDir: string, opts: {
119
+ assess?: (a: RiskAction) => RiskAssessment;
120
+ /**
121
+ * @implements A-SPEC-149
122
+ * REQUIRED, `undefined` allowed explicitly. The old `?? readApprovalFromEnv()` fallback let an
123
+ * exported HOLMES_APPROVAL reach the decision: measured, a root with planted debt produced NO
124
+ * denial because `codeWriteCovered` became true, while the same call under `env -u` denied. That
125
+ * failure mode is worse than the cwd one — it makes tests PASS. The out-of-band channel stays;
126
+ * only the entry point reads it.
127
+ */
128
+ approval: Approval | undefined;
129
+ resolveRmSignals?: (command: string) => Record<string, RmSignal> | undefined;
130
+ /**
131
+ * @implements A-SPEC-149
132
+ * REQUIRED. The implicit `process.cwd()` fallback is gone: it made the gate read whichever
133
+ * project the process happened to sit in, so a unit test's verdict depended on the repository's
134
+ * own `.ax/state`. Measured — thirteen tests failed deterministically while constitution debt was
135
+ * recorded here, and the code was correct. A missing root is now a compile error, which is the
136
+ * only way the coupling stops being re-introduced by the next caller.
137
+ */
138
+ projectRoot: string;
139
+ now?: string;
140
+ }): {
141
+ permissionDecision: 'allow' | 'deny';
142
+ permissionDecisionReason?: string;
143
+ };
144
+ export declare function readSpecsSync(root: string): Spec[];
@@ -0,0 +1,91 @@
1
+ import { Spec } from '../spec/spec-parser';
2
+ /**
3
+ * @implements A-SPEC-100.2
4
+ * Stop-hook governance gate (Phase-2 #1: push, not pull).
5
+ *
6
+ * RTM integrity (rtm_check) and spec correctness (spec_validate) were only reachable as MCP tools
7
+ * the model had to VOLUNTARILY invoke — and across a full benchmark it invoked them 0/143 times. A
8
+ * "deterministic control plane" cannot depend on the LLM choosing to call its own governance. This
9
+ * hook runs those checks at every turn boundary (PUSH): if the governed spec set has unresolved RTM
10
+ * or validation errors, it BLOCKS the stop and feeds the findings back so the model must fix them
11
+ * before finishing. No model cooperation required — the governance fires whether or not it is asked.
12
+ *
13
+ * HARDENING H1 (GWT-실테스트 bridge): declared 4-quadrant coverage previously verified only the
14
+ * T-SPEC DOCUMENT. Now, for every approved T-SPEC, its parent A-SPEC must have at least one anchored
15
+ * TEST FILE containing REAL test cases (`it(`/`test(`) — declared coverage over an empty/absent test
16
+ * suite blocks the stop. Evidence is supplied by the CLI (I/O half); evaluateStop stays pure.
17
+ *
18
+ * Pure and testable: evaluateStop takes the loaded spec set (+ optional evidence); the CLI does I/O.
19
+ */
20
+ export interface StopEvidence {
21
+ /** A-SPEC id -> number of real test cases (`it(`/`test(`) found in test files anchored to it. */
22
+ testCasesByAspec?: Record<string, number>;
23
+ /** Provenance-chain verification result (CLI-supplied). A broken chain blocks the stop. */
24
+ provenance?: {
25
+ ok: boolean;
26
+ brokenAt?: number;
27
+ detail?: string;
28
+ };
29
+ /** A-SPEC -> cases that ACTUALLY EXECUTED in a fresh run; authoritative over the syntactic count. */
30
+ executedByAspec?: Record<string, number>;
31
+ /** @implements A-SPEC-160 — recorded findings, for ART-6. @implements A-SPEC-191 — severity rides along for ART-7. */
32
+ findings?: {
33
+ id: string;
34
+ status: 'open' | 'resolved';
35
+ basis?: string;
36
+ severity?: string;
37
+ summary?: string;
38
+ }[];
39
+ /**
40
+ * @implements A-SPEC-191 (§4a)
41
+ * The findings ledger EXISTS but could not be read (any list() throw — ENOENT is absorbed as an
42
+ * empty list upstream). Measured: swallowing this into `undefined` let an open critical pass AND
43
+ * cleared standing ART-7 debt — an unreadable ledger laundering the very state it records.
44
+ */
45
+ findingsUnreadable?: boolean;
46
+ }
47
+ /**
48
+ * @implements A-SPEC-175
49
+ * The turn-boundary answer to "this project is governed but its specs are gone".
50
+ *
51
+ * Extracted so it is reachable by a test: the runtime block below only runs under
52
+ * `require.main === module`, and a check that lives only there is a check nothing can mutate against.
53
+ * Returns the reason to block with, or null when there is nothing to report.
54
+ */
55
+ export declare function governanceLostPreflight(specsDir: string, projectRoot?: string): string | null;
56
+ export declare function evaluateStop(specs: Spec[], evidence?: StopEvidence): {
57
+ block: boolean;
58
+ reason?: string;
59
+ articles?: string[];
60
+ };
61
+ /**
62
+ * @implements A-SPEC-134
63
+ * Pure decision for the constitution-debt state at a turn boundary. Replaces the Stop gate's silent
64
+ * cap-yield: a clean turn clears any standing debt; a cap-yield (the gate giving up its block after
65
+ * MAX_CONSECUTIVE_BLOCKS) RECORDS the unresolved articles as debt so new code is blocked until the
66
+ * constitution is clean again; a normal block (still within the cap) touches nothing.
67
+ */
68
+ export declare function stopDebtAction(evaluation: {
69
+ block: boolean;
70
+ articles?: string[];
71
+ }, guard: {
72
+ block: boolean;
73
+ capped: boolean;
74
+ }): {
75
+ action: 'write' | 'clear' | 'none';
76
+ articles: string[];
77
+ };
78
+ export declare const MAX_CONSECUTIVE_BLOCKS = 3;
79
+ export declare function decideStopGuard(wantsBlock: boolean, priorConsecutiveBlocks: number, cap?: number): {
80
+ block: boolean;
81
+ nextCount: number;
82
+ capped: boolean;
83
+ };
84
+ /** Test seam for the same reason as the two exports below — the CLI sets this on a real run. */
85
+ export declare function __setWiredSpecsForTest(v: string | undefined): void;
86
+ export declare function readGuardCount(sessionId: string): number;
87
+ /**
88
+ * Exported for the §19 race discriminator: the persistence layer was the untested half (round-11),
89
+ * and a test that can only reach it through a spawned hook cannot pin what the lock does.
90
+ */
91
+ export declare function writeGuardCount(sessionId: string, n: number): boolean;
@@ -0,0 +1,100 @@
1
+ /**
2
+ * What a computation was based on.
3
+ *
4
+ * Measured 2026-08-08: the live MCP server answered `impactedSpecs: []` for a commit that touched 39
5
+ * specs, while the same handler in a fresh process answered correctly. The server had been running
6
+ * since 8/5 against a 8/8 build. Nothing in the response said which build, which scan generation, or
7
+ * which graph produced it — so a reviewer had no way to distrust it, and `doctor` reported
8
+ * "handshake ok".
9
+ *
10
+ * The fix is not staleness detection. That catches one cause; the next divergence (a graph
11
+ * generation, a spec corpus) slips through the same way. Carrying the basis makes divergence a FACT
12
+ * the caller reads rather than a verdict the server renders.
13
+ */
14
+ export interface Basis {
15
+ /** The build this process loaded, read once at start-up. `unknown` when unstamped. */
16
+ loadedBuild: string;
17
+ /** What is on disk NOW. Present only in the detail — see `collectBasis`. */
18
+ diskBuild?: string;
19
+ /**
20
+ * Fingerprint of the scan artefact. `undefined` when the axis could not be measured.
21
+ *
22
+ * A fingerprint, not a file count: measured 2026-08-08, `cachedScan` costs 69ms on this repo, and
23
+ * charging that to every one of the tools on every call would break this module's own constant-I/O
24
+ * requirement. A stat over the scan cache costs nothing and changes whenever the scan does, which
25
+ * is all divergence detection needs.
26
+ */
27
+ scanFp?: number;
28
+ specs?: number;
29
+ }
30
+ /**
31
+ * There is deliberately NO graph axis. `RtmGraph` is in-memory only (ADR-011 — a derived index,
32
+ * rebuilt every time), so the graph is a FUNCTION of specs and scan and cannot diverge from them
33
+ * independently. Including it would mean either an always-`?` field or a rebuild per call: token
34
+ * cost with no information in the first case, a constant-I/O violation in the second.
35
+ */
36
+ /**
37
+ * Cap on the digest. A basis rides on every one of the tools, so its size multiplies across a
38
+ * session — this bound is what keeps "carry the basis" compatible with REQ-122's requirement that
39
+ * the harness be net-token-positive, rather than opposed to it.
40
+ */
41
+ export declare const MAX_DIGEST_LEN = 64;
42
+ /**
43
+ * Cap on the build id alone. Truncating the whole digest at the end would eat the trailing axes, so
44
+ * a long build id could make `x:` disappear entirely and the digest would read as if that axis had
45
+ * never existed — the exact silent-omission failure `?` exists to prevent. Bounding the variable
46
+ * part instead keeps the structure intact whatever the stamp looks like.
47
+ */
48
+ export declare const MAX_BUILD_LEN = 32;
49
+ /**
50
+ * @implements A-SPEC-160
51
+ * Appended when the loaded and on-disk builds are BOTH known and differ.
52
+ *
53
+ * This is an observation, not a verdict, and the distinction matters: REQ-156 deliberately refused to
54
+ * let the server decide whether it was stale, because that logic would be running inside the stale
55
+ * build. Reporting that two measured values disagree is a fact; what to do about it is ART-6's call.
56
+ *
57
+ * An unknown disk build never carries the marker. Reading absence as divergence would fire on every
58
+ * tool but `review_record` — none of the others read disk — and an article that fires constantly is
59
+ * one people route around within a day.
60
+ */
61
+ export declare const BASIS_DIVERGED = "/!";
62
+ /** Reads the marker back out of a sealed digest. */
63
+ export declare function basisDiverged(digest: string | undefined): boolean;
64
+ /**
65
+ * The build identity of the code that is RUNNING. Read once at start-up by the server, never per
66
+ * call: hashing `dist` on every request buys I/O for no new information, and mtime changes on a
67
+ * checkout or a copy, which would manufacture divergence that is not there.
68
+ *
69
+ * An unstamped build is reported as `unknown` rather than guessed. Absence is a fact too, and a
70
+ * fabricated id would be worse than none — it would look like agreement.
71
+ */
72
+ export declare function loadedBuildId(root: string): string;
73
+ export interface BasisContext {
74
+ scanFp?: () => number;
75
+ specs?: () => number;
76
+ /** Project root, for reading the on-disk build id. Omit to skip that axis. */
77
+ root?: string;
78
+ /** The build this process loaded. Injected so the server reads it exactly once. */
79
+ loadedBuild?: string;
80
+ }
81
+ export declare function collectBasis(ctx: BasisContext): Basis;
82
+ /**
83
+ * `b:<build>/s:<scanFp>/x:<specs>`
84
+ *
85
+ * Short by design: a caller compares the value against another and learns that something diverged
86
+ * without needing to interpret any field. An unmeasured axis renders as `?` rather than being
87
+ * dropped — omitting it would let a reader take "these two matched" for agreement when the third
88
+ * was never measured at all.
89
+ */
90
+ export declare function basisDigest(b: Basis): string;
91
+ /**
92
+ * Attaches a basis to a handler's result.
93
+ *
94
+ * Exported and pure so the contract is testable without a handler that exercises each branch. The
95
+ * array guard in particular has no live caller today — every tool returns an object — and an
96
+ * untested guard is one a later change deletes for free. Spreading an array into an object yields
97
+ * `{0:…, 1:…, basis}`, silently reshaping a response into something no caller can read, so the
98
+ * branch has to survive on its own merits rather than on nobody having tried it yet.
99
+ */
100
+ export declare function withBasis<A>(fn: (a: A) => Promise<unknown>, basisOf: (a: A) => Basis): (a: A) => Promise<unknown>;