@holmes-lab/holmes-kit 0.7.1 → 0.9.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 +102 -0
- package/README.md +10 -6
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/agents.js +21 -11
- package/dist/holmes/cli/doctor.js +25 -2
- package/dist/holmes/cli/init.js +3 -0
- package/dist/holmes/cli/mcp-schema-cost.d.ts +18 -0
- package/dist/holmes/cli/mcp-schema-cost.js +28 -0
- package/dist/holmes/cli/settings-merge.d.ts +1 -0
- package/dist/holmes/cli/settings-merge.js +6 -1
- package/dist/holmes/config/config.d.ts +8 -0
- package/dist/holmes/config/config.js +1 -1
- package/dist/holmes/governance/autonomy.d.ts +14 -0
- package/dist/holmes/governance/autonomy.js +75 -0
- package/dist/holmes/governance/constitution.d.ts +26 -0
- package/dist/holmes/governance/constitution.js +33 -0
- package/dist/holmes/guardrail/write-target.d.ts +25 -0
- package/dist/holmes/guardrail/write-target.js +143 -0
- package/dist/holmes/hooks/pre-tool-use.js +131 -48
- package/dist/holmes/hooks/session-start.d.ts +23 -0
- package/dist/holmes/hooks/session-start.js +111 -0
- package/dist/holmes/hooks/stop.d.ts +24 -0
- package/dist/holmes/hooks/stop.js +83 -3
- package/dist/holmes/mcp/handlers.d.ts +19 -0
- package/dist/holmes/mcp/handlers.js +126 -21
- package/dist/holmes/mcp/server-instructions.d.ts +8 -0
- package/dist/holmes/mcp/server-instructions.js +13 -0
- package/dist/holmes/mcp/server.js +21 -1
- package/dist/holmes/mcp/tool-schemas.js +1 -0
- package/dist/holmes/review/mutate.d.ts +17 -0
- package/dist/holmes/review/mutate.js +66 -0
- package/dist/holmes/review/test-outcomes.d.ts +35 -0
- package/dist/holmes/review/test-outcomes.js +108 -0
- package/dist/holmes/review/test-runner.d.ts +30 -0
- package/dist/holmes/review/test-runner.js +71 -5
- package/dist/holmes/spec/kills.d.ts +14 -0
- package/dist/holmes/spec/kills.js +28 -0
- package/dist/holmes/spec/spec-store.d.ts +9 -0
- package/dist/holmes/spec/spec-store.js +17 -0
- package/dist/holmes/spec/validator.js +18 -0
- package/dist/holmes/update/update-notice.d.ts +28 -0
- package/dist/holmes/update/update-notice.js +131 -0
- package/package.json +1 -1
- package/playbooks/tdd-slice/PLAYBOOK.md +82 -0
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.MAX_CONSECUTIVE_BLOCKS = void 0;
|
|
37
|
+
exports.changedAnchoredAspecs = changedAnchoredAspecs;
|
|
37
38
|
exports.unanchoredChangedSources = unanchoredChangedSources;
|
|
38
39
|
exports.unrecordedApprovals = unrecordedApprovals;
|
|
39
40
|
exports.rolledBackLedgers = rolledBackLedgers;
|
|
@@ -56,10 +57,50 @@ const test_scope_1 = require("../rtm/test-scope");
|
|
|
56
57
|
const constitution_1 = require("../governance/constitution");
|
|
57
58
|
const provenance_chain_1 = require("../governance/provenance-chain");
|
|
58
59
|
const test_evidence_1 = require("../review/test-evidence");
|
|
60
|
+
const test_outcomes_1 = require("../review/test-outcomes");
|
|
61
|
+
const config_1 = require("../config/config");
|
|
59
62
|
const pre_tool_use_1 = require("./pre-tool-use");
|
|
60
63
|
const governance_history_1 = require("../guardrail/governance-history");
|
|
61
64
|
const constitution_debt_1 = require("../governance/constitution-debt");
|
|
62
65
|
const root_1 = require("../project/root");
|
|
66
|
+
/**
|
|
67
|
+
* @implements A-SPEC-534.4
|
|
68
|
+
* ART-8 evidence (I/O half): the A-SPECs whose DIRTY source files carry an @implements anchor. git is
|
|
69
|
+
* a refinement — no repository means `undefined` (no signal), never a false clean.
|
|
70
|
+
*/
|
|
71
|
+
function changedAnchoredAspecs(root) {
|
|
72
|
+
const SOURCE = /\.(?:ts|tsx|mts|cts|js|jsx|mjs|cjs|py|go|rs|java|kt|cs|rb|php|swift)$/;
|
|
73
|
+
const VENDORED = /^(?:reference|node_modules|dist|build|vendor|third_party)\//;
|
|
74
|
+
let raw;
|
|
75
|
+
try {
|
|
76
|
+
raw = (0, node_child_process_1.execFileSync)('git', ['status', '--porcelain', '-uall'], {
|
|
77
|
+
cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], env: (0, root_1.cleanSubprocessEnv)(),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
const ids = new Set();
|
|
84
|
+
for (const line of raw.split('\n')) {
|
|
85
|
+
if (line.trim() === '')
|
|
86
|
+
continue;
|
|
87
|
+
let rel = line.slice(3).trim().replace(/^"|"$/g, '');
|
|
88
|
+
if (rel.includes(' -> '))
|
|
89
|
+
rel = rel.split(' -> ')[1]; // renames name the destination
|
|
90
|
+
if (!SOURCE.test(rel) || VENDORED.test(rel))
|
|
91
|
+
continue;
|
|
92
|
+
let text;
|
|
93
|
+
try {
|
|
94
|
+
text = fs.readFileSync(path.join(root, rel), 'utf8');
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
continue;
|
|
98
|
+
} // deleted/unreadable
|
|
99
|
+
for (const m of text.matchAll(/@implements\s+(A-SPEC-\d+(?:\.\d+)?)/g))
|
|
100
|
+
ids.add(m[1]);
|
|
101
|
+
}
|
|
102
|
+
return [...ids].sort();
|
|
103
|
+
}
|
|
63
104
|
/**
|
|
64
105
|
* @implements A-SPEC-452
|
|
65
106
|
* ART-1 evidence: which changed source files claim nothing.
|
|
@@ -248,7 +289,21 @@ function evaluateStop(specs, evidence) {
|
|
|
248
289
|
// L1: the Stop gate IS the constitution's re-verification point — every turn boundary re-runs the
|
|
249
290
|
// inviolable articles (ART-2 RTM, ART-3 validity incl. 4-quadrant GWT, ART-4 coverage evidence).
|
|
250
291
|
// The articles live in ONE place (governance/constitution.ts); this gate merely executes them.
|
|
251
|
-
|
|
292
|
+
// @implements A-SPEC-534.4 — ART-8 evidence rides through to the constitution, which emits a
|
|
293
|
+
// BLOCKING ART-8 violation only in `strict` mode. `track`/`off` produce none here.
|
|
294
|
+
const violations = (0, constitution_1.verifyConstitution)({
|
|
295
|
+
specs, testCasesByAspec: evidence?.testCasesByAspec, executedByAspec: evidence?.executedByAspec, findings: evidence?.findings,
|
|
296
|
+
redFirstMode: evidence?.redFirstMode, changedAspecs: evidence?.changedAspecs, outcomesByAspec: evidence?.outcomesByAspec,
|
|
297
|
+
});
|
|
298
|
+
// @implements A-SPEC-534.4 — `track` records ART-8 findings without blocking the turn. Computed
|
|
299
|
+
// separately (the constitution stays silent on ART-8 outside strict) and returned in `tracked` for
|
|
300
|
+
// the CLI to record; it never enters `problems`/the block decision.
|
|
301
|
+
let tracked;
|
|
302
|
+
if (evidence?.redFirstMode === 'track' && evidence.changedAspecs && evidence.outcomesByAspec) {
|
|
303
|
+
const t = (0, constitution_1.redFirstViolations)(evidence.changedAspecs, evidence.outcomesByAspec);
|
|
304
|
+
if (t.length)
|
|
305
|
+
tracked = t;
|
|
306
|
+
}
|
|
252
307
|
const problems = violations.map((x) => `[${x.article}] ${x.detail}`);
|
|
253
308
|
// @implements A-SPEC-247 — structured list so the caller can ask acknowledgeStop which of these
|
|
254
309
|
// are waiting on an owner. Mirrors `problems` exactly, including the two synthesized below.
|
|
@@ -287,7 +342,7 @@ function evaluateStop(specs, evidence) {
|
|
|
287
342
|
structured.push({ article: 'ART-2', detail });
|
|
288
343
|
}
|
|
289
344
|
if (problems.length === 0)
|
|
290
|
-
return { block: false };
|
|
345
|
+
return { block: false, ...(tracked ? { tracked } : {}) };
|
|
291
346
|
const shown = problems.slice(0, 20);
|
|
292
347
|
const more = problems.length > shown.length ? `\n…and ${problems.length - shown.length} more` : '';
|
|
293
348
|
// @implements A-SPEC-134 — the distinct articles feed the constitution-debt state on a cap-yield.
|
|
@@ -301,6 +356,7 @@ function evaluateStop(specs, evidence) {
|
|
|
301
356
|
block: true,
|
|
302
357
|
articles,
|
|
303
358
|
violations: structured,
|
|
359
|
+
...(tracked ? { tracked } : {}),
|
|
304
360
|
reason: `[Holmes-Kit] constitution gate: ${problems.length} article violation(s) must be ` +
|
|
305
361
|
`fixed before finishing:\n${shown.join('\n')}${more}`,
|
|
306
362
|
};
|
|
@@ -656,6 +712,25 @@ if (require.main === module) {
|
|
|
656
712
|
catch {
|
|
657
713
|
executedByAspec = undefined;
|
|
658
714
|
}
|
|
715
|
+
// @implements A-SPEC-534.4 — ART-8 RED-first evidence (I/O half). Outcomes recorded at the
|
|
716
|
+
// current baseline HEAD (where the dirty work's red+green ran); changed anchored A-SPECs from
|
|
717
|
+
// the working tree; the posture from config. Fail-open: any error leaves ART-8 inert this turn.
|
|
718
|
+
let redFirstMode;
|
|
719
|
+
let changedAspecs;
|
|
720
|
+
let outcomesByAspec;
|
|
721
|
+
try {
|
|
722
|
+
redFirstMode = (0, config_1.loadConfig)(stopProjectRoot()).guardrail.redFirstEvidence;
|
|
723
|
+
if (redFirstMode !== 'off') {
|
|
724
|
+
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();
|
|
725
|
+
changedAspecs = changedAnchoredAspecs(stopProjectRoot());
|
|
726
|
+
outcomesByAspec = (0, test_outcomes_1.groupOutcomesByAspec)((0, test_outcomes_1.readOutcomes)(stopProjectRoot()), head);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
catch {
|
|
730
|
+
redFirstMode = undefined;
|
|
731
|
+
changedAspecs = undefined;
|
|
732
|
+
outcomesByAspec = undefined;
|
|
733
|
+
}
|
|
659
734
|
// Provenance-chain verification (fail-open: a verify error skips the check, never crashes).
|
|
660
735
|
let provenance;
|
|
661
736
|
// @implements A-SPEC-148
|
|
@@ -690,7 +765,12 @@ if (require.main === module) {
|
|
|
690
765
|
const unrecorded = unrecordedApprovals(stopProjectRoot());
|
|
691
766
|
// @implements A-SPEC-455
|
|
692
767
|
const rolledBack = rolledBackLedgers(stopProjectRoot());
|
|
693
|
-
let out = evaluateStop(specs, { testCasesByAspec, provenance, executedByAspec, findings, findingsUnreadable, unanchoredChangedSources: unanchored, unrecordedApprovals: unrecorded, rolledBackLedgers: rolledBack });
|
|
768
|
+
let out = evaluateStop(specs, { testCasesByAspec, provenance, executedByAspec, findings, findingsUnreadable, unanchoredChangedSources: unanchored, unrecordedApprovals: unrecorded, rolledBackLedgers: rolledBack, redFirstMode, changedAspecs, outcomesByAspec });
|
|
769
|
+
// @implements A-SPEC-534.4 — track mode records ART-8 findings without blocking: surface them so
|
|
770
|
+
// the operator observes RED-first gaps before an owner promotes the posture to strict.
|
|
771
|
+
if (out.tracked && out.tracked.length > 0) {
|
|
772
|
+
process.stderr.write(`[Holmes-Kit] ART-8 RED-first (track): ${out.tracked.map((t) => t.detail).join(' | ')}\n`);
|
|
773
|
+
}
|
|
694
774
|
// @implements A-SPEC-247 — before deciding to re-block, ask whether every unresolved debt is
|
|
695
775
|
// already queued for the owner. If so, tell the user ONCE and let the turn finish; a single
|
|
696
776
|
// non-waiting violation and we block exactly as before.
|
|
@@ -347,7 +347,25 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
|
|
|
347
347
|
head?: string;
|
|
348
348
|
since?: string;
|
|
349
349
|
mark?: string;
|
|
350
|
+
mutate?: string;
|
|
350
351
|
}): Promise<{
|
|
352
|
+
mutate: {
|
|
353
|
+
tspec: string;
|
|
354
|
+
aspec: string | undefined;
|
|
355
|
+
coveringFiles: string[];
|
|
356
|
+
results: ({
|
|
357
|
+
mutation: import("../spec/kills").Mutation;
|
|
358
|
+
applied: boolean;
|
|
359
|
+
reason: string;
|
|
360
|
+
} | {
|
|
361
|
+
applied: boolean;
|
|
362
|
+
verdict?: "killed" | "survived";
|
|
363
|
+
mutation: import("../spec/kills").Mutation;
|
|
364
|
+
reason?: undefined;
|
|
365
|
+
})[];
|
|
366
|
+
survivors: import("../spec/kills").Mutation[];
|
|
367
|
+
};
|
|
368
|
+
} | {
|
|
351
369
|
baselineRecorded?: string | undefined;
|
|
352
370
|
scopeFallback?: "full" | undefined;
|
|
353
371
|
tier: import("../rtm/test-scope").RegressionTier;
|
|
@@ -359,6 +377,7 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
|
|
|
359
377
|
tail: string;
|
|
360
378
|
unresolvedFiles: string[];
|
|
361
379
|
changeSource: ChangeSourceInfo;
|
|
380
|
+
mutate?: undefined;
|
|
362
381
|
}>;
|
|
363
382
|
issue_localize(a: {
|
|
364
383
|
root: string;
|
|
@@ -68,7 +68,10 @@ const rtm_check_1 = require("../rtm/rtm-check");
|
|
|
68
68
|
const test_scope_1 = require("../rtm/test-scope");
|
|
69
69
|
const gap_analyzer_1 = require("../rtm/gap-analyzer");
|
|
70
70
|
const test_runner_1 = require("../review/test-runner");
|
|
71
|
+
const kills_1 = require("../spec/kills");
|
|
72
|
+
const mutate_1 = require("../review/mutate");
|
|
71
73
|
const test_evidence_1 = require("../review/test-evidence");
|
|
74
|
+
const test_outcomes_1 = require("../review/test-outcomes");
|
|
72
75
|
const localize_1 = require("../rtm/localize");
|
|
73
76
|
const maintenance_analyze_1 = require("./maintenance-analyze");
|
|
74
77
|
const maintenance_evidence_1 = require("./maintenance-evidence");
|
|
@@ -90,6 +93,53 @@ const root_1 = require("../project/root");
|
|
|
90
93
|
// under that tree stopped at the minted marker. This is the harm §8 closed for `review_record`,
|
|
91
94
|
// left open on the scan path. When the store is bound, the cache is the bound project's; otherwise
|
|
92
95
|
// only an anchored answer may be written to, and an unanchored one falls back to a temp cache.
|
|
96
|
+
// @implements A-SPEC-534.8
|
|
97
|
+
// The governed source file a `kills` mutation targets: one that @implements the A-SPEC AND contains
|
|
98
|
+
// the `where` literal. Bounded walk, skips vendored/test/hidden dirs. null when none qualifies.
|
|
99
|
+
function sourceFileWithMutation(root, aspecId, where) {
|
|
100
|
+
if (!where)
|
|
101
|
+
return null;
|
|
102
|
+
const SOURCE = /\.(?:ts|tsx|mts|cts|js|jsx|mjs|cjs|py|go|rs|java|kt|cs|rb|php|swift)$/;
|
|
103
|
+
let found = null;
|
|
104
|
+
const walk = (d) => {
|
|
105
|
+
if (found)
|
|
106
|
+
return;
|
|
107
|
+
let entries;
|
|
108
|
+
try {
|
|
109
|
+
entries = fs.readdirSync(d, { withFileTypes: true });
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
for (const e of entries) {
|
|
115
|
+
if (found)
|
|
116
|
+
return;
|
|
117
|
+
if (e.name === 'node_modules' || e.name === 'dist' || e.name === 'reference'
|
|
118
|
+
|| e.name === 'vendor' || e.name === 'third_party' || e.name.startsWith('.'))
|
|
119
|
+
continue;
|
|
120
|
+
const p = path.join(d, e.name);
|
|
121
|
+
if (e.isDirectory()) {
|
|
122
|
+
walk(p);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (!SOURCE.test(e.name) || /\.test\./.test(e.name))
|
|
126
|
+
continue;
|
|
127
|
+
let text;
|
|
128
|
+
try {
|
|
129
|
+
text = fs.readFileSync(p, 'utf8');
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (text.includes(`@implements ${aspecId}`) && text.includes(where)) {
|
|
135
|
+
found = p;
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
walk(root);
|
|
141
|
+
return found;
|
|
142
|
+
}
|
|
93
143
|
const cacheDirFor = (root) => {
|
|
94
144
|
const r = (0, root_2.resolveProjectRoot)(root);
|
|
95
145
|
if (r.marker !== 'given')
|
|
@@ -130,6 +180,7 @@ const package_1 = require("../review/package");
|
|
|
130
180
|
const risk_classifier_1 = require("../guardrail/risk-classifier");
|
|
131
181
|
const risk_gate_1 = require("../guardrail/risk-gate");
|
|
132
182
|
const elicit_approval_1 = require("./elicit-approval");
|
|
183
|
+
const autonomy_1 = require("../governance/autonomy");
|
|
133
184
|
const anchor_comment_1 = require("../rtm/anchor-comment");
|
|
134
185
|
const consistency_lints_1 = require("../cpg/consistency-lints");
|
|
135
186
|
const approval_queue_1 = require("../governance/approval-queue");
|
|
@@ -669,6 +720,15 @@ function makeRawHandlers(store, opts) {
|
|
|
669
720
|
token: crypto.randomUUID(),
|
|
670
721
|
rationale: reason ?? 'elicitation grant',
|
|
671
722
|
});
|
|
723
|
+
// @implements A-SPEC-532.2 — the autonomous channel: the SAME synthesized-Approval shape that
|
|
724
|
+
// rides the existing seal path, but its actor names `autonomous:<client>` so an audit can tell a
|
|
725
|
+
// self-approved seal from a human-approved (elicitation) or operator (env/grant) one. Single-use
|
|
726
|
+
// by construction — it exists only inside this call, persisted nowhere.
|
|
727
|
+
const autonomousApproval = () => ({
|
|
728
|
+
actor: `autonomous:${opts?.clientName?.() ?? 'unknown'}`,
|
|
729
|
+
token: crypto.randomUUID(),
|
|
730
|
+
rationale: `autonomous grant (${'HOLMES_AUTONOMOUS_APPROVAL'} enabled, spec grade auto)`,
|
|
731
|
+
});
|
|
672
732
|
/**
|
|
673
733
|
* Where the audit record for a governance act belongs — resolved BEFORE the act writes anything.
|
|
674
734
|
*
|
|
@@ -1134,26 +1194,38 @@ function makeRawHandlers(store, opts) {
|
|
|
1134
1194
|
if (approveResolved === undefined) {
|
|
1135
1195
|
const target = await store.read(a.id).catch(() => null);
|
|
1136
1196
|
if (target) {
|
|
1137
|
-
|
|
1138
|
-
//
|
|
1139
|
-
//
|
|
1140
|
-
//
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1197
|
+
// @implements A-SPEC-532.2 — the autonomous gate sits BEFORE the human ask: when the
|
|
1198
|
+
// out-of-band autonomy switch is on AND the spec is low/mid-risk (never gate-behavior, an
|
|
1199
|
+
// architecture/taint file, or an upstream REQ/H/C — those stay human), the agent seals it
|
|
1200
|
+
// itself. The switch is env-only and an agent cannot set it (pre-tool-use blocks that,
|
|
1201
|
+
// A-SPEC-532.2). Off, or a hitl-classed spec, falls straight through to the elicitor
|
|
1202
|
+
// unchanged — the autonomous-OFF path is byte-identical to before.
|
|
1203
|
+
if ((0, autonomy_1.autonomousApprovalEnabled)(process.env)
|
|
1204
|
+
&& (0, autonomy_1.specApprovalAutonomy)(target.spec, resolver([target.spec])) === 'auto') {
|
|
1205
|
+
approveResolved = { approval: autonomousApproval(), source: 'autonomous' };
|
|
1144
1206
|
}
|
|
1145
|
-
else
|
|
1146
|
-
|
|
1147
|
-
//
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1207
|
+
else {
|
|
1208
|
+
const resealing = typeof target.spec.frontmatter.approved_digest === 'string';
|
|
1209
|
+
// The MODEL text is capped BEFORE the server markers are appended (round-2): a ~185+ char
|
|
1210
|
+
// title pushed '(재봉인)' past the dialog's 200-char summary cap, dressing a re-seal (the
|
|
1211
|
+
// more consequential act) as a first approval. The cap cuts the title, never the marker.
|
|
1212
|
+
const out = await tryElicit('spec-approve', a.id, `${a.id} — ${target.spec.title.slice(0, 120)}${resealing ? ' (재봉인)' : ''}`);
|
|
1213
|
+
if (out.kind === 'answered' && out.decision.granted) {
|
|
1214
|
+
approveResolved = { approval: elicitApproval(out.decision.reason), source: 'elicitation' };
|
|
1215
|
+
}
|
|
1216
|
+
else if (out.kind === 'answered') {
|
|
1217
|
+
// The human ANSWERED (deny/question/decline): the answer is the message, and no queue
|
|
1218
|
+
// entry is filed — a decided request is not a pending one (REQ-246 visibility).
|
|
1219
|
+
return { ok: false, reason: `spec_approve: 세션에서 거부됨 — ${out.decision.reason ?? '(사유 없음)'}. 사유를 해소한 뒤 다시 시도하십시오.` };
|
|
1220
|
+
}
|
|
1221
|
+
else if (out.kind === 'expired') {
|
|
1222
|
+
// @implements A-SPEC-497.1 — only the expiry earns a name: the notice LEADS the same
|
|
1223
|
+
// fail-closed refusal + queue path, so the semantics stay refusal+queue and only the
|
|
1224
|
+
// message learned to say what happened.
|
|
1225
|
+
elicitExpiredMs = out.waitedMs;
|
|
1226
|
+
}
|
|
1227
|
+
// silent: the channel gave no answer — fall through to the byte-identical refusal.
|
|
1155
1228
|
}
|
|
1156
|
-
// silent: the channel gave no answer — fall through to the byte-identical refusal.
|
|
1157
1229
|
}
|
|
1158
1230
|
}
|
|
1159
1231
|
if (approveResolved === undefined) {
|
|
@@ -1237,8 +1309,15 @@ function makeRawHandlers(store, opts) {
|
|
|
1237
1309
|
// element instead leaves a window in which an external edit is silently destroyed and the
|
|
1238
1310
|
// STALE content gets sealed (measured: 17 of 40 concurrent edits lost, 35-55ms window).
|
|
1239
1311
|
const cur = await store.read(a.id);
|
|
1240
|
-
|
|
1241
|
-
|
|
1312
|
+
// @implements A-SPEC-536.1 — BUG-1: a spec whose YAML is broken is dropped by read()/list(),
|
|
1313
|
+
// so a bare "not found" hid that the file EXISTS but cannot be parsed. Surface the skipped
|
|
1314
|
+
// files when there are any; byte-identical to the legacy message when there are none. The
|
|
1315
|
+
// store's specsRoot is read through the same cast the reachability checks use (A-SPEC-169).
|
|
1316
|
+
if (!cur) {
|
|
1317
|
+
const dir = store.specsRoot;
|
|
1318
|
+
const unreadable = typeof dir === 'string' ? (0, spec_store_1.unreadableSpecFiles)(dir) : [];
|
|
1319
|
+
return { ok: false, reason: (0, spec_store_1.notFoundReason)(a.id, unreadable) };
|
|
1320
|
+
}
|
|
1242
1321
|
const spec = cur.spec;
|
|
1243
1322
|
// @implements A-SPEC-188 — duplicates make the id ambiguous, for the SPEC and for its
|
|
1244
1323
|
// PARENTS alike. Round-3 probed the parent half: with a stray duplicate of the parent
|
|
@@ -1807,6 +1886,26 @@ function makeRawHandlers(store, opts) {
|
|
|
1807
1886
|
// Closes the decision->execution loop: scope -> run -> durable per-A-SPEC EXECUTION evidence
|
|
1808
1887
|
// (what the constitution's ART-4 prefers over the syntactic count).
|
|
1809
1888
|
const { root, specs, scanned, changedFiles, changedSymbols, changeSource, scopeFallback, anchorImpactedSpecs, changedTestFiles, unresolvedFiles } = await deriveChangedContext(store, a.root, a, 'test_run');
|
|
1889
|
+
// @implements A-SPEC-534.8 — `mutate`: run a T-SPEC's declared `kills` mutations against the
|
|
1890
|
+
// A-SPEC's source and report which SURVIVED (a discriminating-power gap). Selective, opt-in via
|
|
1891
|
+
// the argument; absent → the ordinary run below is untouched.
|
|
1892
|
+
if (a.mutate) {
|
|
1893
|
+
const tspec = specs.find((s) => s.id === a.mutate && s.type === 'T-SPEC');
|
|
1894
|
+
const kills = tspec ? (0, kills_1.parseKills)(tspec.frontmatter) : [];
|
|
1895
|
+
const aspecId = tspec?.dependsOn[0];
|
|
1896
|
+
const anchors = (0, test_scope_1.scanTestAnchors)(root);
|
|
1897
|
+
const coveringFiles = aspecId
|
|
1898
|
+
? Object.entries(anchors).filter(([, ids]) => ids.includes(aspecId)).map(([f]) => f) : [];
|
|
1899
|
+
const results = kills.map((m) => {
|
|
1900
|
+
const src = aspecId ? sourceFileWithMutation(root, aspecId, m.where) : null;
|
|
1901
|
+
if (!src)
|
|
1902
|
+
return { mutation: m, applied: false, reason: 'no governed source anchors this A-SPEC and contains `where`' };
|
|
1903
|
+
const r = (0, mutate_1.runKillsOnFile)(src, m, coveringFiles, (files) => (0, test_runner_1.runJestOutcomes)(files, root));
|
|
1904
|
+
return { mutation: m, ...r };
|
|
1905
|
+
});
|
|
1906
|
+
const survivors = results.filter((r) => r.verdict === 'survived').map((r) => r.mutation);
|
|
1907
|
+
return { mutate: { tspec: a.mutate, aspec: aspecId, coveringFiles, results, survivors } };
|
|
1908
|
+
}
|
|
1810
1909
|
const g = new rtm_graph_1.RtmGraph();
|
|
1811
1910
|
let testScope;
|
|
1812
1911
|
try {
|
|
@@ -1847,6 +1946,12 @@ function makeRawHandlers(store, opts) {
|
|
|
1847
1946
|
if (verified) {
|
|
1848
1947
|
(0, test_evidence_1.writeTestEvidence)(root, { ts: new Date().toISOString(), head, tier: testScope.tier, passed: true, executedByAspec });
|
|
1849
1948
|
}
|
|
1949
|
+
// @implements A-SPEC-534.5 — RED-first outcome evidence for ART-8. Recorded UNCONDITIONALLY,
|
|
1950
|
+
// unlike the green-only baseline above: a red-assertion recorded before the code is exactly what
|
|
1951
|
+
// the red→green sequence needs. Append-only, one record per (A-SPEC, outcome) at this HEAD.
|
|
1952
|
+
if (result.outcomeByFile) {
|
|
1953
|
+
(0, test_outcomes_1.appendOutcomes)(root, (0, test_outcomes_1.buildOutcomeRecords)(result.outcomeByFile, anchors, head, new Date().toISOString()));
|
|
1954
|
+
}
|
|
1850
1955
|
// @implements A-SPEC-128
|
|
1851
1956
|
// The baseline is written under EXACTLY the condition that already gates evidence: a run that
|
|
1852
1957
|
// actually executed and passed. A red or skipped run must never become the reference point for
|
|
@@ -3038,7 +3143,7 @@ id: ${aspecId}
|
|
|
3038
3143
|
type: A-SPEC
|
|
3039
3144
|
title: ${(0, yaml_scalar_1.yamlScalar)(`Architecture Specification for ${a.title}`)}
|
|
3040
3145
|
status: draft
|
|
3041
|
-
slice: ${a.sliceName}
|
|
3146
|
+
slice: ${(0, yaml_scalar_1.yamlScalar)(a.sliceName)}
|
|
3042
3147
|
priority: P1
|
|
3043
3148
|
independent_test: true
|
|
3044
3149
|
depends_on:
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { InstallMode } from '../update/update-notice';
|
|
2
|
+
export interface ServerInstructionsInput {
|
|
3
|
+
version: string;
|
|
4
|
+
home: string;
|
|
5
|
+
mode: InstallMode;
|
|
6
|
+
readFile: (p: string) => string;
|
|
7
|
+
}
|
|
8
|
+
export declare function buildServerInstructions(input: ServerInstructionsInput): string;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildServerInstructions = buildServerInstructions;
|
|
4
|
+
// @implements A-SPEC-531.2
|
|
5
|
+
// The MCP server's `instructions` banner. A harness with no SessionStart hook (codex, antigravity)
|
|
6
|
+
// still gets the intro (+ optional update notice) delivered to the agent through the server's
|
|
7
|
+
// initialize response. PURE and fail-soft — a cache read failure yields the intro only, and never
|
|
8
|
+
// throws, so server construction is never blocked by the banner.
|
|
9
|
+
const update_notice_1 = require("../update/update-notice");
|
|
10
|
+
function buildServerInstructions(input) {
|
|
11
|
+
const cached = (0, update_notice_1.readCache)(input.home, input.readFile); // readCache already swallows a throw → null
|
|
12
|
+
return (0, update_notice_1.composeBanner)({ current: input.version, cached, mode: input.mode, npmUrl: update_notice_1.NPM_URL });
|
|
13
|
+
}
|
|
@@ -54,7 +54,27 @@ const PKG_VERSION = (() => {
|
|
|
54
54
|
return '0.0.0';
|
|
55
55
|
}
|
|
56
56
|
})();
|
|
57
|
-
|
|
57
|
+
// @implements A-SPEC-531.2 — the banner rides `instructions` so a harness without a SessionStart
|
|
58
|
+
// hook still delivers the intro (+ update notice) to the agent. Fail-soft: any failure omits it and
|
|
59
|
+
// the server starts normally.
|
|
60
|
+
const SERVER_INSTRUCTIONS = (() => {
|
|
61
|
+
try {
|
|
62
|
+
const { buildServerInstructions } = require('./server-instructions');
|
|
63
|
+
const { detectInstallMode } = require('../hooks/session-start');
|
|
64
|
+
const os = require('node:os');
|
|
65
|
+
const fs = require('node:fs');
|
|
66
|
+
return buildServerInstructions({
|
|
67
|
+
version: PKG_VERSION,
|
|
68
|
+
home: os.homedir(),
|
|
69
|
+
mode: detectInstallMode(require('node:path').resolve(__dirname, '..', '..', '..')),
|
|
70
|
+
readFile: (p) => fs.readFileSync(p, 'utf8'),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
})();
|
|
77
|
+
const server = new index_js_1.Server({ name: 'holmes-kit', version: PKG_VERSION }, { capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS });
|
|
58
78
|
const fullProfile = process.env.HOLMES_MCP_PROFILE === 'full';
|
|
59
79
|
// Register each handler as a tool with its real typed inputSchema so MCP
|
|
60
80
|
// clients can marshal complex (array/object) arguments; fall back to a
|
|
@@ -203,6 +203,7 @@ exports.TOOL_SCHEMAS = {
|
|
|
203
203
|
head: str('Head rev (e.g. HEAD). Optional — omit to compare against a baseline.'),
|
|
204
204
|
since: str('Baseline label to compare against when no git range is given (default `last-green`, recorded by a passing test_run). Works with or without version control.'),
|
|
205
205
|
mark: str('Baseline label to record when the run passes (default `last-green`). Nothing is recorded for a red or skipped run.'),
|
|
206
|
+
mutate: str('T-SPEC id (e.g. T-SPEC-129.1). When set, runs that spec’s declared `kills` mutations against the A-SPEC’s source and reports which SURVIVED — a discriminating-power gap. Selective, opt-in; the ordinary run is skipped.'),
|
|
206
207
|
},
|
|
207
208
|
required: ['root'],
|
|
208
209
|
},
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Mutation } from '../spec/kills';
|
|
2
|
+
import { TestOutcome } from './test-runner';
|
|
3
|
+
/**
|
|
4
|
+
* Did the mutation KILL the covering tests? A kill is a `red-assertion` in at least one covering
|
|
5
|
+
* file — the mutation broke behaviour and a test caught it. A green (or absent, or red-error) result
|
|
6
|
+
* is `survived`: the tests did not catch the change, which is a discriminating-power gap. Pure.
|
|
7
|
+
*/
|
|
8
|
+
export declare function mutationVerdict(outcomeByFile: Record<string, TestOutcome>, coveringFiles: string[]): 'killed' | 'survived';
|
|
9
|
+
/**
|
|
10
|
+
* Apply one mutation to a source file, run the covering tests through the injected runner, judge the
|
|
11
|
+
* verdict, and ALWAYS restore the original source. `applied:false` when `where` is absent (the file
|
|
12
|
+
* is never touched).
|
|
13
|
+
*/
|
|
14
|
+
export declare function runKillsOnFile(sourceFile: string, mutation: Mutation, coveringFiles: string[], run: (files: string[]) => Record<string, TestOutcome>): {
|
|
15
|
+
applied: boolean;
|
|
16
|
+
verdict?: 'killed' | 'survived';
|
|
17
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
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.mutationVerdict = mutationVerdict;
|
|
37
|
+
exports.runKillsOnFile = runKillsOnFile;
|
|
38
|
+
// @implements A-SPEC-534.8
|
|
39
|
+
const fs = __importStar(require("node:fs"));
|
|
40
|
+
const kills_1 = require("../spec/kills");
|
|
41
|
+
/**
|
|
42
|
+
* Did the mutation KILL the covering tests? A kill is a `red-assertion` in at least one covering
|
|
43
|
+
* file — the mutation broke behaviour and a test caught it. A green (or absent, or red-error) result
|
|
44
|
+
* is `survived`: the tests did not catch the change, which is a discriminating-power gap. Pure.
|
|
45
|
+
*/
|
|
46
|
+
function mutationVerdict(outcomeByFile, coveringFiles) {
|
|
47
|
+
return coveringFiles.some((f) => outcomeByFile[f] === 'red-assertion') ? 'killed' : 'survived';
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Apply one mutation to a source file, run the covering tests through the injected runner, judge the
|
|
51
|
+
* verdict, and ALWAYS restore the original source. `applied:false` when `where` is absent (the file
|
|
52
|
+
* is never touched).
|
|
53
|
+
*/
|
|
54
|
+
function runKillsOnFile(sourceFile, mutation, coveringFiles, run) {
|
|
55
|
+
const original = fs.readFileSync(sourceFile, 'utf8');
|
|
56
|
+
const mutated = (0, kills_1.applyMutation)(original, mutation);
|
|
57
|
+
if (mutated === null)
|
|
58
|
+
return { applied: false }; // where absent: never touch the file
|
|
59
|
+
fs.writeFileSync(sourceFile, mutated);
|
|
60
|
+
try {
|
|
61
|
+
return { applied: true, verdict: mutationVerdict(run(coveringFiles), coveringFiles) };
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
fs.writeFileSync(sourceFile, original); // ALWAYS restore, even if run() threw
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { TestOutcome } from './test-runner';
|
|
2
|
+
export declare const OUTCOMES_FILE: string;
|
|
3
|
+
/**
|
|
4
|
+
* A durable, append-only record of a per-A-SPEC test outcome (REQ-534 RED-first evidence). Unlike
|
|
5
|
+
* `test-evidence.json` (overwritten each run), the SEQUENCE matters here — a red-assertion followed
|
|
6
|
+
* by a green is what ART-8 reads — so outcomes accumulate. `head` stamps the baseline commit the
|
|
7
|
+
* run sat on; the red (before code) and green (after code) of one slice share it while the tree is
|
|
8
|
+
* still dirty, which is exactly when the Stop gate reads them.
|
|
9
|
+
*/
|
|
10
|
+
export interface OutcomeRecord {
|
|
11
|
+
aspec: string;
|
|
12
|
+
outcome: TestOutcome;
|
|
13
|
+
ts: string;
|
|
14
|
+
head: string;
|
|
15
|
+
testFileDigest?: string;
|
|
16
|
+
}
|
|
17
|
+
/** Append outcome records as JSONL lines. Fail-open (recording must never break a run). */
|
|
18
|
+
export declare function appendOutcomes(root: string, records: OutcomeRecord[]): boolean;
|
|
19
|
+
/** Read every outcome record; missing file → [], broken lines skipped (same convention as the other ledgers). */
|
|
20
|
+
export declare function readOutcomes(root: string): OutcomeRecord[];
|
|
21
|
+
/**
|
|
22
|
+
* @implements A-SPEC-534.5
|
|
23
|
+
* Expand per-file outcomes into per-A-SPEC records via the anchor map, stamping each with the run's
|
|
24
|
+
* head and ts. A file with no anchor (or an empty anchor list) contributes nothing — an outcome that
|
|
25
|
+
* cannot be attributed to an A-SPEC is not evidence about one. Pure.
|
|
26
|
+
*/
|
|
27
|
+
export declare function buildOutcomeRecords(outcomeByFile: Record<string, TestOutcome>, anchors: Record<string, string[]>, head: string, ts: string): OutcomeRecord[];
|
|
28
|
+
/**
|
|
29
|
+
* Group outcomes by A-SPEC, keeping only records stamped with the given baseline `head` — a stale
|
|
30
|
+
* record from another commit cannot vouch for the current work (the isFresh discipline). Pure.
|
|
31
|
+
*/
|
|
32
|
+
export declare function groupOutcomesByAspec(records: OutcomeRecord[], head: string): Record<string, Array<{
|
|
33
|
+
outcome: TestOutcome;
|
|
34
|
+
ts: string;
|
|
35
|
+
}>>;
|