@holmes-lab/holmes-kit 0.2.1 → 0.3.1

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 (132) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/dist/.build-id +1 -1
  3. package/dist/holmes/assoc/assoc-arm.d.ts +94 -0
  4. package/dist/holmes/assoc/assoc-arm.js +187 -0
  5. package/dist/holmes/assoc/explore.d.ts +21 -0
  6. package/dist/holmes/assoc/explore.js +160 -0
  7. package/dist/holmes/assoc/impact-baseline.d.ts +13 -0
  8. package/dist/holmes/assoc/impact-baseline.js +48 -0
  9. package/dist/holmes/assoc/ppr.d.ts +64 -0
  10. package/dist/holmes/assoc/ppr.js +110 -0
  11. package/dist/holmes/cli/agents.d.ts +24 -11
  12. package/dist/holmes/cli/agents.js +93 -21
  13. package/dist/holmes/cli/doctor.d.ts +56 -0
  14. package/dist/holmes/cli/doctor.js +311 -5
  15. package/dist/holmes/cli/index.js +13 -1
  16. package/dist/holmes/cli/semantic-key.d.ts +19 -0
  17. package/dist/holmes/cli/semantic-key.js +93 -0
  18. package/dist/holmes/config/config.d.ts +9 -0
  19. package/dist/holmes/config/config.js +8 -1
  20. package/dist/holmes/cpg/language-capability.d.ts +65 -0
  21. package/dist/holmes/cpg/language-capability.js +145 -0
  22. package/dist/holmes/cpg/language-parser-walk.js +179 -34
  23. package/dist/holmes/cpg/language-parser.d.ts +1 -1
  24. package/dist/holmes/governance/ledger-rechain.d.ts +12 -0
  25. package/dist/holmes/governance/ledger-rechain.js +17 -2
  26. package/dist/holmes/governance/provenance-ledger.js +21 -0
  27. package/dist/holmes/guardrail/blind-spots.js +12 -1
  28. package/dist/holmes/guardrail/impact-gate.d.ts +77 -0
  29. package/dist/holmes/guardrail/impact-gate.js +263 -0
  30. package/dist/holmes/guardrail/write-target.d.ts +38 -1
  31. package/dist/holmes/guardrail/write-target.js +41 -4
  32. package/dist/holmes/hooks/adapters/antigravity.js +12 -1
  33. package/dist/holmes/hooks/corrupt-state-run.d.ts +33 -0
  34. package/dist/holmes/hooks/corrupt-state-run.js +16 -0
  35. package/dist/holmes/hooks/pre-tool-use.d.ts +26 -0
  36. package/dist/holmes/hooks/pre-tool-use.js +203 -11
  37. package/dist/holmes/hooks/stop.d.ts +65 -0
  38. package/dist/holmes/hooks/stop.js +200 -2
  39. package/dist/holmes/mcp/elicit-approval.d.ts +32 -2
  40. package/dist/holmes/mcp/elicit-approval.js +30 -2
  41. package/dist/holmes/mcp/handlers.d.ts +88 -0
  42. package/dist/holmes/mcp/handlers.js +636 -35
  43. package/dist/holmes/mcp/maintenance-analyze.d.ts +435 -0
  44. package/dist/holmes/mcp/maintenance-analyze.js +994 -0
  45. package/dist/holmes/mcp/maintenance-evidence.d.ts +140 -0
  46. package/dist/holmes/mcp/maintenance-evidence.js +253 -0
  47. package/dist/holmes/mcp/server.js +13 -10
  48. package/dist/holmes/mcp/tool-schemas.js +71 -0
  49. package/dist/holmes/project/root.js +3 -1
  50. package/dist/holmes/review/baseline-arm.d.ts +37 -0
  51. package/dist/holmes/review/baseline-arm.js +51 -0
  52. package/dist/holmes/review/captured-stdin-guard.d.ts +8 -0
  53. package/dist/holmes/review/captured-stdin-guard.js +48 -0
  54. package/dist/holmes/review/coherence-verify.d.ts +31 -0
  55. package/dist/holmes/review/coherence-verify.js +144 -0
  56. package/dist/holmes/review/commit-text.d.ts +50 -0
  57. package/dist/holmes/review/commit-text.js +76 -0
  58. package/dist/holmes/review/confidence-calibration.d.ts +39 -0
  59. package/dist/holmes/review/confidence-calibration.js +39 -0
  60. package/dist/holmes/review/content-baseline.d.ts +38 -0
  61. package/dist/holmes/review/content-baseline.js +103 -0
  62. package/dist/holmes/review/content-verify.d.ts +20 -0
  63. package/dist/holmes/review/content-verify.js +73 -0
  64. package/dist/holmes/review/dense-retrieval.d.ts +66 -0
  65. package/dist/holmes/review/dense-retrieval.js +97 -0
  66. package/dist/holmes/review/edge-quality.d.ts +44 -0
  67. package/dist/holmes/review/edge-quality.js +117 -0
  68. package/dist/holmes/review/evaluation-metrics.d.ts +138 -0
  69. package/dist/holmes/review/evaluation-metrics.js +175 -0
  70. package/dist/holmes/review/graph-verifier.d.ts +34 -0
  71. package/dist/holmes/review/graph-verifier.js +62 -0
  72. package/dist/holmes/review/hop-ablation.d.ts +100 -0
  73. package/dist/holmes/review/hop-ablation.js +89 -0
  74. package/dist/holmes/review/manual-baseline.d.ts +209 -0
  75. package/dist/holmes/review/manual-baseline.js +2846 -0
  76. package/dist/holmes/review/oracle-gap.d.ts +32 -0
  77. package/dist/holmes/review/oracle-gap.js +102 -0
  78. package/dist/holmes/review/point-in-time-replay.d.ts +41 -0
  79. package/dist/holmes/review/point-in-time-replay.js +161 -0
  80. package/dist/holmes/review/rank-diagnosis.d.ts +43 -0
  81. package/dist/holmes/review/rank-diagnosis.js +163 -0
  82. package/dist/holmes/review/replay-calibration.d.ts +62 -0
  83. package/dist/holmes/review/replay-calibration.js +83 -0
  84. package/dist/holmes/review/replay-corpus.d.ts +135 -0
  85. package/dist/holmes/review/replay-corpus.js +210 -0
  86. package/dist/holmes/review/run-replay.d.ts +260 -0
  87. package/dist/holmes/review/run-replay.js +729 -0
  88. package/dist/holmes/review/semantic-arm.d.ts +271 -0
  89. package/dist/holmes/review/semantic-arm.js +717 -0
  90. package/dist/holmes/review/semantic-retrieval.d.ts +55 -0
  91. package/dist/holmes/review/semantic-retrieval.js +156 -0
  92. package/dist/holmes/review/spec-layer-stats.d.ts +38 -0
  93. package/dist/holmes/review/spec-layer-stats.js +52 -0
  94. package/dist/holmes/review/temporal-prior.d.ts +33 -0
  95. package/dist/holmes/review/temporal-prior.js +53 -0
  96. package/dist/holmes/review/test-runner.d.ts +15 -0
  97. package/dist/holmes/review/test-runner.js +41 -4
  98. package/dist/holmes/review/test-selection-breadth.d.ts +75 -0
  99. package/dist/holmes/review/test-selection-breadth.js +57 -0
  100. package/dist/holmes/review/traceability-benchmark.d.ts +81 -0
  101. package/dist/holmes/review/traceability-benchmark.js +135 -0
  102. package/dist/holmes/review/union-verify.d.ts +12 -0
  103. package/dist/holmes/review/union-verify.js +70 -0
  104. package/dist/holmes/rtm/graph-store.d.ts +51 -0
  105. package/dist/holmes/rtm/graph-store.js +122 -0
  106. package/dist/holmes/rtm/incremental.d.ts +25 -1
  107. package/dist/holmes/rtm/incremental.js +18 -1
  108. package/dist/holmes/rtm/localize.d.ts +28 -0
  109. package/dist/holmes/rtm/localize.js +272 -14
  110. package/dist/holmes/rtm/rtm-builder.d.ts +118 -3
  111. package/dist/holmes/rtm/rtm-builder.js +265 -28
  112. package/dist/holmes/rtm/rtm-graph.d.ts +117 -2
  113. package/dist/holmes/rtm/rtm-graph.js +194 -31
  114. package/dist/holmes/rtm/taint-benchmark.d.ts +97 -0
  115. package/dist/holmes/rtm/taint-benchmark.js +141 -0
  116. package/dist/holmes/rtm/test-scope.js +24 -1
  117. package/dist/holmes/semantic/credentials.d.ts +15 -0
  118. package/dist/holmes/semantic/credentials.js +134 -0
  119. package/dist/holmes/semantic/embedder.d.ts +44 -0
  120. package/dist/holmes/semantic/embedder.js +185 -0
  121. package/dist/holmes/semantic/hit-rerank.d.ts +4 -0
  122. package/dist/holmes/semantic/hit-rerank.js +38 -0
  123. package/dist/holmes/semantic/tier.d.ts +37 -0
  124. package/dist/holmes/semantic/tier.js +54 -0
  125. package/dist/holmes/semantic/vector-cache.d.ts +11 -0
  126. package/dist/holmes/semantic/vector-cache.js +91 -0
  127. package/dist/holmes/spec/acceptance-quality.d.ts +81 -0
  128. package/dist/holmes/spec/acceptance-quality.js +169 -0
  129. package/dist/holmes/spec/validator.js +33 -1
  130. package/dist/holmes/spec/yaml-scalar.d.ts +1 -0
  131. package/dist/holmes/spec/yaml-scalar.js +43 -0
  132. package/package.json +1 -1
@@ -35,13 +35,19 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.STRIPPED_FOR_PROBE = void 0;
37
37
  exports.globalInstallDir = globalInstallDir;
38
+ exports.npmCliEntry = npmCliEntry;
38
39
  exports.prefixVerdict = prefixVerdict;
39
40
  exports.probeEnv = probeEnv;
40
41
  exports.runDoctor = runDoctor;
41
42
  exports.formatChecks = formatChecks;
43
+ exports.wiringHandshakeChecks = wiringHandshakeChecks;
44
+ exports.semanticTierVerdict = semanticTierVerdict;
45
+ exports.detectTreeKeyTemporary = detectTreeKeyTemporary;
46
+ // @implements A-SPEC-442
42
47
  // @implements A-SPEC-207
43
48
  // @implements A-SPEC-100.2
44
49
  const fs = __importStar(require("node:fs"));
50
+ const tier_1 = require("../semantic/tier");
45
51
  const path = __importStar(require("node:path"));
46
52
  const role_policy_1 = require("../governance/role-policy");
47
53
  const blind_spots_1 = require("../guardrail/blind-spots");
@@ -52,7 +58,25 @@ const playbook_skills_1 = require("./playbook-skills");
52
58
  const init_1 = require("./init");
53
59
  const mcp_version_1 = require("./mcp-version");
54
60
  const codex_toml_1 = require("./codex-toml");
61
+ const agents_1 = require("./agents");
55
62
  const mcp_launcher_1 = require("./mcp-launcher");
63
+ /**
64
+ * Checks if a script path belongs to the packageRoot, supporting symlinked global installs.
65
+ */
66
+ function resolvesToPackage(filePath, packageRoot) {
67
+ if (!filePath || !fs.existsSync(filePath))
68
+ return false;
69
+ try {
70
+ if (path.resolve(filePath).startsWith(path.resolve(packageRoot) + path.sep))
71
+ return true;
72
+ const realFile = fs.realpathSync(filePath);
73
+ const realPkg = fs.realpathSync(packageRoot);
74
+ return realFile.startsWith(realPkg + path.sep);
75
+ }
76
+ catch {
77
+ return false;
78
+ }
79
+ }
56
80
  const GRAMMARS = [
57
81
  'tree-sitter-typescript', 'tree-sitter-python', 'tree-sitter-c-sharp', 'tree-sitter-java',
58
82
  'tree-sitter-go', 'tree-sitter-rust', 'tree-sitter-cpp',
@@ -83,6 +107,33 @@ function globalInstallDir(prefix, platform) {
83
107
  ? `${prefix.replace(/[\\/]+$/, '')}\\node_modules`
84
108
  : `${prefix.replace(/\/+$/, '')}/lib/node_modules`;
85
109
  }
110
+ /**
111
+ * @implements A-SPEC-466 — W-5. npm's own JS entry, so the prefix probe can run it with THIS node
112
+ * binary instead of spawning the name 'npm' — which on Windows is a `.cmd` shim that execFileSync
113
+ * cannot start (ENOENT; naming the .cmd is EINVAL under Node's batch-file mitigation), leaving the
114
+ * global-prefix check a permanent WARN there.
115
+ *
116
+ * Candidates, in order: the `npm_execpath` this process was launched under (exact when running via
117
+ * npm/npx — guarded to npm-cli.js so an npx/yarn launcher is not mistaken for npm), then the two
118
+ * standard layouts next to the node binary — `<dir>\node_modules\npm` (Windows official,
119
+ * nvm-windows) and `<dir>/../lib/node_modules/npm` (POSIX standard, nvm). Homebrew's Cellar
120
+ * matches neither (measured) — null there, and the caller keeps the plain-'npm' spawn, which is
121
+ * today's behavior and has no .cmd problem on POSIX. env/execPath are injectable for tests only.
122
+ */
123
+ function npmCliEntry(env = process.env, execPath = process.execPath) {
124
+ const fromEnv = env.npm_execpath;
125
+ if (fromEnv && path.basename(fromEnv) === 'npm-cli.js' && fs.existsSync(fromEnv))
126
+ return fromEnv;
127
+ const bin = path.dirname(execPath);
128
+ for (const cand of [
129
+ path.join(bin, 'node_modules', 'npm', 'bin', 'npm-cli.js'),
130
+ path.join(bin, '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
131
+ ]) {
132
+ if (fs.existsSync(cand))
133
+ return cand;
134
+ }
135
+ return null;
136
+ }
86
137
  /**
87
138
  * Judge the global prefix. Pure — the probing lives with the caller.
88
139
  *
@@ -252,6 +303,12 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
252
303
  // before it answers (a false FAIL this check produced on its first run).
253
304
  if (built) {
254
305
  checks.push(await mcpHandshakeCheck(packageRoot));
306
+ // @implements A-SPEC-323
307
+ // Every wired harness, actually started. The check above proves THIS INSTALL can serve; these
308
+ // prove the wiring each harness will really run reaches a server. Measured: 367ms for three.
309
+ // A doctor run with no target has no harness wiring to prove — skipped, not failed.
310
+ if (target)
311
+ checks.push(...await wiringHandshakeChecks(target));
255
312
  }
256
313
  // 7. Target wiring — do the configured commands point at THIS installed package and exist?
257
314
  if (target) {
@@ -276,7 +333,7 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
276
333
  else {
277
334
  const stale = cmds.filter((c) => {
278
335
  const p = (0, settings_merge_1.hookScriptPath)(c);
279
- return !p || !fs.existsSync(p) || !path.resolve(p).startsWith(path.resolve(packageRoot) + path.sep);
336
+ return !resolvesToPackage(p, packageRoot);
280
337
  });
281
338
  add('target wiring', stale.length === 0 ? 'PASS' : 'FAIL', stale.length === 0 ? `${cmds.length} holmes hook(s) resolve to this install` : `stale/foreign command(s): ${stale.join(' | ')}`, stale.length === 0 ? undefined : 'Re-run `holmes-kit init --force` in the target to refresh the absolute paths.');
282
339
  }
@@ -366,6 +423,53 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
366
423
  // 8. Environment — presence only, never values (these hold out-of-band secrets).
367
424
  const present = ['HOLMES_SPECS', 'HOLMES_APPROVAL', 'HOLMES_LEDGER_KEY', 'HOLMES_MCP_PROFILE']
368
425
  .map((k) => `${k}=${process.env[k] ? 'set' : 'unset'}`).join(' ');
426
+ // @implements A-SPEC-454
427
+ // The CONSEQUENCE, not the fact. `environment` already prints HOLMES_LEDGER_KEY=set|unset among
428
+ // three siblings, and that told nobody what it decides. Measured 2026-08-30: with no key the
429
+ // provenance chain is a plain hash chain, so appending a chain-valid `spec-approved` line with
430
+ // this package's own API silences ART-5 while chain.verify() still returns ok. A-SPEC-453's
431
+ // ledger comparison is then a COST — one more forged artifact — rather than a barrier.
432
+ //
433
+ // WARN, not FAIL: running unsigned is a deployment choice, and marking choices as failures
434
+ // teaches operators to skip doctor (the judgement A-SPEC-153 already made for role policy). WARN,
435
+ // not PASS: `codex gate` set the precedent — a thing that is wired but not yet enforcing is not
436
+ // a pass. Presence only; the value is an out-of-band secret and is never read into the report.
437
+ if (process.env.HOLMES_LEDGER_KEY) {
438
+ add('ledger signing', 'PASS', '원장이 HOLMES_LEDGER_KEY 로 서명됩니다 — 키 없는 위조자는 체인 정합한 spec-approved 기록을 만들 수 없습니다');
439
+ }
440
+ else {
441
+ add('ledger signing', 'WARN', '원장이 서명되지 않습니다(HOLMES_LEDGER_KEY 없음) — 체인이 평문 해시라 파일을 쓸 수 있는 자가 승인 기록도 만들 수 있고, ART-5 의 원장 대조는 장벽이 아니라 비용이 됩니다', 'HOLMES_LEDGER_KEY 를 대역외(에이전트를 기동하는 사람의 환경)에서 설정한 뒤 서버를 다시 시작하십시오. 값은 이 보고서에 절대 출력되지 않습니다.');
442
+ }
443
+ // @implements A-SPEC-457
444
+ // Whether the ART-2 truncation backstop (A-SPEC-455) is even armed. That backstop compares the
445
+ // committed ledger to the working copy and enumerates targets with `git ls-files .ax/ledger`; an
446
+ // UNTRACKED ledger yields an empty list, so truncation goes uncaught (measured 2026-08-30). The
447
+ // ledger is untracked whenever the .gitignore re-include is missing or the first ledger commit
448
+ // has not happened. Same silent-precondition gap as A-SPEC-454 (signing) — surface it, do not fix
449
+ // the user's .gitignore. Only when a ledger actually exists in a git repo; nothing to protect
450
+ // otherwise.
451
+ if (target !== undefined) {
452
+ try {
453
+ const ledgerDir = path.join(target, '.ax', 'ledger');
454
+ const onDisk = fs.existsSync(ledgerDir)
455
+ ? fs.readdirSync(ledgerDir).filter((f) => /^provenance[\w.-]*\.jsonl$/.test(f))
456
+ : [];
457
+ if (onDisk.length > 0) {
458
+ // Throws outside a git repo -> caught below -> no check added (no tracking concept).
459
+ const tracked = new Set((0, node_child_process_1.execFileSync)('git', ['ls-files', '.ax/ledger'], {
460
+ cwd: target, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
461
+ }).split('\n').map((l) => l.trim().split('/').pop()).filter(Boolean));
462
+ const untracked = onDisk.filter((f) => !tracked.has(f));
463
+ if (untracked.length > 0) {
464
+ 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` 로 추적에 추가한 뒤 커밋하십시오.');
465
+ }
466
+ else {
467
+ add('ledger tracked', 'PASS', '원장이 git 에 추적됩니다 — ART-2 절단 백스톱이 커밋된 이력을 기준으로 롤백을 잡습니다');
468
+ }
469
+ }
470
+ }
471
+ catch { /* not a git repo, or git unavailable: no tracking concept, no check */ }
472
+ }
369
473
  // @implements A-SPEC-153
370
474
  // Reports, never judges. Not having roles on is a choice, not a defect; a FAIL here would teach
371
475
  // operators that doctor's failures are noise, which costs more than this check is worth.
@@ -666,8 +770,13 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
666
770
  add('mcp wiring spawn', 'WARN', `.mcp.json carries no runnable ${init_1.SERVER_NAME} entry`, 'Run `holmes-kit init` in the target to write the wiring.');
667
771
  }
668
772
  else if (server.command === 'node') {
773
+ // @implements A-SPEC-323
774
+ // Existence only, and it no longer stands in for the handshake. This branch used to be
775
+ // the whole check for the common wiring, so "the file is there" was reported as if the
776
+ // server had been proven to start. `wiringHandshakeChecks` below now proves that, for
777
+ // every wired harness; this stays as the cheap, fast FAIL for a path that is simply gone.
669
778
  const bin = server.args[0] ?? '';
670
- add('mcp wiring spawn', fs.existsSync(bin) ? 'PASS' : 'FAIL', fs.existsSync(bin) ? `node wiring resolves: ${bin}` : `node wiring points at a missing file: ${bin}`, fs.existsSync(bin) ? undefined : 'Run `holmes-kit init` in the target to rewire the absolute path.');
779
+ add('mcp wiring spawn', fs.existsSync(bin) ? 'PASS' : 'FAIL', fs.existsSync(bin) ? `node wiring resolves: ${bin} (handshake proven separately)` : `node wiring points at a missing file: ${bin}`, fs.existsSync(bin) ? undefined : 'Run `holmes-kit init` in the target to rewire the absolute path.');
671
780
  }
672
781
  else {
673
782
  checks.push(await wiringSpawnCheck(server.command, server.args));
@@ -690,7 +799,7 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
690
799
  const stopCmd = entry?.Stop?.[0]?.command ?? '';
691
800
  const stale = [preCmd, stopCmd].filter((c) => {
692
801
  const p2 = (0, settings_merge_1.hookScriptPath)(c);
693
- return !p2 || !fs.existsSync(p2) || !path.resolve(p2).startsWith(path.resolve(packageRoot) + path.sep);
802
+ return !resolvesToPackage(p2, packageRoot);
694
803
  });
695
804
  if (entry === undefined) {
696
805
  add('antigravity wiring', 'WARN', `${agyHooks} 에 holmes-kit 항목이 없습니다`, 'holmes-kit init --target <dir> --agent antigravity');
@@ -755,6 +864,45 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
755
864
  const pin = (0, mcp_version_1.mcpLaunchVersion)({ command: entry.command, args: entry.args });
756
865
  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 로 다시 배선하십시오.');
757
866
  }
867
+ // @implements A-SPEC-442 (was A-SPEC-423)
868
+ // Codex CAN hard-enforce, but only from an INSTALLED plugin: it loads plugins from
869
+ // marketplace snapshots, never from a manifest sitting in the project (measured, four
870
+ // probes on codex-cli 0.151.0). doctor is offline and cannot see whether the user ran the
871
+ // two install commands, so PASS is not available here — the presence of files is not the
872
+ // presence of a gate. That inference is what A-SPEC-441 got wrong, and reporting PASS for
873
+ // it made the tool agree with the mistake.
874
+ const cdxPluginJson = path.join(target, agents_1.CODEX_PLUGIN_DIR, '.codex-plugin', 'plugin.json');
875
+ const cdxHooksJson = path.join(target, agents_1.CODEX_PLUGIN_DIR, 'hooks.json');
876
+ const cdxMarketplace = path.join(target, '.agents', 'plugins', 'marketplace.json');
877
+ const installCmds = `codex plugin marketplace add "${target}" 후 codex plugin add holmes-kit@${agents_1.CODEX_MARKETPLACE}`;
878
+ if (fs.existsSync(cdxPluginJson) && fs.existsSync(cdxHooksJson) && fs.existsSync(cdxMarketplace)) {
879
+ try {
880
+ const h = JSON.parse(fs.readFileSync(cdxHooksJson, 'utf8'));
881
+ const manifest = JSON.parse(fs.readFileSync(cdxPluginJson, 'utf8'));
882
+ const preCmd = h.hooks?.PreToolUse?.[0]?.hooks?.[0]?.command ?? '';
883
+ const stopCmd = h.hooks?.Stop?.[0]?.command ?? '';
884
+ const stale = [preCmd, stopCmd].filter((c) => {
885
+ const p2 = (0, settings_merge_1.hookScriptPath)(c);
886
+ return !resolvesToPackage(p2, packageRoot);
887
+ });
888
+ if (stale.length > 0) {
889
+ add('codex gate', 'FAIL', `이 설치본을 가리키지 않는 훅 명령: ${stale.join(' | ')}`, 'holmes-kit init --target <dir> --agent codex --force 로 절대 경로를 갱신하십시오.');
890
+ }
891
+ else if (manifest.hooks !== './hooks.json') {
892
+ // The one path measured to load. `./hooks/hooks.json` parses fine and never fires.
893
+ add('codex gate', 'FAIL', `플러그인 매니페스트의 hooks 가 './hooks.json' 이 아닙니다: ${String(manifest.hooks)} — Codex 가 적재하지 않습니다`, 'holmes-kit init --target <dir> --agent codex --force 로 다시 배선하십시오.');
894
+ }
895
+ else {
896
+ add('codex gate', 'WARN', '플러그인 배선은 옳습니다 — 그러나 설치되기 전까지 게이트는 집행되지 않습니다(Codex 는 설치된 마켓플레이스에서만 플러그인을 적재하며, doctor 는 설치 여부를 오프라인에서 확인할 수 없습니다)', `${installCmds} 를 실행한 뒤 새 세션에서 사용하십시오.`);
897
+ }
898
+ }
899
+ catch {
900
+ add('codex gate', 'FAIL', `${cdxHooksJson} 를 읽을 수 없습니다(JSON 아님)`, '파일을 고치거나 지우고 다시 배선하십시오.');
901
+ }
902
+ }
903
+ else {
904
+ add('codex gate', 'WARN', 'codex 플러그인 배선(.agents/plugins/marketplace.json, .holmes/codex-plugin/holmes-kit)이 없습니다 — MCP 도구만 동작하며 게이트가 집행되지 않습니다', `holmes-kit init --target <dir> --agent codex 로 배선한 뒤 ${installCmds} 로 설치하십시오.`);
905
+ }
758
906
  }
759
907
  }
760
908
  else if (fs.existsSync(cdxJson)) {
@@ -779,7 +927,14 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
779
927
  {
780
928
  let verdict;
781
929
  try {
782
- const prefix = (0, node_child_process_1.execFileSync)('npm', ['config', 'get', 'prefix'], { encoding: 'utf8' }).trim();
930
+ // @implements A-SPEC-466 W-5. Windows npm is a .cmd shim, so execFileSync('npm') is ENOENT
931
+ // and this check was a permanent WARN on every Windows machine. npm's own JS entry run with
932
+ // THIS node binary removes the shim; when no entry is found (e.g. Homebrew's Cellar layout —
933
+ // measured), the plain name keeps today's POSIX behavior, where there is no .cmd problem.
934
+ const npmCli = npmCliEntry();
935
+ const prefix = (npmCli !== null
936
+ ? (0, node_child_process_1.execFileSync)(process.execPath, [npmCli, 'config', 'get', 'prefix'], { encoding: 'utf8' })
937
+ : (0, node_child_process_1.execFileSync)('npm', ['config', 'get', 'prefix'], { encoding: 'utf8' })).trim();
783
938
  const dir = globalInstallDir(prefix, process.platform);
784
939
  let probe = dir;
785
940
  while (!fs.existsSync(probe)) {
@@ -801,6 +956,19 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
801
956
  }
802
957
  add('global prefix', verdict.level, verdict.detail, verdict.fix);
803
958
  }
959
+ {
960
+ const t = (0, tier_1.resolveSemanticTier)();
961
+ const v = semanticTierVerdict(t);
962
+ add('semantic tier', v.level, v.detail, v.fix);
963
+ // @implements A-SPEC-477 — the ".env 임시조치" detection: a key in a project-tree file is a
964
+ // commit-accident surface, world-readable at 644, and readable by every in-session tool. The
965
+ // value is read to detect the pattern and never printed.
966
+ const envTemp = detectTreeKeyTemporary(target ?? process.cwd());
967
+ if (envTemp.found) {
968
+ add('semantic key location', 'WARN', `프로젝트 .env 에서 semantic 키 패턴을 발견했습니다 — 트리 안 키는 커밋 사고·세션 열람 표면입니다`
969
+ + (envTemp.mode !== undefined && (envTemp.mode & 0o077) !== 0 ? ` (파일 권한 ${(envTemp.mode & 0o777).toString(8)} — 0600 초과)` : '') + '.', '`holmes-kit semantic-key set` 으로 트리 밖(키체인 또는 ~/.holmes/credentials.json 0600)에 저장한 뒤 .env 에서 해당 줄을 제거하십시오.');
970
+ }
971
+ }
804
972
  add('environment', 'PASS', present);
805
973
  if (extraChecks) {
806
974
  checks.push(...extraChecks);
@@ -892,7 +1060,23 @@ function wiringSpawnCheck(command, args, timeoutMs = 30000) {
892
1060
  try {
893
1061
  const m = JSON.parse(line);
894
1062
  if (m?.result?.serverInfo) {
895
- finish('PASS', `wiring reaches a live server (v${m.result.serverInfo.version ?? '?'}): \`${quoted}\``);
1063
+ // @implements A-SPEC-422
1064
+ // The version was printed and never checked. Measured on the installed build: all three
1065
+ // harness handshakes said `reaches a live server (v0.1.18)` for a wiring pinned at
1066
+ // `@0.2.1` and all three PASSed, so a harness governed by an older server — missing
1067
+ // whatever gates and disclosures that version lacks — read as fully verified.
1068
+ //
1069
+ // Only compared when the wiring NAMES a version. An absolute-path or globally-installed
1070
+ // wiring pins nothing, and warning without a baseline would be the false alarm this
1071
+ // repository has already had to undo twice.
1072
+ const live = m.result.serverInfo.version ?? '?';
1073
+ const pinned = /@holmes-lab\/holmes-kit@([0-9][^\s"'`]*)/.exec(quoted)?.[1];
1074
+ if (pinned !== undefined && live !== '?' && live !== pinned) {
1075
+ finish('WARN', `wiring pins ${pinned} but the live server answered v${live}: \`${quoted}\``, 'npx 는 캐시된 옛 버전으로 해석될 수 있습니다 — `npx clear-npx-cache` 후 다시 실행하거나,'
1076
+ + ' 그 버전이 레지스트리에 게시됐는지 확인하십시오. 하네스는 응답한 버전으로 집행합니다.');
1077
+ return;
1078
+ }
1079
+ finish('PASS', `wiring reaches a live server (v${live}): \`${quoted}\``);
896
1080
  return;
897
1081
  }
898
1082
  }
@@ -1039,3 +1223,125 @@ function formatChecks(checks) {
1039
1223
  lines.push(`\n${checks.length - fails - warns} pass, ${warns} warn, ${fails} fail`);
1040
1224
  return lines.join('\n');
1041
1225
  }
1226
+ // @implements A-SPEC-323
1227
+ /**
1228
+ * Prove that EVERY wired harness can actually start a server, not merely that its file parses.
1229
+ *
1230
+ * A user reported that `holmes-kit init --agent codex` wrote a file Codex never reads and that doctor
1231
+ * passed on it anyway. Measured against the current tree, the first half was already fixed by
1232
+ * A-SPEC-266 — init writes `.codex/config.toml` with `[mcp_servers.*]`, migrates the legacy JSON, and
1233
+ * doctor flags the old location. But the second half was real and sat one layer further in:
1234
+ *
1235
+ * PASS mcp wiring spawn — node wiring resolves: …/holmes-mcp.js <- existence only
1236
+ * PASS codex wiring — MCP 배선이 이 설치본으로 해석됩니다 <- path resolution only
1237
+ *
1238
+ * The one check that drives a real `initialize -> tools/list` ran for `.mcp.json` ONLY, and only when
1239
+ * the command was not `node` — which is what init writes for all three harnesses. So on the common
1240
+ * path nothing ever started a server through the wiring a harness would actually use.
1241
+ *
1242
+ * The `node` fast-path is gone. Existence survives as a fast FAIL, because spending a 30s deadline on
1243
+ * a path that does not exist tells nobody anything new.
1244
+ */
1245
+ async function wiringHandshakeChecks(target) {
1246
+ const wirings = [
1247
+ {
1248
+ label: 'claude', file: path.join(target, '.mcp.json'),
1249
+ read: (raw) => entryOf(JSON.parse(raw)?.mcpServers?.[init_1.SERVER_NAME]),
1250
+ },
1251
+ {
1252
+ label: 'codex', file: path.join(target, '.codex', 'config.toml'),
1253
+ read: (raw) => entryOf((0, codex_toml_1.readCodexHolmesEntry)(raw)),
1254
+ },
1255
+ {
1256
+ label: 'antigravity', file: path.join(target, '.agents', 'mcp_config.json'),
1257
+ read: (raw) => entryOf(JSON.parse(raw)?.mcpServers?.[init_1.SERVER_NAME]),
1258
+ },
1259
+ ];
1260
+ const out = [];
1261
+ for (const wiring of wirings) {
1262
+ // An absent wiring is not a dead one — the harness simply is not wired here.
1263
+ if (!fs.existsSync(wiring.file))
1264
+ continue;
1265
+ const name = `${wiring.label} wiring handshake`;
1266
+ let entry = null;
1267
+ try {
1268
+ entry = wiring.read(fs.readFileSync(wiring.file, 'utf8'));
1269
+ }
1270
+ catch {
1271
+ entry = null;
1272
+ }
1273
+ if (!entry) {
1274
+ out.push({ name, level: 'WARN', detail: `${wiring.file} 에서 실행 가능한 ${init_1.SERVER_NAME} 배선을 읽지 못했습니다`,
1275
+ fix: `holmes-kit init --target <dir> --agent ${wiring.label} 로 다시 배선하십시오.` });
1276
+ continue;
1277
+ }
1278
+ if (entry.command === 'node') {
1279
+ const bin = entry.args[0] ?? '';
1280
+ if (!fs.existsSync(bin)) {
1281
+ out.push({ name, level: 'FAIL', detail: `배선이 없는 파일을 가리킵니다: ${bin}`,
1282
+ fix: `holmes-kit init --target <dir> --agent ${wiring.label} --force 로 경로를 갱신하십시오.` });
1283
+ continue;
1284
+ }
1285
+ }
1286
+ const check = await wiringSpawnCheck(entry.command, entry.args);
1287
+ out.push({ ...check, name });
1288
+ }
1289
+ return out;
1290
+ }
1291
+ /** A wiring entry is usable only if BOTH halves are present; a half-written one is not runnable. */
1292
+ function entryOf(value) {
1293
+ const e = value;
1294
+ if (!e || typeof e.command !== 'string' || !Array.isArray(e.args))
1295
+ return null;
1296
+ return { command: e.command, args: e.args };
1297
+ }
1298
+ /**
1299
+ * @implements A-SPEC-476
1300
+ * The semantic tier's doctor verdict, pure so the three states are unit-testable offline.
1301
+ * The cloud notice NAMES the egress — what leaves the machine and where — because the key being
1302
+ * set is the consent, and consent must be able to see what it consented to. The 'none' state
1303
+ * quotes the measured stakes (lexical-zero recovery 0% -> 52% local / 92% cloud, S-491) so the
1304
+ * user decides the trade with numbers rather than adjectives.
1305
+ */
1306
+ function semanticTierVerdict(t) {
1307
+ if (t.tier === 'cloud') {
1308
+ // @implements A-SPEC-477 — the SOURCE of the consent is shown (env/keychain/file); the value
1309
+ // never is. Consent must be able to see what it consented to, and where it lives.
1310
+ const src = t.keySource === 'env' ? 'HOLMES_SEMANTIC_API_KEY'
1311
+ : t.keySource === 'env-compat' ? 'GEMINI_API_KEY'
1312
+ : t.keySource === 'keychain' ? 'macOS 키체인'
1313
+ : t.keySource === 'file' ? '~/.holmes/credentials.json' : '알 수 없음';
1314
+ return {
1315
+ level: 'PASS',
1316
+ detail: `의미 검색 티어: cloud (${t.model}, 키 출처: ${src}) — 스펙 산문·파일 경로·심볼명이 외부(Google API)로 전송됩니다. 키 설정이 이 전송에 대한 동의이며, 값은 이 보고서에 절대 출력되지 않습니다.`,
1317
+ };
1318
+ }
1319
+ if (t.tier === 'local') {
1320
+ return {
1321
+ level: 'PASS',
1322
+ detail: `의미 검색 티어: local (${t.model}, ${(0, tier_1.POOLING_OF)(t.model ?? '')} 풀링) — 이그레스 없음. 어휘가 닿지 않는 요청의 52%를 회수합니다(실측).`,
1323
+ };
1324
+ }
1325
+ return {
1326
+ level: 'PASS',
1327
+ detail: '의미 검색 티어: none — 어휘·인용·그래프만 사용 중입니다. 어휘가 닿지 않는 요청(실측 16.4%)은 회수율 0%입니다.',
1328
+ fix: '로컬 티어(이그레스 없음): npm i @xenova/transformers — 어휘-제로 회수 0→52%. cloud 티어(opt-in, 외부 전송 동의): GEMINI_API_KEY 설정 — 0→92%.',
1329
+ };
1330
+ }
1331
+ /**
1332
+ * @implements A-SPEC-477
1333
+ * Detects the "임시 조치": a semantic key pattern inside the PROJECT's .env. The value is read
1334
+ * only to detect the pattern and is never returned or printed.
1335
+ */
1336
+ function detectTreeKeyTemporary(root) {
1337
+ try {
1338
+ const p = path.join(root, '.env');
1339
+ const text = fs.readFileSync(p, 'utf8');
1340
+ if (!/^(?:GEMINI_API_KEY|GOOGLE_API_KEY|HOLMES_SEMANTIC_API_KEY)\s*=/m.test(text))
1341
+ return { found: false };
1342
+ return { found: true, mode: fs.statSync(p).mode & 0o777 };
1343
+ }
1344
+ catch {
1345
+ return { found: false };
1346
+ }
1347
+ }
@@ -78,6 +78,8 @@ const KNOWN_FLAGS = {
78
78
  serve: ['help', 'target', 'port'],
79
79
  approve: ['help', 'target', 'list', 'grant', 'deny', 'ask', 'reason', 'question', 'ttl', 'rationale', 'watch', 'poll-ms'],
80
80
  ledger: ['help', 'target', 'ref', 'dry-run'],
81
+ // @implements A-SPEC-477 — the human's opt-in act for the cloud semantic tier.
82
+ 'semantic-key': ['help'],
81
83
  };
82
84
  // @implements A-SPEC-171 — subcommands that render their OWN usage on `--help`. A-SPEC-171 governs
83
85
  // "help before any side effect"; a command whose usage lives past this handler (approve, whose
@@ -200,6 +202,10 @@ const USAGE = `holmes-kit — deterministic ASE governance for a project
200
202
  <ref> list index (1,2,…), full id, or a unique id prefix
201
203
  --target <dir> project root (default: cwd)
202
204
 
205
+ holmes-kit semantic-key set|unset|status cloud semantic 티어 opt-in 키 관리
206
+ set 은 stdin 으로만 입력받습니다(값은 argv·출력 어디에도 남지 않음).
207
+ 키 설정은 이그레스(스펙 산문·경로·심볼명 외부 전송) 동의입니다.
208
+
203
209
  holmes-kit doctor [flags] diagnose the install
204
210
  --target <dir> also verify that target's wiring points at this install
205
211
  --json machine-readable output
@@ -246,7 +252,8 @@ async function main(argv) {
246
252
  }
247
253
  // Positionals these commands do not take are refused, not dropped: `skills refresh <dir>`
248
254
  // silently wrote into CWD while the named target stayed stale (round-3 HIGH).
249
- const expected = cmd === 'skills' ? ['refresh'] : cmd === 'ledger' ? ['rechain'] : [];
255
+ const expected = cmd === 'skills' ? ['refresh'] : cmd === 'ledger' ? ['rechain']
256
+ : cmd === 'semantic-key' ? ['set', 'unset', 'status'] : [];
250
257
  const unexpected = positionals.filter((p) => !expected.includes(p));
251
258
  if (unexpected.length > 0) {
252
259
  const hint = fs.existsSync(unexpected[0]) ? ` — did you mean --target ${unexpected[0]}?` : '';
@@ -276,6 +283,11 @@ async function main(argv) {
276
283
  process.stdout.write(`${v}\n`);
277
284
  return 0;
278
285
  }
286
+ if (cmd === 'semantic-key') {
287
+ // @implements A-SPEC-477 — set|unset|status; the key rides stdin, never argv.
288
+ const { runSemanticKey } = await Promise.resolve().then(() => __importStar(require('./semantic-key')));
289
+ return runSemanticKey(positionals[0]);
290
+ }
279
291
  if (cmd === 'doctor') {
280
292
  // A nonexistent target read as "healthy but unwired" and both advised remedies then refused it
281
293
  // (round-6) — the typo is the diagnosis, so say it.
@@ -0,0 +1,19 @@
1
+ /**
2
+ * `holmes-kit semantic-key set|unset|status` — the human's consent act for the cloud semantic
3
+ * tier, made convenient AND safe:
4
+ *
5
+ * - `set` reads the key from STDIN (hidden prompt on a TTY, one piped line otherwise). The key
6
+ * never appears in argv — ps and shell history are exactly the surfaces a third-party PC leaks
7
+ * from. Storage prefers the OS keychain (darwin, fed over stdin too) and falls back to
8
+ * ~/.holmes/credentials.json at 0600 under 0700.
9
+ * - `status` prints the SOURCE only. The value never appears in any output — the same promise
10
+ * doctor's ledger-signing check already makes for HOLMES_LEDGER_KEY.
11
+ * - `unset` clears both stores.
12
+ */
13
+ import { type CredentialOpts } from '../semantic/credentials';
14
+ export interface SemanticKeyIo {
15
+ /** Resolves one secret line. The default hides TTY echo; tests inject. */
16
+ readSecret: (prompt: string) => Promise<string>;
17
+ write: (line: string) => void;
18
+ }
19
+ export declare function runSemanticKey(sub: string | undefined, io?: SemanticKeyIo, cred?: CredentialOpts): Promise<number>;
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runSemanticKey = runSemanticKey;
4
+ // @implements A-SPEC-477
5
+ /**
6
+ * `holmes-kit semantic-key set|unset|status` — the human's consent act for the cloud semantic
7
+ * tier, made convenient AND safe:
8
+ *
9
+ * - `set` reads the key from STDIN (hidden prompt on a TTY, one piped line otherwise). The key
10
+ * never appears in argv — ps and shell history are exactly the surfaces a third-party PC leaks
11
+ * from. Storage prefers the OS keychain (darwin, fed over stdin too) and falls back to
12
+ * ~/.holmes/credentials.json at 0600 under 0700.
13
+ * - `status` prints the SOURCE only. The value never appears in any output — the same promise
14
+ * doctor's ledger-signing check already makes for HOLMES_LEDGER_KEY.
15
+ * - `unset` clears both stores.
16
+ */
17
+ const credentials_1 = require("../semantic/credentials");
18
+ const defaultIo = {
19
+ readSecret: (prompt) => new Promise((resolve, reject) => {
20
+ process.stderr.write(prompt);
21
+ const chunks = [];
22
+ const stdin = process.stdin;
23
+ if (stdin.isTTY) {
24
+ stdin.setRawMode?.(true);
25
+ stdin.resume();
26
+ let buf = '';
27
+ const onData = (d) => {
28
+ const c = d.toString('utf8');
29
+ if (c === '\r' || c === '\n' || c === '') {
30
+ stdin.setRawMode?.(false);
31
+ stdin.pause();
32
+ stdin.off('data', onData);
33
+ process.stderr.write('\n');
34
+ resolve(buf);
35
+ }
36
+ else if (c === '') {
37
+ stdin.setRawMode?.(false);
38
+ stdin.pause();
39
+ stdin.off('data', onData);
40
+ reject(new Error('aborted'));
41
+ }
42
+ else if (c === '' || c === '\b') {
43
+ buf = buf.slice(0, -1);
44
+ }
45
+ else {
46
+ buf += c; // no echo — the hiding IS the point
47
+ }
48
+ };
49
+ stdin.on('data', onData);
50
+ }
51
+ else {
52
+ stdin.on('data', (d) => chunks.push(Buffer.from(d)));
53
+ stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf8').split('\n')[0] ?? ''));
54
+ stdin.on('error', reject);
55
+ }
56
+ }),
57
+ write: (line) => process.stdout.write(line + '\n'),
58
+ };
59
+ const SOURCE_LABEL = {
60
+ 'env': 'HOLMES_SEMANTIC_API_KEY (환경변수)',
61
+ 'env-compat': 'GEMINI_API_KEY (환경변수)',
62
+ 'keychain': 'macOS 키체인 (holmes-kit/semantic)',
63
+ 'file': '~/.holmes/credentials.json (0600)',
64
+ };
65
+ async function runSemanticKey(sub, io = defaultIo, cred = {}) {
66
+ if (sub === 'set') {
67
+ const key = (await io.readSecret('cloud semantic 티어 API 키 입력(표시되지 않음): ')).trim();
68
+ if (key === '') {
69
+ io.write('빈 키는 저장하지 않습니다.');
70
+ return 2;
71
+ }
72
+ const where = (0, credentials_1.storeSemanticKey)(key, cred);
73
+ io.write(`저장됨: ${SOURCE_LABEL[where]} — 키 설정은 이그레스(스펙 산문·경로·심볼명 외부 전송) 동의입니다. 값은 어떤 출력에도 표시되지 않습니다.`);
74
+ return 0;
75
+ }
76
+ if (sub === 'unset') {
77
+ (0, credentials_1.removeSemanticKey)(cred);
78
+ io.write('semantic 키 제거됨 (키체인·파일 양쪽).');
79
+ return 0;
80
+ }
81
+ if (sub === 'status' || sub === undefined) {
82
+ const r = (0, credentials_1.resolveSemanticKey)(cred);
83
+ if (r === null) {
84
+ io.write('semantic 키: 없음 — cloud 티어 비활성. `holmes-kit semantic-key set` 으로 opt-in 하십시오(키 설정 = 이그레스 동의).');
85
+ }
86
+ else {
87
+ io.write(`semantic 키: 설정됨 — 출처 ${SOURCE_LABEL[r.source]}. 값은 표시되지 않습니다.`);
88
+ }
89
+ return 0;
90
+ }
91
+ io.write(`unknown subcommand: ${sub} (set|unset|status)`);
92
+ return 2;
93
+ }
@@ -3,6 +3,15 @@ export interface AxConfig {
3
3
  enforcement: 'block' | 'warn' | 'off';
4
4
  enforceHighRisk: boolean;
5
5
  overrideRequiresAdr: boolean;
6
+ /**
7
+ * @implements A-SPEC-278
8
+ * Pre-edit impact evidence enforcement. Defaults to `off` — unlike `enforcement`, whose strict
9
+ * default is the safe posture, the safe default HERE is off: switching it on changes the working
10
+ * discipline of every session in the repository, and that belongs to the repository's owner.
11
+ * A malformed config also lands here, so a broken file can neither weaken governance nor
12
+ * silently impose a new one.
13
+ */
14
+ preEditEvidence: 'off' | 'warn' | 'block';
6
15
  };
7
16
  highRiskDomains: string[];
8
17
  storage: {
@@ -35,12 +35,19 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.DEFAULT_CONFIG = void 0;
37
37
  exports.loadConfig = loadConfig;
38
+ // @implements A-SPEC-278
38
39
  // @implements A-SPEC-125.5
39
40
  // src/holmes/config/config.ts
40
41
  const fs = __importStar(require("node:fs"));
41
42
  const path = __importStar(require("node:path"));
42
43
  exports.DEFAULT_CONFIG = {
43
- guardrail: { enforcement: 'block', enforceHighRisk: true, overrideRequiresAdr: true },
44
+ // @implements A-SPEC-332
45
+ // `warn`, not `off` and not `block`. Off left Phase 2 present in code and absent in behaviour;
46
+ // block would refuse every source edit today, because `.ax/evidence` is empty (n=0) and the gate's
47
+ // first refusal is `no-analysis`. Warn reports the missing evidence without stopping work, which
48
+ // is also the only path that accumulates the data a later block decision would need. Raised as an
49
+ // authority question rather than decided by measurement — the user chose this level.
50
+ guardrail: { enforcement: 'block', enforceHighRisk: true, overrideRequiresAdr: true, preEditEvidence: 'warn' },
44
51
  highRiskDomains: ['@auth', '@payment'],
45
52
  storage: { specStore: 'local-markdown' },
46
53
  };