@holmes-lab/holmes-kit 0.12.1 → 0.13.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 +104 -0
  2. package/README.md +13 -4
  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 +9 -2
  27. package/dist/holmes/governance/autonomy.js +166 -5
  28. package/dist/holmes/guardrail/blind-spots.js +15 -15
  29. package/dist/holmes/hooks/pre-tool-use.js +111 -42
  30. package/dist/holmes/hooks/session-start.js +74 -34
  31. package/dist/holmes/hooks/stop.d.ts +1 -1
  32. package/dist/holmes/hooks/stop.js +12 -12
  33. package/dist/holmes/mcp/handlers.js +14 -1
  34. package/dist/holmes/mcp/server.js +19 -0
  35. package/dist/holmes/semantic/credentials.js +1 -1
  36. package/dist/holmes/spec/id-collision.js +2 -2
  37. package/dist/holmes/update/refresh.d.ts +49 -0
  38. package/dist/holmes/update/refresh.js +106 -0
  39. package/package.json +2 -2
  40. package/playbooks/publish/PLAYBOOK.md +47 -35
  41. package/playbooks/remediation/PLAYBOOK.md +1 -1
@@ -157,21 +157,21 @@ function npmCliEntry(env = process.env, execPath = process.execPath) {
157
157
  */
158
158
  function prefixVerdict(input) {
159
159
  const reason = input.unreadable ?? (typeof input.prefix !== 'string' || input.prefix === ''
160
- ? `npm 돌려준 prefix 해석할 없습니다 (${String(input.prefix)})`
160
+ ? `npm returned a prefix that cannot be parsed (${String(input.prefix)})`
161
161
  : undefined);
162
162
  if (reason !== undefined) {
163
- return { level: 'WARN', detail: `전역 prefix 판독하지 못했습니다 — ${reason}. 로컬 설치(--save-dev) 판정과 무관하게 동작합니다.` };
163
+ return { level: 'WARN', detail: `Could not read the global prefix — ${reason}. A local install (--save-dev) works regardless of this verdict.` };
164
164
  }
165
165
  const prefix = input.prefix;
166
166
  if (input.writable) {
167
- return { level: 'PASS', detail: `전역 prefix(${prefix}) 현재 계정으로 쓰기 가능합니다 — npm install -g 권한 문제 없이 동작합니다.` };
167
+ return { level: 'PASS', detail: `Global prefix (${prefix}) is writable by the current account — npm install -g works without permission trouble.` };
168
168
  }
169
169
  const fix = input.platform === 'win32'
170
- ? 'npm config set prefix "%APPDATA%\\npm" 실행 %APPDATA%\\npm PATH 추가하고 터미널을 다시 여세요. 로컬 설치(npm install --save-dev) 설정 없이도 동작합니다.'
171
- : 'npm config set prefix "$HOME/.npm-global" 실행 $HOME/.npm-global/bin PATH 추가하세요. 로컬 설치(npm install --save-dev) 설정 없이도 동작합니다.';
170
+ ? 'Run npm config set prefix "%APPDATA%\\npm", add %APPDATA%\\npm to PATH, and reopen the terminal. A local install (npm install --save-dev) works without this setting.'
171
+ : 'Run npm config set prefix "$HOME/.npm-global" and add $HOME/.npm-global/bin to PATH. A local install (npm install --save-dev) works without this setting.';
172
172
  return {
173
173
  level: 'WARN',
174
- detail: `전역 prefix(${prefix}) 현재 계정으로 쓰기 불가입니다 — npm install -g ${input.dir ?? ''} 디렉터리를 만들다 EPERM 으로 죽습니다. 패키지가 도착하기 단계라 어떤 패키지도 이를 고칠 수 없습니다.`,
174
+ detail: `Global prefix (${prefix}) is not writable by the current account — npm install -g dies with EPERM while creating a directory in ${input.dir ?? ''}. This happens before any package is transferred, so no package can fix it.`,
175
175
  fix,
176
176
  };
177
177
  }
@@ -219,7 +219,7 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
219
219
  add('build output', built ? 'PASS' : 'FAIL', built ? `compiled output present under ${path.join(packageRoot, 'dist')}` : `missing ${serverJs}`,
220
220
  // @implements A-SPEC-190 §16 (round 12) — the old remedy named a `prepare` script package.json
221
221
  // never declared (build/mcp/release/test/typecheck only); dist/ ships because `files` lists it.
222
- built ? undefined : ' 패키지는 컴파일된 dist/ 함께 배포합니다 저장소에서 쓰는 중이라면 `npm run build`, 설치본이라면 재설치하십시오.');
222
+ built ? undefined : 'This package ships compiled dist/ if you are working from the repository run `npm run build`; if this is an install, reinstall it.');
223
223
  // 2. Node version vs better-sqlite3's declared range (WARN only — a mismatch still often works).
224
224
  let sqliteRange = '(unknown)';
225
225
  try {
@@ -278,7 +278,7 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
278
278
  // A probe must observe the gate, never feed its audit trail.
279
279
  (() => {
280
280
  const probeCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'holmes-doctor-probe-'));
281
- cleanupOnSignal(probeCwd); // round-9: 인터럽트 사망에서도 남지 않는다
281
+ cleanupOnSignal(probeCwd); // round-9: leaves nothing behind even on an interrupt-kill
282
282
  try {
283
283
  return (0, node_child_process_1.spawnSync)(process.execPath, [script], { input: JSON.stringify(payload), encoding: 'utf8', timeout: 20000, env: childEnv, cwd: probeCwd });
284
284
  }
@@ -441,10 +441,10 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
441
441
  // not PASS: `codex gate` set the precedent — a thing that is wired but not yet enforcing is not
442
442
  // a pass. Presence only; the value is an out-of-band secret and is never read into the report.
443
443
  if (process.env.HOLMES_LEDGER_KEY) {
444
- add('ledger signing', 'PASS', '원장이 HOLMES_LEDGER_KEY 서명됩니다 없는 위조자는 체인 정합한 spec-approved 기록을 만들 수 없습니다');
444
+ add('ledger signing', 'PASS', 'The ledger is signed with HOLMES_LEDGER_KEY without the key, a forger cannot produce a chain-valid spec-approved record');
445
445
  }
446
446
  else {
447
- add('ledger signing', 'WARN', '원장이 서명되지 않습니다(HOLMES_LEDGER_KEY 없음) — 체인이 평문 해시라 파일을 있는 자가 승인 기록도 만들 있고, ART-5 원장 대조는 장벽이 아니라 비용이 됩니다', 'HOLMES_LEDGER_KEY 대역외(에이전트를 기동하는 사람의 환경)에서 설정한 서버를 다시 시작하십시오. 값은 보고서에 절대 출력되지 않습니다.');
447
+ add('ledger signing', 'WARN', 'The ledger is not signed (HOLMES_LEDGER_KEY unset) — the chain is a plain hash, so anyone who can write the file can also mint approval records, and ART-5\'s ledger comparison becomes a cost rather than a barrier', 'Set HOLMES_LEDGER_KEY out-of-band (in the environment of whoever launches the agent), then restart the server. The value is never printed into this report.');
448
448
  }
449
449
  // @implements A-SPEC-535.1
450
450
  // Advisory: holmes-kit's OWN advertised MCP schema cost, and the one holmes-controlled
@@ -477,10 +477,10 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
477
477
  }).split('\n').map((l) => l.trim().split('/').pop()).filter(Boolean));
478
478
  const untracked = onDisk.filter((f) => !tracked.has(f));
479
479
  if (untracked.length > 0) {
480
- add('ledger tracked', 'WARN', `추적되지 않는 원장: ${untracked.join(', ')} — git 없어 ART-2 절단 백스톱(A-SPEC-455) 파일에 대해 꺼져 있습니다. 원장을 통째로 비워도 Stop 게이트가 잡지 못합니다`, '.gitignore `!.ax/ledger/provenance*.jsonl` 되살림 규칙을 두고 `git add -f .ax/ledger/provenance*.jsonl` 추적에 추가한 뒤 커밋하십시오.');
480
+ add('ledger tracked', 'WARN', `Untracked ledger(s): ${untracked.join(', ')} — not in git, so the ART-2 truncation backstop (A-SPEC-455) is off for these files. The Stop gate would not catch even a wholesale emptying of the ledger`, 'Add a `!.ax/ledger/provenance*.jsonl` re-include rule to .gitignore, then `git add -f .ax/ledger/provenance*.jsonl`, and commit.');
481
481
  }
482
482
  else {
483
- add('ledger tracked', 'PASS', '원장이 git 추적됩니다 — ART-2 절단 백스톱이 커밋된 이력을 기준으로 롤백을 잡습니다');
483
+ add('ledger tracked', 'PASS', 'The ledger is tracked in git the ART-2 truncation backstop catches a rollback against the committed history');
484
484
  }
485
485
  }
486
486
  }
@@ -520,12 +520,12 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
520
520
  const { rtmCheck } = require('../rtm/rtm-check');
521
521
  const dups = rtmCheck(all).filter((i) => i.kind === 'duplicate-id');
522
522
  add('duplicate spec ids', dups.length === 0 ? 'PASS' : 'WARN', dups.length === 0
523
- ? '같은 id를 가진 스펙 파일이 없습니다'
524
- : `같은 id의 스펙 파일이 이상: ${dups.map((d) => d.id).join(', ')}`, dups.length === 0 ? undefined
525
- : '게이트는 승인되지 않은 쪽으로 판정합니다남길 파일 하나만 두고 사본을 지우세요.');
523
+ ? 'No two spec files share an id'
524
+ : `Two or more spec files share an id: ${dups.map((d) => d.id).join(', ')}`, dups.length === 0 ? undefined
525
+ : 'The gate rules toward the unapproved side keep one file and delete the copy.');
526
526
  add('spec chain', 'PASS', orphaned.length === 0
527
- ? '승인된 스펙의 상위가 모두 approved입니다'
528
- : `상위가 approved 아닌 승인 스펙 ${orphaned.length}건: ${orphaned.map((s) => s.id).slice(0, 5).join(', ')}`);
527
+ ? 'Every approved spec has approved parents'
528
+ : `${orphaned.length} approved spec(s) with a non-approved parent: ${orphaned.map((s) => s.id).slice(0, 5).join(', ')}`);
529
529
  }
530
530
  catch { /* an unreadable corpus is doctor's other checks' business, not this one's */ }
531
531
  // @implements A-SPEC-254.1
@@ -570,7 +570,7 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
570
570
  // "Nothing there" is not "something escaped judgment" (round-4, extended round-7): a
571
571
  // NONEXISTENT (ENOENT) or path-broken (ENOTDIR — .ax created as a stray FILE;
572
572
  // ENAMETOOLONG) root/dir holds nothing that could have been judged, so counting it stamped
573
- // a phantom '판정 1건' on pre-init targets, indistinguishable from a genuinely
573
+ // a phantom "1 outside the verdict" on pre-init targets, indistinguishable from a genuinely
574
574
  // permission-locked store. Real refusals (EACCES) and cycles (ELOOP — an entry exists, it
575
575
  // just cannot be entered) still count as unexamined regions.
576
576
  try {
@@ -665,26 +665,26 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
665
665
  collect(specsRoot);
666
666
  // No cross-reference to `spec readability` (round-3): that walker does not follow dir
667
667
  // symlinks, so the pointer was false for symlink-reached files — doctor contradicting itself.
668
- const scopeNote = skipped > 0 ? ` (판정 밖 ${skipped})` : '';
668
+ const scopeNote = skipped > 0 ? ` (${skipped} outside the verdict)` : '';
669
669
  const found = detectIdCollisions(entries);
670
670
  const collisions = found.filter((i) => i.kind === 'id-collision');
671
671
  const families = found.filter((i) => i.kind === 'family-coexistence');
672
- add('spec id 선점 충돌', collisions.length === 0 ? 'PASS' : 'WARN', collisions.length === 0
673
- ? `같은 id 서로 다른 스펙이 쓰는 충돌이 없습니다${scopeNote}`
672
+ add('spec id preemption collision', collisions.length === 0 ? 'PASS' : 'WARN', collisions.length === 0
673
+ ? `No id is claimed by two different specs${scopeNote}`
674
674
  : collisions.map((c) => `${c.id}: ${c.files.join(' ↔ ')} (${c.detail})`).join('; ') + scopeNote, collisions.length === 0 ? undefined
675
- : '같은 번호를 스펙이 선점했습니다한쪽을 리넘버하고(REQ-255 전까지는 수동) 재봉인하세요.');
676
- add('spec id 가족 공존', families.length === 0 ? 'PASS' : 'WARN', families.length === 0
677
- ? `bare id dot-suffix 가족의 공존이 없습니다${scopeNote}`
675
+ : 'Two specs preempted the same number renumber one (manual until REQ-255) and re-seal.');
676
+ add('spec id family coexistence', families.length === 0 ? 'PASS' : 'WARN', families.length === 0
677
+ ? `No bare id coexists with its dot-suffix family${scopeNote}`
678
678
  : families.map((f) => `${f.id}: ${f.files.join(', ')}`).join('; ') + scopeNote, families.length === 0 ? undefined
679
- : 'bare 스펙과 하위 가족이 공존합니다 워크스페이스가 같은 번호를 다르게 전개한 신호이니 한쪽 전개로 통일하세요.');
679
+ : 'A bare spec coexists with a dotted sub-family a sign two workspaces expanded the same number differently; unify on one expansion.');
680
680
  }
681
681
  catch (e) {
682
682
  // Never a silent vanish: the corpus most likely to hold a collision (a messy half-merged
683
683
  // store) is exactly where an upstream throw would otherwise suppress the report. String(e),
684
684
  // not e.message — a thrown non-Error must not crash the survival branch itself (round-2).
685
685
  const msg = String(e instanceof Error ? e.message : e).split('\n')[0];
686
- add('spec id 선점 충돌', 'WARN', `검사를 실행하지 못했습니다: ${msg}`);
687
- add('spec id 가족 공존', 'WARN', `검사를 실행하지 못했습니다: ${msg}`);
686
+ add('spec id preemption collision', 'WARN', `could not run the check: ${msg}`);
687
+ add('spec id family coexistence', 'WARN', `could not run the check: ${msg}`);
688
688
  }
689
689
  }
690
690
  // @implements A-SPEC-176
@@ -696,8 +696,8 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
696
696
  const { unreadableSpecFiles } = require('../spec/spec-store');
697
697
  const unreadable = unreadableSpecFiles(path.join(target, '.ax', 'specs'));
698
698
  add('spec readability', unreadable.length === 0 ? 'PASS' : 'WARN', unreadable.length === 0
699
- ? '스펙 디렉터리의 모든 .md 파싱됩니다'
700
- : `파싱되지 않아 스펙으로 취급되지 않는 파일 ${unreadable.length}건: ${unreadable.slice(0, 5).join(', ')}`, unreadable.length === 0 ? undefined : '해당 파일의 프론트매터를 확인하세요존재하지만 게이트에는 보이지 않습니다.');
699
+ ? 'Every .md in the spec directory parses'
700
+ : `${unreadable.length} file(s) not treated as specs because they do not parse: ${unreadable.slice(0, 5).join(', ')}`, unreadable.length === 0 ? undefined : 'Check the frontmatter of those files they exist but are invisible to the gate.');
701
701
  }
702
702
  // @implements A-SPEC-178
703
703
  // Two checks that report the wiring a USER will meet, not what this process happens to see.
@@ -725,16 +725,32 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
725
725
  } // absent or unreadable is "not guaranteed", which is the point
726
726
  const procApproval = !!process.env.HOLMES_APPROVAL;
727
727
  if (mcpApproval) {
728
- add('approval channel', 'PASS', '승인 변수가 .mcp.json 서버 환경에 있습니다 — spec_approve 도달합니다');
728
+ add('approval channel', 'PASS', 'The approval variable is in .mcp.json\'s server env — spec_approve reaches it');
729
729
  }
730
730
  else if (!procApproval) {
731
731
  // A project that has not started approving is healthy. Warning here would put a permanent line
732
732
  // on every install, and a line that is always there is one nobody reads (REQ-153's judgement).
733
- add('approval channel', 'PASS', '승인 변수가 설정되어 있지 않습니다승인이 필요할 설정하세요');
733
+ add('approval channel', 'PASS', 'The approval variable is not set set it when an approval is needed');
734
734
  }
735
735
  else {
736
- add('approval channel', 'WARN', ' 프로세스에는 HOLMES_APPROVAL 있으나 .mcp.json 서버 환경에는 없습니다 — spec_approve 서버 환경을 읽습니다', 'settings.local.json env MCP 서버까지 전달된다는 보장은 없습니다. 변수를 설정한 셸에서 Claude Code를 띄우면 자식 서버가 상속합니다.');
736
+ add('approval channel', 'WARN', 'This process has HOLMES_APPROVAL but .mcp.json\'s server env does not — spec_approve reads the server env', 'There is no guarantee that settings.local.json\'s env reaches the MCP server. Launch Claude Code from the shell where you set that variable, and the child server inherits it.');
737
737
  }
738
+ // @implements A-SPEC-552.1 — surface the project's autonomy posture so a human always knows
739
+ // whether the agent may self-approve. Read from the SAME place the MCP server does (.mcp.json
740
+ // server env), never from doctor's own process, for the reason stated above.
741
+ let autonomyOn = false;
742
+ try {
743
+ const raw = fs.readFileSync(path.join(target, '.mcp.json'), 'utf8');
744
+ const env = JSON.parse(raw)
745
+ .mcpServers?.[init_1.SERVER_NAME]?.env;
746
+ autonomyOn = !!env && !!env['HOLMES_AUTONOMOUS_APPROVAL'];
747
+ }
748
+ catch {
749
+ autonomyOn = false;
750
+ }
751
+ add('autonomous spec approval', 'PASS', autonomyOn
752
+ ? 'ON — the agent may self-approve low-risk specs; governance-critical still ask you (holmes-kit autonomy off to disable)'
753
+ : 'OFF — every spec approval is human (enable at init, or holmes-kit autonomy on for one session)');
738
754
  // @implements A-SPEC-251.2 — the version this project's MCP server will ACTUALLY launch, vs this
739
755
  // CLI. The stale-global trap (measured 2026-08-23: .mcp.json pinned a 0.1.9 global while the user
740
756
  // believed they had upgraded) was invisible because nothing compared them. Offline: the launch
@@ -757,17 +773,17 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
757
773
  const launch = (0, mcp_version_1.mcpLaunchVersion)({ command: server.command, args: server.args }, readPkgVersionAt);
758
774
  const verdict = (0, mcp_version_1.versionDriftVerdict)(launch, cli);
759
775
  if (verdict === 'drift') {
760
- add('mcp server version', 'WARN', `이 프로젝트의 MCP 서버는 holmes-kit ${launch} 띄우지만 CLI ${cli} 입니다 재배선 전까지 서버가 계속 뜹니다`,
776
+ add('mcp server version', 'WARN', `This project's MCP server launches holmes-kit ${launch}, but this CLI is ${cli} — the old server keeps starting until it is rewired`,
761
777
  // @implements A-SPEC-544.1 — one command installs + re-pins every workspace; init --force is the per-workspace path.
762
- `${(0, npx_bin_1.npxBin)()} holmes-kit upgrade 최신 설치 + 워크스페이스 재핀 ( 워크스페이스만 고치려면 ${(0, npx_bin_1.npxBin)()} holmes-kit init --force).`);
778
+ `Run ${(0, npx_bin_1.npxBin)()} holmes-kit upgrade to install the latest and re-pin every workspace (to fix only this workspace, ${(0, npx_bin_1.npxBin)()} holmes-kit init --force).`);
763
779
  }
764
780
  else if (verdict === 'match') {
765
- add('mcp server version', 'PASS', `MCP 서버 버전 ${launch} CLI 와 일치합니다`);
781
+ add('mcp server version', 'PASS', `MCP server version ${launch} matches this CLI`);
766
782
  }
767
- // 'unknown' 항목 없음: 판정 불능을 결함으로 만들지 않는다.
783
+ // 'unknown' -> no entry: an undecidable check is not turned into a defect.
768
784
  }
769
785
  }
770
- catch { /* .mcp.json 부재/파싱 실패 점검 없이 진행 */ }
786
+ catch { /* .mcp.json absent or unparseable -> proceed without this check */ }
771
787
  // @implements A-SPEC-259 — the wiring is judged by SPAWNING it. String comparison is exactly how
772
788
  // the dead `npx -y <pkg>@<ver> holmes-mcp` form (npx resolves the default bin and hands
773
789
  // `holmes-mcp` to it as an argument) passed the launcher test, this doctor's own handshake check
@@ -805,8 +821,8 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
805
821
  }
806
822
  }
807
823
  }
808
- // @implements A-SPEC-193 — 배선된 하네스마다 하네스의 배선을 검사한다. 배선되지 않은
809
- // 하네스는 진단하지 않는다(없는 것을 결함이라 부르면 doctor 소음이 된다).
824
+ // @implements A-SPEC-193 — for each wired harness, check that harness's wiring. A harness that is
825
+ // not wired is not diagnosed (calling an absence a defect turns doctor into noise).
810
826
  const agyHooks = path.join(target, '.agents', 'hooks.json');
811
827
  if (fs.existsSync(agyHooks)) {
812
828
  try {
@@ -820,25 +836,25 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
820
836
  const stale = [scriptOf(pre?.hooks?.[0]), scriptOf(entry?.Stop?.[0])]
821
837
  .filter((p2) => !resolvesToPackage(p2, packageRoot));
822
838
  if (entry === undefined) {
823
- add('antigravity wiring', 'WARN', `${agyHooks} holmes-kit 항목이 없습니다`, 'holmes-kit init --target <dir> --agent antigravity');
839
+ add('antigravity wiring', 'WARN', `${agyHooks} has no holmes-kit entry`, 'holmes-kit init --target <dir> --agent antigravity');
824
840
  }
825
841
  else if (stale.length > 0) {
826
- add('antigravity wiring', 'FAIL', `이 설치본을 가리키지 않는 경로: ${stale.join(' | ')}`, 'holmes-kit init --target <dir> --agent antigravity --force 절대 경로를 갱신하십시오.');
842
+ add('antigravity wiring', 'FAIL', `hook path(s) not pointing at this install: ${stale.join(' | ')}`, 'Run holmes-kit init --target <dir> --agent antigravity --force to refresh the absolute paths.');
827
843
  }
828
844
  else if ((pre?.matcher ?? '') !== '*') {
829
- // 매처가 좁으면 밖의 도구가 게이트를 지나간다조용한 구멍이므로 말한다.
830
- add('antigravity wiring', 'WARN', `PreToolUse 매처가 '${pre?.matcher ?? ''}' 입니다 — '*' 아니면 밖의 도구는 게이트에 닿지 않습니다`, "matcher '*' 로 되돌리십시오.");
845
+ // A narrow matcher lets other tools pass the gate a silent hole, so say it.
846
+ add('antigravity wiring', 'WARN', `The PreToolUse matcher is '${pre?.matcher ?? ''}' — unless it is '*', other tools never reach the gate`, "Restore the matcher to '*'.");
831
847
  }
832
848
  else {
833
- add('antigravity wiring', 'PASS', 'PreToolUse·Stop 설치본을 가리킵니다');
849
+ add('antigravity wiring', 'PASS', 'PreToolUse and Stop point at this install');
834
850
  }
835
851
  }
836
852
  catch {
837
- add('antigravity wiring', 'FAIL', `${agyHooks} 읽을 없습니다(JSON 아님)`, '파일을 고치거나 지우고 다시 배선하십시오.');
853
+ add('antigravity wiring', 'FAIL', `${agyHooks} cannot be read (not JSON)`, 'Fix or delete the file and rewire.');
838
854
  }
839
- // 스킬이 하네스에 보이는가링크가 끊기면 스킬은 조용히 사라진다.
855
+ // Are the skills visible to that harness a broken link makes skills silently disappear.
840
856
  const agySkills = path.join(target, '.agents', 'skills');
841
- add('antigravity skills', fs.existsSync(agySkills) ? 'PASS' : 'WARN', fs.existsSync(agySkills) ? `${agySkills} 스킬을 가리킵니다` : `${agySkills} 없습니다 하네스는 스킬을 보지 못합니다`, fs.existsSync(agySkills) ? undefined : `${path.join('..', '.claude', 'skills')} 로 링크하거나 복사하십시오.`);
857
+ add('antigravity skills', fs.existsSync(agySkills) ? 'PASS' : 'WARN', fs.existsSync(agySkills) ? `${agySkills} points at the skills` : `${agySkills} is missingthis harness cannot see the skills`, fs.existsSync(agySkills) ? undefined : `Link or copy it to ${path.join('..', '.claude', 'skills')}.`);
842
858
  }
843
859
  // @implements A-SPEC-266 (was A-SPEC-264) — Codex reads MCP servers from `.codex/config.toml`
844
860
  // (TOML `[mcp_servers.*]`), so we check the file Codex actually loads — not the
@@ -854,17 +870,18 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
854
870
  }
855
871
  catch { /* handled below */ }
856
872
  if (raw === null) {
857
- add('codex wiring', 'FAIL', `${cdxToml} 읽을 없습니다`, '파일 권한을 확인하거나 지우고 다시 배선하십시오.');
873
+ add('codex wiring', 'FAIL', `${cdxToml} cannot be read`, 'Check the file permissions, or delete it and rewire.');
858
874
  }
859
875
  else {
860
876
  const entry = (0, codex_toml_1.readCodexHolmesEntry)(raw);
861
877
  if (!entry) {
862
- add('codex wiring', 'WARN', `${cdxToml} 실행 가능한 [mcp_servers.${init_1.SERVER_NAME}] 항목이 없습니다 반쯤 배선입니다`, 'holmes-kit init --target <dir> --agent codex 로 다시 배선하십시오.');
878
+ add('codex wiring', 'WARN', `${cdxToml} has no runnable [mcp_servers.${init_1.SERVER_NAME}] entrya half-done wiring`, 'Rewire with holmes-kit init --target <dir> --agent codex.');
863
879
  }
864
880
  else if (entry.command === 'node') {
865
- // @implements A-SPEC-266 — " 설치본으로 해석됩니다" 존재만으로는 부족하다: node 배선은
866
- // holmes-mcp.js 가리켜야 한다. 아무 존재 파일(예: /etc/hosts) PASS 를 주면 "해석된다"는
867
- // 주장이 검사보다 강해진다(적대 라운드 Finding 2). 파일명까지 확인해 그 간극을 좁힌다.
881
+ // @implements A-SPEC-266 — "resolves to this install" is not enough on existence alone: a
882
+ // node wiring must point at holmes-mcp.js. Passing any existing file (e.g. /etc/hosts) makes
883
+ // the "resolves" claim stronger than the check (adversarial round, Finding 2). Verify the
884
+ // basename too, to close that gap.
868
885
  const bin = entry.args[0] ?? '';
869
886
  // Resolve the link before judging the name (round-2 F4): a symlink literally named
870
887
  // `holmes-mcp.js` pointing at /etc/hosts must not read as "resolves to this install".
@@ -875,12 +892,12 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
875
892
  return '';
876
893
  } })();
877
894
  const resolvesHere = realBin !== '' && path.basename(realBin) === 'holmes-mcp.js';
878
- add('codex wiring', resolvesHere ? 'PASS' : 'FAIL', resolvesHere ? `MCP 배선이 설치본으로 해석됩니다: ${bin}`
879
- : (fs.existsSync(bin) ? `MCP 배선이 holmes-mcp.js 가 아닌 파일을 가리킵니다: ${bin}` : `MCP 배선이 없는 파일을 가리킵니다: ${bin}`), resolvesHere ? undefined : 'holmes-kit init --target <dir> --agent codex --force 절대 경로를 갱신하십시오.');
895
+ add('codex wiring', resolvesHere ? 'PASS' : 'FAIL', resolvesHere ? `MCP wiring resolves to this install: ${bin}`
896
+ : (fs.existsSync(bin) ? `MCP wiring points at a file that is not holmes-mcp.js: ${bin}` : `MCP wiring points at a missing file: ${bin}`), resolvesHere ? undefined : 'Run holmes-kit init --target <dir> --agent codex --force to refresh the absolute path.');
880
897
  }
881
898
  else {
882
899
  const pin = (0, mcp_version_1.mcpLaunchVersion)({ command: entry.command, args: entry.args });
883
- add('codex wiring', pin !== null ? 'PASS' : 'FAIL', pin !== null ? `npx ${pin} 으로 해석됩니다` : `배선에서 실행 버전을 읽을 없습니다: ${entry.command} ${entry.args.join(' ')}`, pin !== null ? undefined : 'holmes-kit init --target <dir> --agent codex 로 다시 배선하십시오.');
900
+ add('codex wiring', pin !== null ? 'PASS' : 'FAIL', pin !== null ? `resolves via the npx pin ${pin}` : `cannot read a launch version from the wiring: ${entry.command} ${entry.args.join(' ')}`, pin !== null ? undefined : 'Rewire with holmes-kit init --target <dir> --agent codex.');
884
901
  }
885
902
  // @implements A-SPEC-442 (was A-SPEC-423)
886
903
  // Codex CAN hard-enforce, but only from an INSTALLED plugin: it loads plugins from
@@ -897,7 +914,7 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
897
914
  // location only) is caught instead of passing as "wiring is correct" when it cannot install.
898
915
  const cdxMarketplaceNew = path.join(target, '.claude-plugin', 'marketplace.json');
899
916
  const cdxMarketplaceOld = path.join(target, '.agents', 'plugins', 'marketplace.json');
900
- const installCmds = `codex plugin marketplace add "${target}" codex plugin add holmes-kit@${agents_1.CODEX_MARKETPLACE}`;
917
+ const installCmds = `codex plugin marketplace add "${target}", then codex plugin add holmes-kit@${agents_1.CODEX_MARKETPLACE}`;
901
918
  if (fs.existsSync(cdxPluginJson) && fs.existsSync(cdxHooksJson) && (fs.existsSync(cdxMarketplaceNew) || fs.existsSync(cdxMarketplaceOld))) {
902
919
  try {
903
920
  const h = JSON.parse(fs.readFileSync(cdxHooksJson, 'utf8'));
@@ -909,33 +926,33 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
909
926
  return !resolvesToPackage(p2, packageRoot);
910
927
  });
911
928
  if (stale.length > 0) {
912
- add('codex gate', 'FAIL', `이 설치본을 가리키지 않는 명령: ${stale.join(' | ')}`, 'holmes-kit init --target <dir> --agent codex --force 절대 경로를 갱신하십시오.');
929
+ add('codex gate', 'FAIL', `hook command(s) not pointing at this install: ${stale.join(' | ')}`, 'Run holmes-kit init --target <dir> --agent codex --force to refresh the absolute paths.');
913
930
  }
914
931
  else if (manifest.hooks !== './hooks.json') {
915
932
  // The one path measured to load. `./hooks/hooks.json` parses fine and never fires.
916
- add('codex gate', 'FAIL', `플러그인 매니페스트의 hooks './hooks.json' 이 아닙니다: ${String(manifest.hooks)} — Codex 적재하지 않습니다`, 'holmes-kit init --target <dir> --agent codex --force 로 다시 배선하십시오.');
933
+ add('codex gate', 'FAIL', `the plugin manifest's hooks is not './hooks.json': ${String(manifest.hooks)} — Codex does not load it`, 'Rewire with holmes-kit init --target <dir> --agent codex --force.');
917
934
  }
918
935
  else if (!fs.existsSync(cdxMarketplaceNew)) {
919
936
  // @implements A-SPEC-533.3 — hooks are correct, but the marketplace manifest is only at
920
937
  // the old `.agents/plugins/` location; codex 0.152.x reads `.claude-plugin/` and cannot
921
938
  // install this. Judged AFTER the stale-hook / manifest FAILs so those still win.
922
- add('codex gate', 'WARN', `codex 0.152.x 읽는 .claude-plugin/marketplace.json 없습니다 위치(.agents/plugins/marketplace.json) 있어 codex plugin marketplace add "manifest 없음" 으로 실패합니다 (0.151.x .agents/plugins/, 0.152.x .claude-plugin/)`, 'holmes-kit init --target <dir> --agent codex --force 다시 배선하면 위치에 매니페스트가 놓입니다.');
939
+ add('codex gate', 'WARN', `.claude-plugin/marketplace.json (read by codex 0.152.x) is missing only the old location (.agents/plugins/marketplace.json) is present, so codex plugin marketplace add fails with "no manifest" (0.151.x reads .agents/plugins/, 0.152.x reads .claude-plugin/)`, 'Rewiring with holmes-kit init --target <dir> --agent codex --force places the manifest in both locations.');
923
940
  }
924
941
  else {
925
- add('codex gate', 'WARN', '플러그인 배선은 옳습니다그러나 설치되기 전까지 게이트는 집행되지 않습니다(Codex 설치된 마켓플레이스에서만 플러그인을 적재하며, doctor 설치 여부를 오프라인에서 확인할 없습니다)', `${installCmds} 실행한 세션에서 사용하십시오.`);
942
+ add('codex gate', 'WARN', 'The plugin wiring is correct but the gate is not enforced until it is installed (Codex only loads plugins from an installed marketplace, and doctor cannot verify installation offline)', `Run ${installCmds}, then use it in a new session.`);
926
943
  }
927
944
  }
928
945
  catch {
929
- add('codex gate', 'FAIL', `${cdxHooksJson} 읽을 없습니다(JSON 아님)`, '파일을 고치거나 지우고 다시 배선하십시오.');
946
+ add('codex gate', 'FAIL', `${cdxHooksJson} cannot be read (not JSON)`, 'Fix or delete the file and rewire.');
930
947
  }
931
948
  }
932
949
  else {
933
- add('codex gate', 'WARN', 'codex 플러그인 배선(.agents/plugins/marketplace.json, .holmes/codex-plugin/holmes-kit) 없습니다 — MCP 도구만 동작하며 게이트가 집행되지 않습니다', `holmes-kit init --target <dir> --agent codex 배선한 ${installCmds} 로 설치하십시오.`);
950
+ add('codex gate', 'WARN', 'The codex plugin wiring (.agents/plugins/marketplace.json, .holmes/codex-plugin/holmes-kit) is missing only the MCP tools work; the gate is not enforced', `Wire it with holmes-kit init --target <dir> --agent codex, then install with ${installCmds}.`);
934
951
  }
935
952
  }
936
953
  }
937
954
  else if (fs.existsSync(cdxJson)) {
938
- add('codex wiring', 'WARN', `codex 배선이 Codex 읽지 않는 구식 위치(${cdxJson}) 있습니다 — config.toml 옮겨야 합니다`, 'holmes-kit init --target <dir> --agent codex 다시 배선하면 config.toml 이전되고 구식 파일이 정리됩니다.');
955
+ add('codex wiring', 'WARN', `codex wiring sits in a legacy location Codex does not read (${cdxJson}) — it must move to config.toml`, 'Rewiring with holmes-kit init --target <dir> --agent codex migrates it to config.toml and cleans up the legacy file.');
939
956
  }
940
957
  try {
941
958
  const sp = wiredSettingsPath(target);
@@ -945,8 +962,8 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
945
962
  .map((g) => g.matcher ?? '');
946
963
  const drifted = wired.filter((m) => m !== init_1.MATCHERS.guardrail);
947
964
  add('hook matcher', drifted.length === 0 ? 'PASS' : 'WARN', drifted.length === 0
948
- ? `배선된 매처가 출하 기본값과 같습니다 (${init_1.MATCHERS.guardrail})`
949
- : `배선 ${drifted.join(' | ')} / 출하 기본값 ${init_1.MATCHERS.guardrail} — 매처에 걸리지 않는 도구는 훅에 도달하지 않습니다`, drifted.length === 0 ? undefined : 'holmes-kit init --target <dir> --force 갱신하고 Claude Code를 재시작하세요.');
965
+ ? `The wired matcher equals the shipped default (${init_1.MATCHERS.guardrail})`
966
+ : `wired ${drifted.join(' | ')} / shipped default ${init_1.MATCHERS.guardrail} — a tool the matcher does not catch never reaches the hook`, drifted.length === 0 ? undefined : 'Run holmes-kit init --target <dir> --force to refresh it, and restart Claude Code.');
950
967
  }
951
968
  catch { /* an unreadable settings file is `target wiring`'s business, not this check's */ }
952
969
  }
@@ -989,13 +1006,13 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
989
1006
  const t = (0, tier_1.resolveSemanticTier)();
990
1007
  const v = semanticTierVerdict(t);
991
1008
  add('semantic tier', v.level, v.detail, v.fix);
992
- // @implements A-SPEC-477 — the ".env 임시조치" detection: a key in a project-tree file is a
1009
+ // @implements A-SPEC-477 — the ".env stopgap" detection: a key in a project-tree file is a
993
1010
  // commit-accident surface, world-readable at 644, and readable by every in-session tool. The
994
1011
  // value is read to detect the pattern and never printed.
995
1012
  const envTemp = detectTreeKeyTemporary(target ?? process.cwd());
996
1013
  if (envTemp.found) {
997
- add('semantic key location', 'WARN', `프로젝트 .env 에서 semantic 패턴을 발견했습니다트리 키는 커밋 사고·세션 열람 표면입니다`
998
- + (envTemp.mode !== undefined && (envTemp.mode & 0o077) !== 0 ? ` (파일 권한 ${(envTemp.mode & 0o777).toString(8)} — 0600 초과)` : '') + '.', '`holmes-kit semantic-key set` 으로 트리 밖(키체인 또는 ~/.holmes/credentials.json 0600) 저장한 .env 에서 해당 줄을 제거하십시오.');
1014
+ add('semantic key location', 'WARN', `Found a semantic-key pattern in the project .env an in-tree key is a commit-accident and in-session-read surface`
1015
+ + (envTemp.mode !== undefined && (envTemp.mode & 0o077) !== 0 ? ` (file mode ${(envTemp.mode & 0o777).toString(8)} — wider than 0600)` : '') + '.', 'Store it out of the tree with `holmes-kit semantic-key set` (the keychain, or ~/.holmes/credentials.json at 0600), then remove that line from .env.');
999
1016
  }
1000
1017
  }
1001
1018
  add('environment', 'PASS', present);
@@ -1074,7 +1091,7 @@ function wiringSpawnCheck(command, args, timeoutMs = 30000, platform = process.p
1074
1091
  const spec = (0, spawn_spec_1.spawnSpecFor)(command, args, platform);
1075
1092
  // The re-init prescription is only honest when the wiring might actually be stale. A command the
1076
1093
  // OS cannot find (ENOENT) re-inits into the identical failure forever — the measured loop.
1077
- const enoentFix = `\`${command}\` 명령을 찾지 못했습니다(PATH 또는 플랫폼) — 배선 문자열이 옳다면 재-init 도움이 되지 않습니다. 같은 명령을 셸에서 직접 실행해 확인하십시오.`;
1094
+ const enoentFix = `The \`${command}\` command was not found (PATH or platform) — if the wiring string is correct, re-init does not help. Run the same command directly in a shell to check.`;
1078
1095
  const spawnFailFix = (e) => e?.code === 'ENOENT' ? enoentFix : 'Re-run `holmes-kit init` in the target to rewrite the wiring.';
1079
1096
  return new Promise((resolve) => {
1080
1097
  let done = false;
@@ -1120,8 +1137,8 @@ function wiringSpawnCheck(command, args, timeoutMs = 30000, platform = process.p
1120
1137
  const live = m.result.serverInfo.version ?? '?';
1121
1138
  const pinned = /@holmes-lab\/holmes-kit@([0-9][^\s"'`]*)/.exec(quoted)?.[1];
1122
1139
  if (pinned !== undefined && live !== '?' && live !== pinned) {
1123
- finish('WARN', `wiring pins ${pinned} but the live server answered v${live}: \`${quoted}\``, 'npx 캐시된 버전으로 해석될 있습니다 — `npx clear-npx-cache` 다시 실행하거나,'
1124
- + ' 버전이 레지스트리에 게시됐는지 확인하십시오. 하네스는 응답한 버전으로 집행합니다.');
1140
+ finish('WARN', `wiring pins ${pinned} but the live server answered v${live}: \`${quoted}\``, 'npx may resolve to a cached old versionrun `npx clear-npx-cache` and retry, or'
1141
+ + ' confirm that version is published to the registry. The harness enforces with whatever version answered.');
1125
1142
  return;
1126
1143
  }
1127
1144
  finish('PASS', `wiring reaches a live server (v${live}): \`${quoted}\``);
@@ -1227,7 +1244,7 @@ function mcpHandshakeCheck(packageRoot, timeoutMs = 15000) {
1227
1244
  }
1228
1245
  catch {
1229
1246
  continue;
1230
- } // 서버의 로그 줄은 프로토콜이 아니다
1247
+ } // a server log line is not the protocol
1231
1248
  if (msg.error && (msg.id === 1 || msg.id === 2)) {
1232
1249
  finish('FAIL', `MCP server answered ${msg.id === 1 ? 'initialize' : 'tools/list'} with an error: ${String(msg.error.message ?? msg.error.code)}`, 'The server started but refused the handshake — check the installed version against this package.');
1233
1250
  return;
@@ -1279,11 +1296,11 @@ function pushGateCheck(target) {
1279
1296
  const hookPath = path.join(effectiveHooksDir(target), 'pre-push');
1280
1297
  const installed = fs.existsSync(hookPath) && fs.readFileSync(hookPath, 'utf8').includes(PUSH_HOOK_SIGNATURE);
1281
1298
  return installed
1282
- ? { name: 'push gate', level: 'PASS', detail: 'pre-push 증빙 게이트가 설치되어 있습니다 (test_run 원장 대조)' }
1299
+ ? { name: 'push gate', level: 'PASS', detail: 'The pre-push evidence gate is installed (test_run ledger comparison)' }
1283
1300
  : {
1284
1301
  name: 'push gate', level: 'WARN',
1285
- detail: 'pre-push 증빙 게이트가 설치되어 있지 않습니다미검증 HEAD 다른 에이전트의 기준선이 될 수 있습니다',
1286
- fix: `${(0, npx_bin_1.npxBin)()} holmes-kit install-push-gate 실행하십시오 (기존 훅은 덮지 않습니다)`,
1302
+ detail: 'The pre-push evidence gate is not installedan unverified HEAD could become another agent\'s baseline',
1303
+ fix: `Run ${(0, npx_bin_1.npxBin)()} holmes-kit install-push-gate (it does not overwrite an existing hook)`,
1287
1304
  };
1288
1305
  }
1289
1306
  catch {
@@ -1310,7 +1327,7 @@ function formatChecks(checks) {
1310
1327
  * doctor flags the old location. But the second half was real and sat one layer further in:
1311
1328
  *
1312
1329
  * PASS mcp wiring spawn — node wiring resolves: …/holmes-mcp.js <- existence only
1313
- * PASS codex wiring — MCP 배선이 설치본으로 해석됩니다 <- path resolution only
1330
+ * PASS codex wiring — MCP wiring resolves to this install <- path resolution only
1314
1331
  *
1315
1332
  * The one check that drives a real `initialize -> tools/list` ran for `.mcp.json` ONLY, and only when
1316
1333
  * the command was not `node` — which is what init writes for all three harnesses. So on the common
@@ -1348,15 +1365,15 @@ async function wiringHandshakeChecks(target) {
1348
1365
  entry = null;
1349
1366
  }
1350
1367
  if (!entry) {
1351
- out.push({ name, level: 'WARN', detail: `${wiring.file} 에서 실행 가능한 ${init_1.SERVER_NAME} 배선을 읽지 못했습니다`,
1352
- fix: `holmes-kit init --target <dir> --agent ${wiring.label} 로 다시 배선하십시오.` });
1368
+ out.push({ name, level: 'WARN', detail: `Could not read a runnable ${init_1.SERVER_NAME} wiring from ${wiring.file}`,
1369
+ fix: `Rewire with holmes-kit init --target <dir> --agent ${wiring.label}.` });
1353
1370
  continue;
1354
1371
  }
1355
1372
  if (entry.command === 'node') {
1356
1373
  const bin = entry.args[0] ?? '';
1357
1374
  if (!fs.existsSync(bin)) {
1358
- out.push({ name, level: 'FAIL', detail: `배선이 없는 파일을 가리킵니다: ${bin}`,
1359
- fix: `holmes-kit init --target <dir> --agent ${wiring.label} --force 경로를 갱신하십시오.` });
1375
+ out.push({ name, level: 'FAIL', detail: `The wiring points at a missing file: ${bin}`,
1376
+ fix: `Run holmes-kit init --target <dir> --agent ${wiring.label} --force to refresh the path.` });
1360
1377
  continue;
1361
1378
  }
1362
1379
  }
@@ -1386,28 +1403,28 @@ function semanticTierVerdict(t) {
1386
1403
  // never is. Consent must be able to see what it consented to, and where it lives.
1387
1404
  const src = t.keySource === 'env' ? 'HOLMES_SEMANTIC_API_KEY'
1388
1405
  : t.keySource === 'env-compat' ? 'GEMINI_API_KEY'
1389
- : t.keySource === 'keychain' ? 'macOS 키체인'
1390
- : t.keySource === 'file' ? '~/.holmes/credentials.json' : '알 수 없음';
1406
+ : t.keySource === 'keychain' ? 'macOS keychain'
1407
+ : t.keySource === 'file' ? '~/.holmes/credentials.json' : 'unknown';
1391
1408
  return {
1392
1409
  level: 'PASS',
1393
- detail: `의미 검색 티어: cloud (${t.model}, 출처: ${src}) — 스펙 산문·파일 경로·심볼명이 외부(Google API) 전송됩니다. 설정이 전송에 대한 동의이며, 값은 보고서에 절대 출력되지 않습니다.`,
1410
+ detail: `Semantic search tier: cloud (${t.model}, key source: ${src}) — spec prose, file paths, and symbol names are sent to an external service (Google API). Setting the key is consent to that transfer, and the value is never printed into this report.`,
1394
1411
  };
1395
1412
  }
1396
1413
  if (t.tier === 'local') {
1397
1414
  return {
1398
1415
  level: 'PASS',
1399
- detail: `의미 검색 티어: local (${t.model}, ${(0, tier_1.POOLING_OF)(t.model ?? '')} 풀링) — 이그레스 없음. 어휘가 닿지 않는 요청의 52%를 회수합니다(실측).`,
1416
+ detail: `Semantic search tier: local (${t.model}, ${(0, tier_1.POOLING_OF)(t.model ?? '')} pooling) — no egress. Recovers 52% of requests lexical search misses (measured).`,
1400
1417
  };
1401
1418
  }
1402
1419
  return {
1403
1420
  level: 'PASS',
1404
- detail: '의미 검색 티어: none — 어휘·인용·그래프만 사용 중입니다. 어휘가 닿지 않는 요청(실측 16.4%) 회수율 0%입니다.',
1405
- fix: '로컬 티어(이그레스 없음): npm i @xenova/transformers — 어휘-제로 회수 0→52%. cloud 티어(opt-in, 외부 전송 동의): GEMINI_API_KEY 설정 — 0→92%.',
1421
+ detail: 'Semantic search tier: none — using lexical, citation, and graph signals only. Requests lexical search misses (measured 16.4%) have 0% recall.',
1422
+ fix: 'Local tier (no egress): npm i @xenova/transformers — lexical-zero recall 0→52%. Cloud tier (opt-in, consent to external transfer): set GEMINI_API_KEY — 0→92%.',
1406
1423
  };
1407
1424
  }
1408
1425
  /**
1409
1426
  * @implements A-SPEC-477
1410
- * Detects the "임시 조치": a semantic key pattern inside the PROJECT's .env. The value is read
1427
+ * Detects the "stopgap": a semantic key pattern inside the PROJECT's .env. The value is read
1411
1428
  * only to detect the pattern and is never returned or printed.
1412
1429
  */
1413
1430
  function detectTreeKeyTemporary(root) {