@holmes-lab/holmes-kit 0.1.6 → 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 +14 -0
  2. package/README.md +13 -9
  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 +5 -4
@@ -0,0 +1,154 @@
1
+ import { LockOptions } from './ledger-lock';
2
+ /**
3
+ * N3 — Decision PROVENANCE chain (target-architecture §7-N N3): "누가·언제·왜"의 완전 인과 재구성.
4
+ *
5
+ * Upgrades the L4 ledgers from plain append-only JSONL to a TAMPER-EVIDENT HASH CHAIN: every event
6
+ * carries `prevHash` and `hash = sha256(prevHash | canonical(event))`, so any rewrite, deletion, or
7
+ * reordering of history breaks verification at the exact seam. Honest naming: this is tamper-EVIDENT
8
+ * (a forger cannot alter past events without detection), not a cryptographic SIGNATURE — there is no
9
+ * key infrastructure, so a forger who rewrites the ENTIRE chain from the tampered point forward would
10
+ * pass verification; the defense against that is the write-gate on `.ax/ledger` (HIGH-3 hardening)
11
+ * plus out-of-band copies. Full signing (HMAC/asymmetric) is a documented future step.
12
+ *
13
+ * Each event is a Decision Event node: consumed inputs (evidence refs), rationale, actor, timestamp,
14
+ * and the authorization that unlocked it — the GAIE "Decision Event" concept realized on the RTM side.
15
+ *
16
+ * Pure half: canonicalize / computeHash / chainNext / verifyChain (caller supplies ts; no clock).
17
+ * I/O half: ProvenanceChain over a JSONL file, ENOENT -> empty, other read errors THROW (silently
18
+ * returning [] would erase the audit trail this exists to guarantee — same policy as DecisionLedger).
19
+ */
20
+ export interface ProvenanceEvent {
21
+ seq: number;
22
+ ts: string;
23
+ actor: string;
24
+ kind: string;
25
+ summary: string;
26
+ inputs?: string[];
27
+ rationale?: string;
28
+ authorization?: string;
29
+ prevHash: string;
30
+ hash: string;
31
+ }
32
+ type ProvenanceBody = Omit<ProvenanceEvent, 'prevHash' | 'hash'>;
33
+ export declare const GENESIS = "GENESIS";
34
+ /** Ordered-key serialization so identical bodies always produce identical bytes (hash stability). */
35
+ export declare function canonicalize(b: ProvenanceBody): string;
36
+ /**
37
+ * Event hash. WITHOUT a key: plain sha256 chain (tamper-EVIDENT — a full rewrite from point k still
38
+ * passes, the documented no-key limit). WITH an out-of-band key (HOLMES_LEDGER_KEY env, same channel
39
+ * discipline as HOLMES_APPROVAL): HMAC-SHA256 — a forger without the key cannot recompute ANY link,
40
+ * so even the full-rewrite attack fails verification. Key presence upgrades the guarantee from
41
+ * tamper-evident to tamper-PROOF-against-keyless-forgers; absence degrades gracefully to the chain.
42
+ */
43
+ export declare function computeHash(prevHash: string, body: ProvenanceBody, key?: string): string;
44
+ /** Build the next chained event from the current chain tail (pure). */
45
+ export declare function chainNext(chain: ProvenanceEvent[], body: Omit<ProvenanceBody, 'seq'>, key?: string): ProvenanceEvent;
46
+ /**
47
+ * Verify the whole chain: recomputed hashes, prevHash linkage, contiguous seq, AND no unknown fields.
48
+ * The unknown-field check closes the canonicalization-gap attack (adversarial review #2): canonicalize
49
+ * hashes only the known fields, so a tamperer could smuggle forged data in an EXTRA field that the
50
+ * hash ignores but a consumer might read — any field outside the schema now fails verification.
51
+ * Returns the FIRST broken seam so remediation is addressable. An empty chain is trivially valid.
52
+ */
53
+ export declare function verifyChain(chain: ProvenanceEvent[], key?: string): {
54
+ ok: boolean;
55
+ brokenAt?: number;
56
+ detail?: string;
57
+ };
58
+ /**
59
+ * Reference an approval credential WITHOUT persisting it: a short sha256 fingerprint identifies which
60
+ * token authorized an action while the ledger never stores the replayable secret (adversarial review
61
+ * #1 — the raw token in an audit file meant any local reader could harvest live gate-unlock
62
+ * credentials).
63
+ */
64
+ export declare function authorizationRef(actor: string, token: string): string;
65
+ /**
66
+ * Redact a tool's target before it enters the audit trail.
67
+ *
68
+ * The chain used to store the raw command text. Audited on this repository's own ledger: 29 records,
69
+ * 15 carrying absolute home paths and 6 matching secret-ish patterns. The hazard is structural, not
70
+ * incidental — the gate DENIES commands like `echo $HOLMES_APPROVAL` and `HOLMES_LEDGER_KEY=…`, and
71
+ * a denial is exactly what gets recorded, so the ledger is the one place guaranteed to collect the
72
+ * secrets the gate exists to protect. Committing it would publish them irreversibly.
73
+ *
74
+ * Same principle `authorizationRef` already applies to tokens: fingerprint, never store.
75
+ *
76
+ * - A FILE PATH is kept, because "which file was touched" is the audit question, but normalised to
77
+ * repo-relative; anything outside the repository becomes a placeholder rather than a host path.
78
+ * - A COMMAND is never kept. Its digest is recorded instead, so identical attempts remain
79
+ * correlatable across records without the text being recoverable. Nothing is lost for audit: the
80
+ * event's `summary` already carries the human-readable classification ("hard-hitl risk: git push
81
+ * --force rewrites remote history"), which is what a reader actually needs.
82
+ */
83
+ export declare function redactTarget(kind: 'path' | 'command', value: string, repoRoot?: string): string;
84
+ export declare function blankNonce(nonce: unknown): boolean;
85
+ /** Declared at all — the operand for "does this grant claim single use?" (falsy values included). */
86
+ export declare function nonceDeclared(nonce: unknown): boolean;
87
+ export declare function nonceFingerprint(nonce: string): string;
88
+ export declare function approvalMarkers(approval: {
89
+ scope?: {
90
+ kind: string;
91
+ pattern: string;
92
+ }[];
93
+ expires?: string;
94
+ nonce?: string;
95
+ } | undefined): string;
96
+ export declare class ProvenanceChain {
97
+ private readonly file;
98
+ constructor(file: string);
99
+ /** Load all events. ENOENT -> []; any other error throws (audit trail must not silently vanish). */
100
+ load(): ProvenanceEvent[];
101
+ /** Append a new event chained onto the CURRENT on-disk tail. Returns the appended event.
102
+ * The signing key comes from HOLMES_LEDGER_KEY (out-of-band env, never tool-settable in-session).
103
+ *
104
+ * @implements A-SPEC-141
105
+ * The read-and-write span runs under exclusion. Measured without it: two processes read the same
106
+ * tail, both wrote events claiming the same `seq`, and `verify()` then reported `ok:false` —
107
+ * tampering that never happened, 17 times out of 20. */
108
+ append(body: Omit<ProvenanceBody, 'seq'>): ProvenanceEvent;
109
+ /**
110
+ * Append assuming exclusion is ALREADY held. Only for callers running inside `withLedgerLock` on
111
+ * this same file — `consumeNonceExclusively` needs the read and the write in one hold, and calling
112
+ * `append()` from there would deadlock against its own budget.
113
+ */
114
+ appendInsideLock(body: Omit<ProvenanceBody, 'seq'>): ProvenanceEvent;
115
+ /** The append itself, assuming exclusion is already held. Only callers INSIDE a hold may use it. */
116
+ private appendUnlocked;
117
+ /**
118
+ * Records a broken stale hold into the chain itself. Bound so it can be handed to the lock layer,
119
+ * which must not import this module (the lock is the lower layer).
120
+ *
121
+ * Written WITHOUT re-entering the lock: this fires from inside the acquire loop, where the hold has
122
+ * just been released and not yet re-taken, so calling `append()` here would deadlock against the
123
+ * budget. The event may therefore race a concurrent writer, which is acceptable for a diagnostic —
124
+ * a missing note is better than a wedged harness, and `verify()` still reports any resulting seam.
125
+ */
126
+ staleBreakRecorder(): NonNullable<LockOptions['onStaleBreak']>;
127
+ private recordStaleBreak;
128
+ verify(): {
129
+ ok: boolean;
130
+ brokenAt?: number;
131
+ detail?: string;
132
+ };
133
+ }
134
+ /** Default chain location, beside the other L4 ledgers (protected surface). */
135
+ export declare const PROVENANCE_FILE: string;
136
+ /**
137
+ * Spend a single-use approval, atomically. Returns `true` iff THIS caller spent it.
138
+ *
139
+ * @implements A-SPEC-141
140
+ * THIS is the operation REQ-141 exists for. The gate used to ask `isNonceConsumed(...)` and only
141
+ * afterwards append the consumption record, so two agents both observed "not consumed" and both
142
+ * proceeded: measured, a single-use approval for a destructive command was honoured TWICE in 20 of
143
+ * 20 barrier-synchronized trials. Locking the append alone would not have helped — the unguarded
144
+ * span is the READ followed by the WRITE, so both must live inside one hold.
145
+ *
146
+ * A loser writes NOTHING. Recording a consumption the caller was refused would put a second
147
+ * `nonce-consumed` event in the audit trail for an action that never happened.
148
+ *
149
+ * Throws `LedgerLockError` when exclusion cannot be obtained. The direction to fail is the CALLER's
150
+ * decision, not this function's: an authorization gate must deny, while a record-keeping append must
151
+ * not flip a decision that was already made.
152
+ */
153
+ export declare function consumeNonceExclusively(nonce: string, ledgerFile: string, body: Omit<ProvenanceBody, 'seq'>, opts?: LockOptions): boolean;
154
+ export {};
@@ -0,0 +1,102 @@
1
+ import { ProvenanceChain, ProvenanceEvent } from './provenance-chain';
2
+ /**
3
+ * The ledger as a SET of per-replica chains, read together and written one at a time.
4
+ *
5
+ * WHY, measured: ADR-012 made `provenance.jsonl` a shared SSOT tracked in git, explicitly so a
6
+ * colleague who clones can answer "who decided this, when, and why". That decision opens a use case
7
+ * the format cannot survive — two machines appending to one chain and merging produces
8
+ * `{"ok":false,"detail":"seq gap: expected 2, found 1"}`. Tamper detection fires when nobody
9
+ * tampered, and because `isBaselineTrusted` requires an intact chain, baseline trust collapses with
10
+ * it. REQ-141 solved the same shape WITHIN one machine with a lock; across machines there is no
11
+ * shared filesystem to lock.
12
+ *
13
+ * The primitive is deliberately untouched. `ProvenanceChain` remains "one file, one chain", so the
14
+ * legacy `provenance.jsonl` keeps loading and verifying with no migration at all — this change obeys
15
+ * the ADR-013 tier-① rule it was written under — and REQ-141's per-file lock keeps working unchanged.
16
+ *
17
+ * WHAT IS LOST: the globally monotonic `seq`, and with it a total order proven by hashes. After the
18
+ * split that proof holds only within a replica.
19
+ *
20
+ * WHAT REPLACES IT: git. These files are tracked (ADR-012), any deletion or rewrite shows up in
21
+ * history, and a git commit is itself a hash chain. The cross-replica anchor does not disappear; it
22
+ * moves one layer up. That is why splitting is not the same as giving up detection.
23
+ */
24
+ export interface LedgerEvent extends ProvenanceEvent {
25
+ /** Which chain this came from; `null` for the pre-split legacy file. */
26
+ replicaId: string | null;
27
+ }
28
+ export interface LedgerVerifyResult {
29
+ ok: boolean;
30
+ /** Per-chain failures, naming the replica so a report is actionable rather than merely alarming. */
31
+ broken: {
32
+ replicaId: string | null;
33
+ file: string;
34
+ detail?: string;
35
+ brokenAt?: number;
36
+ }[];
37
+ }
38
+ export declare class ProvenanceLedger {
39
+ private readonly dir;
40
+ /**
41
+ * Resolved LAZILY. Constructing a reader must not write anything, and `resolveReplicaId`
42
+ * persists `.ax/state/replica-id` on first use — doing that from `at()` made every read create
43
+ * a `.ax/state/` directory, which changed what `resolveProjectRoot` found in test fixtures and
44
+ * broke ten unrelated tests. A read that leaves a footprint is a defect regardless of what it
45
+ * happens to break.
46
+ */
47
+ private readonly replicaIdFn;
48
+ private indexCache?;
49
+ private constructor();
50
+ private cachedId;
51
+ private get replicaId();
52
+ /**
53
+ * `replicaId` is normally resolved from the machine; tests and CI pass it explicitly. The project
54
+ * root is taken to be the ledger directory's grandparent (`<root>/.ax/ledger`), which is where
55
+ * `.ax/state/replica-id` lives.
56
+ */
57
+ static at(dir: string, replicaId?: string): ProvenanceLedger;
58
+ /**
59
+ * This writer's replica id WITHOUT persisting it.
60
+ *
61
+ * @implements A-SPEC-150
62
+ * Asking "who am I" is a read, and the ledger port's conformance requires a read to leave no
63
+ * trace. `resolveReplicaId` persists by default because the value has to be stable for a WRITER;
64
+ * a reader gets the same answer with `persist: false`. Merely calling this used to create
65
+ * `.ax/state/`, which is the REQ-148 footprint defect returning through a new door.
66
+ */
67
+ replicaIdOf(): string;
68
+ /** Every chain file present, legacy first. `replicaId` is null for the legacy chain. */
69
+ chains(): {
70
+ replicaId: string | null;
71
+ file: string;
72
+ }[];
73
+ /** The file this machine appends to. Never the legacy chain — see `append`. */
74
+ currentFile(): string;
75
+ private computeMtimeKey;
76
+ private cloneEvents;
77
+ /**
78
+ * Every event across every chain, ordered by `(ts, replicaId, seq)`.
79
+ *
80
+ * Deterministic, and deliberately NOT a total causal order: two events on different replicas have
81
+ * no causal relation, and their timestamps differ by whatever the machines' clocks differ by. The
82
+ * ordering exists so output is stable and readable, not so it can be read as "this happened first".
83
+ */
84
+ loadAll(): LedgerEvent[];
85
+ /** Each chain verified on its own; one bad chain fails the whole ledger, and is named. */
86
+ verifyAll(): LedgerVerifyResult;
87
+ /**
88
+ * Append to THIS replica's chain.
89
+ *
90
+ * Never to the legacy file: continuing to write there would put two machines back on one chain,
91
+ * which is the defect this class exists to remove.
92
+ */
93
+ append(body: Parameters<ProvenanceChain['append']>[0]): ProvenanceEvent;
94
+ /**
95
+ * Has this nonce been spent on ANY replica?
96
+ *
97
+ * The trap the split creates: judging from one chain alone lets a single-use approval be spent once
98
+ * per replica, which would resurrect REQ-141's double-spend along a new axis. The question is only
99
+ * meaningful across the whole set.
100
+ */
101
+ isNonceConsumed(nonce: string): boolean;
102
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The provenance ledger's schema version, read from the FILE NAME rather than from the records.
3
+ *
4
+ * WHY the filename and not a field, measured rather than chosen: adding `v` to a record fails
5
+ * verification with `unknown field(s) outside the hashed schema: v (smuggled data)` — the
6
+ * anti-smuggling check an earlier adversarial review added, working exactly as intended. The two
7
+ * ways around it are both worse than the problem. Allowing an exempt field creates one field a
8
+ * forger can write freely, inside the artefact whose whole purpose is tamper evidence. Adding the
9
+ * field to `canonicalize` changes the canonical form of every record, so all 1804 existing entries
10
+ * stop matching their own hashes — a compatibility rule that destroys the tier-① artefact it was
11
+ * written to protect.
12
+ *
13
+ * That leaves the name. It is not a workaround: the replica split is about to rename these files
14
+ * anyway, so the name is already the thing that changes when the schema changes, and reading it
15
+ * costs nothing.
16
+ *
17
+ * - `provenance.jsonl` → 1 (single global chain, contiguous seq)
18
+ * - `provenance.<replicaId>.jsonl` → 2 (per-replica chain, seq local to the replica)
19
+ */
20
+ export type ProvenanceSchema = 1 | 2;
21
+ export declare function provenanceSchemaOf(fileName: string): ProvenanceSchema;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Reduce a raw name to something safe to embed in `provenance.<id>.jsonl`.
3
+ *
4
+ * DOTS ARE THE POINT. macOS hostnames end in `.local`, and the schema rule reads
5
+ * `provenance.<segment>.jsonl` with `<segment>` containing no dot — an unsanitised
6
+ * `SungNamui-MacStudio.local` would produce `provenance.SungNamui-MacStudio.local.jsonl`, which the
7
+ * schema check would not recognise as a replica chain at all. Two rules disagreeing about the same
8
+ * file is worse than either rule being wrong.
9
+ */
10
+ export declare function sanitizeReplicaId(raw: string): string;
11
+ /**
12
+ * Resolve this machine's replica id: explicit env, then the stored value, then the hostname.
13
+ *
14
+ * The env override exists for CI and for tests, where the hostname is neither stable nor meaningful.
15
+ * Writing the generated value down is what makes the identity stable — an id derived fresh each run
16
+ * would scatter one machine's history across several chains.
17
+ */
18
+ export declare function resolveReplicaId(root: string, env?: NodeJS.ProcessEnv, opts?: {
19
+ persist?: boolean;
20
+ }): string;
@@ -0,0 +1,52 @@
1
+ import { Identity } from './identity';
2
+ /**
3
+ * What each role may do, as project-owned governance config.
4
+ *
5
+ * The allow-list entries are the action names `classifyAction` already produces (`WRITE_CODE`,
6
+ * `WRITE_TEST`, …). Inventing a separate vocabulary would let what the gate decides and what a role
7
+ * claims drift apart, and the drift would be invisible until someone was wrongly allowed.
8
+ *
9
+ * OPT-IN. A project with no `.ax/roles/` has no policy and the gate behaves exactly as before —
10
+ * making roles mandatory would deny every existing project at once, which is the bricking REQ-146
11
+ * measured when it tried to make a spec field required.
12
+ *
13
+ * The definition lives under a PROTECTED path. Blocking role claims while leaving the policy
14
+ * writable would be one lock on a door with two entrances: an agent that cannot change its role
15
+ * would simply change what that role is allowed to do.
16
+ */
17
+ export interface RolePolicy {
18
+ roles: Record<string, {
19
+ allow: string[];
20
+ }>;
21
+ /**
22
+ * Which role an UNSPECIFIED agent is treated as. Declared by the project, never assumed by code.
23
+ *
24
+ * That distinction is the whole security argument. If the code defaulted to a full-capability role
25
+ * whenever `HOLMES_ROLE` was absent, every restriction would be bypassable by unsetting one
26
+ * environment variable — the rule that binds a Reviewer would become "don't set it". Because this
27
+ * lives in `.ax/roles/`, which is a PROTECTED path, an agent cannot write it either.
28
+ *
29
+ * Omitting it keeps the fail-closed behaviour: unspecified is refused.
30
+ */
31
+ default?: string;
32
+ }
33
+ /** `null` when the project has not opted in. A PRESENT-but-broken policy is never null — see below. */
34
+ export declare function loadRolePolicy(root: string): RolePolicy | null;
35
+ /** What `doctor` reports. Three states, because "no policy" and "guidance present but off" differ. */
36
+ export interface RolePolicyStatus {
37
+ state: 'absent' | 'inactive' | 'active';
38
+ roles: string[];
39
+ default?: string;
40
+ }
41
+ /**
42
+ * Describes the project's role configuration WITHOUT judging it. Not having roles on is not a
43
+ * defect; reporting it as one would teach operators to ignore doctor, which costs more than it saves.
44
+ */
45
+ export declare function rolePolicyStatus(root: string): RolePolicyStatus;
46
+ /**
47
+ * Whether this identity may perform this action. Returns the refusal reason, or `null` to allow.
48
+ *
49
+ * Fail-closed once a policy exists: an absent role, an unregistered role, and a role without the
50
+ * action are all refusals. Before a policy exists there is no opinion at all.
51
+ */
52
+ export declare function roleCheck(action: string, identity: Identity, policy: RolePolicy | null): string | null;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * L5 — quantitative TRUST SCORE → automation level (target-architecture §7 L5).
3
+ *
4
+ * bkit's trust-score pattern, deterministic: recent governed-work metrics fold into a 0..1 score and
5
+ * a discrete automation LEVEL that callers (risk gate, orchestrators) can use to graduate autonomy —
6
+ * high sustained trust earns wider auto-approval; faults/regressions/critical findings walk it back.
7
+ *
8
+ * L0 observe — read/analyze only
9
+ * L1 propose — may draft changes, human applies
10
+ * L2 gated — may apply changes behind every gate (default posture)
11
+ * L3 trusted — routine (auto-level) actions skip confirm-level friction
12
+ * L4 autonomous— may batch routine work; hard-hitl still ALWAYS gates (never waived — ART-5)
13
+ *
14
+ * Pure and clock-free: the caller aggregates the window (e.g. from the L4 ledgers) into TrustMetrics.
15
+ * Conservative by construction: zero history -> L2 default-gated, never trusted; any fault or
16
+ * regression caps the level regardless of volume (a burst of green cannot buy back a fresh fault).
17
+ */
18
+ export interface TrustMetrics {
19
+ slicesCompleted: number;
20
+ suiteGreenRate: number;
21
+ guardrailDenials: number;
22
+ faults: number;
23
+ regressions: number;
24
+ criticalFindings: number;
25
+ }
26
+ export type AutomationLevel = 'L0' | 'L1' | 'L2' | 'L3' | 'L4';
27
+ export interface TrustAssessment {
28
+ score: number;
29
+ level: AutomationLevel;
30
+ rationale: string;
31
+ }
32
+ export declare function computeTrustScore(m: TrustMetrics): TrustAssessment;
@@ -0,0 +1,8 @@
1
+ export declare function claimedAnchors(content: string): string[];
2
+ /**
3
+ * The anchors this write ADDS, relative to what the file already had.
4
+ *
5
+ * Only the additions are judged. Re-judging anchors already in the file would block every edit to a
6
+ * file whose spec later moved back to draft — including the edits needed to fix exactly that.
7
+ */
8
+ export declare function newlyClaimed(content: string, previous: string | undefined): string[];
@@ -0,0 +1,32 @@
1
+ /**
2
+ * What the guardrail CANNOT see, declared in code rather than prose.
3
+ *
4
+ * The guardrail inspects the command STRING. Anything whose behaviour lives elsewhere — a script
5
+ * file, a variable, stdin — is opaque to it, and no amount of pattern work changes that: what
6
+ * `node ./x.js` does is not statically decidable. Measured 2026-08-08: every entry below reached
7
+ * `allow`.
8
+ *
9
+ * Declared here, not in a document, because a test can read this and assert that each example really
10
+ * is allowed. A limit written in prose drifts silently from the code; twice this session a claim in
11
+ * a comment turned out to be false when measured. Keeping the claim next to an executable check
12
+ * means whoever later closes one of these has to come here and rewrite it.
13
+ *
14
+ * REQ-167 already closed the half that mattered most: WRITING a script now needs an approved spec,
15
+ * so `x.sh` followed by `bash ./x.sh` no longer completes on its own. What remains is running a
16
+ * script that was ALREADY in the repository — code a human committed.
17
+ */
18
+ export interface BlindSpot {
19
+ /** A command the gate allows, verbatim, so a test can check the claim. */
20
+ example: string;
21
+ /** Why it is opaque. */
22
+ why: string;
23
+ }
24
+ export declare const GATE_BLIND_SPOTS: readonly BlindSpot[];
25
+ /**
26
+ * One line for `doctor`.
27
+ *
28
+ * Deliberately neither "the gate stops everything" nor "the gate stops nothing". The first gets
29
+ * someone hurt; the second makes a reader distrust the parts that do work — and after REQ-167 those
30
+ * parts include creating the script in the first place.
31
+ */
32
+ export declare function blindSpotSummary(): string;
@@ -0,0 +1,29 @@
1
+ import { RiskAction, RiskLevel } from './risk-types';
2
+ export interface DecisionEvent {
3
+ ts: string;
4
+ actor: string;
5
+ action: RiskAction;
6
+ level: RiskLevel;
7
+ decision: 'auto' | 'approved' | 'proceeded-unconfirmed' | 'blocked' | 'rejected';
8
+ rationale: string;
9
+ reasons: string[];
10
+ }
11
+ /**
12
+ * Append-only, provenance audit trail for riskGate decisions. Each call to
13
+ * `record` appends one JSON line per event to a `.jsonl` file -- never
14
+ * truncated, never rewritten -- so the file itself is the durable evidence
15
+ * trail.
16
+ *
17
+ * Timestamps and ids are caller-supplied: this ledger has no clock, so
18
+ * recording is fully deterministic given its inputs. Ledger I/O is
19
+ * deliberately separate from riskGate's decision -- a write failure here
20
+ * must never change a gate verdict that was already computed.
21
+ */
22
+ export declare class DecisionLedger {
23
+ private readonly path;
24
+ constructor(path: string);
25
+ record(events: DecisionEvent[]): void;
26
+ list(): DecisionEvent[];
27
+ /** Ordered-object serialization so identical events always produce identical bytes. */
28
+ private static serialize;
29
+ }
@@ -0,0 +1 @@
1
+ export declare function isExecutableArtifact(relPath: string, content?: string): boolean;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The two ways out, and why the project is being judged governed at all.
3
+ *
4
+ * Shared by both hooks so they cannot describe the same situation differently. Naming only the
5
+ * restore path would be no guidance to a user whose actual intent is to stop being governed — they
6
+ * would keep deleting things until it stopped.
7
+ */
8
+ export declare const GOVERNANCE_LOST_HINT: string;
9
+ /**
10
+ * True if any provenance replica records a spec approval.
11
+ *
12
+ * Fails toward `true` on anything unreadable: if "cannot read" meant "never governed", corrupting a
13
+ * ledger file would be a way to switch the gate off — worse than the hole this closes.
14
+ */
15
+ export declare function hasGovernanceHistory(root: string): boolean;
@@ -0,0 +1,27 @@
1
+ import { Spec } from '../spec/spec-parser';
2
+ export declare const ACTIONS: readonly ["AUTHOR_REQ", "AUTHOR_HSPEC", "AUTHOR_ASPEC", "AUTHOR_CSPEC", "AUTHOR_TSPEC", "WRITE_TEST", "WRITE_CODE"];
3
+ export type Action = (typeof ACTIONS)[number];
4
+ export interface CheckResult {
5
+ decision: 'allow' | 'deny';
6
+ phase?: string;
7
+ missing?: string[];
8
+ remediation?: {
9
+ message: string;
10
+ next_action: string;
11
+ discipline: string;
12
+ };
13
+ }
14
+ /**
15
+ * Classify a write target. `p` must be PROJECT-RELATIVE (POSIX or Windows separators both fine).
16
+ *
17
+ * Relative matters for the test rules: several of them key on a directory segment, so an absolute
18
+ * path would let a project checked out under `/…/test/` classify its production code as tests —
19
+ * DOWNGRADING the gate, since WRITE_TEST does not require an approved T-SPEC. Callers that hold an
20
+ * absolute path make it relative first (see evaluateHook).
21
+ */
22
+ export declare function classifyAction(p: string, content?: string): Action | null;
23
+ export declare function phaseCheck(action: Action, ctx: {
24
+ specs: Spec[];
25
+ targetParentReqId?: string;
26
+ targetAspecId?: string;
27
+ }): CheckResult;
@@ -0,0 +1,52 @@
1
+ import { AxisVerdict, RiskAction, RiskAssessment, RiskThresholds } from './risk-types';
2
+ export declare function rmCommandTargets(cmd: string): string[];
3
+ /**
4
+ * Neutralise `.ax` paths that belong to ANOTHER project before the protected-path rules run.
5
+ *
6
+ * `.ax/specs` is this project's governance directory, and deleting it must stay non-bypassable. But
7
+ * the rules match the substring anywhere in a command, so `rm /other/project/.ax/cpg_cache/x.json`
8
+ * was gated too — found by dogfooding while cleaning up a scan cache left in a DIFFERENT repository,
9
+ * which is exactly what a user juggling several projects does every day. The governance claim only
10
+ * extends to this project's own `.ax`.
11
+ *
12
+ * Only ABSOLUTE paths outside the project root are masked. A relative `.ax/...` resolves against the
13
+ * project and stays protected, so nothing is weakened for the case the rule exists to cover. With no
14
+ * projectRoot supplied the command is returned unchanged — unknown scope stays conservative.
15
+ */
16
+ export declare function scopeAxToProject(cmd: string, projectRoot?: string): string;
17
+ /**
18
+ * Normalize a command string for protected-path matching: collapse `//` runs and `./` segments so
19
+ * `./.ax//ledger/x` matches the same rules as `.ax/ledger/x` (adversarial probe: those forms slipped
20
+ * every protected-path regex). Also flattens a leading `cd <dir> &&` so a cd-relative write
21
+ * (`cd .ax/ledger && cat > provenance.jsonl`) is seen against its real directory.
22
+ */
23
+ export declare function normalizeCommandPaths(cmd: string): string;
24
+ /**
25
+ * @implements A-SPEC-125.1
26
+ * Reversibility axis: can the action's effect be undone? Conservative default —
27
+ * unknown kinds and ambiguous (tracked===undefined) states never resolve to 'auto'.
28
+ */
29
+ export declare function classifyReversibility(action: RiskAction): AxisVerdict;
30
+ /**
31
+ * @implements A-SPEC-125.1
32
+ * Architecture axis: does this action reverse a previously accepted architectural decision?
33
+ */
34
+ export declare function classifyArchitectureReversal(action: RiskAction): AxisVerdict;
35
+ /**
36
+ * @implements A-SPEC-125.1
37
+ * Secret axis: does the staged content contain a credential-shaped string?
38
+ */
39
+ export declare function classifySecret(action: RiskAction): AxisVerdict;
40
+ /**
41
+ * @implements A-SPEC-125.3
42
+ * Blast-radius axis: how far does this change's impact ripple through the spec graph?
43
+ * The graph impact signal (`action.blastRadius`) is INJECTED by the caller — this function
44
+ * remains pure, performing no graph traversal or I/O of its own.
45
+ */
46
+ export declare function classifyBlastRadius(action: RiskAction, cfg?: RiskThresholds): AxisVerdict;
47
+ /**
48
+ * @implements A-SPEC-125.1
49
+ * Runs all four axes and folds them into a single overall risk assessment.
50
+ * Pure function: no I/O, no clocks, no randomness — same input always yields the same output.
51
+ */
52
+ export declare function assessRisk(action: RiskAction, cfg?: RiskThresholds): RiskAssessment;
@@ -0,0 +1,77 @@
1
+ import { RiskAssessment, RiskLevel } from './risk-types';
2
+ export type Enforcement = 'block' | 'warn' | 'off';
3
+ export interface Approval {
4
+ actor: string;
5
+ token: string;
6
+ rationale: string;
7
+ /** If present, the approval authorizes ONLY actions matching some entry (deny-by-default within). */
8
+ scope?: {
9
+ kind: string;
10
+ pattern: string;
11
+ }[];
12
+ /** ISO 8601. If present, the approval is dead once the action timestamp passes it. */
13
+ expires?: string;
14
+ /** If present, a hard-hitl grant is single-use: consumed on the ledger, a replay is denied. */
15
+ nonce?: string;
16
+ }
17
+ export interface GateResult {
18
+ blocked: boolean;
19
+ level: RiskLevel;
20
+ requiresApproval: boolean;
21
+ reasons: string[];
22
+ }
23
+ /** All of actor/token/rationale must be present and non-empty (whitespace-only counts as empty). */
24
+ export declare function isValidApproval(a?: Approval): boolean;
25
+ /**
26
+ * @implements A-SPEC-133
27
+ * Does this well-formed approval AUTHORIZE this specific action, right now? This is the seam P2
28
+ * adds beside isValidApproval: the gates ask "covers?" instead of "well-formed?", so one token
29
+ * stops being a master key. Pure — `now` is passed in (the action timestamp), never read from the
30
+ * clock, so the predicate stays deterministic and testable.
31
+ *
32
+ * - malformed → false (isValidApproval's meaning is preserved).
33
+ * - expired (epoch comparison, both operands must parse; unreadable expires OR now → false). Absent expires → no expiry.
34
+ * - scope present → true iff some entry FULL-matches (kind exact or `*`; the pattern is anchored
35
+ * at both ends, `*` a wildcard run, trailing `/` a directory prefix). No entry matches → false: an existing-but-empty scope denies
36
+ * everything, which is the safe reading of "scoped but matches nothing".
37
+ * - scope absent → true: the documented UNSCOPED master-key path (the operator's session token).
38
+ */
39
+ export declare function approvalCovers(a: Approval | undefined, action: {
40
+ kind: string;
41
+ target: string;
42
+ }, now: string): boolean;
43
+ /**
44
+ * @implements A-SPEC-133
45
+ * Has this single-use nonce already been spent? Sync-reads the append-only provenance chain for a
46
+ * `nonce-consumed` entry carrying the nonce. A missing/unreadable ledger means nothing was consumed
47
+ * yet — false, never a throw (the gate must not crash on a fresh project).
48
+ */
49
+ export declare function isNonceConsumed(nonce: string, ledgerFile: string): boolean;
50
+ /**
51
+ * Graduated response for a bare risk level, with no notion of approval.
52
+ * Pure: no I/O, no clock, no randomness.
53
+ *
54
+ * `enforcement: 'off'` softens `confirm` to a clean pass, but MUST NOT soften
55
+ * `hard-hitl` -- that is the non-bypassable minimum this gate exists to guarantee.
56
+ */
57
+ export declare function gradedResponse(level: RiskLevel, enforcement: Enforcement): GateResult;
58
+ /**
59
+ * @implements A-SPEC-125.2
60
+ * Pure decision function: given a risk assessment, an optional out-of-band
61
+ * approval, and an enforcement mode, decides whether the action is blocked.
62
+ *
63
+ * No I/O and no ledger writes happen here -- recording a DecisionEvent is a
64
+ * separate caller step (see decision-ledger.ts), so a ledger write failure
65
+ * can never change the gate verdict.
66
+ *
67
+ * hard-hitl is non-bypassable: only a valid Approval (actor+token+rationale,
68
+ * all non-empty) unblocks it. enforcement:'off' softens confirm to a pass,
69
+ * but never softens hard-hitl.
70
+ */
71
+ export declare function riskGate(assessment: RiskAssessment, approval?: Approval, enforcement?: Enforcement, cover?: {
72
+ action: {
73
+ kind: string;
74
+ target: string;
75
+ };
76
+ now: string;
77
+ }): GateResult;