@holmes-lab/holmes-kit 0.12.2 → 0.14.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 +127 -0
- package/README.md +13 -3
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/approve-context.js +10 -10
- package/dist/holmes/cli/approve-ref.js +5 -5
- package/dist/holmes/cli/approve-watch.d.ts +1 -1
- package/dist/holmes/cli/approve-watch.js +6 -6
- package/dist/holmes/cli/approve.d.ts +3 -3
- package/dist/holmes/cli/approve.js +57 -57
- package/dist/holmes/cli/autonomy.d.ts +22 -0
- package/dist/holmes/cli/autonomy.js +145 -0
- package/dist/holmes/cli/colophon.d.ts +6 -0
- package/dist/holmes/cli/colophon.js +24 -0
- package/dist/holmes/cli/doctor.d.ts +2 -2
- package/dist/holmes/cli/doctor.js +104 -87
- package/dist/holmes/cli/index.js +122 -63
- package/dist/holmes/cli/init.d.ts +2 -0
- package/dist/holmes/cli/init.js +31 -19
- package/dist/holmes/cli/interactive-prompt.d.ts +8 -0
- package/dist/holmes/cli/interactive-prompt.js +23 -0
- package/dist/holmes/cli/semantic-key.js +9 -9
- package/dist/holmes/cli/settings-merge.d.ts +2 -1
- package/dist/holmes/cli/settings-merge.js +15 -3
- package/dist/holmes/cli/upgrade.js +7 -7
- package/dist/holmes/cpg/proposed-content.js +2 -2
- package/dist/holmes/governance/autonomy.d.ts +10 -2
- package/dist/holmes/governance/autonomy.js +191 -5
- package/dist/holmes/guardrail/blind-spots.js +15 -15
- package/dist/holmes/guardrail/dependency-delta.d.ts +3 -0
- package/dist/holmes/guardrail/dependency-delta.js +118 -0
- package/dist/holmes/hooks/pre-tool-use.js +111 -42
- package/dist/holmes/hooks/session-start.js +17 -0
- package/dist/holmes/hooks/stop.d.ts +22 -1
- package/dist/holmes/hooks/stop.js +176 -14
- package/dist/holmes/mcp/handlers.js +14 -1
- package/dist/holmes/project/root.js +9 -1
- package/dist/holmes/semantic/credentials.js +1 -1
- package/dist/holmes/spec/id-collision.js +2 -2
- package/package.json +2 -2
- package/playbooks/publish/PLAYBOOK.md +47 -35
- package/playbooks/remediation/PLAYBOOK.md +1 -1
|
@@ -36,6 +36,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.MAX_CONSECUTIVE_BLOCKS = void 0;
|
|
37
37
|
exports.changedAnchoredAspecs = changedAnchoredAspecs;
|
|
38
38
|
exports.unanchoredChangedSources = unanchoredChangedSources;
|
|
39
|
+
exports.dependencyReappraisals = dependencyReappraisals;
|
|
40
|
+
exports.reappraisalWarningLine = reappraisalWarningLine;
|
|
41
|
+
exports.escalateReappraisals = escalateReappraisals;
|
|
39
42
|
exports.unrecordedApprovals = unrecordedApprovals;
|
|
40
43
|
exports.rolledBackLedgers = rolledBackLedgers;
|
|
41
44
|
exports.governanceLostPreflight = governanceLostPreflight;
|
|
@@ -50,6 +53,9 @@ exports.readGuardCount = readGuardCount;
|
|
|
50
53
|
exports.writeGuardCount = writeGuardCount;
|
|
51
54
|
const fs = __importStar(require("node:fs"));
|
|
52
55
|
const npx_bin_1 = require("../project/npx-bin");
|
|
56
|
+
const approval_queue_1 = require("../governance/approval-queue");
|
|
57
|
+
const dependency_delta_1 = require("../guardrail/dependency-delta");
|
|
58
|
+
const autonomy_1 = require("../governance/autonomy");
|
|
53
59
|
const risk_gate_1 = require("../guardrail/risk-gate");
|
|
54
60
|
const json_state_1 = require("../project/json-state");
|
|
55
61
|
const node_child_process_1 = require("node:child_process");
|
|
@@ -74,7 +80,9 @@ function changedAnchoredAspecs(root) {
|
|
|
74
80
|
const VENDORED = /^(?:reference|node_modules|dist|build|vendor|third_party)\//;
|
|
75
81
|
let raw;
|
|
76
82
|
try {
|
|
77
|
-
|
|
83
|
+
// @implements A-SPEC-560.3 — quotepath OFF, or a non-ASCII-named source arrives octal-escaped
|
|
84
|
+
// and its ART-8 evidence silently vanishes (shared defect with the reappraisal detector).
|
|
85
|
+
raw = (0, node_child_process_1.execFileSync)('git', ['-c', 'core.quotepath=false', 'status', '--porcelain', '-uall'], {
|
|
78
86
|
cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], env: (0, root_1.cleanSubprocessEnv)(),
|
|
79
87
|
});
|
|
80
88
|
}
|
|
@@ -120,7 +128,8 @@ function unanchoredChangedSources(root) {
|
|
|
120
128
|
try {
|
|
121
129
|
// `-uall`: without it git folds a wholly-untracked directory into one `?? src/` line and the
|
|
122
130
|
// files inside it are never seen — which is precisely where a bypassed write lands.
|
|
123
|
-
|
|
131
|
+
// @implements A-SPEC-560.3 — quotepath OFF (same octal-escape blindness as the siblings).
|
|
132
|
+
raw = (0, node_child_process_1.execFileSync)('git', ['-c', 'core.quotepath=false', 'status', '--porcelain', '-uall'], {
|
|
124
133
|
cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], env: (0, root_1.cleanSubprocessEnv)(),
|
|
125
134
|
});
|
|
126
135
|
}
|
|
@@ -148,6 +157,141 @@ function unanchoredChangedSources(root) {
|
|
|
148
157
|
}
|
|
149
158
|
return out;
|
|
150
159
|
}
|
|
160
|
+
const REAPPRAISAL_VENDORED = /^(?:reference|node_modules|dist|build|vendor|third_party)\//;
|
|
161
|
+
function depLangOf(rel) {
|
|
162
|
+
if (/\.(?:ts|tsx|mts|cts|js|jsx|mjs|cjs)$/.test(rel))
|
|
163
|
+
return 'ts';
|
|
164
|
+
if (/\.py$/.test(rel))
|
|
165
|
+
return 'py';
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
function dependencyReappraisals(root, isApproved, io = {}) {
|
|
169
|
+
// @implements A-SPEC-560.3 — quotepath OFF: with the default core.quotepath, a Korean- (or any
|
|
170
|
+
// non-ASCII/quote-) named file arrives octal-escaped, the read then ENOENTs, and the signal is
|
|
171
|
+
// silently lost. Measured: 2 of 4 drifting files detected before this flag.
|
|
172
|
+
const status = io.status ?? (() => {
|
|
173
|
+
try {
|
|
174
|
+
return (0, node_child_process_1.execFileSync)('git', ['-c', 'core.quotepath=false', 'status', '--porcelain', '-uall'], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], env: (0, root_1.cleanSubprocessEnv)() });
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
const show = io.show ?? ((rel) => {
|
|
181
|
+
try {
|
|
182
|
+
return (0, node_child_process_1.execFileSync)('git', ['-c', 'core.quotepath=false', 'show', `HEAD:${rel}`], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], env: (0, root_1.cleanSubprocessEnv)() });
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
const read = io.read ?? ((rel) => {
|
|
189
|
+
try {
|
|
190
|
+
return fs.readFileSync(path.join(root, rel), 'utf8');
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
let raw;
|
|
197
|
+
try {
|
|
198
|
+
raw = status();
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
raw = undefined;
|
|
202
|
+
}
|
|
203
|
+
if (raw === undefined)
|
|
204
|
+
return [];
|
|
205
|
+
const out = [];
|
|
206
|
+
for (const line of raw.split('\n')) {
|
|
207
|
+
if (line.trim() === '')
|
|
208
|
+
continue;
|
|
209
|
+
let rel = line.slice(3).trim().replace(/^"|"$/g, '');
|
|
210
|
+
if (rel.includes(' -> '))
|
|
211
|
+
rel = rel.split(' -> ')[1]; // renames name the destination
|
|
212
|
+
const lang = depLangOf(rel);
|
|
213
|
+
if (lang === undefined || REAPPRAISAL_VENDORED.test(rel))
|
|
214
|
+
continue;
|
|
215
|
+
let after;
|
|
216
|
+
try {
|
|
217
|
+
after = read(rel);
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
after = undefined;
|
|
221
|
+
}
|
|
222
|
+
if (after === undefined)
|
|
223
|
+
continue; // deleted / unreadable
|
|
224
|
+
const aspecs = [...new Set([...after.matchAll(/@implements\s+(A-SPEC-\d+(?:\.\d+)?)/g)].map((m) => m[1]))].filter(isApproved).sort();
|
|
225
|
+
if (aspecs.length === 0)
|
|
226
|
+
continue; // not inside an APPROVED A-SPEC scope
|
|
227
|
+
let before;
|
|
228
|
+
try {
|
|
229
|
+
before = show(rel);
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
before = undefined;
|
|
233
|
+
}
|
|
234
|
+
if (before === undefined)
|
|
235
|
+
continue; // no HEAD version = a new file, not in-scope drift
|
|
236
|
+
let packages = [];
|
|
237
|
+
try {
|
|
238
|
+
packages = (0, dependency_delta_1.dependencyDelta)(before, after, lang);
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
packages = [];
|
|
242
|
+
}
|
|
243
|
+
if (packages.length > 0)
|
|
244
|
+
out.push({ file: rel, aspecs, packages });
|
|
245
|
+
}
|
|
246
|
+
return out;
|
|
247
|
+
}
|
|
248
|
+
function reappraisalWarningLine(reaps) {
|
|
249
|
+
if (reaps.length === 0)
|
|
250
|
+
return null;
|
|
251
|
+
return reaps.map((r) => `[Holmes-Kit] spec-reappraisal: ${r.file} introduces external dependency ${r.packages.join(', ')} inside approved ${r.aspecs.join(', ')} scope — this looks like an architecture change; update the spec or open a sub-slice.`).join('\n');
|
|
252
|
+
}
|
|
253
|
+
// @implements A-SPEC-560.3 — decision-respecting escalation: a reappraisal the owner already decided
|
|
254
|
+
// (granted/denied in the queue ledger) must not re-pend every turn while the tree stays dirty, and an
|
|
255
|
+
// already-pending one needs no duplicate append. A reader failure fails OPEN (the signal survives).
|
|
256
|
+
function escalateReappraisals(root, reaps, opts) {
|
|
257
|
+
if (!opts.autonomous)
|
|
258
|
+
return 0;
|
|
259
|
+
const readLines = opts.readQueueLines ?? ((r) => fs.readFileSync(path.join(r, approval_queue_1.QUEUE_RELPATH), 'utf8').split('\n'));
|
|
260
|
+
let lines;
|
|
261
|
+
try {
|
|
262
|
+
lines = readLines(root);
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
lines = [];
|
|
266
|
+
} // a broken reader fails OPEN — the signal survives
|
|
267
|
+
let n = 0;
|
|
268
|
+
for (const r of reaps) {
|
|
269
|
+
try {
|
|
270
|
+
const id = (0, approval_queue_1.approvalRequestId)('spec-reappraisal', r.file);
|
|
271
|
+
let decided = false;
|
|
272
|
+
let lastEvent;
|
|
273
|
+
for (const raw of lines) {
|
|
274
|
+
if (raw.trim() === '')
|
|
275
|
+
continue;
|
|
276
|
+
try {
|
|
277
|
+
const e = JSON.parse(raw);
|
|
278
|
+
if (e.id !== id)
|
|
279
|
+
continue;
|
|
280
|
+
lastEvent = String(e.event);
|
|
281
|
+
if (lastEvent === 'granted' || lastEvent === 'denied')
|
|
282
|
+
decided = true;
|
|
283
|
+
}
|
|
284
|
+
catch { /* a corrupt line is not this id's history */ }
|
|
285
|
+
}
|
|
286
|
+
if (decided || lastEvent === 'requested')
|
|
287
|
+
continue; // decided, or already pending — no re-append
|
|
288
|
+
if (opts.enqueue(root, { kind: 'spec-reappraisal', target: r.file, why: `new external dependency ${r.packages.join(', ')} inside approved ${r.aspecs.join(', ')} — spec update or sub-slice needed` }))
|
|
289
|
+
n += 1;
|
|
290
|
+
}
|
|
291
|
+
catch { /* enqueue failure is best-effort; the warning still surfaced */ }
|
|
292
|
+
}
|
|
293
|
+
return n;
|
|
294
|
+
}
|
|
151
295
|
/**
|
|
152
296
|
* @implements A-SPEC-453
|
|
153
297
|
* ART-5 evidence: approvals that happened without the approving act.
|
|
@@ -318,27 +462,27 @@ function evaluateStop(specs, evidence) {
|
|
|
318
462
|
// @implements A-SPEC-191 (§4a) — an existing-but-unreadable findings ledger is not a clean turn:
|
|
319
463
|
// it may hold an open critical, and a "clean" verdict here would also CLEAR standing ART-7 debt.
|
|
320
464
|
if (evidence?.findingsUnreadable) {
|
|
321
|
-
problems.push('[ART-7] findings
|
|
322
|
-
structured.push({ article: 'ART-7', detail: 'findings
|
|
465
|
+
problems.push('[ART-7] the findings ledger cannot be read — a turn that cannot confirm whether an open critical exists is not a clean turn (repair the ledger file\'s permissions/format)');
|
|
466
|
+
structured.push({ article: 'ART-7', detail: 'the findings ledger cannot be read' });
|
|
323
467
|
}
|
|
324
468
|
// @implements A-SPEC-452 — ART-1's second enforcement point. `undefined` is no signal and says
|
|
325
469
|
// nothing; an empty array is measured and clean. A file that claims nothing is a file the write
|
|
326
470
|
// gate never judged, which is exactly what the two-step generator bypass produces.
|
|
327
471
|
for (const file of evidence?.unanchoredChangedSources ?? []) {
|
|
328
|
-
const detail = `${file}:
|
|
472
|
+
const detail = `${file}: the changed source has no @implements anchor — it does not say what it implements (create an approved A-SPEC and add the anchor)`;
|
|
329
473
|
problems.push(`[ART-1] ${detail}`);
|
|
330
474
|
structured.push({ article: 'ART-1', detail });
|
|
331
475
|
}
|
|
332
476
|
// @implements A-SPEC-453 — ART-5's second enforcement point. The seal is forgeable; the record
|
|
333
477
|
// of the approving act is not.
|
|
334
478
|
for (const id of evidence?.unrecordedApprovals ?? []) {
|
|
335
|
-
const detail = `${id}:
|
|
479
|
+
const detail = `${id}: it is in the approved state but the ledger has no record of the approving act — a seal is a content hash and cannot prove itself (approve with spec_approve)`;
|
|
336
480
|
problems.push(`[ART-5] ${detail}`);
|
|
337
481
|
structured.push({ article: 'ART-5', detail });
|
|
338
482
|
}
|
|
339
483
|
// @implements A-SPEC-455
|
|
340
484
|
for (const file of evidence?.rolledBackLedgers ?? []) {
|
|
341
|
-
const detail = `${file}:
|
|
485
|
+
const detail = `${file}: the committed ledger history was rolled back — the working copy does not contain the committed copy as a prefix (the ledger is append-only; check with holmes-kit ledger rechain)`;
|
|
342
486
|
problems.push(`[ART-2] ${detail}`);
|
|
343
487
|
structured.push({ article: 'ART-2', detail });
|
|
344
488
|
}
|
|
@@ -440,18 +584,18 @@ function acknowledgeStop(violations, pending) {
|
|
|
440
584
|
*/
|
|
441
585
|
function describeTokenHealth(raw) {
|
|
442
586
|
if (raw === undefined || raw === '')
|
|
443
|
-
return 'HOLMES_APPROVAL
|
|
587
|
+
return 'no HOLMES_APPROVAL token (read-only — only actions that need approval are blocked)';
|
|
444
588
|
let parsed;
|
|
445
589
|
try {
|
|
446
590
|
parsed = JSON.parse(raw);
|
|
447
591
|
}
|
|
448
592
|
catch {
|
|
449
|
-
return 'HOLMES_APPROVAL
|
|
593
|
+
return 'HOLMES_APPROVAL invalid: JSON parse failed — actions that need approval are blocked';
|
|
450
594
|
}
|
|
451
595
|
if (!(0, risk_gate_1.isValidApproval)(parsed)) {
|
|
452
|
-
return 'HOLMES_APPROVAL
|
|
596
|
+
return 'HOLMES_APPROVAL invalid: missing required field(s) (actor/token/rationale) — actions that need approval are blocked';
|
|
453
597
|
}
|
|
454
|
-
return 'HOLMES_APPROVAL
|
|
598
|
+
return 'HOLMES_APPROVAL valid';
|
|
455
599
|
}
|
|
456
600
|
function decideStopGuard(wantsBlock, priorConsecutiveBlocks, cap = exports.MAX_CONSECUTIVE_BLOCKS) {
|
|
457
601
|
if (!wantsBlock)
|
|
@@ -523,7 +667,7 @@ function findProjectRoot(specsDir) {
|
|
|
523
667
|
if (r.marker !== 'given')
|
|
524
668
|
return r.root;
|
|
525
669
|
}
|
|
526
|
-
catch { /*
|
|
670
|
+
catch { /* if there is no marker, build nothing */ }
|
|
527
671
|
return null;
|
|
528
672
|
}
|
|
529
673
|
return null;
|
|
@@ -769,6 +913,24 @@ if (require.main === module) {
|
|
|
769
913
|
let out = evaluateStop(specs, { testCasesByAspec, provenance, executedByAspec, findings, findingsUnreadable, unanchoredChangedSources: unanchored, unrecordedApprovals: unrecorded, rolledBackLedgers: rolledBack, redFirstMode, changedAspecs, outcomesByAspec });
|
|
770
914
|
// @implements A-SPEC-534.4 — track mode records ART-8 findings without blocking: surface them so
|
|
771
915
|
// the operator observes RED-first gaps before an owner promotes the posture to strict.
|
|
916
|
+
// @implements A-SPEC-559.2 — spec-evolution trigger (observe-first, NEVER blocks): a dirty
|
|
917
|
+
// in-scope source that newly introduces an external dependency is an architecture-drift signal.
|
|
918
|
+
// Manual mode warns; autonomy mode also enqueues it so the owner sees the drift async. Fail-open.
|
|
919
|
+
// @implements A-SPEC-560.3 — placed BEFORE the guard decisions and their early returns, so a
|
|
920
|
+
// degraded-yield turn (guard state unwritable) still surfaces the drift; stderr-only, so no
|
|
921
|
+
// verdict path is affected.
|
|
922
|
+
try {
|
|
923
|
+
const approvedIds = new Set(specs.filter((s) => s.type === 'A-SPEC' && s.status === 'approved').map((s) => s.id));
|
|
924
|
+
const reaps = dependencyReappraisals(stopProjectRoot(), (id) => approvedIds.has(id));
|
|
925
|
+
const line = reappraisalWarningLine(reaps);
|
|
926
|
+
if (line)
|
|
927
|
+
process.stderr.write(`${line}\n`);
|
|
928
|
+
const nowIso = new Date().toISOString();
|
|
929
|
+
if (line && (0, autonomy_1.autonomousApprovalEnabled)(process.env, stopProjectRoot(), nowIso)) {
|
|
930
|
+
escalateReappraisals(stopProjectRoot(), reaps, { autonomous: true, enqueue: approval_queue_1.enqueueApprovalRequest });
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
catch { /* the reappraisal signal is advisory; a failure never affects the stop verdict */ }
|
|
772
934
|
if (out.tracked && out.tracked.length > 0) {
|
|
773
935
|
process.stderr.write(`[Holmes-Kit] ART-8 RED-first (track): ${out.tracked.map((t) => t.detail).join(' | ')}\n`);
|
|
774
936
|
}
|
|
@@ -814,7 +976,7 @@ if (require.main === module) {
|
|
|
814
976
|
else if (ackWaiting.length > 0) {
|
|
815
977
|
// @implements A-SPEC-247 — acknowledged, not clean: name what is waiting so the user sees
|
|
816
978
|
// the standing approval debt exactly once, and the agent knows the ball is not in its court.
|
|
817
|
-
process.stderr.write(`[Holmes-Kit]
|
|
979
|
+
process.stderr.write(`[Holmes-Kit] awaiting approval — items waiting on the owner's decision: ${ackWaiting.join(', ')}. Decide them at ${(0, npx_bin_1.npxBin)()} holmes-kit approve. (This debt does not re-block.)\n`);
|
|
818
980
|
}
|
|
819
981
|
else if (guard.capped) {
|
|
820
982
|
process.stderr.write(`[Holmes-Kit] governance gate YIELDING after ${exports.MAX_CONSECUTIVE_BLOCKS} consecutive blocks — issues remain UNRESOLVED:\n${out.reason ?? ''}\n`);
|
|
@@ -826,7 +988,7 @@ if (require.main === module) {
|
|
|
826
988
|
}
|
|
827
989
|
catch (err) {
|
|
828
990
|
process.stderr.write(`[Holmes-Kit Stop Hook] Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
829
|
-
process.exitCode = 0; // fail-open —
|
|
991
|
+
process.exitCode = 0; // fail-open — drain the pipe via exitCode, then exit naturally (avoids truncation)
|
|
830
992
|
}
|
|
831
993
|
});
|
|
832
994
|
}
|
|
@@ -1383,10 +1383,23 @@ function makeRawHandlers(store, opts) {
|
|
|
1383
1383
|
// itself. The switch is env-only and an agent cannot set it (pre-tool-use blocks that,
|
|
1384
1384
|
// A-SPEC-532.2). Off, or a hitl-classed spec, falls straight through to the elicitor
|
|
1385
1385
|
// unchanged — the autonomous-OFF path is byte-identical to before.
|
|
1386
|
-
|
|
1386
|
+
// @implements A-SPEC-553.1 — autonomy is the out-of-band env switch OR a valid, non-expired
|
|
1387
|
+
// session envelope marker under this project's `.ax/state/` (which an agent cannot write).
|
|
1388
|
+
const autonomyOn = (0, autonomy_1.autonomousApprovalEnabled)(process.env, a.root, new Date().toISOString());
|
|
1389
|
+
if (autonomyOn
|
|
1387
1390
|
&& (0, autonomy_1.specApprovalAutonomy)(target.spec, resolver([target.spec])) === 'auto') {
|
|
1388
1391
|
approveResolved = { approval: autonomousApproval(), source: 'autonomous' };
|
|
1389
1392
|
}
|
|
1393
|
+
else if (autonomyOn) {
|
|
1394
|
+
// @implements A-SPEC-551.1 — hitl-grade spec under autonomy: the in-session elicitation
|
|
1395
|
+
// dialog is auto-acceptable by an auto-mode client (the protocol cannot tell a human
|
|
1396
|
+
// from an auto-accept), so a governance-critical spec is never offered it. Do nothing
|
|
1397
|
+
// here — approveResolved stays undefined and the fail-closed refuse+enqueue path below
|
|
1398
|
+
// routes the act to the out-of-band human queue (holmes-kit approve). The A-SPEC-532.1
|
|
1399
|
+
// bound ("governance-critical specs never leave the human channel") thus becomes
|
|
1400
|
+
// ENFORCED, not aspirational. Autonomy OFF (the else) and the auto-grade branch above
|
|
1401
|
+
// stay byte-identical to before.
|
|
1402
|
+
}
|
|
1390
1403
|
else {
|
|
1391
1404
|
const resealing = typeof target.spec.frontmatter.approved_digest === 'string';
|
|
1392
1405
|
// The MODEL text is capped BEFORE the server markers are appended (round-2): a ~185+ char
|
|
@@ -77,7 +77,15 @@ function cleanSubprocessEnv(env = process.env) {
|
|
|
77
77
|
* evidence run would disarm every test that verifies a gate, and that contamination masks red as
|
|
78
78
|
* green — the worse direction of the two.
|
|
79
79
|
*/
|
|
80
|
-
|
|
80
|
+
// @implements A-SPEC-561.1
|
|
81
|
+
// `HOLMES_AUTONOMOUS_APPROVAL` joins the list for the SAME reason, pointing the same way as the
|
|
82
|
+
// HOLMES_SPECS incident above: measured 2026-09-06, a project that opted into autonomy
|
|
83
|
+
// (`init --autonomy` persists the switch into .mcp.json) starts its MCP server with that env, the
|
|
84
|
+
// server's `test_run` handed it to the jest children, and REQ-551's autonomy branch then skipped the
|
|
85
|
+
// elicitation path — 9 elicitation tests red on a tree whose full suite was green, twice, and
|
|
86
|
+
// identically on the previous commit. A posture is an approval channel, never a property of the code
|
|
87
|
+
// under test; an evidence run must judge the same in an autonomous workspace and a human-gated one.
|
|
88
|
+
const TEST_SCRUB_KEYS = new Set(['HOLMES_SPECS', 'HOLMES_GATE_BYPASS', 'HOLMES_MCP_AUTORELOAD', 'HOLMES_MCP_PROFILE', 'HOLMES_AUTONOMOUS_APPROVAL']);
|
|
81
89
|
function cleanTestEnv(env = process.env) {
|
|
82
90
|
const cleaned = cleanSubprocessEnv(env);
|
|
83
91
|
for (const k of Object.keys(cleaned)) {
|
|
@@ -44,7 +44,7 @@ exports.removeSemanticKey = removeSemanticKey;
|
|
|
44
44
|
* an agent assigning the env names (self-granted egress, the HOLMES_ROLE class); this module owns
|
|
45
45
|
* the storage the human's CLI act writes to. A project-tree file is NEVER a source — a tree file
|
|
46
46
|
* is a commit-accident surface and readable by every in-session tool, which is exactly what the
|
|
47
|
-
* owner's ".env
|
|
47
|
+
* owner's ".env is a stopgap" call-out named.
|
|
48
48
|
*
|
|
49
49
|
* Resolution chain, the order being the contract:
|
|
50
50
|
* 1. HOLMES_SEMANTIC_API_KEY — dedicated name, CI/headless.
|
|
@@ -49,13 +49,13 @@ function detectIdCollisions(entries) {
|
|
|
49
49
|
sealContents.set(en.approvedDigest, new Set([en.contentDigest]));
|
|
50
50
|
}
|
|
51
51
|
const sharedSeals = [...sealContents.entries()].filter(([, cs]) => cs.size > 1).map(([s]) => s).sort();
|
|
52
|
-
const parts = [...byKey.entries()].sort().map(([k, ens]) => `${k}[seal=${[...new Set(ens.map((en) => en.approvedDigest ?? '
|
|
52
|
+
const parts = [...byKey.entries()].sort().map(([k, ens]) => `${k}[seal=${[...new Set(ens.map((en) => en.approvedDigest ?? 'none'))].sort().join('|')}] ← ${ens.map((en) => en.file).sort().join(', ')}`);
|
|
53
53
|
issues.push({
|
|
54
54
|
kind: 'id-collision',
|
|
55
55
|
id,
|
|
56
56
|
files: group.map((en) => en.file).sort(),
|
|
57
57
|
detail: parts.join(' / ')
|
|
58
|
-
+ (sharedSeals.length > 0 ? ` —
|
|
58
|
+
+ (sharedSeals.length > 0 ? ` — different contents share the same approved_digest (${sharedSeals.join(', ')}): suspected post-approval edit or seal copy` : ''),
|
|
59
59
|
});
|
|
60
60
|
}
|
|
61
61
|
// --- family-coexistence: a bare A/T-SPEC id alongside its own dot-suffix family ---
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"//": "@implements A-SPEC-209",
|
|
3
3
|
"name": "@holmes-lab/holmes-kit",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.14.0",
|
|
5
5
|
"description": "Holmes-Kit — deterministic Agentic Software Engineering (ASE) harness with causal traceability (spec chain + D-CPG + RTM + phase guardrail)",
|
|
6
6
|
"main": "dist/holmes/mcp/server.js",
|
|
7
7
|
"types": "dist/holmes/mcp/server.d.ts",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"mcp",
|
|
43
43
|
"guardrail"
|
|
44
44
|
],
|
|
45
|
-
"author": "SungNam Park <
|
|
45
|
+
"author": "SungNam Park <sungnam.park.korea@gmail.com>",
|
|
46
46
|
"license": "MIT",
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/better-sqlite3": "^7.6.13",
|
|
@@ -2,72 +2,85 @@
|
|
|
2
2
|
name: holmes-publish
|
|
3
3
|
description: >-
|
|
4
4
|
Use when asked to publish holmes-kit to NPM registry or run release workflow governed by
|
|
5
|
-
"HOLMES_APPROVAL", "hard-hitl", or "A-SPEC-133"
|
|
6
|
-
|
|
5
|
+
"HOLMES_APPROVAL", "hard-hitl", or "A-SPEC-133" — the repository owner authorizes an
|
|
6
|
+
irreversible / high-risk release Human-In-The-Loop, while a low-risk release may self-publish
|
|
7
|
+
under autonomous mode.
|
|
7
8
|
---
|
|
8
9
|
|
|
9
|
-
# holmes-publish — NPM
|
|
10
|
+
# holmes-publish — NPM 배포 및 릴리스 자율/HITL 절차
|
|
10
11
|
|
|
11
|
-
이 플레이북은 `@holmes-lab/holmes-kit`
|
|
12
|
-
|
|
12
|
+
이 플레이북은 `@holmes-lab/holmes-kit` 를 NPM Registry에 안전하게 배포하는 **규정 절차**를 정의합니다.
|
|
13
|
+
npm publish 는 **비가역·외부노출**이라 기본은 HITL(사람 승인)이지만, **저위험 릴리스**(문서·테스트·양성
|
|
14
|
+
내부 변경의 patch/minor)는 **자율 모드에서 자율 처리**할 수 있고, **비가역/high-risk**(게이트·보안·아키텍처
|
|
15
|
+
변경, major 범프, 상위 스펙)는 반드시 **오너의 대역외 HITL 승인**을 거칩니다. 판정은 결정론적 분류기
|
|
16
|
+
`releaseAutonomy`(A-SPEC-555.1, `governance/autonomy.ts`)가 담당합니다.
|
|
13
17
|
|
|
14
18
|
---
|
|
15
19
|
|
|
16
|
-
## 배포
|
|
20
|
+
## 배포 절차 (Release Workflow)
|
|
17
21
|
|
|
18
22
|
### 1단계: 사전 품질 감사 (Pre-flight Quality Audit)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
3. `npm run build`
|
|
23
|
+
1. `npm run typecheck` (타입 오류 0건)
|
|
24
|
+
2. 전체 스위트 green — `mcp__holmes-kit__test_run`(HEAD~1..HEAD) 또는 `npx jest`. 부하가 높으면 대시보드
|
|
25
|
+
canary(A-SPEC-435)가 타임아웃할 수 있으니 부하를 낮춘 뒤 재실행한다(코드가 아니라 부하 문제).
|
|
26
|
+
3. `npm run build` (`dist/` 최신 + `.build-id`).
|
|
23
27
|
|
|
24
28
|
> [!IMPORTANT]
|
|
25
|
-
>
|
|
29
|
+
> 타입 오류나 실제(부하-무관) 테스트 실패가 하나라도 있으면 즉시 배포 중단(Abort).
|
|
26
30
|
|
|
27
31
|
---
|
|
28
32
|
|
|
29
33
|
### 2단계: 패키지 타르볼 시뮬레이션 및 검수 (Tarball Inspection)
|
|
30
|
-
|
|
31
|
-
- `
|
|
32
|
-
- 출력된 타르볼 패키지 목록 검수:
|
|
33
|
-
- `package.json` (`@holmes-lab/holmes-kit` 명칭 및 버전 확인)
|
|
34
|
-
- `bin/` (`holmes-kit.js`, `holmes-mcp.js`, `holmes-hook-antigravity.js` 등)
|
|
35
|
-
- `dist/` (전체 컴파일 산출물)
|
|
36
|
-
- `playbooks/` (`adopt`, `author-slice`, `promote-slice`, `publish`)
|
|
37
|
-
- `CHANGELOG.md`, `README.md`
|
|
34
|
+
- `npm publish --dry-run`
|
|
35
|
+
- 타르볼 구성 검수: `package.json`(명칭·버전), `bin/`, `dist/`, `playbooks/`, `CHANGELOG.md`, `README.md`.
|
|
38
36
|
|
|
39
37
|
---
|
|
40
38
|
|
|
41
|
-
###
|
|
42
|
-
|
|
39
|
+
### 2.5단계: 문서 정합성 게이트 (Docs Currency Gate) — 배포는 정직한 고지다
|
|
40
|
+
타르볼에 문서가 **포함**됐는지가 아니라 **최신인지**를 diff로 검사한다(이 단계 없이 폐기된 동작이 README에
|
|
41
|
+
현재형으로 남는 사고가 실제로 있었다):
|
|
42
|
+
1. 직전 릴리스 태그 이후 승인된 스펙 열거: `git log <last-tag>..HEAD --name-only -- .ax/specs/03_a-spec/`.
|
|
43
|
+
2. 각 A-SPEC 중 **사용자-대면**(CLI 명령/플래그, 동작 변경, env 스위치, 게이트 행동)인 것마다:
|
|
44
|
+
- `CHANGELOG.md` 의 이번 버전 항목이 그 변화를 기술하는가.
|
|
45
|
+
- `README.md` 의 기능 목록/CLI 치트시트가 새 명령·플래그를 담고, **폐기된 동작을 현재형으로 서술하지
|
|
46
|
+
않는가**(바뀐 동작의 옛 문구를 `grep` 으로 점검).
|
|
47
|
+
> [!CAUTION]
|
|
48
|
+
> 사용자-대면 변화가 CHANGELOG/README 에 반영되지 않았으면 배포 중단 — 문서 drift 는 거짓 주장이다.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
### 3단계: 릴리스 자율 판정 (Release Autonomy) — auto 또는 HITL
|
|
53
|
+
`releaseAutonomy(specsSinceTag, versionBump, process.env, root, now)` 로 이번 릴리스를 분류한다
|
|
54
|
+
(`versionBumpKind(현재버전, 대상버전)` 으로 범프 종류 산출):
|
|
55
|
+
- **`'auto'`** (자율 ON + 모든 스펙 auto 등급 + patch/minor): 사람 없이 4단계로 진행하되, 원장에 배포
|
|
56
|
+
근거를 남긴다(무엇을·왜·어느 버전). 저위험 릴리스의 자율 처리.
|
|
57
|
+
- **`'hitl'`** (자율 OFF · major 범프 · 게이트/보안/아키텍처 스펙 · 상위 REQ/H/C · 비가역 breaking_change):
|
|
58
|
+
**오너에게 배포 요약을 보고하고 명시적 대역외 승인**을 받은 뒤에만 진행한다. AI Agent 는 독단적으로
|
|
59
|
+
`npm publish` 하지 않는다. 승인은 채팅의 명시적 확인 또는 `HOLMES_APPROVAL` 로 전달된다.
|
|
43
60
|
|
|
44
|
-
**[보고 양식]**:
|
|
45
|
-
|
|
46
|
-
- **대상 버전**: `vX.Y.Z`
|
|
47
|
-
- **테스트 결과**: PASS (100%)
|
|
48
|
-
- **타르볼 파일 수 및 용량**: N개 / XXX kB
|
|
61
|
+
**[HITL 보고 양식]**: 패키지 `@holmes-lab/holmes-kit` · 대상 `vX.Y.Z`(범프: major/minor/patch) ·
|
|
62
|
+
테스트 PASS · 타르볼 N개/XXX kB · 분류 사유(어느 스펙이 hitl 인지).
|
|
49
63
|
|
|
50
64
|
> [!CAUTION]
|
|
51
|
-
>
|
|
52
|
-
> 승인은 채팅을 통한 명시적 확인 또는 `HOLMES_APPROVAL` 환경변수를 통해 전달됩니다.
|
|
65
|
+
> hitl 판정에서 오너 승인이 없으면 즉시 중단. 자율(auto) 판정이라도 2.5단계 문서 게이트를 통과해야 한다.
|
|
53
66
|
|
|
54
67
|
---
|
|
55
68
|
|
|
56
69
|
### 4단계: NPM 게시 실행 (NPM Publishing Execution)
|
|
57
|
-
승인이 완료된 경우에만 실제 게시 명령을 수행합니다:
|
|
58
70
|
```bash
|
|
59
71
|
npm publish --access public
|
|
60
72
|
```
|
|
61
|
-
- 2FA / Web OTP
|
|
73
|
+
- 2FA / Web OTP 가 필요하면 인증 URL 을 사용자에게 제공하고 대기.
|
|
74
|
+
- verify-release 가 전체 `npm test` 를 돌려 플레이크할 수 있으므로, 1단계를 이미 green 으로 통과했다면
|
|
75
|
+
`npm publish --ignore-scripts` + 수동 무결성 확인(트리 클린·build-id==HEAD·CHANGELOG 항목)로 우회할 수 있다.
|
|
62
76
|
|
|
63
77
|
---
|
|
64
78
|
|
|
65
79
|
### 5단계: 배포 후 검증 및 Git 태깅 (Post-Release Verification)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
2. Git 커밋 및 태그 생성:
|
|
80
|
+
1. NPM 반영 확인: `npm view @holmes-lab/holmes-kit version` (레지스트리 read-cache 지연 시 레지스트리 JSON 직접 조회).
|
|
81
|
+
2. 커밋 및 태그:
|
|
69
82
|
```bash
|
|
70
|
-
git add package.json
|
|
83
|
+
git add package.json CHANGELOG.md README.md
|
|
71
84
|
git commit -m "chore: release vX.Y.Z"
|
|
72
85
|
git tag -a vX.Y.Z -m "vX.Y.Z Release"
|
|
73
86
|
```
|
|
@@ -75,7 +88,6 @@ npm publish --access public
|
|
|
75
88
|
---
|
|
76
89
|
|
|
77
90
|
## 플레이북 트리거 조건
|
|
78
|
-
다음 요청 시 자동 트리거됩니다:
|
|
79
91
|
- "npm publish"
|
|
80
92
|
- "release to npm"
|
|
81
93
|
- "HOLMES_APPROVAL"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: holmes-remediation
|
|
3
3
|
description: >-
|
|
4
|
-
Automatically triggered when Holmes-Kit pre-tool-use hook gate denies a tool call with "
|
|
4
|
+
Automatically triggered when Holmes-Kit pre-tool-use hook gate denies a tool call with "Target specification (...) is not approved" or missing code anchor. Guides the agent to execute 1-call spec_remediate or 3-step recovery workflow.
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Holmes-Kit Remediation Playbook (Self-Healing Recovery)
|