@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.
Files changed (41) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/README.md +13 -3
  3. package/dist/.build-id +1 -1
  4. package/dist/holmes/cli/approve-context.js +10 -10
  5. package/dist/holmes/cli/approve-ref.js +5 -5
  6. package/dist/holmes/cli/approve-watch.d.ts +1 -1
  7. package/dist/holmes/cli/approve-watch.js +6 -6
  8. package/dist/holmes/cli/approve.d.ts +3 -3
  9. package/dist/holmes/cli/approve.js +57 -57
  10. package/dist/holmes/cli/autonomy.d.ts +22 -0
  11. package/dist/holmes/cli/autonomy.js +145 -0
  12. package/dist/holmes/cli/colophon.d.ts +6 -0
  13. package/dist/holmes/cli/colophon.js +24 -0
  14. package/dist/holmes/cli/doctor.d.ts +2 -2
  15. package/dist/holmes/cli/doctor.js +104 -87
  16. package/dist/holmes/cli/index.js +122 -63
  17. package/dist/holmes/cli/init.d.ts +2 -0
  18. package/dist/holmes/cli/init.js +31 -19
  19. package/dist/holmes/cli/interactive-prompt.d.ts +8 -0
  20. package/dist/holmes/cli/interactive-prompt.js +23 -0
  21. package/dist/holmes/cli/semantic-key.js +9 -9
  22. package/dist/holmes/cli/settings-merge.d.ts +2 -1
  23. package/dist/holmes/cli/settings-merge.js +15 -3
  24. package/dist/holmes/cli/upgrade.js +7 -7
  25. package/dist/holmes/cpg/proposed-content.js +2 -2
  26. package/dist/holmes/governance/autonomy.d.ts +10 -2
  27. package/dist/holmes/governance/autonomy.js +191 -5
  28. package/dist/holmes/guardrail/blind-spots.js +15 -15
  29. package/dist/holmes/guardrail/dependency-delta.d.ts +3 -0
  30. package/dist/holmes/guardrail/dependency-delta.js +118 -0
  31. package/dist/holmes/hooks/pre-tool-use.js +111 -42
  32. package/dist/holmes/hooks/session-start.js +17 -0
  33. package/dist/holmes/hooks/stop.d.ts +22 -1
  34. package/dist/holmes/hooks/stop.js +176 -14
  35. package/dist/holmes/mcp/handlers.js +14 -1
  36. package/dist/holmes/project/root.js +9 -1
  37. package/dist/holmes/semantic/credentials.js +1 -1
  38. package/dist/holmes/spec/id-collision.js +2 -2
  39. package/package.json +2 -2
  40. package/playbooks/publish/PLAYBOOK.md +47 -35
  41. 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
- raw = (0, node_child_process_1.execFileSync)('git', ['status', '--porcelain', '-uall'], {
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
- raw = (0, node_child_process_1.execFileSync)('git', ['status', '--porcelain', '-uall'], {
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 reada 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}: 변경된 소스에 @implements 앵커가 없습니다 파일이 무엇을 구현하는지 말하지 않습니다 (승인된 A-SPEC 만들고 앵커를 다십시오)`;
472
+ const detail = `${file}: the changed source has no @implements anchorit 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}: 승인 상태인데 원장에 승인 행위 기록이 없습니다봉인은 내용 해시라 스스로를 증명하지 못합니다 (spec_approve 승인하십시오)`;
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}: 커밋된 원장 이력이 되돌려졌습니다작업본이 커밋본을 접두로 포함하지 않습니다 (원장은 덧붙이기만 합니다; holmes-kit ledger rechain 으로 확인하십시오)`;
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-onlyonly 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 무효: JSON 파싱 실패승인이 필요한 행위가 막힙니다';
593
+ return 'HOLMES_APPROVAL invalid: JSON parse failedactions that need approval are blocked';
450
594
  }
451
595
  if (!(0, risk_gate_1.isValidApproval)(parsed)) {
452
- return 'HOLMES_APPROVAL 무효: 필수 필드(actor·token·rationale) 누락 승인이 필요한 행위가 막힙니다';
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] 승인 대기 오너 결정을 기다리는 항목: ${ackWaiting.join(', ')}. ${(0, npx_bin_1.npxBin)()} holmes-kit approve 에서 결재하십시오. (이 부채로는 재차단하지 않습니다)\n`);
979
+ process.stderr.write(`[Holmes-Kit] awaiting approvalitems 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 — exitCode 파이프를 비운 자연 종료(잘림 방지)
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
- if ((0, autonomy_1.autonomousApprovalEnabled)(process.env)
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
- const TEST_SCRUB_KEYS = new Set(['HOLMES_SPECS', 'HOLMES_GATE_BYPASS', 'HOLMES_MCP_AUTORELOAD', 'HOLMES_MCP_PROFILE']);
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 임시 조치" call-out named.
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 ?? '없음'))].sort().join('|')}] ← ${ens.map((en) => en.file).sort().join(', ')}`);
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 ? ` — 같은 approved_digest 서로 다른 내용이 공유(${sharedSeals.join(', ')}): post-approval edit 또는 봉인 복사 의심` : ''),
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.12.2",
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 <snpark.phd@gmail.com>",
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" with mandatory Human-In-The-Loop (HITL)
6
- authorization by 박성남 그룹장님.
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 서버 배포 및 HITL 승인 절차
10
+ # holmes-publish — NPM 배포 및 릴리스 자율/HITL 절차
10
11
 
11
- 이 플레이북은 `@holmes-lab/holmes-kit` 패키지를 NPM Registry에 안전하게 배포하기 위한 **5단계 규정 절차**를 정의합니다.
12
- 배포 직전에는 반드시 **박성남 그룹장님**의 명시적 대역외 승인(HITL — Human-In-The-Loop) 거쳐야만 실제 배포 명령어가 실행됩니다.
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
- ## 배포 5단계 절차 (5-Step Release Workflow)
20
+ ## 배포 절차 (Release Workflow)
17
21
 
18
22
  ### 1단계: 사전 품질 감사 (Pre-flight Quality Audit)
19
- 배포를 시작하기 코드베이스의 무결성과 타입 안전성을 검증합니다:
20
- 1. `npm run typecheck` 실행 (타입 오류 0건 검증)
21
- 2. `npm test` 실행 (전체 145+ 테스트 수트 100% PASS 검증)
22
- 3. `npm run build` 실행 (`dist/` 최신 빌드 `.build-id` 생성)
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
- > 하나의 테스트라도 실패하거나 타입 오류가 발생하면 즉시 배포 절차를 중단(Abort)합니다.
29
+ > 타입 오류나 실제(부하-무관) 테스트 실패가 하나라도 있으면 즉시 배포 중단(Abort).
26
30
 
27
31
  ---
28
32
 
29
33
  ### 2단계: 패키지 타르볼 시뮬레이션 및 검수 (Tarball Inspection)
30
- NPM에 게시될 패키지 구성 요소를 사전 시뮬레이션하여 검수합니다:
31
- - `npm publish --dry-run` 실행
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
- ### 3단계: 필수 사람 승인 (Mandatory HITL Approval) — 보안 게이트
42
- **가장 중요한 보안 지점입니다.** AI Agent는 독단적으로 `npm publish`를 실행할 없으며, 반드시 **박성남 그룹장님**께 배포 요약을 보고하고 명시적 승인을 받아야 합니다.
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
- - **배포 패키지**: `@holmes-lab/holmes-kit`
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 인증이 필요한 경우 터미널 인증 URL을 사용자에게 제공하고 대기합니다.
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
- 1. `npm view @holmes-lab/holmes-kit version` 실행하여 NPM Registry에 반영되었는지 확인
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 "구현 대상 A-SPEC(...) approved가 아닙니다" or missing code anchor. Guides the agent to execute 1-call spec_remediate or 3-step recovery workflow.
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)