@holmes-lab/holmes-kit 0.19.6 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +8 -1
  3. package/dist/.build-id +1 -1
  4. package/dist/holmes/cli/approve-status.d.ts +24 -0
  5. package/dist/holmes/cli/approve-status.js +166 -0
  6. package/dist/holmes/cli/approve-watch.d.ts +6 -0
  7. package/dist/holmes/cli/approve-watch.js +6 -0
  8. package/dist/holmes/cli/approve.d.ts +27 -0
  9. package/dist/holmes/cli/approve.js +68 -7
  10. package/dist/holmes/cli/cli-execution.d.ts +2 -0
  11. package/dist/holmes/cli/cli-execution.js +24 -0
  12. package/dist/holmes/cli/gitignore-merge.d.ts +0 -5
  13. package/dist/holmes/cli/gitignore-merge.js +6 -2
  14. package/dist/holmes/cli/index.js +176 -139
  15. package/dist/holmes/cli/playbook-skills.d.ts +2 -22
  16. package/dist/holmes/cli/playbook-skills.js +10 -54
  17. package/dist/holmes/governance/approval-grants.d.ts +22 -0
  18. package/dist/holmes/governance/approval-grants.js +55 -1
  19. package/dist/holmes/governance/approval-queue.d.ts +43 -4
  20. package/dist/holmes/governance/approval-queue.js +86 -12
  21. package/dist/holmes/governance/display-id-aliases.d.ts +29 -0
  22. package/dist/holmes/governance/display-id-aliases.js +86 -0
  23. package/dist/holmes/governance/ledger-store.d.ts +42 -14
  24. package/dist/holmes/governance/ledger-store.js +49 -3
  25. package/dist/holmes/governance/ledger-timeline.d.ts +3 -1
  26. package/dist/holmes/governance/ledger-timeline.js +11 -1
  27. package/dist/holmes/governance/provenance-chain.d.ts +2 -0
  28. package/dist/holmes/governance/provenance-chain.js +13 -2
  29. package/dist/holmes/governance/provenance-ledger.d.ts +28 -0
  30. package/dist/holmes/governance/provenance-ledger.js +69 -16
  31. package/dist/holmes/governance/session-context.d.ts +3 -0
  32. package/dist/holmes/governance/session-context.js +26 -8
  33. package/dist/holmes/guardrail/risk-classifier.js +2 -2
  34. package/dist/holmes/guardrail/write-target.js +2 -1
  35. package/dist/holmes/hooks/pre-tool-use.js +12 -1
  36. package/dist/holmes/hooks/stop.js +14 -3
  37. package/dist/holmes/mcp/handlers/adoption.d.ts +118 -0
  38. package/dist/holmes/mcp/handlers/adoption.js +186 -0
  39. package/dist/holmes/mcp/handlers/entity-integration.d.ts +100 -0
  40. package/dist/holmes/mcp/handlers/entity-integration.js +324 -0
  41. package/dist/holmes/mcp/handlers/entity-renumber.d.ts +137 -0
  42. package/dist/holmes/mcp/handlers/entity-renumber.js +228 -0
  43. package/dist/holmes/mcp/handlers/entity-store.d.ts +78 -0
  44. package/dist/holmes/mcp/handlers/entity-store.js +212 -0
  45. package/dist/holmes/mcp/handlers/graph-operations.d.ts +90 -0
  46. package/dist/holmes/mcp/handlers/graph-operations.js +260 -0
  47. package/dist/holmes/mcp/handlers/maintenance-analysis.d.ts +34 -0
  48. package/dist/holmes/mcp/handlers/maintenance-analysis.js +288 -0
  49. package/dist/holmes/mcp/handlers/maintenance-evidence.d.ts +40 -0
  50. package/dist/holmes/mcp/handlers/maintenance-evidence.js +104 -0
  51. package/dist/holmes/mcp/handlers/operator-inspection.d.ts +167 -0
  52. package/dist/holmes/mcp/handlers/operator-inspection.js +284 -0
  53. package/dist/holmes/mcp/handlers/phase-queries.d.ts +34 -0
  54. package/dist/holmes/mcp/handlers/phase-queries.js +86 -0
  55. package/dist/holmes/mcp/handlers/review-evidence.d.ts +47 -0
  56. package/dist/holmes/mcp/handlers/review-evidence.js +370 -0
  57. package/dist/holmes/mcp/handlers/review-queries.d.ts +68 -0
  58. package/dist/holmes/mcp/handlers/review-queries.js +79 -0
  59. package/dist/holmes/mcp/handlers/risk-assessment.d.ts +25 -0
  60. package/dist/holmes/mcp/handlers/risk-assessment.js +181 -0
  61. package/dist/holmes/mcp/handlers/slice-orchestration.d.ts +55 -0
  62. package/dist/holmes/mcp/handlers/slice-orchestration.js +324 -0
  63. package/dist/holmes/mcp/handlers/spec-approval.d.ts +105 -0
  64. package/dist/holmes/mcp/handlers/spec-approval.js +504 -0
  65. package/dist/holmes/mcp/handlers/spec-authoring.d.ts +57 -0
  66. package/dist/holmes/mcp/handlers/spec-authoring.js +326 -0
  67. package/dist/holmes/mcp/handlers/spec-lifecycle.d.ts +122 -0
  68. package/dist/holmes/mcp/handlers/spec-lifecycle.js +339 -0
  69. package/dist/holmes/mcp/handlers/spec-queries.d.ts +30 -0
  70. package/dist/holmes/mcp/handlers/spec-queries.js +74 -0
  71. package/dist/holmes/mcp/handlers/taint-screening.d.ts +48 -0
  72. package/dist/holmes/mcp/handlers/taint-screening.js +183 -0
  73. package/dist/holmes/mcp/handlers/test-execution.d.ts +63 -0
  74. package/dist/holmes/mcp/handlers/test-execution.js +210 -0
  75. package/dist/holmes/mcp/handlers/workspace-identity.d.ts +30 -0
  76. package/dist/holmes/mcp/handlers/workspace-identity.js +68 -0
  77. package/dist/holmes/mcp/handlers/workspace-queries.d.ts +77 -0
  78. package/dist/holmes/mcp/handlers/workspace-queries.js +193 -0
  79. package/dist/holmes/mcp/handlers.d.ts +653 -424
  80. package/dist/holmes/mcp/handlers.js +267 -3250
  81. package/dist/holmes/mcp/server.js +2 -1
  82. package/dist/holmes/mcp/tool-schemas.js +43 -2
  83. package/dist/holmes/project/execution-context.d.ts +17 -0
  84. package/dist/holmes/project/execution-context.js +120 -0
  85. package/dist/holmes/project/installer-markers.d.ts +34 -0
  86. package/dist/holmes/project/installer-markers.js +65 -0
  87. package/dist/holmes/project/root.d.ts +3 -1
  88. package/dist/holmes/project/root.js +4 -3
  89. package/dist/holmes/project/workspace-identity.d.ts +29 -0
  90. package/dist/holmes/project/workspace-identity.js +181 -0
  91. package/dist/holmes/review/test-outcomes.d.ts +6 -1
  92. package/dist/holmes/review/test-outcomes.js +23 -5
  93. package/dist/holmes/review/test-runner.d.ts +18 -0
  94. package/dist/holmes/review/test-runner.js +132 -4
  95. package/dist/holmes/rtm/graph-store.d.ts +50 -1
  96. package/dist/holmes/rtm/graph-store.js +244 -3
  97. package/dist/holmes/rtm/incremental.d.ts +1 -0
  98. package/dist/holmes/rtm/incremental.js +12 -3
  99. package/dist/holmes/rtm/localize.js +7 -0
  100. package/dist/holmes/rtm/rtm-graph.d.ts +9 -0
  101. package/dist/holmes/rtm/rtm-graph.js +13 -0
  102. package/dist/holmes/semantic/vector-cache.d.ts +16 -1
  103. package/dist/holmes/semantic/vector-cache.js +155 -19
  104. package/dist/holmes/spec/approval-status.d.ts +10 -0
  105. package/dist/holmes/spec/approval-status.js +7 -3
  106. package/dist/holmes/spec/entity-git-snapshot.d.ts +50 -0
  107. package/dist/holmes/spec/entity-git-snapshot.js +276 -0
  108. package/dist/holmes/spec/entity-integration-discharge.d.ts +73 -0
  109. package/dist/holmes/spec/entity-integration-discharge.js +233 -0
  110. package/dist/holmes/spec/entity-integration-transaction.d.ts +179 -0
  111. package/dist/holmes/spec/entity-integration-transaction.js +1221 -0
  112. package/dist/holmes/spec/entity-integration.d.ts +207 -0
  113. package/dist/holmes/spec/entity-integration.js +747 -0
  114. package/dist/holmes/spec/entity-renumber-transaction.d.ts +82 -0
  115. package/dist/holmes/spec/entity-renumber-transaction.js +711 -0
  116. package/dist/holmes/spec/entity-renumber.d.ts +32 -0
  117. package/dist/holmes/spec/entity-renumber.js +156 -0
  118. package/dist/holmes/spec/entity-store.d.ts +135 -0
  119. package/dist/holmes/spec/entity-store.js +1051 -0
  120. package/dist/holmes/spec/entity-transaction.d.ts +89 -0
  121. package/dist/holmes/spec/entity-transaction.js +701 -0
  122. package/dist/holmes/spec/renumber.d.ts +58 -0
  123. package/dist/holmes/spec/renumber.js +200 -1
  124. package/dist/holmes/spec/spec-store.d.ts +3 -2
  125. package/dist/holmes/spec/spec-store.js +23 -2
  126. package/dist/holmes/spec/transition-policy.d.ts +36 -0
  127. package/dist/holmes/spec/transition-policy.js +135 -0
  128. package/package.json +1 -1
@@ -33,12 +33,53 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.MemoryLedgerStore = exports.FileLedgerStore = void 0;
36
+ exports.MemoryLedgerStore = exports.FileLedgerStore = exports.LedgerConflictRefusal = void 0;
37
+ exports.writerChainFile = writerChainFile;
38
+ exports.ledgerConflictReason = ledgerConflictReason;
37
39
  exports.nonceConsumedIn = nonceConsumedIn;
38
- // @implements A-SPEC-150
40
+ // @implements A-SPEC-150, A-SPEC-624
41
+ // @implements A-SPEC-643
42
+ // @implements A-SPEC-642
39
43
  const provenance_chain_1 = require("./provenance-chain");
40
44
  const provenance_ledger_1 = require("./provenance-ledger");
41
45
  const path = __importStar(require("node:path"));
46
+ /**
47
+ * @implements A-SPEC-643
48
+ * The chain file THIS process writes: `provenance.<replica>.jsonl`, resolved exactly as every other
49
+ * writer resolves it (the run's execution replica when registered, else the stored or hostname-derived
50
+ * id). Measured 2026-09-14: the shell gate, `risk_check` and `review_record` handed
51
+ * `consumeNonceExclusively` the legacy `provenance.jsonl`, so two clones spending the same approval
52
+ * offline both appended to one file and the origin's merge died with CONFLICT (add/add) before any
53
+ * double-spend could be seen. Consumptions belong on the writer's chain like every other event.
54
+ */
55
+ function writerChainFile(ledgerDir) {
56
+ return provenance_ledger_1.ProvenanceLedger.at(ledgerDir).currentFile();
57
+ }
58
+ /** A-SPEC-642 — thrown by approval resolution while the merged ledger holds an unreconciled double-spend. */
59
+ class LedgerConflictRefusal extends Error {
60
+ holmesRefusal = true;
61
+ code = 'ledger-conflict';
62
+ constructor(message) { super(message); this.name = 'LedgerConflictRefusal'; }
63
+ }
64
+ exports.LedgerConflictRefusal = LedgerConflictRefusal;
65
+ /**
66
+ * @implements A-SPEC-642
67
+ * The one sentence every authority-spending act refuses with while the merged ledger holds an
68
+ * unreconciled cross-replica double-spend, or null. Names the nonce, the replicas and the act that
69
+ * clears it, because a refusal that cannot be acted on is an obstacle rather than a gate. An absent
70
+ * ledger directory is a clean ledger. Not caught: an unreadable chain file throws here exactly as it
71
+ * throws in `isNonceConsumed`, and the callers' fail-closed handling applies.
72
+ */
73
+ function ledgerConflictReason(dir) {
74
+ const pending = new FileLedgerStore(dir).doubleSpends();
75
+ if (pending.length === 0)
76
+ return null;
77
+ const d = pending[0];
78
+ const replicas = [...new Set(d.events.map((e) => e.replicaId ?? 'legacy'))].join(', ');
79
+ return `the merged ledger holds an unreconciled cross-replica double-spend: single-use approval ${d.nonce} was consumed on replicas ${replicas}`
80
+ + ` — approvals, applies and recoveries are refused until an operator reconciles it (ledger_reconcile { nonce: "${d.nonce}" })`
81
+ + (pending.length > 1 ? `; ${pending.length - 1} more pending` : '');
82
+ }
42
83
  /**
43
84
  * Local adapter — DELEGATES to `ProvenanceLedger` rather than re-implementing it.
44
85
  *
@@ -66,6 +107,9 @@ class FileLedgerStore {
66
107
  this.inner = provenance_ledger_1.ProvenanceLedger.at(dir, givenId);
67
108
  }
68
109
  replicaId() {
110
+ const execution = this.inner.executionContext();
111
+ if (execution)
112
+ return execution.replicaId;
69
113
  if (this.cachedId === undefined)
70
114
  this.cachedId = this.givenId ?? this.inner.replicaIdOf();
71
115
  return this.cachedId;
@@ -77,12 +121,13 @@ class FileLedgerStore {
77
121
  loadAll() { return this.inner.loadAll(); }
78
122
  verifyAll() {
79
123
  const r = this.inner.verifyAll();
80
- return { ok: r.ok, broken: r.broken.map((b) => ({ replicaId: b.replicaId, detail: b.detail })) };
124
+ return { ok: r.ok, broken: r.broken.map((b) => ({ replicaId: b.replicaId, detail: b.detail, ...(b.kind ? { kind: b.kind } : {}), ...(b.nonce ? { nonce: b.nonce } : {}), ...(b.events ? { events: b.events } : {}) })) };
81
125
  }
82
126
  append(body) {
83
127
  return this.inner.append(body);
84
128
  }
85
129
  isNonceConsumed(nonce) { return this.inner.isNonceConsumed(nonce); }
130
+ doubleSpends() { return this.inner.doubleSpends(); }
86
131
  }
87
132
  exports.FileLedgerStore = FileLedgerStore;
88
133
  /**
@@ -137,6 +182,7 @@ class MemoryLedgerStore {
137
182
  return this.loadAll().some((e) => e.kind === 'nonce-consumed'
138
183
  && ((e.inputs ?? []).includes(nonce) || (e.inputs ?? []).includes(fp)));
139
184
  }
185
+ doubleSpends() { return (0, provenance_ledger_1.unreconciledDoubleSpends)(this.loadAll()); }
140
186
  }
141
187
  exports.MemoryLedgerStore = MemoryLedgerStore;
142
188
  // @implements A-SPEC-574.3
@@ -1,6 +1,8 @@
1
+ import { ExecutionContext } from '../project/execution-context';
1
2
  import { ProvenanceEvent } from './provenance-chain';
2
3
  /** One governance event, projected to what a timeline shows — chain-integrity fields dropped. */
3
4
  export interface TimelineEntry {
5
+ execution?: ExecutionContext;
4
6
  ts: string;
5
7
  kind: string;
6
8
  actor: string;
@@ -9,7 +11,7 @@ export interface TimelineEntry {
9
11
  }
10
12
  /** The subset of a ledger event this projection reads (structurally typed so callers can pass
11
13
  * FileLedgerStore.loadAll() results directly). */
12
- type TimelineSource = Pick<ProvenanceEvent, 'ts' | 'kind' | 'actor' | 'summary'> & Partial<Pick<ProvenanceEvent, 'inputs' | 'seq'>>;
14
+ type TimelineSource = Pick<ProvenanceEvent, 'ts' | 'kind' | 'actor' | 'summary'> & Partial<Pick<ProvenanceEvent, 'inputs' | 'seq' | 'execution'>>;
13
15
  /**
14
16
  * @implements A-SPEC-538.3
15
17
  * Pure: order ledger events into a timeline, optionally narrowed to one spec. Ascending by `ts`
@@ -1,6 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.timelineFrom = timelineFrom;
4
+ // @implements A-SPEC-538.3, A-SPEC-624
5
+ // @implements A-SPEC-645
6
+ const execution_context_1 = require("../project/execution-context");
7
+ const display_id_aliases_1 = require("./display-id-aliases");
4
8
  /**
5
9
  * @implements A-SPEC-538.3
6
10
  * Pure: order ledger events into a timeline, optionally narrowed to one spec. Ascending by `ts`
@@ -9,7 +13,12 @@ exports.timelineFrom = timelineFrom;
9
13
  * (hash/prevHash/seq/replicaId) are plumbing, not timeline content, and are projected away.
10
14
  */
11
15
  function timelineFrom(events, id) {
12
- const selected = id ? events.filter((e) => (e.inputs ?? []).includes(id)) : events.slice();
16
+ // @implements A-SPEC-645 — an id selects the events of every display id the entity has carried,
17
+ // resolved from the same events (renames are ledger records); an id with no history is itself.
18
+ const ids = id ? new Set((0, display_id_aliases_1.aliasesOf)(events, id)) : undefined;
19
+ // A rename record names the ids as `old-id:`/`new-id:` inputs; those reference the entity too.
20
+ const refersTo = (input) => input.startsWith('old-id:') ? input.slice(7) : input.startsWith('new-id:') ? input.slice(7) : input;
21
+ const selected = ids ? events.filter((e) => (e.inputs ?? []).some((input) => ids.has(refersTo(input)))) : events.slice();
13
22
  selected.sort((a, b) => (a.ts < b.ts ? -1 : a.ts > b.ts ? 1 : (a.seq ?? 0) - (b.seq ?? 0)));
14
23
  return selected.map((e) => ({
15
24
  ts: e.ts,
@@ -17,5 +26,6 @@ function timelineFrom(events, id) {
17
26
  actor: e.actor,
18
27
  summary: e.summary,
19
28
  inputs: e.inputs ?? [],
29
+ ...('execution' in e ? { execution: (0, execution_context_1.validateExecution)(e.execution) } : {}),
20
30
  }));
21
31
  }
@@ -1,3 +1,4 @@
1
+ import { ExecutionContext } from '../project/execution-context';
1
2
  import { LockOptions } from './ledger-lock';
2
3
  /**
3
4
  * N3 — Decision PROVENANCE chain (target-architecture §7-N N3): "누가·언제·왜"의 완전 인과 재구성.
@@ -18,6 +19,7 @@ import { LockOptions } from './ledger-lock';
18
19
  * returning [] would erase the audit trail this exists to guarantee — same policy as DecisionLedger).
19
20
  */
20
21
  export interface ProvenanceEvent {
22
+ execution?: ExecutionContext;
21
23
  seq: number;
22
24
  ts: string;
23
25
  actor: string;
@@ -50,7 +50,8 @@ exports.keyedSelfConsistent = keyedSelfConsistent;
50
50
  exports.bodyCanon = bodyCanon;
51
51
  exports.parseLedgerLines = parseLedgerLines;
52
52
  exports.consumeNonceExclusively = consumeNonceExclusively;
53
- // @implements A-SPEC-125.2
53
+ // @implements A-SPEC-125.2, A-SPEC-624
54
+ const execution_context_1 = require("../project/execution-context");
54
55
  const crypto = __importStar(require("node:crypto"));
55
56
  const fs = __importStar(require("node:fs"));
56
57
  const path = __importStar(require("node:path"));
@@ -61,6 +62,7 @@ function canonicalize(b) {
61
62
  return JSON.stringify({
62
63
  seq: b.seq, ts: b.ts, actor: b.actor, kind: b.kind, summary: b.summary,
63
64
  inputs: b.inputs ?? [], rationale: b.rationale ?? '', authorization: b.authorization ?? '',
65
+ ...('execution' in b ? { execution: (0, execution_context_1.validateExecution)(b.execution) } : {}),
64
66
  });
65
67
  }
66
68
  /**
@@ -83,7 +85,7 @@ function chainNext(chain, body, key) {
83
85
  const full = { seq: chain.length, ...body };
84
86
  return { ...full, prevHash, hash: computeHash(prevHash, full, key) };
85
87
  }
86
- const KNOWN_FIELDS = new Set(['seq', 'ts', 'actor', 'kind', 'summary', 'inputs', 'rationale', 'authorization', 'prevHash', 'hash']);
88
+ const KNOWN_FIELDS = new Set(['seq', 'ts', 'actor', 'kind', 'summary', 'inputs', 'rationale', 'authorization', 'prevHash', 'hash', 'execution']);
87
89
  /**
88
90
  * Verify the whole chain: recomputed hashes, prevHash linkage, contiguous seq, AND no unknown fields.
89
91
  * The unknown-field check closes the canonicalization-gap attack (adversarial review #2): canonicalize
@@ -102,6 +104,14 @@ function verifyChain(chain, key) {
102
104
  let keyedSeen = false;
103
105
  for (let i = 0; i < chain.length; i++) {
104
106
  const e = chain[i];
107
+ if ('execution' in e) {
108
+ try {
109
+ (0, execution_context_1.validateExecution)(e.execution);
110
+ }
111
+ catch {
112
+ return { ok: false, brokenAt: i, detail: 'invalid execution context' };
113
+ }
114
+ }
105
115
  const unknown = Object.keys(e).filter((k) => !KNOWN_FIELDS.has(k));
106
116
  if (unknown.length > 0)
107
117
  return { ok: false, brokenAt: i, detail: `unknown field(s) outside the hashed schema: ${unknown.join(', ')} (smuggled data)` };
@@ -276,6 +286,7 @@ function bodyCanon(b) {
276
286
  return JSON.stringify({
277
287
  ts: b.ts, actor: b.actor, kind: b.kind, summary: b.summary,
278
288
  inputs: b.inputs ?? [], rationale: b.rationale ?? '', authorization: b.authorization ?? '',
289
+ ...('execution' in b ? { execution: (0, execution_context_1.validateExecution)(b.execution) } : {}),
279
290
  });
280
291
  }
281
292
  /**
@@ -25,6 +25,28 @@ export interface LedgerEvent extends ProvenanceEvent {
25
25
  /** Which chain this came from; `null` for the pre-split legacy file. */
26
26
  replicaId: string | null;
27
27
  }
28
+ /** A-SPEC-642 — one single-use nonce consumed by several events (any replicas), not yet reconciled. */
29
+ export interface DoubleSpend {
30
+ nonce: string;
31
+ events: {
32
+ replicaId: string | null;
33
+ seq: number;
34
+ hash: string;
35
+ ts: string;
36
+ }[];
37
+ }
38
+ /** A-SPEC-642 — the event kind an operator's reconciliation appends. */
39
+ export declare const DOUBLE_SPEND_RECONCILED = "nonce-double-spend-reconciled";
40
+ /**
41
+ * @implements A-SPEC-642
42
+ * Single-use nonces consumed by two or more `nonce-consumed` events (on any replicas) that no
43
+ * `nonce-double-spend-reconciled` event has covered. Pure and hash-free on purpose: the gate and
44
+ * the approval channel ask this on every guarded act, at the cost `isNonceConsumed` already pays;
45
+ * chain integrity stays with `verifyAll`. A reconciliation covers a group only when it names the
46
+ * nonce input AND `event:<hash>` for every member — a later consumption of the same nonce is a new
47
+ * event the old reconciliation never saw, so the group is open again.
48
+ */
49
+ export declare function unreconciledDoubleSpends(events: LedgerEvent[]): DoubleSpend[];
28
50
  export interface LedgerVerifyResult {
29
51
  ok: boolean;
30
52
  /** Per-chain failures, naming the replica so a report is actionable rather than merely alarming. */
@@ -33,6 +55,9 @@ export interface LedgerVerifyResult {
33
55
  file: string;
34
56
  detail?: string;
35
57
  brokenAt?: number;
58
+ kind?: 'chain' | 'double-spend';
59
+ nonce?: string;
60
+ events?: DoubleSpend['events'];
36
61
  }[];
37
62
  }
38
63
  /**
@@ -72,6 +97,7 @@ export declare class ProvenanceLedger {
72
97
  * `.ax/state/`, which is the REQ-148 footprint defect returning through a new door.
73
98
  */
74
99
  replicaIdOf(): string;
100
+ executionContext(): import("../project/execution-context").ExecutionContext | undefined;
75
101
  /** Every chain file present, legacy first. `replicaId` is null for the legacy chain. */
76
102
  chains(): {
77
103
  replicaId: string | null;
@@ -89,6 +115,8 @@ export declare class ProvenanceLedger {
89
115
  * ordering exists so output is stable and readable, not so it can be read as "this happened first".
90
116
  */
91
117
  loadAll(): LedgerEvent[];
118
+ /** @implements A-SPEC-642 — unreconciled cross-replica double-spends, structured. */
119
+ doubleSpends(): DoubleSpend[];
92
120
  /** Each chain verified on its own; one bad chain fails the whole ledger, and is named. */
93
121
  verifyAll(): LedgerVerifyResult;
94
122
  /**
@@ -33,14 +33,50 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.ProvenanceLedger = void 0;
36
+ exports.ProvenanceLedger = exports.DOUBLE_SPEND_RECONCILED = void 0;
37
+ exports.unreconciledDoubleSpends = unreconciledDoubleSpends;
37
38
  exports.isLedgerFilename = isLedgerFilename;
38
- // @implements A-SPEC-206
39
+ // @implements A-SPEC-206, A-SPEC-624
40
+ // @implements A-SPEC-642
41
+ const execution_context_1 = require("../project/execution-context");
39
42
  // @implements A-SPEC-148
40
43
  const fs = __importStar(require("node:fs"));
41
44
  const path = __importStar(require("node:path"));
42
45
  const provenance_chain_1 = require("./provenance-chain");
43
46
  const replica_id_1 = require("./replica-id");
47
+ /** A-SPEC-642 — the event kind an operator's reconciliation appends. */
48
+ exports.DOUBLE_SPEND_RECONCILED = 'nonce-double-spend-reconciled';
49
+ /**
50
+ * @implements A-SPEC-642
51
+ * Single-use nonces consumed by two or more `nonce-consumed` events (on any replicas) that no
52
+ * `nonce-double-spend-reconciled` event has covered. Pure and hash-free on purpose: the gate and
53
+ * the approval channel ask this on every guarded act, at the cost `isNonceConsumed` already pays;
54
+ * chain integrity stays with `verifyAll`. A reconciliation covers a group only when it names the
55
+ * nonce input AND `event:<hash>` for every member — a later consumption of the same nonce is a new
56
+ * event the old reconciliation never saw, so the group is open again.
57
+ */
58
+ function unreconciledDoubleSpends(events) {
59
+ const groups = new Map();
60
+ for (const e of events) {
61
+ if (e.kind !== 'nonce-consumed')
62
+ continue;
63
+ for (const input of e.inputs ?? []) {
64
+ const list = groups.get(input) ?? [];
65
+ list.push({ replicaId: e.replicaId, seq: e.seq, hash: e.hash, ts: e.ts });
66
+ groups.set(input, list);
67
+ }
68
+ }
69
+ const reconciliations = events.filter((e) => e.kind === exports.DOUBLE_SPEND_RECONCILED).map((e) => new Set(e.inputs ?? []));
70
+ const out = [];
71
+ for (const [nonce, list] of groups) {
72
+ if (list.length < 2)
73
+ continue;
74
+ const covered = reconciliations.some((inputs) => inputs.has(nonce) && list.every((ev) => inputs.has('event:' + ev.hash)));
75
+ if (!covered)
76
+ out.push({ nonce, events: list });
77
+ }
78
+ return out.sort((a, b) => (a.nonce < b.nonce ? -1 : a.nonce > b.nonce ? 1 : 0));
79
+ }
44
80
  const LEGACY = 'provenance.jsonl';
45
81
  const REPLICA_FILE = /^provenance\.([^.]+)\.jsonl$/;
46
82
  /**
@@ -85,6 +121,9 @@ class ProvenanceLedger {
85
121
  }
86
122
  cachedId;
87
123
  get replicaId() {
124
+ const execution = this.executionContext();
125
+ if (execution)
126
+ return execution.replicaId;
88
127
  if (this.cachedId === undefined)
89
128
  this.cachedId = this.replicaIdFn();
90
129
  return this.cachedId;
@@ -107,8 +146,12 @@ class ProvenanceLedger {
107
146
  * `.ax/state/`, which is the REQ-148 footprint defect returning through a new door.
108
147
  */
109
148
  replicaIdOf() {
149
+ const execution = this.executionContext();
150
+ if (execution)
151
+ return execution.replicaId;
110
152
  return this.cachedId ?? (0, replica_id_1.resolveReplicaId)(rootForLedgerDir(this.dir), process.env, { persist: false });
111
153
  }
154
+ executionContext() { return (0, execution_context_1.executionForRoot)(rootForLedgerDir(this.dir)); }
112
155
  /** Every chain file present, legacy first. `replicaId` is null for the legacy chain. */
113
156
  chains() {
114
157
  let names;
@@ -149,6 +192,7 @@ class ProvenanceLedger {
149
192
  return events.map((e) => ({
150
193
  ...e,
151
194
  inputs: e.inputs ? [...e.inputs] : undefined,
195
+ ...(e.execution ? { execution: { ...e.execution } } : {}),
152
196
  }));
153
197
  }
154
198
  /**
@@ -175,13 +219,20 @@ class ProvenanceLedger {
175
219
  this.indexCache = { key, events: out };
176
220
  return this.cloneEvents(out);
177
221
  }
222
+ /** @implements A-SPEC-642 — unreconciled cross-replica double-spends, structured. */
223
+ doubleSpends() { return unreconciledDoubleSpends(this.loadAll()); }
178
224
  /** Each chain verified on its own; one bad chain fails the whole ledger, and is named. */
179
225
  verifyAll() {
180
226
  const broken = [];
181
227
  for (const c of this.chains()) {
182
228
  const r = new provenance_chain_1.ProvenanceChain(c.file).verify();
183
229
  if (!r.ok)
184
- broken.push({ replicaId: c.replicaId, file: c.file, detail: r.detail, brokenAt: r.brokenAt });
230
+ broken.push({ replicaId: c.replicaId, file: c.file, kind: 'chain', detail: r.detail, brokenAt: r.brokenAt });
231
+ if (r.ok) {
232
+ const mismatch = new provenance_chain_1.ProvenanceChain(c.file).load().find(e => e.execution && e.execution.replicaId !== c.replicaId);
233
+ if (mismatch)
234
+ broken.push({ replicaId: c.replicaId, file: c.file, kind: 'chain', detail: 'execution replica differs from chain partition', brokenAt: mismatch.seq });
235
+ }
185
236
  }
186
237
  // @implements A-SPEC-458
187
238
  // Cross-replica double-spend. consumeNonceExclusively's lock is per-file, so two machines can
@@ -191,18 +242,14 @@ class ProvenanceLedger {
191
242
  // records is always a violation (REQ-141): there is no legitimate re-spend. Counted by the raw
192
243
  // input value, the same rule isNonceConsumed matches on — one consume path writes one form, so
193
244
  // a real double-spend records the same value twice.
194
- const counts = new Map();
195
- for (const e of this.loadAll()) {
196
- if (e.kind !== 'nonce-consumed')
197
- continue;
198
- for (const inp of e.inputs ?? [])
199
- counts.set(inp, (counts.get(inp) ?? 0) + 1);
200
- }
201
- for (const [nonce, n] of counts) {
202
- if (n > 1) {
203
- broken.push({ replicaId: null, file: '(cross-replica)',
204
- detail: `single-use nonce ${nonce.slice(0, 16)}… spent ${n} times across replicas — REQ-141 double-spend` });
205
- }
245
+ // @implements A-SPEC-642 structured, and RECONCILABLE: an operator's `nonce-double-spend-reconciled`
246
+ // event naming the nonce and every consuming event's hash retires the group; a later consumption
247
+ // reopens it. Measured 2026-09-14: the prose-only entry left the Stop hook blocking every turn
248
+ // end "at seq undefined" with no act that could ever clear it.
249
+ for (const d of unreconciledDoubleSpends(this.loadAll())) {
250
+ const replicas = [...new Set(d.events.map((e) => e.replicaId ?? 'legacy'))].join(', ');
251
+ broken.push({ replicaId: null, file: '(cross-replica)', kind: 'double-spend', nonce: d.nonce, events: d.events,
252
+ detail: `single-use nonce ${d.nonce} spent ${d.events.length} times across replicas (${replicas}) — REQ-141 double-spend; reconcile with ledger_reconcile` });
206
253
  }
207
254
  return { ok: broken.length === 0, broken };
208
255
  }
@@ -213,8 +260,14 @@ class ProvenanceLedger {
213
260
  * which is the defect this class exists to remove.
214
261
  */
215
262
  append(body) {
263
+ const execution = this.executionContext();
264
+ if ('execution' in body) {
265
+ const supplied = (0, execution_context_1.validateExecution)(body.execution);
266
+ if (execution && JSON.stringify(supplied) !== JSON.stringify(execution))
267
+ throw new Error('Supplied execution differs from the owned run');
268
+ }
216
269
  this.indexCache = undefined;
217
- return new provenance_chain_1.ProvenanceChain(this.currentFile()).append(body);
270
+ return new provenance_chain_1.ProvenanceChain(this.currentFile()).append(execution ? { ...body, execution } : body);
218
271
  }
219
272
  /**
220
273
  * Has this nonce been spent on ANY replica?
@@ -1,3 +1,4 @@
1
+ import { ExecutionContext } from '../project/execution-context';
1
2
  /**
2
3
  * The session-context ledger: WHO attached (agent name@version) and — where a harness exposes it —
3
4
  * WITH WHAT (model ids, token totals). REQ-564's whole point is that none of this existed anywhere
@@ -21,6 +22,7 @@ export interface UsageTotals {
21
22
  cacheCreation: number;
22
23
  }
23
24
  export interface ClientRecord {
25
+ execution?: ExecutionContext;
24
26
  kind: 'client';
25
27
  sessionKey: string;
26
28
  client: string;
@@ -29,6 +31,7 @@ export interface ClientRecord {
29
31
  replica?: string;
30
32
  }
31
33
  export interface UsageRecord {
34
+ execution?: ExecutionContext;
32
35
  kind: 'usage';
33
36
  sessionKey: string;
34
37
  models: string[];
@@ -39,7 +39,8 @@ exports.appendSessionContext = appendSessionContext;
39
39
  exports.readSessionContext = readSessionContext;
40
40
  exports.makeSessionStamper = makeSessionStamper;
41
41
  exports.summarizeTranscript = summarizeTranscript;
42
- // @implements A-SPEC-564.1
42
+ // @implements A-SPEC-564.1, A-SPEC-625
43
+ const execution_context_1 = require("../project/execution-context");
43
44
  const fs = __importStar(require("node:fs"));
44
45
  const path = __importStar(require("node:path"));
45
46
  const replica_id_1 = require("./replica-id");
@@ -56,14 +57,19 @@ function appendSessionContext(root, rec) {
56
57
  try {
57
58
  if (!fs.existsSync(path.join(root, '.ax')))
58
59
  return false;
60
+ const execution = (0, execution_context_1.observationExecution)(root, rec);
59
61
  let replica = 'local';
60
- try {
61
- replica = (0, replica_id_1.resolveReplicaId)(root) || 'local';
62
+ if (execution)
63
+ replica = execution.replicaId;
64
+ else {
65
+ try {
66
+ replica = (0, replica_id_1.resolveReplicaId)(root) || 'local';
67
+ }
68
+ catch { /* keep the fallback */ }
62
69
  }
63
- catch { /* keep the fallback */ }
64
70
  const file = path.join(root, '.ax', 'ledger', sessionContextFilename(replica));
65
71
  fs.mkdirSync(path.dirname(file), { recursive: true });
66
- fs.appendFileSync(file, `${JSON.stringify({ ...rec, replica })}\n`);
72
+ fs.appendFileSync(file, `${JSON.stringify({ ...rec, replica, ...(execution ? { execution } : {}) })}\n`);
67
73
  return true;
68
74
  }
69
75
  catch {
@@ -98,6 +104,11 @@ function readSessionContext(root) {
98
104
  const r = JSON.parse(s);
99
105
  if (r && typeof r === 'object' && (r.kind === 'client' || r.kind === 'usage')
100
106
  && typeof r.sessionKey === 'string' && typeof r.ts === 'string') {
107
+ if ('execution' in r) {
108
+ r.execution = (0, execution_context_1.validateExecution)(r.execution);
109
+ if (name !== sessionContextFilename(r.execution.replicaId) || r.replica !== r.execution.replicaId)
110
+ continue;
111
+ }
101
112
  out.push({ r: r, i: i++ });
102
113
  }
103
114
  }
@@ -114,24 +125,31 @@ function readSessionContext(root) {
114
125
  * session has one identity; a root-less call defers rather than consumes the chance.
115
126
  */
116
127
  function makeSessionStamper(info, append = appendSessionContext, sessionKey = `mcp-${process.pid}`) {
117
- let stamped = false;
128
+ const stamped = new Set();
118
129
  return (root) => {
119
- if (stamped || typeof root !== 'string' || root === '')
130
+ if (typeof root !== 'string' || root === '')
120
131
  return;
121
132
  try {
133
+ const execution = (0, execution_context_1.executionForRoot)(root);
134
+ const key = execution ? JSON.stringify(execution) : 'legacy';
135
+ if (stamped.has(key))
136
+ return;
122
137
  const v = (() => { try {
123
138
  return info();
124
139
  }
125
140
  catch {
126
141
  return undefined;
127
142
  } })();
128
- stamped = append(root, {
143
+ const success = append(root, {
129
144
  kind: 'client',
130
145
  sessionKey,
131
146
  client: v?.name ?? 'unknown',
132
147
  clientVersion: v?.version ?? 'unknown',
133
148
  ts: new Date().toISOString(),
149
+ ...(execution ? { execution } : {}),
134
150
  });
151
+ if (success)
152
+ stamped.add(key);
135
153
  }
136
154
  catch { /* observation must never break the observed call */ }
137
155
  };
@@ -8,7 +8,7 @@ exports.classifyArchitectureReversal = classifyArchitectureReversal;
8
8
  exports.classifySecret = classifySecret;
9
9
  exports.classifyBlastRadius = classifyBlastRadius;
10
10
  exports.assessRisk = assessRisk;
11
- // @implements A-SPEC-125.1
11
+ // @implements A-SPEC-125.1, A-SPEC-627
12
12
  const risk_types_1 = require("./risk-types");
13
13
  const AWS_KEY_RE = /AKIA[0-9A-Z]{16}/;
14
14
  const PEM_KEY_RE = /-----BEGIN [A-Z ]*PRIVATE KEY-----/;
@@ -263,7 +263,7 @@ function scopeAxToProject(cmd, projectRoot) {
263
263
  // 'echo x > .ax/approvals/grants/forged.json' passed while the Write gate refused the same file —
264
264
  // the exact two-copies drift this repository has paid for with isTestFile and a restated section
265
265
  // splitter. The directories are now written once; each rule keeps its own shape around them.
266
- const PROTECTED_AX_ALT = 'specs|decisions|ledger|cpg_cache|state|roles|approvals';
266
+ const PROTECTED_AX_ALT = 'specs|decisions|ledger|cpg_cache|state|roles|approvals|entities';
267
267
  const PROTECTED_AX_PATH_RE = new RegExp(String.raw `(^|/)\.ax/(${PROTECTED_AX_ALT})(/|$)`);
268
268
  // A removal verb + a protected .ax path anywhere in a shell command — the command form of
269
269
  // the governance-path delete, which the structured `kind:'delete'` branch alone would miss.
@@ -45,7 +45,7 @@ exports.protectedFileKindOf = protectedFileKindOf;
45
45
  exports.protectedKindOf = protectedKindOf;
46
46
  exports.shellSegments = shellSegments;
47
47
  exports.resolvesInside = resolvesInside;
48
- // @implements A-SPEC-163
48
+ // @implements A-SPEC-163, A-SPEC-627
49
49
  const fs = __importStar(require("node:fs"));
50
50
  const path = __importStar(require("node:path"));
51
51
  const os = __importStar(require("node:os"));
@@ -193,6 +193,7 @@ const PROTECTED_DIRS = [
193
193
  path.join('.ax', 'roles'),
194
194
  path.join('.ax', 'cpg_cache'),
195
195
  path.join('.ax', 'state'),
196
+ path.join('.ax', 'entities'),
196
197
  '.claude',
197
198
  ];
198
199
  /**
@@ -46,6 +46,8 @@ exports.wiredSpecsDir = wiredSpecsDir;
46
46
  exports.evaluateHook = evaluateHook;
47
47
  exports.readSpecsSync = readSpecsSync;
48
48
  // @implements A-SPEC-278
49
+ // @implements A-SPEC-643
50
+ // @implements A-SPEC-642
49
51
  // @implements A-SPEC-194, A-SPEC-195
50
52
  const registry_1 = require("../messages/registry");
51
53
  const root_1 = require("../project/root");
@@ -410,7 +412,6 @@ exports.DEFAULT_SPECS_DIR = '.ax/specs';
410
412
  function evaluateHook(input, specsDir, opts) {
411
413
  // @implements A-SPEC-133 — resolved once so every gate in this call shares one timestamp.
412
414
  const nowTs = opts?.now ?? new Date().toISOString();
413
- const ledgerFile = path.join(opts.projectRoot, '.ax', 'ledger', 'provenance.jsonl');
414
415
  // @implements A-SPEC-125.4
415
416
  // Bash/shell risk gate: this branch runs BEFORE the Write/Edit phaseCheck logic below and
416
417
  // is intentionally fail-CLOSED — a classifier throw on the Bash surface denies, never
@@ -483,6 +484,16 @@ function evaluateHook(input, specsDir, opts) {
483
484
  // creating `.ax/ledger` is the wiring working, not a plant. Refusing there would deny the
484
485
  // first legitimate use in every new project. The single-use guarantee still holds: the record
485
486
  // lands in the resolved root, which is the same file every later call resolves to.
487
+ // @implements A-SPEC-643 — the writer's replica chain, resolved only on this path so ordinary hook
488
+ // calls leave no footprint; the spent check below still spans every chain in the directory.
489
+ const ledgerFile = (0, ledger_store_2.writerChainFile)(path.join(opts.projectRoot, '.ax', 'ledger'));
490
+ // @implements A-SPEC-642 — a covered command is still denied, and no nonce spent, while the
491
+ // merged ledger holds an unreconciled cross-replica double-spend; the reason names the exit.
492
+ {
493
+ const conflict = (0, ledger_store_2.ledgerConflictReason)(path.dirname(ledgerFile));
494
+ if (conflict)
495
+ return deny(conflict);
496
+ }
486
497
  if ((0, provenance_chain_1.blankNonce)(acting?.nonce)) {
487
498
  return deny('the approval declared single-use (nonce) but the value is empty — single-use cannot be enforced, so it is denied');
488
499
  }
@@ -53,6 +53,8 @@ exports.guardCountOrZero = guardCountOrZero;
53
53
  exports.readGuardCount = readGuardCount;
54
54
  exports.writeGuardCount = writeGuardCount;
55
55
  const fs = __importStar(require("node:fs"));
56
+ // @implements A-SPEC-645
57
+ // @implements A-SPEC-642
56
58
  const npx_bin_1 = require("../project/npx-bin");
57
59
  const approval_queue_1 = require("../governance/approval-queue");
58
60
  const dependency_delta_1 = require("../guardrail/dependency-delta");
@@ -68,6 +70,7 @@ const cycle_detect_1 = require("../cpg/cycle-detect");
68
70
  const provenance_chain_1 = require("../governance/provenance-chain");
69
71
  const test_evidence_1 = require("../review/test-evidence");
70
72
  const test_outcomes_1 = require("../review/test-outcomes");
73
+ const display_id_aliases_1 = require("../governance/display-id-aliases");
71
74
  const config_1 = require("../config/config");
72
75
  const pre_tool_use_1 = require("./pre-tool-use");
73
76
  const governance_history_1 = require("../guardrail/governance-history");
@@ -501,8 +504,12 @@ function evaluateStop(specs, evidence) {
501
504
  // Tampered audit trail blocks finishing — wires the provenance chain's verify into the live gate
502
505
  // (review finding: verify() previously had no product caller, so the chain's guarantee was inert).
503
506
  if (evidence?.provenance && !evidence.provenance.ok) {
504
- problems.push(`[ART-2] provenance chain broken at seq ${evidence.provenance.brokenAt}: ${evidence.provenance.detail}`);
505
- structured.push({ article: 'ART-2', detail: `provenance chain broken at seq ${evidence.provenance.brokenAt}: ${evidence.provenance.detail}` });
507
+ // @implements A-SPEC-642 a cross-replica double-spend has no sequence; "seq undefined" hid that.
508
+ const provenanceDetail = evidence.provenance.brokenAt === undefined
509
+ ? `provenance ledger conflict: ${evidence.provenance.detail}`
510
+ : `provenance chain broken at seq ${evidence.provenance.brokenAt}: ${evidence.provenance.detail}`;
511
+ problems.push(`[ART-2] ${provenanceDetail}`);
512
+ structured.push({ article: 'ART-2', detail: provenanceDetail });
506
513
  }
507
514
  // @implements A-SPEC-191 (§4a) — an existing-but-unreadable findings ledger is not a clean turn:
508
515
  // it may hold an open critical, and a "clean" verdict here would also CLEAR standing ART-7 debt.
@@ -913,7 +920,11 @@ if (require.main === module) {
913
920
  if (redFirstMode !== 'off') {
914
921
  const head = (0, node_child_process_1.execFileSync)('git', ['rev-parse', 'HEAD'], { cwd: stopProjectRoot(), stdio: ['ignore', 'pipe', 'ignore'], env: (0, root_1.cleanSubprocessEnv)() }).toString().trim();
915
922
  changedAspecs = changedAnchoredAspecs(stopProjectRoot());
916
- outcomesByAspec = (0, test_outcomes_1.groupOutcomesByAspec)((0, test_outcomes_1.readOutcomes)(stopProjectRoot()), head);
923
+ // @implements A-SPEC-645 a renumbered A-SPEC keeps its red-first evidence: records are
924
+ // grouped under the display id the entity carries now, per the ledger's rename records.
925
+ const { FileLedgerStore: LedgerForAliases } = require('../governance/ledger-store');
926
+ const canonical = (0, display_id_aliases_1.displayIdCanonicalizer)(new LedgerForAliases(path.join(stopProjectRoot(), '.ax', 'ledger')).loadAll());
927
+ outcomesByAspec = (0, test_outcomes_1.groupOutcomesByAspec)((0, test_outcomes_1.readOutcomes)(stopProjectRoot()), head, canonical);
917
928
  }
918
929
  }
919
930
  catch {