@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.
- package/CHANGELOG.md +143 -0
- package/README.md +9 -2
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/approve-status.d.ts +24 -0
- package/dist/holmes/cli/approve-status.js +166 -0
- package/dist/holmes/cli/approve-watch.d.ts +6 -0
- package/dist/holmes/cli/approve-watch.js +6 -0
- package/dist/holmes/cli/approve.d.ts +27 -0
- package/dist/holmes/cli/approve.js +68 -7
- package/dist/holmes/cli/cli-execution.d.ts +2 -0
- package/dist/holmes/cli/cli-execution.js +24 -0
- package/dist/holmes/cli/doctor-check.d.ts +15 -0
- package/dist/holmes/cli/doctor-check.js +2 -0
- package/dist/holmes/cli/doctor.d.ts +2 -8
- package/dist/holmes/cli/doctor.js +12 -3
- package/dist/holmes/cli/gitignore-merge.d.ts +0 -5
- package/dist/holmes/cli/gitignore-merge.js +6 -2
- package/dist/holmes/cli/index.js +176 -139
- package/dist/holmes/cli/npx-cache-check.d.ts +16 -0
- package/dist/holmes/cli/npx-cache-check.js +130 -0
- package/dist/holmes/cli/playbook-skills.d.ts +2 -22
- package/dist/holmes/cli/playbook-skills.js +10 -54
- package/dist/holmes/cli/probe-process.d.ts +17 -2
- package/dist/holmes/cli/probe-process.js +44 -11
- package/dist/holmes/governance/approval-grants.d.ts +22 -0
- package/dist/holmes/governance/approval-grants.js +55 -1
- package/dist/holmes/governance/approval-queue.d.ts +43 -4
- package/dist/holmes/governance/approval-queue.js +91 -13
- package/dist/holmes/governance/display-id-aliases.d.ts +29 -0
- package/dist/holmes/governance/display-id-aliases.js +86 -0
- package/dist/holmes/governance/ledger-store.d.ts +42 -14
- package/dist/holmes/governance/ledger-store.js +49 -3
- package/dist/holmes/governance/ledger-timeline.d.ts +3 -1
- package/dist/holmes/governance/ledger-timeline.js +11 -1
- package/dist/holmes/governance/provenance-chain.d.ts +2 -0
- package/dist/holmes/governance/provenance-chain.js +13 -2
- package/dist/holmes/governance/provenance-ledger.d.ts +28 -0
- package/dist/holmes/governance/provenance-ledger.js +69 -16
- package/dist/holmes/governance/session-context.d.ts +3 -0
- package/dist/holmes/governance/session-context.js +26 -8
- package/dist/holmes/guardrail/risk-classifier.js +2 -2
- package/dist/holmes/guardrail/write-target.js +2 -1
- package/dist/holmes/hooks/pre-tool-use.js +12 -1
- package/dist/holmes/hooks/stop.js +14 -3
- package/dist/holmes/mcp/handlers/adoption.d.ts +118 -0
- package/dist/holmes/mcp/handlers/adoption.js +186 -0
- package/dist/holmes/mcp/handlers/entity-integration.d.ts +100 -0
- package/dist/holmes/mcp/handlers/entity-integration.js +324 -0
- package/dist/holmes/mcp/handlers/entity-renumber.d.ts +137 -0
- package/dist/holmes/mcp/handlers/entity-renumber.js +228 -0
- package/dist/holmes/mcp/handlers/entity-store.d.ts +78 -0
- package/dist/holmes/mcp/handlers/entity-store.js +212 -0
- package/dist/holmes/mcp/handlers/graph-operations.d.ts +90 -0
- package/dist/holmes/mcp/handlers/graph-operations.js +260 -0
- package/dist/holmes/mcp/handlers/maintenance-analysis.d.ts +34 -0
- package/dist/holmes/mcp/handlers/maintenance-analysis.js +288 -0
- package/dist/holmes/mcp/handlers/maintenance-evidence.d.ts +40 -0
- package/dist/holmes/mcp/handlers/maintenance-evidence.js +104 -0
- package/dist/holmes/mcp/handlers/operator-inspection.d.ts +167 -0
- package/dist/holmes/mcp/handlers/operator-inspection.js +284 -0
- package/dist/holmes/mcp/handlers/phase-queries.d.ts +34 -0
- package/dist/holmes/mcp/handlers/phase-queries.js +86 -0
- package/dist/holmes/mcp/handlers/review-evidence.d.ts +47 -0
- package/dist/holmes/mcp/handlers/review-evidence.js +370 -0
- package/dist/holmes/mcp/handlers/review-queries.d.ts +68 -0
- package/dist/holmes/mcp/handlers/review-queries.js +79 -0
- package/dist/holmes/mcp/handlers/risk-assessment.d.ts +25 -0
- package/dist/holmes/mcp/handlers/risk-assessment.js +181 -0
- package/dist/holmes/mcp/handlers/slice-orchestration.d.ts +72 -0
- package/dist/holmes/mcp/handlers/slice-orchestration.js +373 -0
- package/dist/holmes/mcp/handlers/spec-approval.d.ts +105 -0
- package/dist/holmes/mcp/handlers/spec-approval.js +504 -0
- package/dist/holmes/mcp/handlers/spec-authoring.d.ts +57 -0
- package/dist/holmes/mcp/handlers/spec-authoring.js +326 -0
- package/dist/holmes/mcp/handlers/spec-lifecycle.d.ts +122 -0
- package/dist/holmes/mcp/handlers/spec-lifecycle.js +339 -0
- package/dist/holmes/mcp/handlers/spec-queries.d.ts +30 -0
- package/dist/holmes/mcp/handlers/spec-queries.js +74 -0
- package/dist/holmes/mcp/handlers/taint-screening.d.ts +48 -0
- package/dist/holmes/mcp/handlers/taint-screening.js +183 -0
- package/dist/holmes/mcp/handlers/test-execution.d.ts +63 -0
- package/dist/holmes/mcp/handlers/test-execution.js +210 -0
- package/dist/holmes/mcp/handlers/workspace-identity.d.ts +30 -0
- package/dist/holmes/mcp/handlers/workspace-identity.js +68 -0
- package/dist/holmes/mcp/handlers/workspace-queries.d.ts +77 -0
- package/dist/holmes/mcp/handlers/workspace-queries.js +193 -0
- package/dist/holmes/mcp/handlers.d.ts +661 -424
- package/dist/holmes/mcp/handlers.js +270 -3251
- package/dist/holmes/mcp/server-specs-dir.d.ts +2 -0
- package/dist/holmes/mcp/server-specs-dir.js +71 -0
- package/dist/holmes/mcp/server.js +12 -2
- package/dist/holmes/mcp/tool-schemas.js +43 -2
- package/dist/holmes/project/execution-context.d.ts +17 -0
- package/dist/holmes/project/execution-context.js +121 -0
- package/dist/holmes/project/installer-markers.d.ts +34 -0
- package/dist/holmes/project/installer-markers.js +65 -0
- package/dist/holmes/project/root.d.ts +12 -1
- package/dist/holmes/project/root.js +22 -3
- package/dist/holmes/project/workspace-identity.d.ts +29 -0
- package/dist/holmes/project/workspace-identity.js +181 -0
- package/dist/holmes/review/test-outcomes.d.ts +6 -1
- package/dist/holmes/review/test-outcomes.js +23 -5
- package/dist/holmes/review/test-runner.d.ts +18 -0
- package/dist/holmes/review/test-runner.js +136 -5
- package/dist/holmes/rtm/graph-store.d.ts +50 -1
- package/dist/holmes/rtm/graph-store.js +244 -3
- package/dist/holmes/rtm/incremental.d.ts +1 -0
- package/dist/holmes/rtm/incremental.js +12 -3
- package/dist/holmes/rtm/localize.js +7 -0
- package/dist/holmes/rtm/rtm-graph.d.ts +9 -0
- package/dist/holmes/rtm/rtm-graph.js +13 -0
- package/dist/holmes/rtm/test-scope.js +4 -1
- package/dist/holmes/semantic/vector-cache.d.ts +16 -1
- package/dist/holmes/semantic/vector-cache.js +155 -19
- package/dist/holmes/spec/approval-status.d.ts +10 -0
- package/dist/holmes/spec/approval-status.js +7 -3
- package/dist/holmes/spec/entity-git-snapshot.d.ts +50 -0
- package/dist/holmes/spec/entity-git-snapshot.js +276 -0
- package/dist/holmes/spec/entity-integration-discharge.d.ts +73 -0
- package/dist/holmes/spec/entity-integration-discharge.js +233 -0
- package/dist/holmes/spec/entity-integration-transaction.d.ts +179 -0
- package/dist/holmes/spec/entity-integration-transaction.js +1221 -0
- package/dist/holmes/spec/entity-integration.d.ts +216 -0
- package/dist/holmes/spec/entity-integration.js +760 -0
- package/dist/holmes/spec/entity-renumber-transaction.d.ts +82 -0
- package/dist/holmes/spec/entity-renumber-transaction.js +711 -0
- package/dist/holmes/spec/entity-renumber.d.ts +32 -0
- package/dist/holmes/spec/entity-renumber.js +156 -0
- package/dist/holmes/spec/entity-store.d.ts +135 -0
- package/dist/holmes/spec/entity-store.js +1053 -0
- package/dist/holmes/spec/entity-transaction.d.ts +105 -0
- package/dist/holmes/spec/entity-transaction.js +741 -0
- package/dist/holmes/spec/renumber.d.ts +58 -0
- package/dist/holmes/spec/renumber.js +200 -1
- package/dist/holmes/spec/spec-store.d.ts +3 -2
- package/dist/holmes/spec/spec-store.js +23 -2
- package/dist/holmes/spec/transition-policy.d.ts +36 -0
- package/dist/holmes/spec/transition-policy.js +135 -0
- 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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
61
|
-
replica =
|
|
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
|
-
|
|
128
|
+
const stamped = new Set();
|
|
118
129
|
return (root) => {
|
|
119
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
505
|
-
|
|
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
|
-
|
|
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
|
+
};
|