@holmes-lab/holmes-kit 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/dist/.build-id +1 -1
- package/dist/holmes/assoc/assoc-arm.d.ts +94 -0
- package/dist/holmes/assoc/assoc-arm.js +187 -0
- package/dist/holmes/assoc/explore.d.ts +21 -0
- package/dist/holmes/assoc/explore.js +160 -0
- package/dist/holmes/assoc/impact-baseline.d.ts +13 -0
- package/dist/holmes/assoc/impact-baseline.js +48 -0
- package/dist/holmes/assoc/ppr.d.ts +64 -0
- package/dist/holmes/assoc/ppr.js +110 -0
- package/dist/holmes/cli/agents.d.ts +24 -11
- package/dist/holmes/cli/agents.js +93 -21
- package/dist/holmes/cli/doctor.d.ts +56 -0
- package/dist/holmes/cli/doctor.js +311 -5
- package/dist/holmes/cli/index.js +13 -1
- package/dist/holmes/cli/semantic-key.d.ts +19 -0
- package/dist/holmes/cli/semantic-key.js +93 -0
- package/dist/holmes/config/config.d.ts +9 -0
- package/dist/holmes/config/config.js +8 -1
- package/dist/holmes/cpg/language-capability.d.ts +65 -0
- package/dist/holmes/cpg/language-capability.js +145 -0
- package/dist/holmes/cpg/language-parser-walk.js +179 -34
- package/dist/holmes/cpg/language-parser.d.ts +1 -1
- package/dist/holmes/governance/ledger-rechain.d.ts +12 -0
- package/dist/holmes/governance/ledger-rechain.js +17 -2
- package/dist/holmes/governance/provenance-ledger.js +21 -0
- package/dist/holmes/guardrail/blind-spots.js +12 -1
- package/dist/holmes/guardrail/impact-gate.d.ts +77 -0
- package/dist/holmes/guardrail/impact-gate.js +263 -0
- package/dist/holmes/guardrail/write-target.d.ts +38 -1
- package/dist/holmes/guardrail/write-target.js +41 -4
- package/dist/holmes/hooks/adapters/antigravity.js +12 -1
- package/dist/holmes/hooks/corrupt-state-run.d.ts +33 -0
- package/dist/holmes/hooks/corrupt-state-run.js +16 -0
- package/dist/holmes/hooks/pre-tool-use.d.ts +26 -0
- package/dist/holmes/hooks/pre-tool-use.js +203 -11
- package/dist/holmes/hooks/stop.d.ts +65 -0
- package/dist/holmes/hooks/stop.js +200 -2
- package/dist/holmes/mcp/handlers.d.ts +88 -0
- package/dist/holmes/mcp/handlers.js +597 -16
- package/dist/holmes/mcp/maintenance-analyze.d.ts +435 -0
- package/dist/holmes/mcp/maintenance-analyze.js +994 -0
- package/dist/holmes/mcp/maintenance-evidence.d.ts +140 -0
- package/dist/holmes/mcp/maintenance-evidence.js +253 -0
- package/dist/holmes/mcp/tool-schemas.js +71 -0
- package/dist/holmes/project/root.js +3 -1
- package/dist/holmes/review/baseline-arm.d.ts +37 -0
- package/dist/holmes/review/baseline-arm.js +51 -0
- package/dist/holmes/review/captured-stdin-guard.d.ts +8 -0
- package/dist/holmes/review/captured-stdin-guard.js +48 -0
- package/dist/holmes/review/coherence-verify.d.ts +31 -0
- package/dist/holmes/review/coherence-verify.js +144 -0
- package/dist/holmes/review/commit-text.d.ts +50 -0
- package/dist/holmes/review/commit-text.js +76 -0
- package/dist/holmes/review/confidence-calibration.d.ts +39 -0
- package/dist/holmes/review/confidence-calibration.js +39 -0
- package/dist/holmes/review/content-baseline.d.ts +38 -0
- package/dist/holmes/review/content-baseline.js +103 -0
- package/dist/holmes/review/content-verify.d.ts +20 -0
- package/dist/holmes/review/content-verify.js +73 -0
- package/dist/holmes/review/dense-retrieval.d.ts +66 -0
- package/dist/holmes/review/dense-retrieval.js +97 -0
- package/dist/holmes/review/edge-quality.d.ts +44 -0
- package/dist/holmes/review/edge-quality.js +117 -0
- package/dist/holmes/review/evaluation-metrics.d.ts +138 -0
- package/dist/holmes/review/evaluation-metrics.js +175 -0
- package/dist/holmes/review/graph-verifier.d.ts +34 -0
- package/dist/holmes/review/graph-verifier.js +62 -0
- package/dist/holmes/review/hop-ablation.d.ts +100 -0
- package/dist/holmes/review/hop-ablation.js +89 -0
- package/dist/holmes/review/manual-baseline.d.ts +209 -0
- package/dist/holmes/review/manual-baseline.js +2846 -0
- package/dist/holmes/review/oracle-gap.d.ts +32 -0
- package/dist/holmes/review/oracle-gap.js +102 -0
- package/dist/holmes/review/point-in-time-replay.d.ts +41 -0
- package/dist/holmes/review/point-in-time-replay.js +161 -0
- package/dist/holmes/review/rank-diagnosis.d.ts +43 -0
- package/dist/holmes/review/rank-diagnosis.js +163 -0
- package/dist/holmes/review/replay-calibration.d.ts +62 -0
- package/dist/holmes/review/replay-calibration.js +83 -0
- package/dist/holmes/review/replay-corpus.d.ts +135 -0
- package/dist/holmes/review/replay-corpus.js +210 -0
- package/dist/holmes/review/run-replay.d.ts +260 -0
- package/dist/holmes/review/run-replay.js +729 -0
- package/dist/holmes/review/semantic-arm.d.ts +271 -0
- package/dist/holmes/review/semantic-arm.js +717 -0
- package/dist/holmes/review/semantic-retrieval.d.ts +55 -0
- package/dist/holmes/review/semantic-retrieval.js +156 -0
- package/dist/holmes/review/spec-layer-stats.d.ts +38 -0
- package/dist/holmes/review/spec-layer-stats.js +52 -0
- package/dist/holmes/review/temporal-prior.d.ts +33 -0
- package/dist/holmes/review/temporal-prior.js +53 -0
- package/dist/holmes/review/test-runner.d.ts +15 -0
- package/dist/holmes/review/test-runner.js +41 -4
- package/dist/holmes/review/test-selection-breadth.d.ts +75 -0
- package/dist/holmes/review/test-selection-breadth.js +57 -0
- package/dist/holmes/review/traceability-benchmark.d.ts +81 -0
- package/dist/holmes/review/traceability-benchmark.js +135 -0
- package/dist/holmes/review/union-verify.d.ts +12 -0
- package/dist/holmes/review/union-verify.js +70 -0
- package/dist/holmes/rtm/graph-store.d.ts +51 -0
- package/dist/holmes/rtm/graph-store.js +122 -0
- package/dist/holmes/rtm/incremental.d.ts +25 -1
- package/dist/holmes/rtm/incremental.js +18 -1
- package/dist/holmes/rtm/localize.d.ts +28 -0
- package/dist/holmes/rtm/localize.js +272 -14
- package/dist/holmes/rtm/rtm-builder.d.ts +118 -3
- package/dist/holmes/rtm/rtm-builder.js +265 -28
- package/dist/holmes/rtm/rtm-graph.d.ts +117 -2
- package/dist/holmes/rtm/rtm-graph.js +194 -31
- package/dist/holmes/rtm/taint-benchmark.d.ts +97 -0
- package/dist/holmes/rtm/taint-benchmark.js +141 -0
- package/dist/holmes/rtm/test-scope.js +24 -1
- package/dist/holmes/semantic/credentials.d.ts +15 -0
- package/dist/holmes/semantic/credentials.js +134 -0
- package/dist/holmes/semantic/embedder.d.ts +44 -0
- package/dist/holmes/semantic/embedder.js +185 -0
- package/dist/holmes/semantic/hit-rerank.d.ts +4 -0
- package/dist/holmes/semantic/hit-rerank.js +38 -0
- package/dist/holmes/semantic/tier.d.ts +37 -0
- package/dist/holmes/semantic/tier.js +54 -0
- package/dist/holmes/semantic/vector-cache.d.ts +11 -0
- package/dist/holmes/semantic/vector-cache.js +91 -0
- package/dist/holmes/spec/acceptance-quality.d.ts +81 -0
- package/dist/holmes/spec/acceptance-quality.js +169 -0
- package/dist/holmes/spec/validator.js +33 -1
- package/dist/holmes/spec/yaml-scalar.d.ts +1 -0
- package/dist/holmes/spec/yaml-scalar.js +43 -0
- 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 !
|
|
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 !
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/holmes/cli/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
};
|