@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
@@ -45,6 +45,7 @@ const os = __importStar(require("node:os"));
45
45
  const path = __importStar(require("node:path"));
46
46
  const init_1 = require("./init");
47
47
  const doctor_1 = require("./doctor");
48
+ const colophon_1 = require("./colophon");
48
49
  /**
49
50
  * `holmes-kit` CLI entry — argv dispatch, hand-rolled (no new dependency).
50
51
  *
@@ -71,7 +72,8 @@ function packageRoot() {
71
72
  * REQ-144's "cannot decide is not permission", applied to CLI arguments.
72
73
  */
73
74
  const KNOWN_FLAGS = {
74
- init: ['help', 'target', 'mode', 'specs-dir', 'settings', 'matcher', 'dry-run', 'no-mcp', 'remove', 'force', 'agent', 'mcp-launcher'],
75
+ init: ['help', 'target', 'mode', 'specs-dir', 'settings', 'matcher', 'dry-run', 'no-mcp', 'remove', 'force', 'agent', 'mcp-launcher', 'autonomy', 'no-autonomy'],
76
+ autonomy: ['help', 'target', 'for'],
75
77
  doctor: ['help', 'target', 'json'],
76
78
  skills: ['help', 'target'],
77
79
  ci: ['help', 'target', 'specs-dir', 'json'],
@@ -91,16 +93,16 @@ const KNOWN_FLAGS = {
91
93
  // the whole-CLI usage over it. This set is that exemption, kept declarative so a future self-help
92
94
  // command is one edit, not a dispatch-order gamble.
93
95
  const SELF_HELP_COMMANDS = new Set(['approve']);
94
- const APPROVE_USAGE = `holmes-kit approve — 승인 대기 요청의 결재 (HITL)
96
+ const APPROVE_USAGE = `holmes-kit approve — decide pending approval requests (HITL)
95
97
 
96
- 대화형(TTY): 항목마다 [a]승인 [e]사유·기간 지정 승인 [v]상세 [d]거부 [q]질문 [s]건너뛰기 [A]남은 전체 승인
97
- --watch 상주 모드요청이 도착하는 즉시 결정, 큐가 비면 대기 (Ctrl-C 종료, --poll-ms <밀리초>)
98
- --list 대기 목록만 출력
99
- --grant <참조> 해당 요청의 좁은 그랜트 발급 (--ttl <분>, --rationale <문구>)
100
- 참조 = 목록 번호(1,2,…) | id | id 접두 id를 옮겨 적을 필요가 없습니다
101
- --deny <id> --reason <문구> 거부사유가 에이전트의 다음 거부 문면에 실립니다
102
- --ask <id> --question <문구> 질문 남기고 보류에이전트가 사용자에게 전달합니다
103
- --target <dir> 프로젝트 루트 (기본: 현재 디렉터리)
98
+ interactive (TTY): per item [a] approve [e] approve with reason/duration [v] details [d] deny [q] ask [s] skip [A] approve all remaining
99
+ --watch resident modedecide as requests arrive, wait when the queue is empty (Ctrl-C to exit, --poll-ms <ms>)
100
+ --list print the pending list only
101
+ --grant <ref> issue a narrow grant for that request (--ttl <minutes>, --rationale <text>)
102
+ ref = list number (1,2,…) | id | id prefixno need to retype a long id
103
+ --deny <id> --reason <text> denythe reason rides the agent's next refusal
104
+ --ask <id> --question <text> leave a question and hold the agent relays it to the user
105
+ --target <dir> project root (default: current directory)
104
106
  `;
105
107
  class UnknownFlagError extends Error {
106
108
  }
@@ -110,7 +112,7 @@ class UnknownFlagError extends Error {
110
112
  * what you'd do" spelling performed a REAL write (round-3 HIGH; same class inverted --remove and
111
113
  * --force). A boolean flag must never consume a token.
112
114
  */
113
- const BOOLEAN_FLAGS = new Set(['help', 'dry-run', 'no-mcp', 'remove', 'force', 'json', 'list', 'watch']);
115
+ const BOOLEAN_FLAGS = new Set(['help', 'dry-run', 'no-mcp', 'remove', 'force', 'json', 'list', 'watch', 'autonomy', 'no-autonomy']);
114
116
  function parseFlags(argv, known) {
115
117
  const flags = {};
116
118
  const positionals = [];
@@ -190,6 +192,10 @@ const USAGE = `holmes-kit — deterministic ASE governance for a project
190
192
  --dry-run print the diff, write nothing
191
193
  --remove remove holmes-kit wiring (holmes-owned entries only)
192
194
  --force re-wire an already-wired target (requires HOLMES_APPROVAL)
195
+ --autonomy grant autonomous spec approval (low-risk only; governance-critical
196
+ still ask you). Non-interactive: requires HOLMES_APPROVAL. At a TTY,
197
+ init asks instead. Off is default; turn off with --no-autonomy.
198
+ --no-autonomy keep every spec approval human (default)
193
199
 
194
200
  holmes-kit ledger rechain re-chain forked ledger files against the committed mainline
195
201
  --target <dir> project to repair (default: cwd)
@@ -198,7 +204,7 @@ const USAGE = `holmes-kit — deterministic ASE governance for a project
198
204
  --dry-run report what would be re-chained, write nothing
199
205
 
200
206
  holmes-kit approve review pending approval requests (HITL)
201
- (no flags, TTY) interactive: [a]승인 [e]지정승인 [v]상세 [d]거부 [q]질문 [s]건너뛰기 [A]전체승인
207
+ (no flags, TTY) interactive: [a]approve [e]approve-with-terms [v]details [d]deny [q]ask [s]skip [A]approve-all
202
208
  --list print the pending queue and exit
203
209
  --grant <ref> [--ttl <min>] [--rationale <text>] mint the narrow grant for one request
204
210
  --deny <ref> --reason <text> record a denial the agent will see
@@ -206,9 +212,15 @@ const USAGE = `holmes-kit — deterministic ASE governance for a project
206
212
  <ref> list index (1,2,…), full id, or a unique id prefix
207
213
  --target <dir> project root (default: cwd)
208
214
 
209
- holmes-kit semantic-key set|unset|status cloud semantic 티어 opt-in 키 관리
210
- set stdin 으로만 입력받습니다(값은 argv·출력 어디에도 남지 않음).
211
- 설정은 이그레스(스펙 산문·경로·심볼명 외부 전송) 동의입니다.
215
+ holmes-kit autonomy status|on|off grant/end per-session autonomous spec approval
216
+ on [--for 2h] self-drive low-risk specs this session (governance-critical still
217
+ ask you); at a TTY it just asks, non-interactive needs HOLMES_APPROVAL
218
+ off end it now (always free)
219
+ --target <dir> project root (default: cwd)
220
+
221
+ holmes-kit semantic-key set|unset|status manage the cloud semantic tier opt-in key
222
+ set reads from stdin only (the value never appears in argv or output).
223
+ Setting the key is consent to egress (spec prose, paths, symbol names sent externally).
212
224
 
213
225
  holmes-kit doctor [flags] diagnose the install
214
226
  --target <dir> also verify that target's wiring points at this install
@@ -261,7 +273,8 @@ async function main(argv) {
261
273
  // Positionals these commands do not take are refused, not dropped: `skills refresh <dir>`
262
274
  // silently wrote into CWD while the named target stayed stale (round-3 HIGH).
263
275
  const expected = cmd === 'skills' ? ['refresh'] : cmd === 'ledger' ? ['rechain']
264
- : cmd === 'semantic-key' ? ['set', 'unset', 'status'] : [];
276
+ : cmd === 'semantic-key' ? ['set', 'unset', 'status']
277
+ : cmd === 'autonomy' ? ['status', 'on', 'off'] : [];
265
278
  const unexpected = positionals.filter((p) => !expected.includes(p));
266
279
  if (unexpected.length > 0) {
267
280
  const hint = fs.existsSync(unexpected[0]) ? ` — did you mean --target ${unexpected[0]}?` : '';
@@ -291,11 +304,32 @@ async function main(argv) {
291
304
  process.stdout.write(`${v}\n`);
292
305
  return 0;
293
306
  }
307
+ // @implements A-SPEC-548.1 — a hidden maker's mark on the detective theme. Undocumented (never in
308
+ // USAGE); a colophon for those who go looking.
309
+ if (cmd === 'elementary' || cmd === 'whodunit') {
310
+ process.stdout.write((0, colophon_1.colophon)());
311
+ return 0;
312
+ }
294
313
  if (cmd === 'semantic-key') {
295
314
  // @implements A-SPEC-477 — set|unset|status; the key rides stdin, never argv.
296
315
  const { runSemanticKey } = await Promise.resolve().then(() => __importStar(require('./semantic-key')));
297
316
  return runSemanticKey(positionals[0]);
298
317
  }
318
+ // @implements A-SPEC-553.1 — session autonomy envelope: status|on [--for 2h]|off. Enabling is a
319
+ // foreground human act (raw-TTY proof, or an out-of-band HOLMES_APPROVAL for the non-interactive
320
+ // path); the marker lives in the agent-write-protected .ax/state/, so a session cannot self-grant.
321
+ if (cmd === 'autonomy') {
322
+ const { runAutonomy } = await Promise.resolve().then(() => __importStar(require('./autonomy')));
323
+ const root = path.resolve(typeof flags.target === 'string' ? flags.target : process.cwd());
324
+ const forStr = typeof flags.for === 'string' ? flags.for : undefined;
325
+ const r = runAutonomy(root, positionals[0], forStr, {
326
+ now: new Date().toISOString(),
327
+ isTTY: !!process.stdin.isTTY && !!process.stdout.isTTY,
328
+ approval: readApproval(),
329
+ });
330
+ process[r.ok ? 'stdout' : 'stderr'].write(`${r.message}\n`);
331
+ return r.exitCode;
332
+ }
299
333
  if (cmd === 'doctor') {
300
334
  // A nonexistent target read as "healthy but unwired" and both advised remedies then refused it
301
335
  // (round-6) — the typo is the diagnosis, so say it.
@@ -335,7 +369,7 @@ async function main(argv) {
335
369
  if (r.written.length)
336
370
  process.stdout.write(`Refreshed ${r.written.length} recovery skill(s) under ${path.join(target, '.claude', 'skills')}: ${r.written.map(invocableSkillName).join(', ')}\n`);
337
371
  if (r.written.length === 0 && r.skipped.length === 0 && r.failed.length === 0) {
338
- // 패키지가 플레이북을 하나도 싣지 않았다 — 'Refreshed 0' 성공처럼 읽힌다.
372
+ // this package shipped no playbooks — 'Refreshed 0' reads like success.
339
373
  process.stderr.write(`This holmes-kit package ships no playbooks — nothing to refresh under ${path.join(target, '.claude', 'skills')}. Reinstall or upgrade holmes-kit itself.\n`);
340
374
  return 1;
341
375
  }
@@ -414,14 +448,14 @@ async function main(argv) {
414
448
  // Name a wrong --target as a wrong PATH, not as 'not a git repo' (round-12: the git error was
415
449
  // misdiagnosed) — the same class doctor's --target guard fixes.
416
450
  if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
417
- process.stderr.write(`--target ${root} 존재하는 디렉터리가 아닙니다.\n`);
451
+ process.stderr.write(`--target ${root} is not an existing directory.\n`);
418
452
  return 2;
419
453
  }
420
454
  try {
421
455
  execFileSync('git', ['-C', root, 'rev-parse', '--git-dir'], { stdio: 'pipe', env: gitEnv });
422
456
  }
423
457
  catch {
424
- process.stderr.write('git 저장소가 아닙니다 정본(ref) 없이는 재연쇄하지 않습니다: 재연쇄는 전량 재서명이며, 무엇이 정본인지는 커밋된 이력만이 말할 있습니다.\n');
458
+ process.stderr.write('not a git repositorywithout a canonical ref there is no rechaining: a rechain is a wholesale re-signing, and only the committed history can say what is canonical.\n');
425
459
  return 1;
426
460
  }
427
461
  // The ref is validated BEFORE the walk (round-2 finding: a typo'd --ref silently degraded the
@@ -432,7 +466,7 @@ async function main(argv) {
432
466
  execFileSync('git', ['-C', root, 'rev-parse', '--verify', '--quiet', `${ref}^{commit}`], { stdio: 'pipe', env: gitEnv });
433
467
  }
434
468
  catch {
435
- process.stderr.write(`--ref ${ref} 커밋으로 해석할 없습니다정본을 지정하지 못한 채로는 재연쇄하지 않습니다.\n`);
469
+ process.stderr.write(`--ref ${ref} cannot be resolved to a commit without a canonical ref there is no rechaining.\n`);
436
470
  return 1;
437
471
  }
438
472
  const ledgerDir = path.join(root, '.ax', 'ledger');
@@ -444,7 +478,7 @@ async function main(argv) {
444
478
  ? fs.readdirSync(ledgerDir).filter(isLedgerFilename)
445
479
  : [];
446
480
  if (files.length === 0) {
447
- process.stdout.write('원장 파일이 없습니다재연쇄할 것이 없습니다.\n');
481
+ process.stdout.write('no ledger filesnothing to rechain.\n');
448
482
  return 0;
449
483
  }
450
484
  // STRICT parse (round-9): a torn TAIL is tolerated, but mid-file corruption or a non-object line
@@ -463,7 +497,7 @@ async function main(argv) {
463
497
  // A rev-list failure (the ref was already rev-parse-verified) is a real git error, not an
464
498
  // empty history — surface it rather than degrading every file to a false 'no mainline'
465
499
  // (round-12).
466
- process.stderr.write(`git rev-list 실패이력을 열거할 없습니다: ${e.message.split('\n')[0]}\n`);
500
+ process.stderr.write(`git rev-list failedcannot enumerate history: ${e.message.split('\n')[0]}\n`);
467
501
  return 1;
468
502
  }
469
503
  // Resolve the SYMBOLIC ref once (round-13): 'HEAD' is ephemeral, so the audit event records the
@@ -482,14 +516,14 @@ async function main(argv) {
482
516
  const audited = [...r.events];
483
517
  audited.push(chainNext(audited, {
484
518
  ts: new Date().toISOString(), actor: 'holmes-kit', kind: 'ledger-rechained',
485
- summary: `rechained against ${refLabel}@${mainlineAt.slice(0, 8)} — 본선 ${mainline.length} + 고아 ${r.orphanCount}`,
519
+ summary: `rechained against ${refLabel}@${mainlineAt.slice(0, 8)} — mainline ${mainline.length} + orphans ${r.orphanCount}`,
486
520
  inputs: [refLabel, mainlineAt, String(r.orphanCount)], rationale: 'ledger rechain repair', authorization: '',
487
521
  }, key));
488
- // Verify the AUDITED chain (round-8: never claim "검증 통과" without verifying; round-16: verify
522
+ // Verify the AUDITED chain (round-8: never claim "verified" without verifying; round-16: verify
489
523
  // the chain we will actually write, WITH its keyed audit tail, not the pre-audit prefix).
490
524
  const v = verifyChain(audited, key);
491
525
  if (!v.ok)
492
- return { refused: `재연쇄 산출이 검증되지 않습니다(brokenAt=${v.brokenAt ?? '?'}: ${v.detail ?? ''}) — 정본(--ref)이나 설정을 확인하십시오.` };
526
+ return { refused: `the rechained output does not verify (brokenAt=${v.brokenAt ?? '?'}: ${v.detail ?? ''}) — check the canonical ref (--ref) or the key setting.` };
493
527
  return { audited, orphanCount: r.orphanCount, alreadyVerified: false, mainlineLen: mainline.length, mainlineAt };
494
528
  };
495
529
  // "Is this committed version VALIDLY KEYED under the current key?" — the load-bearing distinction
@@ -505,7 +539,7 @@ async function main(argv) {
505
539
  working = parse(fs.readFileSync(abs, 'utf8'), name);
506
540
  }
507
541
  catch (e) {
508
- process.stdout.write(`✗ ${name}: ${e.message} — 손상 줄을 조사·복구한 다시 실행하십시오.\n`);
542
+ process.stdout.write(`✗ ${name}: ${e.message} — investigate and repair the corrupt line, then re-run.\n`);
509
543
  failed++;
510
544
  continue;
511
545
  }
@@ -513,7 +547,7 @@ async function main(argv) {
513
547
  // keyed ledger as keyless is the exact attack signature the HMAC detector exists for,
514
548
  // manufactured by the repair tool).
515
549
  if (!key && hasKeyedLookingEvents(working)) {
516
- process.stdout.write(`✗ ${name}: 자기일관 아닌 줄이 있습니다 — keyed(HMAC) 이거나 content-tampered 이며 없이는 구별할 없습니다. keyed 라면 HOLMES_LEDGER_KEY 설정하고, 변조라면 조사하십시오. rechain 포크만 복구하며 줄을 재서명하지 않습니다.\n`);
550
+ process.stdout.write(`✗ ${name}: there is a non-self-consistent line either keyed (HMAC) or content-tampered, and without the key the two cannot be told apart. If keyed, set HOLMES_LEDGER_KEY; if tampered, investigate. rechain only repairs forks and does not re-sign this line.\n`);
517
551
  failed++;
518
552
  continue;
519
553
  }
@@ -601,14 +635,14 @@ async function main(argv) {
601
635
  // rewrite that erases every keyed commit within WALK_LIMIT (rebase/squash) defeats this
602
636
  // git-anchored check — the fundamental limitation named in provenance-chain.ts and spec §22.
603
637
  if (mainline !== null && key && sawKeyedCommitted) {
604
- process.stdout.write(`✗ ${name}: 커밋 이력이 keyed(HMAC)였는데 현재 본선 후보가 keyless입니다 채택은 되돌릴 없습니다(keyless-rollback). 재기록은 keyed 이력을 폐기하고 keyless 내용을 키로 재서명해 위조를 세탁합니다. verify 이미 이를 변조로 표시합니다. 건강한 keyed 정본을 --ref 지정하거나 변조를 조사하십시오.\n`);
638
+ process.stdout.write(`✗ ${name}: the committed history was keyed (HMAC) but the current mainline candidate is keyless — adopting a key is irreversible (keyless-rollback). This rewrite would discard the keyed history and re-sign the keyless content with the key, laundering a forgery. verify already flags this as tampering. Point --ref at a healthy keyed canonical, or investigate the tampering.\n`);
605
639
  failed++;
606
640
  continue;
607
641
  }
608
642
  if (keyedBlocked) {
609
643
  process.stdout.write(key
610
- ? `✗ ${name}: 커밋 이력의 본선이 현재 HOLMES_LEDGER_KEY 검증되지 않습니다키가 틀렸거나 이력이 변조됐습니다. 지나쳐 재연쇄하면 keyed 이력이 틀린 키로 재서명됩니다. 올바른 키를 확인하십시오.\n`
611
- : `✗ ${name}: 커밋 이력의 본선에 자기일관 아닌 줄이 있습니다 — keyed(HMAC) 이거나 content-tampered 입니다. keyed 라면 HOLMES_LEDGER_KEY 설정하고, 변조라면 조사하십시오. 지나쳐 재연쇄하지 않습니다.\n`);
644
+ ? `✗ ${name}: the committed mainline does not verify under the current HOLMES_LEDGER_KEY either the key is wrong or the history was tampered with. Rechaining past this would re-sign a keyed history with the wrong key. Confirm the correct key.\n`
645
+ : `✗ ${name}: the committed mainline has a non-self-consistent lineit is keyed (HMAC) or content-tampered. If keyed, set HOLMES_LEDGER_KEY; if tampered, investigate. Not rechaining past it.\n`);
612
646
  failed++;
613
647
  continue;
614
648
  }
@@ -616,14 +650,14 @@ async function main(argv) {
616
650
  // With no verifying committed mainline nothing can arbitrate. A verifying working file
617
651
  // needs no arbitration; a broken one is refused, not guessed at.
618
652
  if (verifyChain(working, key).ok) {
619
- process.stdout.write(`· ${name}: 커밋 이력에 검증본 없음, 작업본 검증됨무변경\n`);
653
+ process.stdout.write(`· ${name}: no verifying copy in the committed history, working copy verifies no change\n`);
620
654
  continue;
621
655
  }
622
656
  process.stdout.write(presentButCorrupt
623
- ? `✗ ${name}: 커밋 이력의 원장본이 손상되어 있고 작업본도 검증 실패손상 커밋본을 복구하거나 건강한 정본을 --ref 지정하십시오\n`
657
+ ? `✗ ${name}: the committed ledger copy is corrupt and the working copy fails to verify repair the corrupt committed copy or point --ref at a healthy canonical\n`
624
658
  : presentAnywhere
625
- ? `✗ ${name}: first-parent ${WALK_LIMIT}커밋 안에 검증되는 커밋본이 없고 작업본도 검증 실패정본을 --ref 지정하십시오\n`
626
- : `✗ ${name}: ${ref} 이력에 없고 작업본도 검증 실패정본을 --ref 지정하십시오\n`);
659
+ ? `✗ ${name}: no verifying committed copy within the first-parent ${WALK_LIMIT} commits and the working copy fails to verify point --ref at a canonical\n`
660
+ : `✗ ${name}: not in the ${ref} history and the working copy fails to verify point --ref at a canonical\n`);
627
661
  failed++;
628
662
  continue;
629
663
  }
@@ -643,10 +677,10 @@ async function main(argv) {
643
677
  continue;
644
678
  }
645
679
  if (rd.alreadyVerified) {
646
- process.stdout.write(`· ${name}: 이미 검증됨무변경\n`);
680
+ process.stdout.write(`· ${name}: already verifiesno change\n`);
647
681
  continue;
648
682
  }
649
- process.stdout.write(`○ ${name}: 재연쇄 대상고아 ${rd.orphanCount} (dry-run, 미기록)\n`);
683
+ process.stdout.write(`○ ${name}: would rechain — ${rd.orphanCount} orphan(s) (dry-run, not written)\n`);
650
684
  failed++;
651
685
  continue; // drift detected → CI gate fails
652
686
  }
@@ -660,12 +694,12 @@ async function main(argv) {
660
694
  fresh = parse(fs.readFileSync(abs, 'utf8'), name);
661
695
  } // read inside the lock (round-8)
662
696
  catch (e) {
663
- return { line: `✗ ${name}: ${e.message} — 손상 줄을 조사·복구한 다시 실행하십시오.`, fail: true };
697
+ return { line: `✗ ${name}: ${e.message} — investigate and repair the corrupt line, then re-run.`, fail: true };
664
698
  }
665
699
  // Re-checked INSIDE the hold (TOCTOU): a non-self-consistent line appearing between the
666
700
  // snapshot and the lock is either keyed or content-tampered — honest either way (round-8).
667
701
  if (!key && hasKeyedLookingEvents(fresh)) {
668
- return { line: `✗ ${name}: 획득 사이에 자기일관 아닌 줄이 추가되었습니다 — keyed(HOLMES_LEDGER_KEY 설정) 이거나 변조입니다. 재서명하지 않습니다.`, fail: true };
702
+ return { line: `✗ ${name}: a non-self-consistent line appeared while acquiring the lock it is keyed (set HOLMES_LEDGER_KEY) or tampered. Not re-signing.`, fail: true };
669
703
  }
670
704
  // ONE judgment (round-16): rechain, chain the KEYED audit event onto the tail (the ledger's
671
705
  // own doctrine — the audit trail must not silently change; the `ledger-rechained` event
@@ -676,7 +710,7 @@ async function main(argv) {
676
710
  if ('refused' in plan)
677
711
  return { line: `✗ ${name}: ${plan.refused}`, fail: true };
678
712
  if (plan.alreadyVerified)
679
- return { line: `· ${name}: 이미 검증됨무변경` };
713
+ return { line: `· ${name}: already verifiesno change` };
680
714
  const audited = plan.audited;
681
715
  // ATOMIC rewrite: a crash mid-write must leave the old file, never a verifying prefix that
682
716
  // the rerun's no-change guard silently accepts.
@@ -686,17 +720,17 @@ async function main(argv) {
686
720
  fs.renameSync(tmp, abs);
687
721
  // Confirm the ON-DISK result matches what we verified (round-15): the success line must
688
722
  // reflect the file, not just the in-memory array — a torn/short write is reported, not
689
- // silently blessed as '검증 통과'.
723
+ // silently blessed as 'verified'.
690
724
  if (fs.readFileSync(abs, 'utf8') !== serialized) {
691
- return { line: `✗ ${name}: 재기록된 파일이 검증한 내용과 다릅니다(디스크 이상?) — 다시 실행하십시오.`, fail: true };
725
+ return { line: `✗ ${name}: the rewritten file differs from what was verified (disk fault?) — re-run.`, fail: true };
692
726
  }
693
- return { line: `✓ ${name}: 재연쇄 완료 본선 ${fixedMainline.length}(${mainlineAt.slice(0, 8)}) + 고아 ${plan.orphanCount}, 감사 기록 남김, 체인 검증 통과` };
727
+ return { line: `✓ ${name}: rechainedmainline ${fixedMainline.length}(${mainlineAt.slice(0, 8)}) + orphans ${plan.orphanCount}, audit event recorded, chain verifies` };
694
728
  }, { onStaleBreak: new ProvenanceChain(abs).staleBreakRecorder() });
695
729
  }
696
730
  catch (e) {
697
731
  // Lock contention is a per-file refusal, not a crash.
698
732
  if (e instanceof LedgerLockError) {
699
- process.stdout.write(`✗ ${name}: 원장 락을 얻지 못했습니다(다른 기록자 사용 ) — 잠시 후 다시 실행하십시오.\n`);
733
+ process.stdout.write(`✗ ${name}: could not acquire the ledger lock (another writer is active) — retry shortly.\n`);
700
734
  failed++;
701
735
  continue;
702
736
  }
@@ -710,7 +744,7 @@ async function main(argv) {
710
744
  fs.rmSync(abs + '.rechain-tmp', { force: true });
711
745
  }
712
746
  catch { /* best-effort */ }
713
- process.stdout.write(`✗ ${name}: 재기록 오류 — ${e.message.split('\n')[0]}. 파일은 건너뜁니다(다른 파일은 계속). 원인을 조사한 다시 실행하십시오.\n`);
747
+ process.stdout.write(`✗ ${name}: error during rewrite — ${e.message.split('\n')[0]}. This file is skipped (others continue). Investigate the cause, then re-run.\n`);
714
748
  failed++;
715
749
  continue;
716
750
  }
@@ -762,13 +796,13 @@ async function main(argv) {
762
796
  return 1;
763
797
  }
764
798
  const r = grantRequest(root, ref.id, opts);
765
- process.stdout.write(r.ok ? `✓ 승인 — ${ref.subject('✓ 승인 — ')}\n${ref.detail(`${r.expires} 까지 유효`)}\n` : `✗ ${r.reason}\n`);
799
+ process.stdout.write(r.ok ? `✓ approved — ${ref.subject('✓ approved — ')}\n${ref.detail(`valid until ${r.expires}`)}\n` : `✗ ${r.reason}\n`);
766
800
  return r.ok ? 0 : 1;
767
801
  }
768
802
  if (typeof flags.deny === 'string') {
769
803
  const reason = typeof flags.reason === 'string' ? flags.reason.trim() : '';
770
804
  if (reason === '') {
771
- process.stderr.write('✗ --deny --reason 필요합니다 사유 없는 거부는 에이전트가 없습니다\n');
805
+ process.stderr.write('✗ --deny requires --reason a denial with no reason is invisible to the agent\n');
772
806
  return 1;
773
807
  }
774
808
  const ref = resolveRef(flags.deny);
@@ -777,13 +811,13 @@ async function main(argv) {
777
811
  return 1;
778
812
  }
779
813
  const r = denyRequest(root, ref.id, reason, actor);
780
- process.stdout.write(r.ok ? `✓ 거부 — ${ref.subject('✓ 거부 — ')}\n${ref.detail('거부가 기록됐습니다')}\n` : `✗ ${r.reason}\n`);
814
+ process.stdout.write(r.ok ? `✓ denied — ${ref.subject('✓ denied — ')}\n${ref.detail('the denial was recorded')}\n` : `✗ ${r.reason}\n`);
781
815
  return r.ok ? 0 : 1;
782
816
  }
783
817
  if (typeof flags.ask === 'string') {
784
818
  const question = typeof flags.question === 'string' ? flags.question.trim() : '';
785
819
  if (question === '') {
786
- process.stderr.write('✗ --ask --question 이 필요합니다\n');
820
+ process.stderr.write('✗ --ask requires --question\n');
787
821
  return 1;
788
822
  }
789
823
  const ref = resolveRef(flags.ask);
@@ -792,7 +826,7 @@ async function main(argv) {
792
826
  return 1;
793
827
  }
794
828
  const r = holdRequest(root, ref.id, question, actor);
795
- process.stdout.write(r.ok ? `✓ 보류 — ${ref.subject('✓ 보류 — ')}\n${ref.detail('질문은 다음 거부 문면에')}\n` : `✗ ${r.reason}\n`);
829
+ process.stdout.write(r.ok ? `✓ held — ${ref.subject('✓ held — ')}\n${ref.detail('the question rides the next refusal')}\n` : `✗ ${r.reason}\n`);
796
830
  return r.ok ? 0 : 1;
797
831
  }
798
832
  // @implements A-SPEC-507.1 — stale folding is a LIST-VIEW judgment: the clock enters only
@@ -814,12 +848,12 @@ async function main(argv) {
814
848
  if (typeof flags['poll-ms'] === 'string') {
815
849
  pollMs = Number(flags['poll-ms']);
816
850
  if (!Number.isInteger(pollMs) || pollMs <= 0) {
817
- process.stderr.write(`✗ --poll-ms 양의 정수(밀리초)여야 합니다 받은 값: ${flags['poll-ms']}\n`);
851
+ process.stderr.write(`✗ --poll-ms must be a positive integer (milliseconds) — received: ${flags['poll-ms']}\n`);
818
852
  return 1;
819
853
  }
820
854
  }
821
855
  if (!process.stdin.isTTY) {
822
- process.stderr.write('✗ 대화형 상주 모드(--watch) TTY 필요합니다스크립트에서는 --list 쓰십시오\n');
856
+ process.stderr.write('✗ interactive resident mode (--watch) requires a TTYuse --list in scripts\n');
823
857
  return 1;
824
858
  }
825
859
  // @implements A-SPEC-497.2 — the resident surface decides on ONE raw keypress (a/d/q/s, no
@@ -911,7 +945,7 @@ async function main(argv) {
911
945
  if (cmd === 'install-push-gate') {
912
946
  const target = typeof flags.target === 'string' ? path.resolve(flags.target) : process.cwd();
913
947
  if (!fs.existsSync(path.join(target, '.git'))) {
914
- process.stderr.write(`✗ ${target} git 저장소가 아닙니다 — pre-push 게이트는 .git/hooks 에 설치됩니다\n`);
948
+ process.stderr.write(`✗ ${target} is not a git repositorythe pre-push gate installs into .git/hooks\n`);
915
949
  return 1;
916
950
  }
917
951
  const { PUSH_HOOK_SIGNATURE } = require('../governance/push-gate');
@@ -922,21 +956,21 @@ async function main(argv) {
922
956
  const hookPath = path.join(effectiveHooksDir(target), 'pre-push');
923
957
  // dist/holmes/cli → three up is the package root, the same derivation server.ts uses.
924
958
  const runner = path.join(path.resolve(__dirname, '..', '..', '..'), 'bin', 'holmes-pre-push.js');
925
- const script = `#!/usr/bin/env node\n// ${PUSH_HOOK_SIGNATURE} — \`npx holmes-kit install-push-gate\` 가 설치했습니다.\n`
926
- + `// test-evidence 원장과 push HEAD 대조합니다. 로드 실패는 통과(fail-open) — 게이트 오류가 push 막지 않습니다.\n`
959
+ const script = `#!/usr/bin/env node\n// ${PUSH_HOOK_SIGNATURE} — installed by \`npx holmes-kit install-push-gate\`.\n`
960
+ + `// Compares the test-evidence ledger against the push HEAD. A load failure passes (fail-open) — a gate error must not block a push.\n`
927
961
  + `try { require(${JSON.stringify(runner)}); } catch { process.exit(0); }\n`;
928
962
  if (fs.existsSync(hookPath)) {
929
963
  const current = fs.readFileSync(hookPath, 'utf8');
930
964
  if (!current.includes(PUSH_HOOK_SIGNATURE)) {
931
- process.stderr.write(`✗ 기존 pre-push 훅이 있고 holmes-kit 것이 아닙니다덮지 않습니다.\n`
932
- + ` 수동 병합: 기존 끝에 \`node ${runner} || exit 1\` 형태로 이 게이트를 이어 붙이십시오.\n`);
965
+ process.stderr.write(`✗ an existing pre-push hook is present and holmes-kit did not write itnot overwriting.\n`
966
+ + ` Manual merge: append this gate to the end of the existing hook as \`node ${runner} || exit 1\`.\n`);
933
967
  return 1;
934
968
  }
935
969
  }
936
970
  fs.mkdirSync(path.dirname(hookPath), { recursive: true });
937
971
  fs.writeFileSync(hookPath, script);
938
972
  fs.chmodSync(hookPath, 0o755);
939
- process.stdout.write(`✓ pre-push 증빙 게이트 설치 — ${hookPath}\n 통과 조건: test_run 원장 head == push HEAD, 초록, 실행 > 0. 우회: git push --no-verify\n`);
973
+ process.stdout.write(`✓ pre-push evidence gate installed — ${hookPath}\n Passes when: test_run ledger head == push HEAD, green, executed > 0. Bypass: git push --no-verify\n`);
940
974
  return 0;
941
975
  }
942
976
  if (cmd === 'init') {
@@ -951,8 +985,8 @@ async function main(argv) {
951
985
  }
952
986
  mcpLauncher = flags['mcp-launcher'];
953
987
  }
954
- // 형제 열거 오류와 같은 모양(round 7): --settings·--target 은 USAGE 와 함께 exit 2 인데
955
- // --mode 맨몸 exit 1 이었다 같은 실수의 진단이 벌이면 스크립트가 갈린다.
988
+ // Same shape as the sibling-enumeration error (round 7): --settings/--target exit 2 with USAGE,
989
+ // but --mode alone exited a bare 1 — two diagnoses of the same mistake split a script.
956
990
  if (mode !== 'guardrail' && mode !== 'governed') {
957
991
  process.stderr.write(`unknown --mode ${mode} (guardrail|governed)\n\n${USAGE}`);
958
992
  return 2;
@@ -981,8 +1015,8 @@ async function main(argv) {
981
1015
  // could change the meaning of the command line is refused rather than escaped.
982
1016
  if (path.isAbsolute(specsDir) || specsDir.split(/[\\/]/).includes('..') || specsDir.startsWith('~')
983
1017
  || /["'`$\\;&|<>\n\r]/.test(specsDir)) {
984
- // '~' 셸이 확장하지 못했을 때만 남는다그대로 두면 대상 안에 리터럴 '~' 디렉터리를
985
- // 만들고 배선까지 이름으로 간다(round 8).
1018
+ // '~' survives only when the shell failed to expand it left as-is it makes a literal '~'
1019
+ // directory inside the target and wires under that name (round 8).
986
1020
  process.stderr.write(`--specs-dir ${specsDir} must be a plain relative path inside the target (no '..', no '~', no quotes or shell metacharacters)\n\n${USAGE}`);
987
1021
  return 2;
988
1022
  }
@@ -1014,8 +1048,33 @@ async function main(argv) {
1014
1048
  agents = ['claude', 'antigravity'];
1015
1049
  isAdditive = true;
1016
1050
  }
1051
+ // @implements A-SPEC-552.1 — the project autonomy posture. `--autonomy`/`--no-autonomy` are the
1052
+ // non-interactive spellings (enabling then needs an out-of-band approval, enforced in runInit);
1053
+ // otherwise, at a real TTY, ASK — a `y` there is a human answer (autonomyInteractive), so it
1054
+ // needs no token. Non-TTY with no flag leaves it undefined = preserve the current posture.
1055
+ let autonomy;
1056
+ let autonomyInteractive = false;
1057
+ if (flags.autonomy === true) {
1058
+ autonomy = true;
1059
+ }
1060
+ else if (flags['no-autonomy'] === true) {
1061
+ autonomy = false;
1062
+ }
1063
+ else if (process.stdin.isTTY && process.stdout.isTTY && flags['dry-run'] !== true
1064
+ && flags.remove !== true && flags['no-mcp'] !== true) {
1065
+ const { promptAutonomy } = require('./interactive-prompt');
1066
+ if (await promptAutonomy()) {
1067
+ autonomy = true;
1068
+ autonomyInteractive = true;
1069
+ }
1070
+ else {
1071
+ autonomy = false;
1072
+ }
1073
+ }
1017
1074
  const opts = {
1018
1075
  agents,
1076
+ autonomy,
1077
+ autonomyInteractive,
1019
1078
  allowAdditive: isAdditive,
1020
1079
  target: path.resolve(typeof flags.target === 'string' ? flags.target : process.cwd()),
1021
1080
  packageRoot: packageRoot(),
@@ -1090,8 +1149,8 @@ async function main(argv) {
1090
1149
  },
1091
1150
  });
1092
1151
  }
1093
- // round-8: `holmes-kit --target x` `holmes-kit --` 처럼 명령 자리에 플래그가 오면 exit 1,
1094
- // 같은 토큰이 하위 명령 뒤에 오면 exit 2 였다 같은 실수에 가지 진단.
1152
+ // round-8: a flag in the command slot (`holmes-kit --target x` or `holmes-kit --`) exited 1,
1153
+ // while the same token after a subcommand exited 2 — two diagnoses of the same mistake.
1095
1154
  process.stderr.write(cmd.startsWith('-')
1096
1155
  ? `unknown flag in command position: ${cmd}\n\n${USAGE}`
1097
1156
  : `unknown command: ${cmd}\n\n${USAGE}`);
@@ -31,6 +31,8 @@ export interface InitOptions {
31
31
  agents?: Agent[];
32
32
  allowAdditive?: boolean;
33
33
  mcpLauncher?: LauncherMode;
34
+ autonomy?: boolean;
35
+ autonomyInteractive?: boolean;
34
36
  }
35
37
  export interface FileChange {
36
38
  path: string;