@holmes-lab/holmes-kit 0.14.0 → 0.16.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.
- package/CHANGELOG.md +96 -0
- package/README.md +2 -1
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/approve.d.ts +14 -0
- package/dist/holmes/cli/approve.js +60 -3
- package/dist/holmes/cli/gitignore-merge.js +5 -0
- package/dist/holmes/cli/index.js +13 -1
- package/dist/holmes/governance/approval-queue.d.ts +39 -0
- package/dist/holmes/governance/approval-queue.js +105 -10
- package/dist/holmes/governance/session-context.d.ts +74 -0
- package/dist/holmes/governance/session-context.js +179 -0
- package/dist/holmes/hooks/pre-tool-use.js +5 -2
- package/dist/holmes/hooks/rtm-refresh-child.d.ts +1 -0
- package/dist/holmes/hooks/rtm-refresh-child.js +56 -0
- package/dist/holmes/hooks/rtm-refresh.d.ts +13 -0
- package/dist/holmes/hooks/rtm-refresh.js +76 -0
- package/dist/holmes/hooks/stop.js +42 -0
- package/dist/holmes/mcp/handlers.d.ts +5 -6
- package/dist/holmes/mcp/handlers.js +76 -2
- package/dist/holmes/mcp/server.js +12 -0
- package/dist/holmes/mcp/tool-schemas.js +1 -1
- package/dist/holmes/review/judgement-bundle.d.ts +49 -0
- package/dist/holmes/review/judgement-bundle.js +108 -0
- package/dist/holmes/review/run-replay.d.ts +5 -0
- package/dist/holmes/review/run-replay.js +32 -0
- package/dist/holmes/review/test-outcomes.d.ts +17 -2
- package/dist/holmes/review/test-outcomes.js +54 -15
- package/dist/holmes/rtm/impact-advisory.d.ts +48 -0
- package/dist/holmes/rtm/impact-advisory.js +175 -0
- package/dist/holmes/rtm/localize.js +7 -0
- package/dist/holmes/rtm/rtm-builder.d.ts +8 -0
- package/dist/holmes/rtm/rtm-builder.js +42 -1
- package/dist/holmes/rtm/rtm-graph.d.ts +16 -1
- package/dist/holmes/rtm/rtm-graph.js +34 -6
- package/dist/holmes/spec/approval-blockers.js +8 -0
- package/dist/holmes/spec/compat-impact.d.ts +31 -0
- package/dist/holmes/spec/compat-impact.js +141 -0
- package/dist/holmes/spec/spec-types.js +3 -1
- package/package.json +1 -1
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session-context ledger: WHO attached (agent name@version) and — where a harness exposes it —
|
|
3
|
+
* WITH WHAT (model ids, token totals). REQ-564's whole point is that none of this existed anywhere
|
|
4
|
+
* in the ledgers (measured across provenance, outcomes and the queue), while the sources were one
|
|
5
|
+
* line away: MCP clientInfo carries {name, version} for ALL three harnesses (the server used the
|
|
6
|
+
* name and dropped the version), and Claude's transcript carries model + usage per message.
|
|
7
|
+
*
|
|
8
|
+
* HARNESS-NEUTRAL BY CONSTRUCTION: the client stamp is written by the MCP SERVER, which every
|
|
9
|
+
* harness talks to — the first design pinned it on the Stop hook and was caught as Claude-biased
|
|
10
|
+
* (the owner's catch that also produced REQ-565's gate). Enrichment beyond the stamp is
|
|
11
|
+
* capability-declared per harness, never faked.
|
|
12
|
+
*
|
|
13
|
+
* NUMBERS ONLY. Records carry names, versions, model ids and integers. The transcript's
|
|
14
|
+
* conversation text, commands and paths have no field to land in — the same structural privacy
|
|
15
|
+
* QueueEventLite uses. Observation NEVER feeds a verdict (Judgments must not be budgeted).
|
|
16
|
+
*/
|
|
17
|
+
export interface UsageTotals {
|
|
18
|
+
input: number;
|
|
19
|
+
output: number;
|
|
20
|
+
cacheRead: number;
|
|
21
|
+
cacheCreation: number;
|
|
22
|
+
}
|
|
23
|
+
export interface ClientRecord {
|
|
24
|
+
kind: 'client';
|
|
25
|
+
sessionKey: string;
|
|
26
|
+
client: string;
|
|
27
|
+
clientVersion: string;
|
|
28
|
+
ts: string;
|
|
29
|
+
replica?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface UsageRecord {
|
|
32
|
+
kind: 'usage';
|
|
33
|
+
sessionKey: string;
|
|
34
|
+
models: string[];
|
|
35
|
+
usage: UsageTotals;
|
|
36
|
+
turns: number;
|
|
37
|
+
truncated?: boolean;
|
|
38
|
+
ts: string;
|
|
39
|
+
replica?: string;
|
|
40
|
+
}
|
|
41
|
+
export type SessionContextRecord = ClientRecord | UsageRecord;
|
|
42
|
+
export declare function sessionContextFilename(replica: string): string;
|
|
43
|
+
/** Born split: no legacy single file — this ledger never existed before replicas did. */
|
|
44
|
+
export declare function isSessionContextFilename(name: string): boolean;
|
|
45
|
+
/** Append to THIS machine's chain. Fail-open: observation must never break the observed. */
|
|
46
|
+
export declare function appendSessionContext(root: string, rec: SessionContextRecord): boolean;
|
|
47
|
+
/** Every replica's records, merged by ts (stable). Missing dir → []; corrupt lines skipped. */
|
|
48
|
+
export declare function readSessionContext(root: string): SessionContextRecord[];
|
|
49
|
+
/**
|
|
50
|
+
* @implements A-SPEC-564.1
|
|
51
|
+
* The lazy-once stamper the MCP server wires into its call path. Lazy because `getClientVersion()`
|
|
52
|
+
* has a value only AFTER initialize — the first tool call is the earliest honest moment — and
|
|
53
|
+
* because the ledger's location arrives with the first `root`-carrying call. Once, because a
|
|
54
|
+
* session has one identity; a root-less call defers rather than consumes the chance.
|
|
55
|
+
*/
|
|
56
|
+
export declare function makeSessionStamper(info: () => {
|
|
57
|
+
name?: string;
|
|
58
|
+
version?: string;
|
|
59
|
+
} | undefined, append?: (root: string, rec: ClientRecord) => boolean, sessionKey?: string): (root: unknown) => void;
|
|
60
|
+
/**
|
|
61
|
+
* @implements A-SPEC-564.2
|
|
62
|
+
* Numbers out of a Claude transcript — model ids, the four usage sums, a turn count. Streaming and
|
|
63
|
+
* capped: a giant transcript yields an honest partial sum with `truncated`, never a slow Stop (a
|
|
64
|
+
* huge response once blocked the very fix that would have shrunk it — observation must not repeat
|
|
65
|
+
* that shape). The return type has no field a conversation could leak through.
|
|
66
|
+
*/
|
|
67
|
+
export declare function summarizeTranscript(lines: Iterable<string>, opts?: {
|
|
68
|
+
maxBytes?: number;
|
|
69
|
+
}): {
|
|
70
|
+
models: string[];
|
|
71
|
+
usage: UsageTotals;
|
|
72
|
+
turns: number;
|
|
73
|
+
truncated: boolean;
|
|
74
|
+
};
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.sessionContextFilename = sessionContextFilename;
|
|
37
|
+
exports.isSessionContextFilename = isSessionContextFilename;
|
|
38
|
+
exports.appendSessionContext = appendSessionContext;
|
|
39
|
+
exports.readSessionContext = readSessionContext;
|
|
40
|
+
exports.makeSessionStamper = makeSessionStamper;
|
|
41
|
+
exports.summarizeTranscript = summarizeTranscript;
|
|
42
|
+
// @implements A-SPEC-564.1
|
|
43
|
+
const fs = __importStar(require("node:fs"));
|
|
44
|
+
const path = __importStar(require("node:path"));
|
|
45
|
+
const replica_id_1 = require("./replica-id");
|
|
46
|
+
const FILE_RE = /^session-context\.([^.]+)\.jsonl$/;
|
|
47
|
+
function sessionContextFilename(replica) {
|
|
48
|
+
return `session-context.${replica}.jsonl`;
|
|
49
|
+
}
|
|
50
|
+
/** Born split: no legacy single file — this ledger never existed before replicas did. */
|
|
51
|
+
function isSessionContextFilename(name) {
|
|
52
|
+
return FILE_RE.test(name);
|
|
53
|
+
}
|
|
54
|
+
/** Append to THIS machine's chain. Fail-open: observation must never break the observed. */
|
|
55
|
+
function appendSessionContext(root, rec) {
|
|
56
|
+
try {
|
|
57
|
+
if (!fs.existsSync(path.join(root, '.ax')))
|
|
58
|
+
return false;
|
|
59
|
+
let replica = 'local';
|
|
60
|
+
try {
|
|
61
|
+
replica = (0, replica_id_1.resolveReplicaId)(root) || 'local';
|
|
62
|
+
}
|
|
63
|
+
catch { /* keep the fallback */ }
|
|
64
|
+
const file = path.join(root, '.ax', 'ledger', sessionContextFilename(replica));
|
|
65
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
66
|
+
fs.appendFileSync(file, `${JSON.stringify({ ...rec, replica })}\n`);
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** Every replica's records, merged by ts (stable). Missing dir → []; corrupt lines skipped. */
|
|
74
|
+
function readSessionContext(root) {
|
|
75
|
+
const dir = path.join(root, '.ax', 'ledger');
|
|
76
|
+
let names;
|
|
77
|
+
try {
|
|
78
|
+
names = fs.readdirSync(dir).filter(isSessionContextFilename).sort();
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
const out = [];
|
|
84
|
+
let i = 0;
|
|
85
|
+
for (const name of names) {
|
|
86
|
+
let text;
|
|
87
|
+
try {
|
|
88
|
+
text = fs.readFileSync(path.join(dir, name), 'utf8');
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
for (const line of text.split('\n')) {
|
|
94
|
+
const s = line.trim();
|
|
95
|
+
if (!s)
|
|
96
|
+
continue;
|
|
97
|
+
try {
|
|
98
|
+
const r = JSON.parse(s);
|
|
99
|
+
if (r && typeof r === 'object' && (r.kind === 'client' || r.kind === 'usage')
|
|
100
|
+
&& typeof r.sessionKey === 'string' && typeof r.ts === 'string') {
|
|
101
|
+
out.push({ r: r, i: i++ });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch { /* a corrupt line never breaks the read */ }
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return out.sort((a, b) => (a.r.ts < b.r.ts ? -1 : a.r.ts > b.r.ts ? 1 : a.i - b.i)).map((e) => e.r);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* @implements A-SPEC-564.1
|
|
111
|
+
* The lazy-once stamper the MCP server wires into its call path. Lazy because `getClientVersion()`
|
|
112
|
+
* has a value only AFTER initialize — the first tool call is the earliest honest moment — and
|
|
113
|
+
* because the ledger's location arrives with the first `root`-carrying call. Once, because a
|
|
114
|
+
* session has one identity; a root-less call defers rather than consumes the chance.
|
|
115
|
+
*/
|
|
116
|
+
function makeSessionStamper(info, append = appendSessionContext, sessionKey = `mcp-${process.pid}`) {
|
|
117
|
+
let stamped = false;
|
|
118
|
+
return (root) => {
|
|
119
|
+
if (stamped || typeof root !== 'string' || root === '')
|
|
120
|
+
return;
|
|
121
|
+
try {
|
|
122
|
+
const v = (() => { try {
|
|
123
|
+
return info();
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return undefined;
|
|
127
|
+
} })();
|
|
128
|
+
stamped = append(root, {
|
|
129
|
+
kind: 'client',
|
|
130
|
+
sessionKey,
|
|
131
|
+
client: v?.name ?? 'unknown',
|
|
132
|
+
clientVersion: v?.version ?? 'unknown',
|
|
133
|
+
ts: new Date().toISOString(),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
catch { /* observation must never break the observed call */ }
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* @implements A-SPEC-564.2
|
|
141
|
+
* Numbers out of a Claude transcript — model ids, the four usage sums, a turn count. Streaming and
|
|
142
|
+
* capped: a giant transcript yields an honest partial sum with `truncated`, never a slow Stop (a
|
|
143
|
+
* huge response once blocked the very fix that would have shrunk it — observation must not repeat
|
|
144
|
+
* that shape). The return type has no field a conversation could leak through.
|
|
145
|
+
*/
|
|
146
|
+
function summarizeTranscript(lines, opts) {
|
|
147
|
+
const maxBytes = opts?.maxBytes ?? 64 * 1024 * 1024;
|
|
148
|
+
const models = [];
|
|
149
|
+
const usage = { input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
|
|
150
|
+
let turns = 0;
|
|
151
|
+
let bytes = 0;
|
|
152
|
+
let truncated = false;
|
|
153
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0);
|
|
154
|
+
for (const line of lines) {
|
|
155
|
+
bytes += line.length + 1;
|
|
156
|
+
if (bytes > maxBytes) {
|
|
157
|
+
truncated = true;
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
let m;
|
|
161
|
+
try {
|
|
162
|
+
const d = JSON.parse(line);
|
|
163
|
+
m = d && typeof d === 'object' ? d.message : undefined;
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (!m || typeof m !== 'object' || !m.usage || typeof m.usage !== 'object')
|
|
169
|
+
continue;
|
|
170
|
+
turns += 1;
|
|
171
|
+
if (typeof m.model === 'string' && m.model !== '' && !models.includes(m.model))
|
|
172
|
+
models.push(m.model);
|
|
173
|
+
usage.input += num(m.usage.input_tokens);
|
|
174
|
+
usage.output += num(m.usage.output_tokens);
|
|
175
|
+
usage.cacheRead += num(m.usage.cache_read_input_tokens);
|
|
176
|
+
usage.cacheCreation += num(m.usage.cache_creation_input_tokens);
|
|
177
|
+
}
|
|
178
|
+
return { models, usage, turns, truncated };
|
|
179
|
+
}
|
|
@@ -461,8 +461,11 @@ function evaluateHook(input, specsDir, opts) {
|
|
|
461
461
|
// @implements A-SPEC-244 — the refusal itself files the review request. The queue kind is the
|
|
462
462
|
// approval SCOPE kind ('shell'), so the reviewing CLI can mint a covering grant mechanically.
|
|
463
463
|
if (!covers) {
|
|
464
|
-
|
|
465
|
-
|
|
464
|
+
// @implements A-SPEC-564.2 — the base reason's length rides along so the refusal record
|
|
465
|
+
// knows the FULL feedback cost (base + hint), not just the hint's half.
|
|
466
|
+
const base = 'requires an approval that covers this command';
|
|
467
|
+
return deny(base
|
|
468
|
+
+ (0, approval_queue_1.queueHint)(opts.projectRoot, { kind: 'shell', target: command, why: assessment.reasons.join('; ') }, { baseReasonBytes: base.length }));
|
|
466
469
|
}
|
|
467
470
|
// @implements A-SPEC-141
|
|
468
471
|
// Check and spend in ONE atomic operation. The previous shape asked `isNonceConsumed(...)`
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
// @implements A-SPEC-566.4
|
|
37
|
+
/**
|
|
38
|
+
* The detached refresh child — the ONLY place this slice touches the scan/build machinery. Spawned
|
|
39
|
+
* by `maybeSpawnRtmRefresh` (rtm-refresh.ts) outside any turn; nothing in the hook path imports
|
|
40
|
+
* this file, which is what keeps A-SPEC-510.2 honest (no wasm rides into a gate process).
|
|
41
|
+
* rtm_impact's warm path IS the refresh: hash-cached scan, reusable-graph rebuild when stale.
|
|
42
|
+
*/
|
|
43
|
+
const fs = __importStar(require("node:fs"));
|
|
44
|
+
const path = __importStar(require("node:path"));
|
|
45
|
+
if (require.main === module) {
|
|
46
|
+
try {
|
|
47
|
+
const root = process.argv[2];
|
|
48
|
+
if (root && fs.existsSync(path.join(root, '.ax'))) {
|
|
49
|
+
const { makeHandlers } = require('../mcp/handlers');
|
|
50
|
+
const { LocalMarkdownRepository } = require('../spec/spec-store');
|
|
51
|
+
const h = makeHandlers(new LocalMarkdownRepository(path.join(root, '.ax', 'specs')));
|
|
52
|
+
void h.rtm_impact({ root, changed: [] }).catch(() => undefined);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch { /* fail-soft: a failed refresh leaves the old graph, which the advisory tolerates */ }
|
|
56
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const RTM_REFRESH_TTL_MS: number;
|
|
2
|
+
export declare const RTM_REFRESH_MARKER: string;
|
|
3
|
+
export interface RtmRefreshOpts {
|
|
4
|
+
root: string;
|
|
5
|
+
now: number;
|
|
6
|
+
execPath: string;
|
|
7
|
+
scriptPath: string;
|
|
8
|
+
spawn: (cmd: string, args: string[], opts: object) => {
|
|
9
|
+
unref?: () => void;
|
|
10
|
+
};
|
|
11
|
+
ttlMs?: number;
|
|
12
|
+
}
|
|
13
|
+
export declare function maybeSpawnRtmRefresh(opts: RtmRefreshOpts): void;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.RTM_REFRESH_MARKER = exports.RTM_REFRESH_TTL_MS = void 0;
|
|
37
|
+
exports.maybeSpawnRtmRefresh = maybeSpawnRtmRefresh;
|
|
38
|
+
// @implements A-SPEC-566.4
|
|
39
|
+
/**
|
|
40
|
+
* Graph freshness, kept by the product itself. The impact advisory's quality factor was MEASURED to
|
|
41
|
+
* be staleness (an 8-day-old graph found 7 advisories; a 1-second reindex found 17), so the Stop
|
|
42
|
+
* hook — the moment changes exist — spawns a DETACHED reindex behind a TTL gate. The idiom is
|
|
43
|
+
* A-SPEC-547.2's update refresh verbatim: TTL file, detached child, fail-soft everywhere; the
|
|
44
|
+
* parent never waits (zero turn latency) and the approval path still only ever REOPENS the graph.
|
|
45
|
+
*/
|
|
46
|
+
const fs = __importStar(require("node:fs"));
|
|
47
|
+
const path = __importStar(require("node:path"));
|
|
48
|
+
exports.RTM_REFRESH_TTL_MS = 30 * 60 * 1000;
|
|
49
|
+
exports.RTM_REFRESH_MARKER = path.join('.ax', 'state', 'rtm-refresh-last');
|
|
50
|
+
function maybeSpawnRtmRefresh(opts) {
|
|
51
|
+
try {
|
|
52
|
+
if (!fs.existsSync(path.join(opts.root, '.ax')))
|
|
53
|
+
return; // nothing to refresh
|
|
54
|
+
const marker = path.join(opts.root, exports.RTM_REFRESH_MARKER);
|
|
55
|
+
const ttl = opts.ttlMs ?? exports.RTM_REFRESH_TTL_MS;
|
|
56
|
+
try {
|
|
57
|
+
const age = opts.now - fs.statSync(marker).mtimeMs;
|
|
58
|
+
if (age < ttl)
|
|
59
|
+
return; // one refresh per window
|
|
60
|
+
}
|
|
61
|
+
catch { /* absent marker → refresh is due */ }
|
|
62
|
+
// Touch BEFORE spawning: a burst of Stops inside one window must not stampede children.
|
|
63
|
+
try {
|
|
64
|
+
fs.mkdirSync(path.dirname(marker), { recursive: true });
|
|
65
|
+
fs.writeFileSync(marker, String(opts.now));
|
|
66
|
+
fs.utimesSync(marker, new Date(opts.now), new Date(opts.now));
|
|
67
|
+
}
|
|
68
|
+
catch { /* an unwritable marker is not a reason to skip the refresh itself */ }
|
|
69
|
+
const child = opts.spawn(opts.execPath, [opts.scriptPath, opts.root], { detached: true, stdio: 'ignore' });
|
|
70
|
+
child?.unref?.();
|
|
71
|
+
}
|
|
72
|
+
catch { /* freshness is maintenance, never a hook failure */ }
|
|
73
|
+
}
|
|
74
|
+
// The child lives in rtm-refresh-child.ts, NOT here: this module is required by the Stop hook, and
|
|
75
|
+
// the gate path must never carry the AST substrate (A-SPEC-510.2 pinned it, and its static check
|
|
76
|
+
// caught the first cut of this slice doing exactly that). The gate side is fs+path only.
|
|
@@ -910,6 +910,48 @@ if (require.main === module) {
|
|
|
910
910
|
const unrecorded = unrecordedApprovals(stopProjectRoot());
|
|
911
911
|
// @implements A-SPEC-455
|
|
912
912
|
const rolledBack = rolledBackLedgers(stopProjectRoot());
|
|
913
|
+
// @implements A-SPEC-564.2 — the Claude usage enrichment: numbers out of transcript_path into
|
|
914
|
+
// the session-context ledger, once per session. Capability-declared: Codex/AGY pass no
|
|
915
|
+
// transcript_path and skip silently. Fail-open around EVERYTHING — observation never touches
|
|
916
|
+
// the verdict below (Judgments must not be budgeted), and a giant transcript yields a capped
|
|
917
|
+
// partial sum, never a slow Stop.
|
|
918
|
+
try {
|
|
919
|
+
const tPath = input.transcript_path;
|
|
920
|
+
if (typeof tPath === 'string' && tPath !== '') {
|
|
921
|
+
const sc = require('../governance/session-context');
|
|
922
|
+
const root = stopProjectRoot();
|
|
923
|
+
const already = sc.readSessionContext(root).some((r) => r.kind === 'usage' && r.sessionKey === sessionId);
|
|
924
|
+
if (!already) {
|
|
925
|
+
// Adversarial round (2026-09-06): a FIFO at transcript_path blocked this read FOREVER,
|
|
926
|
+
// wedging every turn end — the queue writer's round-7 lesson, replayed on the observer.
|
|
927
|
+
// TYPE BEFORE READ: only a regular file is a transcript; anything else is a silent skip.
|
|
928
|
+
if (!fs.lstatSync(tPath).isFile())
|
|
929
|
+
throw new Error('not a regular file');
|
|
930
|
+
const text = fs.readFileSync(tPath, 'utf8');
|
|
931
|
+
const sum = sc.summarizeTranscript(text.split('\n'));
|
|
932
|
+
if (sum.turns > 0) {
|
|
933
|
+
sc.appendSessionContext(root, {
|
|
934
|
+
kind: 'usage', sessionKey: sessionId, models: sum.models, usage: sum.usage,
|
|
935
|
+
turns: sum.turns, ...(sum.truncated ? { truncated: true } : {}), ts: new Date().toISOString(),
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
catch { /* silent skip — the other harnesses' path, and any read failure, land here */ }
|
|
942
|
+
// @implements A-SPEC-566.4 — graph freshness rides the same turn boundary, detached and
|
|
943
|
+
// TTL-gated (A-SPEC-547.2's idiom): staleness was MEASURED to be the advisory's quality
|
|
944
|
+
// factor (7 findings on an 8-day graph, 17 after a 1s reindex). Never waits, never judges.
|
|
945
|
+
try {
|
|
946
|
+
const { maybeSpawnRtmRefresh } = require('./rtm-refresh');
|
|
947
|
+
const cp = require('node:child_process');
|
|
948
|
+
maybeSpawnRtmRefresh({
|
|
949
|
+
root: stopProjectRoot(), now: Date.now(), execPath: process.execPath,
|
|
950
|
+
scriptPath: path.resolve(__dirname, 'rtm-refresh-child.js'),
|
|
951
|
+
spawn: (cmd, args, o) => cp.spawn(cmd, args, o),
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
catch { /* maintenance, never a hook failure */ }
|
|
913
955
|
let out = evaluateStop(specs, { testCasesByAspec, provenance, executedByAspec, findings, findingsUnreadable, unanchoredChangedSources: unanchored, unrecordedApprovals: unrecorded, rolledBackLedgers: rolledBack, redFirstMode, changedAspecs, outcomesByAspec });
|
|
914
956
|
// @implements A-SPEC-534.4 — track mode records ART-8 findings without blocking: surface them so
|
|
915
957
|
// the operator observes RED-first gaps before an owner promotes the posture to strict.
|
|
@@ -261,23 +261,18 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
|
|
|
261
261
|
reason: string;
|
|
262
262
|
findings?: undefined;
|
|
263
263
|
conflict?: undefined;
|
|
264
|
-
approved?: undefined;
|
|
265
|
-
digest?: undefined;
|
|
266
264
|
} | {
|
|
267
265
|
ok: boolean;
|
|
268
266
|
reason: string;
|
|
269
267
|
findings: import("../spec/validator").Finding[];
|
|
270
268
|
conflict?: undefined;
|
|
271
|
-
approved?: undefined;
|
|
272
|
-
digest?: undefined;
|
|
273
269
|
} | {
|
|
274
270
|
ok: boolean;
|
|
275
271
|
reason: string;
|
|
276
272
|
conflict: import("../spec/version-conflict").ConflictDetail;
|
|
277
273
|
findings?: undefined;
|
|
278
|
-
approved?: undefined;
|
|
279
|
-
digest?: undefined;
|
|
280
274
|
} | {
|
|
275
|
+
impactAdvisory?: import("../rtm/impact-advisory").ImpactAdvisory | undefined;
|
|
281
276
|
approved: string;
|
|
282
277
|
digest: string;
|
|
283
278
|
ok?: undefined;
|
|
@@ -536,6 +531,10 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
|
|
|
536
531
|
}): Promise<{
|
|
537
532
|
breadthWarning?: string | undefined;
|
|
538
533
|
impacted: string[];
|
|
534
|
+
impactedSummaries: {
|
|
535
|
+
id: string;
|
|
536
|
+
summary: string | null;
|
|
537
|
+
}[];
|
|
539
538
|
rankedImpact: {
|
|
540
539
|
file: string;
|
|
541
540
|
score: number;
|
|
@@ -151,7 +151,8 @@ const cacheDirFor = (root) => {
|
|
|
151
151
|
};
|
|
152
152
|
// @implements A-SPEC-283 — bumped whenever the graph's node/edge shape changes, so a store written
|
|
153
153
|
// by an older build is rebuilt rather than read with new assumptions.
|
|
154
|
-
|
|
154
|
+
// @implements A-SPEC-568.1 — /3: nodes gained the intent `summary` column.
|
|
155
|
+
const RTM_GRAPH_SCHEMA = 'rtm-graph/3';
|
|
155
156
|
const RTM_EXTRACTOR_VERSION = 'holmes-rtm/1';
|
|
156
157
|
const cachedScan = (root, repoRoot = root) => new cpg_scanner_1.CpgScanner(undefined, new scan_cache_1.ScanFileCache(cacheDirFor(root))).scan(root, repoRoot);
|
|
157
158
|
// @implements A-SPEC-131
|
|
@@ -189,6 +190,7 @@ const approval_grants_1 = require("../governance/approval-grants");
|
|
|
189
190
|
const spec_digest_1 = require("../spec/spec-digest");
|
|
190
191
|
const spec_store_2 = require("../spec/spec-store");
|
|
191
192
|
const breaking_change_1 = require("../spec/breaking-change");
|
|
193
|
+
const compat_impact_1 = require("../spec/compat-impact");
|
|
192
194
|
const approval_blockers_1 = require("../spec/approval-blockers");
|
|
193
195
|
const approval_status_1 = require("../spec/approval-status");
|
|
194
196
|
const ledger_timeline_1 = require("../governance/ledger-timeline");
|
|
@@ -1560,6 +1562,22 @@ function makeRawHandlers(store, opts) {
|
|
|
1560
1562
|
const breakingIssue = (0, breaking_change_1.checkBreakingChangeDeclared)(candidate);
|
|
1561
1563
|
if (breakingIssue)
|
|
1562
1564
|
return { ok: false, reason: breakingIssue };
|
|
1565
|
+
// @implements A-SPEC-565.1 — the compat declaration duty rides the SAME act (REQ-565): sealing
|
|
1566
|
+
// is when "did you consider the three harnesses and the three OSes" is due, and act-time is
|
|
1567
|
+
// what keeps 512 already-approved specs out of retroactive violation (the 38-violation incident
|
|
1568
|
+
// above). The bound reader feeds the OS cross-check from this root's working tree.
|
|
1569
|
+
const compatIssue = (0, compat_impact_1.checkCompatDeclared)(candidate, {
|
|
1570
|
+
// No root → no working tree to read: the OS cross-check skips file-by-file (fail-open),
|
|
1571
|
+
// while the declaration syntax itself is still enforced — the duty never depends on `root`.
|
|
1572
|
+
readFile: (rel) => { try {
|
|
1573
|
+
return a.root ? fs.readFileSync(path.join(a.root, rel), 'utf8') : null;
|
|
1574
|
+
}
|
|
1575
|
+
catch {
|
|
1576
|
+
return null;
|
|
1577
|
+
} },
|
|
1578
|
+
});
|
|
1579
|
+
if (compatIssue)
|
|
1580
|
+
return { ok: false, reason: compatIssue };
|
|
1563
1581
|
// @implements A-SPEC-182
|
|
1564
1582
|
// A document whose prose is still the generator's placeholder must not be sealed. Measured
|
|
1565
1583
|
// 2026-08-13 on a brownfield adoption: H-SPEC-100 took `status: approved` and an
|
|
@@ -1667,7 +1685,46 @@ function makeRawHandlers(store, opts) {
|
|
|
1667
1685
|
if (approveResolved.source === 'grant' && approveResolved.root && approveResolved.approval.nonce) {
|
|
1668
1686
|
(0, approval_grants_1.consumeGrantFile)(approveResolved.root, approveResolved.approval.nonce);
|
|
1669
1687
|
}
|
|
1670
|
-
|
|
1688
|
+
// @implements A-SPEC-566.2 — the impact advisory rides the SUCCESS, after the seal is done:
|
|
1689
|
+
// the verdict is already committed, so nothing here can change it (advisory, never gate —
|
|
1690
|
+
// Judgments must not be budgeted). Reuses the persisted graph READ-ONLY; it never scans,
|
|
1691
|
+
// parses or builds (scan:build measured 20~38x — an approval must not pay that), and every
|
|
1692
|
+
// failure below degrades to "no advisory field" on an otherwise identical response.
|
|
1693
|
+
let impactAdvisory;
|
|
1694
|
+
try {
|
|
1695
|
+
if (spec.type === 'A-SPEC' && a.root) {
|
|
1696
|
+
const dbPath = path.join(a.root, '.ax', 'rtm.sqlite');
|
|
1697
|
+
if (fs.existsSync(dbPath)) {
|
|
1698
|
+
const { declaredImpactGap, appendImpactAdvisory } = require('../rtm/impact-advisory');
|
|
1699
|
+
const { filesToTouch } = require('../spec/compat-impact');
|
|
1700
|
+
const { RtmGraph } = require('../rtm/rtm-graph');
|
|
1701
|
+
const graph = new RtmGraph(dbPath);
|
|
1702
|
+
const gap = declaredImpactGap(filesToTouch(candidate), graph, (rel) => { try {
|
|
1703
|
+
return fs.readFileSync(path.join(a.root, rel), 'utf8');
|
|
1704
|
+
}
|
|
1705
|
+
catch {
|
|
1706
|
+
return null;
|
|
1707
|
+
} });
|
|
1708
|
+
if (gap) {
|
|
1709
|
+
const graphAsOf = (() => { try {
|
|
1710
|
+
return fs.statSync(dbPath).mtime.toISOString();
|
|
1711
|
+
}
|
|
1712
|
+
catch {
|
|
1713
|
+
return undefined;
|
|
1714
|
+
} })();
|
|
1715
|
+
impactAdvisory = { ...gap, ...(graphAsOf ? { graphAsOf } : {}) };
|
|
1716
|
+
appendImpactAdvisory(a.root, {
|
|
1717
|
+
aspec: a.id, files: gap.files.map((f) => f.path), more: gap.more,
|
|
1718
|
+
...(graphAsOf ? { graphAsOf } : {}), ts: new Date().toISOString(),
|
|
1719
|
+
});
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
catch {
|
|
1725
|
+
impactAdvisory = undefined;
|
|
1726
|
+
}
|
|
1727
|
+
return { approved: a.id, digest, ...(impactAdvisory ? { impactAdvisory } : {}) };
|
|
1671
1728
|
},
|
|
1672
1729
|
async spec_list(a) {
|
|
1673
1730
|
assertSpecStoreReachable('spec_list', store, a.root); // @implements A-SPEC-419
|
|
@@ -2188,6 +2245,9 @@ function makeRawHandlers(store, opts) {
|
|
|
2188
2245
|
// (cached vectors only, set fixed, covered hits move, why-line attached). Any missing
|
|
2189
2246
|
// signal — no tier, no key, cold cache, embed failure — leaves the report untouched;
|
|
2190
2247
|
// localization itself never fails because of the semantic layer.
|
|
2248
|
+
//
|
|
2249
|
+
// A spec-intent-vector assist (REQ-568 S3) was wired ahead of localizeIssue here and REVERTED
|
|
2250
|
+
// on its pre-registered replay — see rtm/localize.ts at the matchedSpecs join for the numbers.
|
|
2191
2251
|
try {
|
|
2192
2252
|
if (report.hits.length > 1
|
|
2193
2253
|
&& (0, localize_1.citationsIn)(a.issue, new Set(governed.map((s) => s.id))).cited.length === 0) {
|
|
@@ -2491,8 +2551,20 @@ function makeRawHandlers(store, opts) {
|
|
|
2491
2551
|
.map((id) => (id.includes('@') ? id.slice(id.lastIndexOf('@') + 1) : ''))
|
|
2492
2552
|
.filter((f) => f !== ''));
|
|
2493
2553
|
const rankedImpact = (0, assoc_arm_1.pprImpactRanked)((0, assoc_arm_1.graphViewOf)(g.dumpCanonical()), riSeeds, riExclude, assoc_arm_1.RANKED_IMPACT_K, assoc_arm_1.RANKED_IMPACT_CONFIG);
|
|
2554
|
+
// @implements A-SPEC-568.2 — the intent sentence beside every impacted spec id, same order
|
|
2555
|
+
// as `impacted` (which stays a bare id list for its existing consumers). Information only:
|
|
2556
|
+
// nothing reads it back into the walk, the ranking or any gate.
|
|
2557
|
+
const impactedSummaries = impacted.map((id) => {
|
|
2558
|
+
let summary = null;
|
|
2559
|
+
try {
|
|
2560
|
+
summary = g.summaryOf(id);
|
|
2561
|
+
}
|
|
2562
|
+
catch { /* summary stays null */ }
|
|
2563
|
+
return { id, summary };
|
|
2564
|
+
});
|
|
2494
2565
|
return {
|
|
2495
2566
|
impacted,
|
|
2567
|
+
impactedSummaries,
|
|
2496
2568
|
rankedImpact,
|
|
2497
2569
|
reachedByDepth,
|
|
2498
2570
|
bounded: stoppedAt.length > 0 ? stoppedAt.slice(0, 20) : undefined,
|
|
@@ -3347,6 +3419,8 @@ independent_test: true
|
|
|
3347
3419
|
depends_on:
|
|
3348
3420
|
- ${hspecId}
|
|
3349
3421
|
breaking_change: 'none'
|
|
3422
|
+
harness_impact: 'none: TODO — 3하네스(claude/codex/agy) 영향 검토 후 기술'
|
|
3423
|
+
os_impact: 'none: TODO — 3OS(windows/mac/linux) 영향 검토 후 기술'
|
|
3350
3424
|
---
|
|
3351
3425
|
|
|
3352
3426
|
## Objective
|
|
@@ -42,6 +42,17 @@ const handlers = (0, handlers_1.makeHandlers)(store, {
|
|
|
42
42
|
return 'unknown';
|
|
43
43
|
} },
|
|
44
44
|
});
|
|
45
|
+
// @implements A-SPEC-564.1 — the session-context stamp: WHO attached, name AND version (the version
|
|
46
|
+
// was received and dropped for months). Written HERE because the server is the one place all three
|
|
47
|
+
// harnesses pass through; lazy-once via the call path (clientInfo exists only after initialize, and
|
|
48
|
+
// the ledger's root arrives with the first rooted call). Fail-open by construction.
|
|
49
|
+
const { makeSessionStamper } = require('../governance/session-context');
|
|
50
|
+
const stampSession = makeSessionStamper(() => { try {
|
|
51
|
+
return server.getClientVersion();
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
} });
|
|
45
56
|
// @implements A-SPEC-259 — the advertised version is the package's own, not a literal that froze at
|
|
46
57
|
// 0.1.0: a hardcoded serverInfo.version blinds any client-side drift diagnosis.
|
|
47
58
|
const PKG_VERSION = (() => {
|
|
@@ -109,6 +120,7 @@ const TOOLS = Object.keys(handlers)
|
|
|
109
120
|
});
|
|
110
121
|
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
111
122
|
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (req) => {
|
|
123
|
+
stampSession(req.params.arguments?.root);
|
|
112
124
|
// @implements A-SPEC-189
|
|
113
125
|
// The server is the first consumer of its own advertised schemas. Before this check, 15 of 26
|
|
114
126
|
// handlers threw raw internal errors at `{}` over the wire, and a one-key typo in reverse_anchor
|