@holmes-lab/holmes-kit 0.19.6 → 0.20.1

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 (139) hide show
  1. package/CHANGELOG.md +143 -0
  2. package/README.md +9 -2
  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/doctor-check.d.ts +15 -0
  13. package/dist/holmes/cli/doctor-check.js +2 -0
  14. package/dist/holmes/cli/doctor.d.ts +2 -8
  15. package/dist/holmes/cli/doctor.js +12 -3
  16. package/dist/holmes/cli/gitignore-merge.d.ts +0 -5
  17. package/dist/holmes/cli/gitignore-merge.js +6 -2
  18. package/dist/holmes/cli/index.js +176 -139
  19. package/dist/holmes/cli/npx-cache-check.d.ts +16 -0
  20. package/dist/holmes/cli/npx-cache-check.js +130 -0
  21. package/dist/holmes/cli/playbook-skills.d.ts +2 -22
  22. package/dist/holmes/cli/playbook-skills.js +10 -54
  23. package/dist/holmes/cli/probe-process.d.ts +17 -2
  24. package/dist/holmes/cli/probe-process.js +44 -11
  25. package/dist/holmes/governance/approval-grants.d.ts +22 -0
  26. package/dist/holmes/governance/approval-grants.js +55 -1
  27. package/dist/holmes/governance/approval-queue.d.ts +43 -4
  28. package/dist/holmes/governance/approval-queue.js +91 -13
  29. package/dist/holmes/governance/display-id-aliases.d.ts +29 -0
  30. package/dist/holmes/governance/display-id-aliases.js +86 -0
  31. package/dist/holmes/governance/ledger-store.d.ts +42 -14
  32. package/dist/holmes/governance/ledger-store.js +49 -3
  33. package/dist/holmes/governance/ledger-timeline.d.ts +3 -1
  34. package/dist/holmes/governance/ledger-timeline.js +11 -1
  35. package/dist/holmes/governance/provenance-chain.d.ts +2 -0
  36. package/dist/holmes/governance/provenance-chain.js +13 -2
  37. package/dist/holmes/governance/provenance-ledger.d.ts +28 -0
  38. package/dist/holmes/governance/provenance-ledger.js +69 -16
  39. package/dist/holmes/governance/session-context.d.ts +3 -0
  40. package/dist/holmes/governance/session-context.js +26 -8
  41. package/dist/holmes/guardrail/risk-classifier.js +2 -2
  42. package/dist/holmes/guardrail/write-target.js +2 -1
  43. package/dist/holmes/hooks/pre-tool-use.js +12 -1
  44. package/dist/holmes/hooks/stop.js +14 -3
  45. package/dist/holmes/mcp/handlers/adoption.d.ts +118 -0
  46. package/dist/holmes/mcp/handlers/adoption.js +186 -0
  47. package/dist/holmes/mcp/handlers/entity-integration.d.ts +100 -0
  48. package/dist/holmes/mcp/handlers/entity-integration.js +324 -0
  49. package/dist/holmes/mcp/handlers/entity-renumber.d.ts +137 -0
  50. package/dist/holmes/mcp/handlers/entity-renumber.js +228 -0
  51. package/dist/holmes/mcp/handlers/entity-store.d.ts +78 -0
  52. package/dist/holmes/mcp/handlers/entity-store.js +212 -0
  53. package/dist/holmes/mcp/handlers/graph-operations.d.ts +90 -0
  54. package/dist/holmes/mcp/handlers/graph-operations.js +260 -0
  55. package/dist/holmes/mcp/handlers/maintenance-analysis.d.ts +34 -0
  56. package/dist/holmes/mcp/handlers/maintenance-analysis.js +288 -0
  57. package/dist/holmes/mcp/handlers/maintenance-evidence.d.ts +40 -0
  58. package/dist/holmes/mcp/handlers/maintenance-evidence.js +104 -0
  59. package/dist/holmes/mcp/handlers/operator-inspection.d.ts +167 -0
  60. package/dist/holmes/mcp/handlers/operator-inspection.js +284 -0
  61. package/dist/holmes/mcp/handlers/phase-queries.d.ts +34 -0
  62. package/dist/holmes/mcp/handlers/phase-queries.js +86 -0
  63. package/dist/holmes/mcp/handlers/review-evidence.d.ts +47 -0
  64. package/dist/holmes/mcp/handlers/review-evidence.js +370 -0
  65. package/dist/holmes/mcp/handlers/review-queries.d.ts +68 -0
  66. package/dist/holmes/mcp/handlers/review-queries.js +79 -0
  67. package/dist/holmes/mcp/handlers/risk-assessment.d.ts +25 -0
  68. package/dist/holmes/mcp/handlers/risk-assessment.js +181 -0
  69. package/dist/holmes/mcp/handlers/slice-orchestration.d.ts +72 -0
  70. package/dist/holmes/mcp/handlers/slice-orchestration.js +373 -0
  71. package/dist/holmes/mcp/handlers/spec-approval.d.ts +105 -0
  72. package/dist/holmes/mcp/handlers/spec-approval.js +504 -0
  73. package/dist/holmes/mcp/handlers/spec-authoring.d.ts +57 -0
  74. package/dist/holmes/mcp/handlers/spec-authoring.js +326 -0
  75. package/dist/holmes/mcp/handlers/spec-lifecycle.d.ts +122 -0
  76. package/dist/holmes/mcp/handlers/spec-lifecycle.js +339 -0
  77. package/dist/holmes/mcp/handlers/spec-queries.d.ts +30 -0
  78. package/dist/holmes/mcp/handlers/spec-queries.js +74 -0
  79. package/dist/holmes/mcp/handlers/taint-screening.d.ts +48 -0
  80. package/dist/holmes/mcp/handlers/taint-screening.js +183 -0
  81. package/dist/holmes/mcp/handlers/test-execution.d.ts +63 -0
  82. package/dist/holmes/mcp/handlers/test-execution.js +210 -0
  83. package/dist/holmes/mcp/handlers/workspace-identity.d.ts +30 -0
  84. package/dist/holmes/mcp/handlers/workspace-identity.js +68 -0
  85. package/dist/holmes/mcp/handlers/workspace-queries.d.ts +77 -0
  86. package/dist/holmes/mcp/handlers/workspace-queries.js +193 -0
  87. package/dist/holmes/mcp/handlers.d.ts +661 -424
  88. package/dist/holmes/mcp/handlers.js +270 -3251
  89. package/dist/holmes/mcp/server-specs-dir.d.ts +2 -0
  90. package/dist/holmes/mcp/server-specs-dir.js +71 -0
  91. package/dist/holmes/mcp/server.js +12 -2
  92. package/dist/holmes/mcp/tool-schemas.js +43 -2
  93. package/dist/holmes/project/execution-context.d.ts +17 -0
  94. package/dist/holmes/project/execution-context.js +121 -0
  95. package/dist/holmes/project/installer-markers.d.ts +34 -0
  96. package/dist/holmes/project/installer-markers.js +65 -0
  97. package/dist/holmes/project/root.d.ts +12 -1
  98. package/dist/holmes/project/root.js +22 -3
  99. package/dist/holmes/project/workspace-identity.d.ts +29 -0
  100. package/dist/holmes/project/workspace-identity.js +181 -0
  101. package/dist/holmes/review/test-outcomes.d.ts +6 -1
  102. package/dist/holmes/review/test-outcomes.js +23 -5
  103. package/dist/holmes/review/test-runner.d.ts +18 -0
  104. package/dist/holmes/review/test-runner.js +136 -5
  105. package/dist/holmes/rtm/graph-store.d.ts +50 -1
  106. package/dist/holmes/rtm/graph-store.js +244 -3
  107. package/dist/holmes/rtm/incremental.d.ts +1 -0
  108. package/dist/holmes/rtm/incremental.js +12 -3
  109. package/dist/holmes/rtm/localize.js +7 -0
  110. package/dist/holmes/rtm/rtm-graph.d.ts +9 -0
  111. package/dist/holmes/rtm/rtm-graph.js +13 -0
  112. package/dist/holmes/rtm/test-scope.js +4 -1
  113. package/dist/holmes/semantic/vector-cache.d.ts +16 -1
  114. package/dist/holmes/semantic/vector-cache.js +155 -19
  115. package/dist/holmes/spec/approval-status.d.ts +10 -0
  116. package/dist/holmes/spec/approval-status.js +7 -3
  117. package/dist/holmes/spec/entity-git-snapshot.d.ts +50 -0
  118. package/dist/holmes/spec/entity-git-snapshot.js +276 -0
  119. package/dist/holmes/spec/entity-integration-discharge.d.ts +73 -0
  120. package/dist/holmes/spec/entity-integration-discharge.js +233 -0
  121. package/dist/holmes/spec/entity-integration-transaction.d.ts +179 -0
  122. package/dist/holmes/spec/entity-integration-transaction.js +1221 -0
  123. package/dist/holmes/spec/entity-integration.d.ts +216 -0
  124. package/dist/holmes/spec/entity-integration.js +760 -0
  125. package/dist/holmes/spec/entity-renumber-transaction.d.ts +82 -0
  126. package/dist/holmes/spec/entity-renumber-transaction.js +711 -0
  127. package/dist/holmes/spec/entity-renumber.d.ts +32 -0
  128. package/dist/holmes/spec/entity-renumber.js +156 -0
  129. package/dist/holmes/spec/entity-store.d.ts +135 -0
  130. package/dist/holmes/spec/entity-store.js +1053 -0
  131. package/dist/holmes/spec/entity-transaction.d.ts +105 -0
  132. package/dist/holmes/spec/entity-transaction.js +741 -0
  133. package/dist/holmes/spec/renumber.d.ts +58 -0
  134. package/dist/holmes/spec/renumber.js +200 -1
  135. package/dist/holmes/spec/spec-store.d.ts +3 -2
  136. package/dist/holmes/spec/spec-store.js +23 -2
  137. package/dist/holmes/spec/transition-policy.d.ts +36 -0
  138. package/dist/holmes/spec/transition-policy.js +135 -0
  139. package/package.json +5 -2
@@ -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 {
@@ -0,0 +1,118 @@
1
+ import { Spec } from '../../spec/spec-parser';
2
+ import { SpecStore } from '../../spec/spec-store';
3
+ import { DraftResult } from '../../reverse/draft';
4
+ import { AnchorMapping } from '../../reverse/anchor';
5
+ export interface AdoptionContext {
6
+ listSpecs(): Promise<Spec[]>;
7
+ writeSpec: SpecStore['write'];
8
+ assertReadableRoot(tool: string, root: string): void;
9
+ occupiedTarget(specs: Spec[]): string | undefined;
10
+ }
11
+ export declare function createAdoptionHandlers(context: AdoptionContext): {
12
+ /**
13
+ * Read-only inventory of a brownfield target: candidate clusters, coverage, and what the scan
14
+ * could not resolve. Deliberately does NOT call assertRepoTopLevel — a target that is not a git
15
+ * repository is a supported case here, reported as `isGit: false`.
16
+ */
17
+ reverse_scan(a: {
18
+ root: string;
19
+ maxFlagged?: number;
20
+ }): Promise<{
21
+ root: string;
22
+ isGit: boolean;
23
+ scanned: number;
24
+ droppedUntracked: number;
25
+ candidates: number;
26
+ anchored: number;
27
+ unanchored: number;
28
+ clusters: import("../../reverse/cluster").Cluster[];
29
+ testClusters: import("../../reverse/cluster").Cluster[];
30
+ coverage: {
31
+ structurallyResolved: number;
32
+ inTestClusters: number;
33
+ cohesionUnmeasured: number;
34
+ testsMatched: number;
35
+ testsUnmatched: number;
36
+ withDynamicIndicators: number;
37
+ indicators: Partial<Record<import("../../reverse/dynamic-wiring").Indicator, number>>;
38
+ uncalibratedIndicatorLanguages: string[];
39
+ ignoreUnsupportedPatterns: string[];
40
+ };
41
+ needsHumanConfirmation: import("../../reverse/scan").FlaggedFile[];
42
+ tests: import("../../reverse/test-map").TestMapping;
43
+ }>;
44
+ /**
45
+ * Draft H-SPEC/A-SPEC/T-SPEC documents for the recovered clusters under an EXISTING parent REQ.
46
+ *
47
+ * The REQ is the human's to write and this tool refuses without one — a requirement states
48
+ * business intent, which is not in the code. Everything emitted is `status: draft`, and writing
49
+ * requires an explicit `dryRun: false`.
50
+ *
51
+ * WHERE THE DOCUMENTS LAND: in the SERVER's configured spec store (`HOLMES_SPECS`, default
52
+ * `.ax/specs` relative to the server's working directory) — not inside `root`. In the intended
53
+ * adoption the two are the same directory, because the server runs inside the target it governs;
54
+ * pointing `root` at a different repository drafts that repository's slices into THIS store,
55
+ * which is a governance decision the caller has to make deliberately.
56
+ */
57
+ reverse_draft(a: {
58
+ root: string;
59
+ parentReqId?: string;
60
+ cluster?: string;
61
+ dryRun?: boolean;
62
+ }): Promise<{
63
+ ok: boolean;
64
+ reason: string;
65
+ availableClusters?: undefined;
66
+ dryRun?: undefined;
67
+ parentReqId?: undefined;
68
+ clusters?: undefined;
69
+ drafted?: undefined;
70
+ skipped?: undefined;
71
+ refused?: undefined;
72
+ } | {
73
+ ok: boolean;
74
+ reason: string;
75
+ availableClusters: string[];
76
+ dryRun?: undefined;
77
+ parentReqId?: undefined;
78
+ clusters?: undefined;
79
+ drafted?: undefined;
80
+ skipped?: undefined;
81
+ refused?: undefined;
82
+ } | {
83
+ ok: boolean;
84
+ dryRun: boolean;
85
+ parentReqId: string;
86
+ clusters: number;
87
+ drafted: DraftResult[];
88
+ skipped: {
89
+ clusterKey: string;
90
+ reason: string;
91
+ }[];
92
+ refused: {
93
+ clusterKey: string;
94
+ reason: string;
95
+ findings?: unknown[];
96
+ }[];
97
+ reason?: undefined;
98
+ availableClusters?: undefined;
99
+ }>;
100
+ /**
101
+ * Insert `@implements` anchors into source files. Dry-run by DEFAULT, and anchoring to a
102
+ * non-approved A-SPEC is refused — a reverse-engineered draft describes code; description does
103
+ * not confer approval.
104
+ */
105
+ reverse_anchor(a: {
106
+ root: string;
107
+ mapping: AnchorMapping[];
108
+ dryRun?: boolean;
109
+ }): Promise<{
110
+ blockers: Record<string, string>;
111
+ applied: number;
112
+ dryRun: boolean;
113
+ would: number;
114
+ failed: import("../../reverse/anchor").RefusedAnchor[];
115
+ edits: import("../../reverse/anchor").AnchorEdit[];
116
+ refused: import("../../reverse/anchor").RefusedAnchor[];
117
+ }>;
118
+ };