@holmes-lab/holmes-kit 0.19.3 → 0.19.5
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 +163 -0
- package/README.md +22 -4
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/agents.d.ts +8 -0
- package/dist/holmes/cli/agents.js +26 -2
- package/dist/holmes/cli/codex-toml.d.ts +10 -0
- package/dist/holmes/cli/codex-toml.js +76 -12
- package/dist/holmes/cli/doctor.d.ts +19 -0
- package/dist/holmes/cli/doctor.js +107 -42
- package/dist/holmes/cli/index.js +13 -0
- package/dist/holmes/cli/init.js +10 -3
- package/dist/holmes/cli/native-deps.js +4 -1
- package/dist/holmes/cli/playbook-skills.js +6 -4
- package/dist/holmes/cli/probe-process.d.ts +8 -0
- package/dist/holmes/cli/probe-process.js +73 -0
- package/dist/holmes/cli/spawn-spec.js +3 -1
- package/dist/holmes/cli/test-platform.d.ts +37 -0
- package/dist/holmes/cli/test-platform.js +126 -1
- package/dist/holmes/governance/approval-grants.js +26 -3
- package/dist/holmes/governance/autonomy.d.ts +17 -1
- package/dist/holmes/governance/autonomy.js +37 -5
- package/dist/holmes/mcp/handlers.d.ts +30 -5
- package/dist/holmes/mcp/handlers.js +111 -13
- package/dist/holmes/mcp/spec-id-guard.d.ts +1 -1
- package/dist/holmes/mcp/spec-id-guard.js +9 -13
- package/dist/holmes/mcp/tool-schemas.js +13 -0
- package/dist/holmes/project/install-scripts-policy.d.ts +16 -2
- package/dist/holmes/project/install-scripts-policy.js +16 -2
- package/dist/holmes/review/point-in-time-replay.js +43 -3
- package/dist/holmes/rtm/graph-store.d.ts +2 -0
- package/dist/holmes/rtm/graph-store.js +14 -0
- package/dist/holmes/rtm/rtm-graph.js +42 -30
- package/dist/holmes/semantic/credentials.js +86 -9
- package/dist/holmes/semantic/embedder.js +6 -39
- package/dist/holmes/semantic/local-model.d.ts +30 -0
- package/dist/holmes/semantic/local-model.js +92 -0
- package/dist/holmes/semantic/model-cache.d.ts +8 -0
- package/dist/holmes/semantic/model-cache.js +67 -0
- package/dist/holmes/semantic/tier.d.ts +7 -0
- package/dist/holmes/semantic/tier.js +9 -3
- package/dist/holmes/spec/renumber.d.ts +72 -0
- package/dist/holmes/spec/renumber.js +341 -0
- package/dist/holmes/spec/spec-id.d.ts +9 -0
- package/dist/holmes/spec/spec-id.js +23 -0
- package/docs/install-guide.md +90 -2
- package/package.json +9 -3
- package/scripts/install.ps1 +30 -27
|
@@ -34,8 +34,10 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.STRIPPED_FOR_PROBE = void 0;
|
|
37
|
+
exports.resolvesToPackage = resolvesToPackage;
|
|
37
38
|
exports.globalInstallDir = globalInstallDir;
|
|
38
39
|
exports.npmCliEntry = npmCliEntry;
|
|
40
|
+
exports.probeWritable = probeWritable;
|
|
39
41
|
exports.prefixVerdict = prefixVerdict;
|
|
40
42
|
exports.grammarProbe = grammarProbe;
|
|
41
43
|
exports.resolveDepDir = resolveDepDir;
|
|
@@ -49,6 +51,9 @@ exports.formatChecks = formatChecks;
|
|
|
49
51
|
exports.wiringHandshakeChecks = wiringHandshakeChecks;
|
|
50
52
|
exports.semanticTierVerdict = semanticTierVerdict;
|
|
51
53
|
exports.detectTreeKeyTemporary = detectTreeKeyTemporary;
|
|
54
|
+
// @implements A-SPEC-594
|
|
55
|
+
// @implements A-SPEC-592
|
|
56
|
+
// @implements A-SPEC-591
|
|
52
57
|
// @implements A-SPEC-442
|
|
53
58
|
// @implements A-SPEC-207
|
|
54
59
|
// @implements A-SPEC-100.2
|
|
@@ -58,6 +63,7 @@ const npx_bin_1 = require("../project/npx-bin");
|
|
|
58
63
|
const install_scripts_policy_1 = require("../project/install-scripts-policy");
|
|
59
64
|
const native_deps_1 = require("./native-deps");
|
|
60
65
|
const tier_1 = require("../semantic/tier");
|
|
66
|
+
const probe_process_1 = require("./probe-process");
|
|
61
67
|
const path = __importStar(require("node:path"));
|
|
62
68
|
const role_policy_1 = require("../governance/role-policy");
|
|
63
69
|
const blind_spots_1 = require("../guardrail/blind-spots");
|
|
@@ -82,8 +88,20 @@ function resolvesToPackage(filePath, packageRoot) {
|
|
|
82
88
|
try {
|
|
83
89
|
if (path.resolve(filePath).startsWith(path.resolve(packageRoot) + path.sep))
|
|
84
90
|
return true;
|
|
85
|
-
|
|
86
|
-
|
|
91
|
+
// @implements A-SPEC-586.2 — canonical, not merely real: the JS `realpathSync` walks lstat and
|
|
92
|
+
// leaves a Windows 8.3 short name (`HOLMES~1`) as it is, so the very hook path init writes for a
|
|
93
|
+
// spaced install read as "another install". `realpathSync.native` asks the OS for the final
|
|
94
|
+
// path, which expands short names and follows links alike (measured 2026-09-10).
|
|
95
|
+
const canonical = (p) => {
|
|
96
|
+
try {
|
|
97
|
+
return fs.realpathSync.native(p);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return fs.realpathSync(p);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
const realFile = canonical(filePath);
|
|
104
|
+
const realPkg = canonical(packageRoot);
|
|
87
105
|
return realFile.startsWith(realPkg + path.sep);
|
|
88
106
|
}
|
|
89
107
|
catch {
|
|
@@ -147,6 +165,31 @@ function npmCliEntry(env = process.env, execPath = process.execPath) {
|
|
|
147
165
|
}
|
|
148
166
|
return null;
|
|
149
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* @implements A-SPEC-584
|
|
170
|
+
* Is `dir` writable by THIS account? Asked by TRYING, not by `fs.accessSync(W_OK)`: on Windows the
|
|
171
|
+
* mode bits accessSync consults do not carry the ACL, so it answered "writable" for
|
|
172
|
+
* `C:\Program Files\nodejs` on a non-admin account while `mkdir` died EPERM (measured 2026-09-10).
|
|
173
|
+
* A mkdir is the very syscall npm's own EPERM comes from (creating the scope directory), so the probe
|
|
174
|
+
* makes one and removes it. A probe entry that could not be removed is reported, never hidden.
|
|
175
|
+
* Nothing thrown; a missing or non-directory `dir` is simply not writable.
|
|
176
|
+
*/
|
|
177
|
+
function probeWritable(dir) {
|
|
178
|
+
const probe = path.join(dir, `.holmes-doctor-probe-${process.pid}-${Math.random().toString(36).slice(2, 8)}`);
|
|
179
|
+
try {
|
|
180
|
+
fs.mkdirSync(probe);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return { writable: false };
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
fs.rmdirSync(probe);
|
|
187
|
+
return { writable: true };
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return { writable: true, leftover: probe };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
150
193
|
/**
|
|
151
194
|
* Judge the global prefix. Pure — the probing lives with the caller.
|
|
152
195
|
*
|
|
@@ -171,6 +214,11 @@ function prefixVerdict(input) {
|
|
|
171
214
|
}
|
|
172
215
|
const prefix = input.prefix;
|
|
173
216
|
if (input.writable) {
|
|
217
|
+
// @implements A-SPEC-584 — writable, but the probe left something behind: a PASS would hide a
|
|
218
|
+
// file the user now has to clean up. Say where it is.
|
|
219
|
+
if (typeof input.leftover === 'string' && input.leftover !== '') {
|
|
220
|
+
return { level: 'WARN', detail: `Global prefix (${prefix}) is writable by the current account, but doctor's probe directory could not be removed: ${input.leftover} — delete it by hand.` };
|
|
221
|
+
}
|
|
174
222
|
return { level: 'PASS', detail: `Global prefix (${prefix}) is writable by the current account — npm install -g works without permission trouble.` };
|
|
175
223
|
}
|
|
176
224
|
const fix = input.platform === 'win32'
|
|
@@ -1039,7 +1087,14 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
|
|
|
1039
1087
|
}
|
|
1040
1088
|
else {
|
|
1041
1089
|
const entry = (0, codex_toml_1.readCodexHolmesEntry)(raw);
|
|
1042
|
-
|
|
1090
|
+
// @implements A-SPEC-585 — an invalid basic-string escape (a raw Windows backslash path is the
|
|
1091
|
+
// measured case) is not "half-done": Codex refuses the whole file, and the reader must not
|
|
1092
|
+
// hand doctor a mangled path to judge. Name the escape and the line.
|
|
1093
|
+
const badEscape = entry ? null : (0, codex_toml_1.invalidTomlEscape)(raw);
|
|
1094
|
+
if (!entry && badEscape) {
|
|
1095
|
+
add('codex wiring', 'WARN', `${cdxToml} has an invalid escape ${badEscape.escape} on line ${badEscape.line} inside [mcp_servers.${init_1.SERVER_NAME}] — Codex refuses this file. On Windows, double each backslash ("C:\\\\path") or use a single-quoted literal string ('C:\\path').`, 'Rewire with holmes-kit init --target <dir> --agent codex --force (it writes valid TOML).');
|
|
1096
|
+
}
|
|
1097
|
+
else if (!entry) {
|
|
1043
1098
|
add('codex wiring', 'WARN', `${cdxToml} has no runnable [mcp_servers.${init_1.SERVER_NAME}] entry — a half-done wiring`, 'Rewire with holmes-kit init --target <dir> --agent codex.');
|
|
1044
1099
|
}
|
|
1045
1100
|
else if (entry.command === 'node') {
|
|
@@ -1170,13 +1225,10 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
|
|
|
1170
1225
|
break;
|
|
1171
1226
|
probe = parent;
|
|
1172
1227
|
}
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
}
|
|
1178
|
-
catch { /* not writable */ }
|
|
1179
|
-
verdict = prefixVerdict({ prefix, dir, writable, platform: process.platform });
|
|
1228
|
+
// @implements A-SPEC-584 — TRY, do not ask: the mode-bit question said "writable" for a
|
|
1229
|
+
// protected Windows prefix (ACLs are invisible to it) and sent users into npm's EPERM.
|
|
1230
|
+
const tried = probeWritable(probe);
|
|
1231
|
+
verdict = prefixVerdict({ prefix, dir, writable: tried.writable, leftover: tried.leftover, platform: process.platform });
|
|
1180
1232
|
}
|
|
1181
1233
|
catch (e) {
|
|
1182
1234
|
verdict = prefixVerdict({ unreadable: e instanceof Error ? e.message.split('\n')[0] : String(e) });
|
|
@@ -1288,15 +1340,15 @@ cwd) {
|
|
|
1288
1340
|
return;
|
|
1289
1341
|
done = true;
|
|
1290
1342
|
clearTimeout(timer);
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
resolve({ name: 'mcp wiring spawn', level, detail, fix });
|
|
1343
|
+
void (0, probe_process_1.stopProbeProcess)(child).then(released => resolve(released
|
|
1344
|
+
? { name: 'mcp wiring spawn', level, detail, fix }
|
|
1345
|
+
: { name: 'mcp wiring spawn', level: 'FAIL', detail: `${detail}; probe cleanup could not be confirmed`,
|
|
1346
|
+
fix: `${fix ?? ''} Close the remaining diagnostic process for \`${command}\`, then retry doctor.` }));
|
|
1296
1347
|
};
|
|
1297
|
-
const timer = setTimeout(() => finish('FAIL', `wiring did not answer initialize within ${timeoutMs / 1000}s: \`${quoted}\``,
|
|
1348
|
+
const timer = setTimeout(() => finish('FAIL', `wiring did not answer initialize within ${timeoutMs / 1000}s: \`${quoted}\``, `Re-run \`holmes-kit init\` in the target to rewrite the wiring for \`${command}\`, then re-run doctor.`), timeoutMs);
|
|
1298
1349
|
try {
|
|
1299
|
-
child = (0, node_child_process_1.spawn)(spec.command, spec.args, { stdio: ['pipe', 'pipe', 'pipe'], ...(cwd === undefined ? {} : { cwd }) });
|
|
1350
|
+
child = (0, node_child_process_1.spawn)(spec.command, spec.args, { ...probe_process_1.PROBE_SPAWN_OPTIONS, stdio: ['pipe', 'pipe', 'pipe'], ...(cwd === undefined ? {} : { cwd }) });
|
|
1351
|
+
(0, probe_process_1.trackProbeProcess)(child);
|
|
1300
1352
|
}
|
|
1301
1353
|
catch (e) {
|
|
1302
1354
|
finish('FAIL', `wiring could not be spawned: \`${quoted}\` — ${e.message}`, spawnFailFix(e));
|
|
@@ -1340,7 +1392,8 @@ cwd) {
|
|
|
1340
1392
|
function mcpHandshakeCheck(packageRoot, timeoutMs = 15000) {
|
|
1341
1393
|
return new Promise((resolve) => {
|
|
1342
1394
|
const mcpCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'holmes-doctor-mcp-'));
|
|
1343
|
-
|
|
1395
|
+
let child;
|
|
1396
|
+
let timer;
|
|
1344
1397
|
let pending = ''; // the unterminated tail of the stdout stream, never the whole history
|
|
1345
1398
|
let err = '';
|
|
1346
1399
|
let done = false;
|
|
@@ -1353,30 +1406,29 @@ function mcpHandshakeCheck(packageRoot, timeoutMs = 15000) {
|
|
|
1353
1406
|
return;
|
|
1354
1407
|
done = true;
|
|
1355
1408
|
clearTimeout(timer);
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
child.unref?.();
|
|
1370
|
-
// Cleanup HERE, not on 'close' — doctor's process can exit before a close handler runs,
|
|
1371
|
-
// stranding the isolated cwd (round-6 §7e measured the leak). A signal death still strands it;
|
|
1372
|
-
// `cleanupOnSignal` below covers that (round-9).
|
|
1373
|
-
try {
|
|
1374
|
-
fs.rmSync(mcpCwd, { recursive: true, force: true });
|
|
1375
|
-
}
|
|
1376
|
-
catch { /* tmp cleaner's job */ }
|
|
1377
|
-
resolve({ name: 'MCP server', level, detail, fix });
|
|
1409
|
+
void (0, probe_process_1.stopProbeProcess)(child).then(async (released) => {
|
|
1410
|
+
let removed = false;
|
|
1411
|
+
try {
|
|
1412
|
+
await fs.promises.rm(mcpCwd, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
1413
|
+
DOCTOR_TEMP_DIRS.delete(mcpCwd);
|
|
1414
|
+
removed = true;
|
|
1415
|
+
}
|
|
1416
|
+
catch { /* signal cleanup retains the path for a later attempt */ }
|
|
1417
|
+
resolve(released && removed
|
|
1418
|
+
? { name: 'MCP server', level, detail, fix }
|
|
1419
|
+
: { name: 'MCP server', level: 'FAIL', detail: `${detail}; probe cleanup could not be confirmed`,
|
|
1420
|
+
fix: 'Close the remaining diagnostic process and retry doctor.' });
|
|
1421
|
+
});
|
|
1378
1422
|
};
|
|
1379
1423
|
cleanupOnSignal(mcpCwd);
|
|
1424
|
+
try {
|
|
1425
|
+
child = (0, node_child_process_1.spawn)(process.execPath, [path.join(packageRoot, 'bin', 'holmes-mcp.js')], { ...probe_process_1.PROBE_SPAWN_OPTIONS, stdio: ['pipe', 'pipe', 'pipe'], cwd: mcpCwd });
|
|
1426
|
+
(0, probe_process_1.trackProbeProcess)(child);
|
|
1427
|
+
}
|
|
1428
|
+
catch (error) {
|
|
1429
|
+
finish('FAIL', `could not spawn the MCP server: ${error.message}`, 'Check the install and executable permissions, then retry doctor.');
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1380
1432
|
// EPIPE arrives as an ASYNC 'error' event, not a synchronous throw — the try/catch below never
|
|
1381
1433
|
// sees it, and an unhandled stream error would crash doctor itself (round-1 REQ-192).
|
|
1382
1434
|
child.stdin.on('error', () => { });
|
|
@@ -1404,7 +1456,7 @@ function mcpHandshakeCheck(packageRoot, timeoutMs = 15000) {
|
|
|
1404
1456
|
child.stdin.write(JSON.stringify(o) + '\n');
|
|
1405
1457
|
}
|
|
1406
1458
|
catch { /* server gone */ } };
|
|
1407
|
-
|
|
1459
|
+
timer = setTimeout(() => finish('FAIL', `no tools/list response within ${timeoutMs}ms (stderr: ${err.slice(0, 160)})`, 'The MCP server did not start. A missing tree-sitter grammar is the usual cause (see above).'), timeoutMs);
|
|
1408
1460
|
// Line-oriented and stateful. The old shape re-scanned the whole accumulated buffer on every
|
|
1409
1461
|
// chunk (quadratic, and it re-fired past triggers) and judged `tools` by
|
|
1410
1462
|
// `typeof result.tools.length === 'number'` — which a NON-ARRAY or an EMPTY list satisfies, so a
|
|
@@ -1600,9 +1652,22 @@ function semanticTierVerdict(t) {
|
|
|
1600
1652
|
};
|
|
1601
1653
|
}
|
|
1602
1654
|
if (t.tier === 'local') {
|
|
1655
|
+
return {
|
|
1656
|
+
level: 'WARN',
|
|
1657
|
+
detail: `Semantic search tier: local (${t.model}, ${(0, tier_1.POOLING_OF)(t.model ?? '')} pooling) — no egress during inference. Runtime and model readiness are unverified by this lightweight check.`,
|
|
1658
|
+
fix: 'Run holmes-kit semantic-check for actual offline inference; if unavailable, run holmes-kit semantic-setup.',
|
|
1659
|
+
};
|
|
1660
|
+
}
|
|
1661
|
+
// @implements A-SPEC-594 — two different 'none' states need two different sentences. When the
|
|
1662
|
+
// runtime is already installed, telling the user to install it is noise; the missing piece is
|
|
1663
|
+
// the model, and the command that fetches it is the whole fix. Both stay PASS: a user who never
|
|
1664
|
+
// asked for a semantic tier is not misconfigured, and a WARN that cannot be resolved by any
|
|
1665
|
+
// action teaches people to skip doctor's output entirely.
|
|
1666
|
+
if (t.localRuntimePresent) {
|
|
1603
1667
|
return {
|
|
1604
1668
|
level: 'PASS',
|
|
1605
|
-
detail:
|
|
1669
|
+
detail: 'Semantic search tier: none — the local runtime is installed but its model was never prepared, so no embedding is produced. Requests lexical search misses (measured 16.4%) have 0% recall.',
|
|
1670
|
+
fix: 'Run `holmes-kit semantic-setup` to download the public model once (shared across projects) — lexical-zero recall 0→52%, no egress. Cloud tier (opt-in, consent to external transfer): set GEMINI_API_KEY — 0→92%.',
|
|
1606
1671
|
};
|
|
1607
1672
|
}
|
|
1608
1673
|
return {
|
package/dist/holmes/cli/index.js
CHANGED
|
@@ -37,6 +37,7 @@ exports.isBrokenPipe = void 0;
|
|
|
37
37
|
exports.packageRoot = packageRoot;
|
|
38
38
|
exports.main = main;
|
|
39
39
|
exports.installPipeGuard = installPipeGuard;
|
|
40
|
+
// @implements A-SPEC-591
|
|
40
41
|
// @implements A-SPEC-100.2
|
|
41
42
|
// @implements A-SPEC-213
|
|
42
43
|
// @implements A-SPEC-215
|
|
@@ -82,6 +83,8 @@ const KNOWN_FLAGS = {
|
|
|
82
83
|
ledger: ['help', 'target', 'ref', 'dry-run'],
|
|
83
84
|
// @implements A-SPEC-477 — the human's opt-in act for the cloud semantic tier.
|
|
84
85
|
'semantic-key': ['help'],
|
|
86
|
+
'semantic-setup': ['help'],
|
|
87
|
+
'semantic-check': ['help'],
|
|
85
88
|
// @implements A-SPEC-509.1 — the explicit installation act for the pre-push evidence gate.
|
|
86
89
|
'install-push-gate': ['help', 'target'],
|
|
87
90
|
// @implements A-SPEC-543.2 — one-command upgrade: install + re-pin every registered workspace.
|
|
@@ -219,6 +222,8 @@ const USAGE = `holmes-kit — deterministic ASE governance for a project
|
|
|
219
222
|
--target <dir> project root (default: cwd)
|
|
220
223
|
|
|
221
224
|
holmes-kit semantic-key set|unset|status manage the cloud semantic tier opt-in key
|
|
225
|
+
holmes-kit semantic-setup install and verify the default local BGE-M3 model
|
|
226
|
+
holmes-kit semantic-check verify BGE-M3 offline without downloading
|
|
222
227
|
set reads from stdin only (the value never appears in argv or output).
|
|
223
228
|
Setting the key is consent to egress (spec prose, paths, symbol names sent externally).
|
|
224
229
|
|
|
@@ -310,6 +315,10 @@ async function main(argv) {
|
|
|
310
315
|
process.stdout.write((0, colophon_1.colophon)());
|
|
311
316
|
return 0;
|
|
312
317
|
}
|
|
318
|
+
if (cmd === 'semantic-setup' || cmd === 'semantic-check') {
|
|
319
|
+
const { runModelSetup } = await Promise.resolve().then(() => __importStar(require('../semantic/local-model')));
|
|
320
|
+
return runModelSetup({ offline: cmd === 'semantic-check' });
|
|
321
|
+
}
|
|
313
322
|
if (cmd === 'semantic-key') {
|
|
314
323
|
// @implements A-SPEC-477 — set|unset|status; the key rides stdin, never argv.
|
|
315
324
|
const { runSemanticKey } = await Promise.resolve().then(() => __importStar(require('./semantic-key')));
|
|
@@ -1141,6 +1150,10 @@ async function main(argv) {
|
|
|
1141
1150
|
catch { /* registry failure never fails init */ }
|
|
1142
1151
|
}
|
|
1143
1152
|
}
|
|
1153
|
+
if (res.ok && !opts.dryRun && !opts.remove) {
|
|
1154
|
+
const { runModelSetup } = await Promise.resolve().then(() => __importStar(require('../semantic/local-model')));
|
|
1155
|
+
runModelSetup({ automatic: true });
|
|
1156
|
+
}
|
|
1144
1157
|
return res.exitCode;
|
|
1145
1158
|
}
|
|
1146
1159
|
// @implements A-SPEC-543.2 — one-command upgrade. Preparation is zero-config (init records the
|
package/dist/holmes/cli/init.js
CHANGED
|
@@ -472,9 +472,16 @@ function runInit(opts) {
|
|
|
472
472
|
}
|
|
473
473
|
changes.push({ path: f.path, before, after: f.content });
|
|
474
474
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
475
|
+
// @implements A-SPEC-586 — an antigravity hook whose path still carries a space is a hook the
|
|
476
|
+
// launcher never runs (it splits on whitespace). Saying "enforced" over it was a false PASS
|
|
477
|
+
// measured on a Windows install under a spaced user directory; the warning replaces the claim.
|
|
478
|
+
const spaced = agent === 'antigravity' ? (0, agents_1.antigravityHookWarnings)(opts.packageRoot) : [];
|
|
479
|
+
if (spaced.length > 0)
|
|
480
|
+
messages.push(...spaced);
|
|
481
|
+
else
|
|
482
|
+
messages.push(agents_1.HARNESS_ENFORCES[agent]
|
|
483
|
+
? `${agent}: the gate is enforced (hooks wired).`
|
|
484
|
+
: `${agent}: only tools and instructions were wired — this harness does not enforce the gate.`);
|
|
478
485
|
// a link is placement, not content — a dry-run only says so, and only the real run creates it.
|
|
479
486
|
for (const link of (0, agents_1.agentLinks)(agent, { target: opts.target, packageRoot: opts.packageRoot, specsDir: opts.specsDir })) {
|
|
480
487
|
if (opts.dryRun) {
|
|
@@ -4,6 +4,7 @@ exports.installKind = installKind;
|
|
|
4
4
|
exports.recoveryCommands = recoveryCommands;
|
|
5
5
|
exports.powershellPolicyNote = powershellPolicyNote;
|
|
6
6
|
exports.nativeVerdict = nativeVerdict;
|
|
7
|
+
// @implements A-SPEC-599
|
|
7
8
|
// @implements A-SPEC-580
|
|
8
9
|
// @implements A-SPEC-580.1
|
|
9
10
|
const npx_bin_1 = require("../project/npx-bin");
|
|
@@ -94,7 +95,9 @@ function nativeVerdict(ev) {
|
|
|
94
95
|
return {
|
|
95
96
|
cause: 'scripts-blocked', level: 'FAIL',
|
|
96
97
|
detail: withPs(`no binary — ${npmSaid} blocks dependency install scripts unless allowScripts covers ${pkg}${denied}; policy home: ${policyHome}.`),
|
|
97
|
-
fix: `Approve the one script that must run, then rebuild: ${recoveryCommands(ev).join(
|
|
98
|
+
fix: `Approve the one script that must run, then rebuild: ${recoveryCommands(ev).join(isWin
|
|
99
|
+
? "; if ($LASTEXITCODE -ne 0) { throw 'Native dependency recovery failed' }; "
|
|
100
|
+
: ' && ')}`,
|
|
98
101
|
};
|
|
99
102
|
}
|
|
100
103
|
// 6b. Early npm 11 did not block scripts, so a missing approval proves nothing about why the
|
|
@@ -45,6 +45,7 @@ exports.skillLabel = skillLabel;
|
|
|
45
45
|
exports.playbookSkillStates = playbookSkillStates;
|
|
46
46
|
exports.classifyExtraEntries = classifyExtraEntries;
|
|
47
47
|
exports.installedPlaybookCount = installedPlaybookCount;
|
|
48
|
+
// @implements A-SPEC-190
|
|
48
49
|
// @implements A-SPEC-142
|
|
49
50
|
const fs = __importStar(require("node:fs"));
|
|
50
51
|
const path = __importStar(require("node:path"));
|
|
@@ -199,13 +200,14 @@ function aliasGroupsOf(packageRoot, target) {
|
|
|
199
200
|
// playbook's body overwrite the other's — the exact harm the round-8 pre-scan was added to stop.
|
|
200
201
|
const identity = (name) => {
|
|
201
202
|
const real = realOf(name);
|
|
203
|
+
// NTFS file IDs can exceed Number precision; rounded IDs alias unrelated files.
|
|
202
204
|
try {
|
|
203
|
-
const st = fs.statSync(real);
|
|
205
|
+
const st = fs.statSync(real, { bigint: true });
|
|
204
206
|
return `${st.dev}:${st.ino}`;
|
|
205
207
|
}
|
|
206
208
|
catch { /* not yet there */ }
|
|
207
209
|
try {
|
|
208
|
-
const st = fs.statSync(path.dirname(real));
|
|
210
|
+
const st = fs.statSync(path.dirname(real), { bigint: true });
|
|
209
211
|
return `${st.dev}:${st.ino}:${path.basename(real).toLowerCase()}`;
|
|
210
212
|
}
|
|
211
213
|
catch { /* dangling */ }
|
|
@@ -652,8 +654,8 @@ function playbookSkillStates(packageRoot, target) {
|
|
|
652
654
|
}
|
|
653
655
|
for (const entry of classifyExtraEntries(entries, shipped, (entry2, canonical) => {
|
|
654
656
|
try {
|
|
655
|
-
const a = fs.statSync(path.join(skillsDir, entry2));
|
|
656
|
-
const b = fs.statSync(path.join(skillsDir, canonical));
|
|
657
|
+
const a = fs.statSync(path.join(skillsDir, entry2), { bigint: true });
|
|
658
|
+
const b = fs.statSync(path.join(skillsDir, canonical), { bigint: true });
|
|
657
659
|
return a.ino === b.ino && a.dev === b.dev;
|
|
658
660
|
}
|
|
659
661
|
catch {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type ChildProcess } from 'node:child_process';
|
|
2
|
+
/** Each POSIX probe gets its own process group; Windows taskkill targets only its owned PID. */
|
|
3
|
+
export declare const PROBE_SPAWN_OPTIONS: {
|
|
4
|
+
detached: boolean;
|
|
5
|
+
windowsHide: boolean;
|
|
6
|
+
};
|
|
7
|
+
export declare function trackProbeProcess(child: ChildProcess): void;
|
|
8
|
+
export declare function stopProbeProcess(child?: ChildProcess): Promise<boolean>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PROBE_SPAWN_OPTIONS = void 0;
|
|
4
|
+
exports.trackProbeProcess = trackProbeProcess;
|
|
5
|
+
exports.stopProbeProcess = stopProbeProcess;
|
|
6
|
+
// @implements A-SPEC-592
|
|
7
|
+
const node_child_process_1 = require("node:child_process");
|
|
8
|
+
/** Each POSIX probe gets its own process group; Windows taskkill targets only its owned PID. */
|
|
9
|
+
exports.PROBE_SPAWN_OPTIONS = { detached: process.platform !== 'win32', windowsHide: true };
|
|
10
|
+
const states = new WeakMap();
|
|
11
|
+
function trackProbeProcess(child) {
|
|
12
|
+
if (states.has(child))
|
|
13
|
+
return;
|
|
14
|
+
const state = { closed: false };
|
|
15
|
+
states.set(child, state);
|
|
16
|
+
child.once('close', () => { state.closed = true; });
|
|
17
|
+
}
|
|
18
|
+
async function stopProbeProcess(child) {
|
|
19
|
+
if (!child)
|
|
20
|
+
return true;
|
|
21
|
+
trackProbeProcess(child);
|
|
22
|
+
let closed = states.get(child).closed;
|
|
23
|
+
let onClose = () => { };
|
|
24
|
+
let closeTimer;
|
|
25
|
+
const closure = new Promise(resolve => {
|
|
26
|
+
onClose = () => { closed = true; clearTimeout(closeTimer); resolve(true); };
|
|
27
|
+
child.once('close', onClose);
|
|
28
|
+
closeTimer = setTimeout(() => resolve(false), 3000);
|
|
29
|
+
if (closed || !child.pid)
|
|
30
|
+
onClose();
|
|
31
|
+
});
|
|
32
|
+
let stopped = true;
|
|
33
|
+
try {
|
|
34
|
+
if (child.pid && process.platform === 'win32' && !closed) {
|
|
35
|
+
stopped = await new Promise(resolve => {
|
|
36
|
+
(0, node_child_process_1.execFile)('taskkill.exe', ['/PID', String(child.pid), '/T', '/F'], { windowsHide: true, timeout: 2500 }, error => {
|
|
37
|
+
resolve(!error);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
else if (child.pid && process.platform !== 'win32') {
|
|
42
|
+
const signalGroup = (signal) => {
|
|
43
|
+
try {
|
|
44
|
+
process.kill(-child.pid, signal);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (error.code !== 'ESRCH')
|
|
48
|
+
throw error;
|
|
49
|
+
if (!closed)
|
|
50
|
+
child.kill(signal);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
signalGroup('SIGTERM');
|
|
54
|
+
// Escalate the group even if its wrapper exits first: a descendant may ignore SIGTERM.
|
|
55
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
56
|
+
signalGroup('SIGKILL');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
stopped = false;
|
|
61
|
+
}
|
|
62
|
+
// Terminate the tree before closing stdin: an EOF could make the wrapper exit first,
|
|
63
|
+
// leaving Windows taskkill with no parent from which to enumerate its descendants.
|
|
64
|
+
child.stdin?.destroy();
|
|
65
|
+
child.stdout?.destroy();
|
|
66
|
+
child.stderr?.destroy();
|
|
67
|
+
const released = await closure;
|
|
68
|
+
clearTimeout(closeTimer);
|
|
69
|
+
child.removeListener('close', onClose);
|
|
70
|
+
if (!released)
|
|
71
|
+
child.unref();
|
|
72
|
+
return stopped && released;
|
|
73
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.spawnSpecFor = spawnSpecFor;
|
|
4
|
+
// @implements A-SPEC-592
|
|
4
5
|
// @implements A-SPEC-499.1
|
|
5
6
|
/**
|
|
6
7
|
* Platform adapter for spawning a WIRING command verbatim. On win32, `npx` is `npx.cmd`, and a
|
|
@@ -14,7 +15,8 @@ exports.spawnSpecFor = spawnSpecFor;
|
|
|
14
15
|
* PURE — platform arrives as an argument so every branch is testable off-Windows.
|
|
15
16
|
*/
|
|
16
17
|
function spawnSpecFor(command, args, platform) {
|
|
17
|
-
|
|
18
|
+
const shim = /\.(cmd|bat)$/i.test(command) || /(?:^|[\\/])(npm|npx)$/i.test(command);
|
|
19
|
+
if (platform !== 'win32' || !shim)
|
|
18
20
|
return { command, args: [...args] };
|
|
19
21
|
const joined = [command, ...args].map((t) => (/\s/.test(t) ? `"${t}"` : t)).join(' ');
|
|
20
22
|
return { command: 'cmd.exe', args: ['/d', '/s', '/c', joined] };
|
|
@@ -23,3 +23,40 @@ export declare function posixOnly(reason: string, platform?: string): typeof it
|
|
|
23
23
|
* it belongs in the same choice rather than in a second early return inside the body.
|
|
24
24
|
*/
|
|
25
25
|
export declare function posixOnlyNonRoot(reason: string, platform?: string, uid?: number | undefined): typeof it | typeof it.skip;
|
|
26
|
+
/**
|
|
27
|
+
* @implements A-SPEC-597
|
|
28
|
+
* Whether THIS directory's filesystem folds case — measured, never inferred from the platform.
|
|
29
|
+
*
|
|
30
|
+
* macOS defaults to case-insensitive APFS and Linux to case-sensitive ext4, but neither is a rule:
|
|
31
|
+
* a case-sensitive volume mounts fine on macOS, and `os.tmpdir()` need not share the repository's
|
|
32
|
+
* volume. The same reasoning already governs the write gate (`pre-tool-use.ts`), which probes the
|
|
33
|
+
* project root rather than trusting `process.platform`; a test must probe the directory it actually
|
|
34
|
+
* writes into.
|
|
35
|
+
*
|
|
36
|
+
* An unanswerable probe returns FALSE. Claiming "folds" when it cannot tell would let a
|
|
37
|
+
* folding-world assertion run — and pass vacuously — on a case-sensitive box; claiming "sensitive"
|
|
38
|
+
* merely runs the other assertion, which fails loudly if it is wrong. Be wrong toward the side that
|
|
39
|
+
* still produces information.
|
|
40
|
+
*/
|
|
41
|
+
export declare function foldsCase(dir: string): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* @implements A-SPEC-597
|
|
44
|
+
* As `posixOnlyNonRoot`, and additionally skipped when `command` is not on PATH.
|
|
45
|
+
*
|
|
46
|
+
* `chflags` is macOS/BSD only. Guarded by platform and uid alone, the assertion that uses it RAN on
|
|
47
|
+
* Linux and died with ENOENT — which reads as a defect in the code under test rather than as a tool
|
|
48
|
+
* this machine does not have. Availability of the tool is knowable at definition time, exactly like
|
|
49
|
+
* the platform and the uid, so it belongs in the same choice.
|
|
50
|
+
*/
|
|
51
|
+
export declare function posixOnlyWithCommand(reason: string, command: string, platform?: string, uid?: number | undefined, lookup?: (command: string) => boolean): typeof it | typeof it.skip;
|
|
52
|
+
/**
|
|
53
|
+
* @implements A-SPEC-255
|
|
54
|
+
* The mirror of `posixOnly`: a test whose SUBJECT only exists on Windows.
|
|
55
|
+
*
|
|
56
|
+
* Without this, the Windows work had nowhere to put such a test and improvised
|
|
57
|
+
* `(process.platform === 'win32' ? it : it.skip)('…')` — a skip with no stated reason, which is the
|
|
58
|
+
* silence `posixOnly` was written to end, in different syntax. NTFS ACLs, PowerShell 5.1 parsing and
|
|
59
|
+
* 8.3 short names are real subjects; they simply cannot be exercised on a POSIX box, and saying so
|
|
60
|
+
* in the report is the honest form. `reason` is required for the same reason it is required there.
|
|
61
|
+
*/
|
|
62
|
+
export declare function win32Only(reason: string, platform?: string): typeof it | typeof it.skip;
|
|
@@ -1,8 +1,48 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
3
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
36
|
exports.posixOnly = posixOnly;
|
|
5
37
|
exports.posixOnlyNonRoot = posixOnlyNonRoot;
|
|
38
|
+
exports.foldsCase = foldsCase;
|
|
39
|
+
exports.posixOnlyWithCommand = posixOnlyWithCommand;
|
|
40
|
+
exports.win32Only = win32Only;
|
|
41
|
+
const fs = __importStar(require("node:fs"));
|
|
42
|
+
const path = __importStar(require("node:path"));
|
|
43
|
+
const node_crypto_1 = require("node:crypto");
|
|
44
|
+
// @implements A-SPEC-220.3
|
|
45
|
+
// @implements A-SPEC-597
|
|
6
46
|
/**
|
|
7
47
|
* A test that cannot run here says so, instead of returning early and being counted as a pass.
|
|
8
48
|
*
|
|
@@ -36,3 +76,88 @@ function posixOnlyNonRoot(reason, platform = process.platform, uid = typeof proc
|
|
|
36
76
|
const base = posixOnly(reason, platform);
|
|
37
77
|
return uid === 0 ? it.skip : base;
|
|
38
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* @implements A-SPEC-597
|
|
81
|
+
* Whether THIS directory's filesystem folds case — measured, never inferred from the platform.
|
|
82
|
+
*
|
|
83
|
+
* macOS defaults to case-insensitive APFS and Linux to case-sensitive ext4, but neither is a rule:
|
|
84
|
+
* a case-sensitive volume mounts fine on macOS, and `os.tmpdir()` need not share the repository's
|
|
85
|
+
* volume. The same reasoning already governs the write gate (`pre-tool-use.ts`), which probes the
|
|
86
|
+
* project root rather than trusting `process.platform`; a test must probe the directory it actually
|
|
87
|
+
* writes into.
|
|
88
|
+
*
|
|
89
|
+
* An unanswerable probe returns FALSE. Claiming "folds" when it cannot tell would let a
|
|
90
|
+
* folding-world assertion run — and pass vacuously — on a case-sensitive box; claiming "sensitive"
|
|
91
|
+
* merely runs the other assertion, which fails loudly if it is wrong. Be wrong toward the side that
|
|
92
|
+
* still produces information.
|
|
93
|
+
*/
|
|
94
|
+
function foldsCase(dir) {
|
|
95
|
+
if (typeof dir !== 'string' || dir === '')
|
|
96
|
+
return false;
|
|
97
|
+
const probe = path.join(dir, `.case-probe-${process.pid}-${(0, node_crypto_1.randomUUID)()}`);
|
|
98
|
+
try {
|
|
99
|
+
fs.writeFileSync(probe, '');
|
|
100
|
+
const flipped = path.join(dir, path.basename(probe).toUpperCase());
|
|
101
|
+
return fs.existsSync(flipped);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
// The probe leaves no trace, on every path out of here.
|
|
108
|
+
try {
|
|
109
|
+
fs.rmSync(probe, { force: true });
|
|
110
|
+
}
|
|
111
|
+
catch { /* nothing was created */ }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** PATH lookup without a shell — spawning one to ask about a command is a cost and a surface. */
|
|
115
|
+
function onPath(command) {
|
|
116
|
+
if (typeof command !== 'string' || command === '')
|
|
117
|
+
return false;
|
|
118
|
+
if (command.includes('/') || command.includes('\\'))
|
|
119
|
+
return fs.existsSync(command);
|
|
120
|
+
const sep = process.platform === 'win32' ? ';' : ':';
|
|
121
|
+
const exts = process.platform === 'win32' ? (process.env.PATHEXT ?? '.EXE;.CMD;.BAT').split(';') : [''];
|
|
122
|
+
for (const dir of (process.env.PATH ?? '').split(sep)) {
|
|
123
|
+
if (dir === '')
|
|
124
|
+
continue;
|
|
125
|
+
for (const ext of exts) {
|
|
126
|
+
try {
|
|
127
|
+
if (fs.existsSync(path.join(dir, command + ext)))
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
catch { /* unreadable PATH entry */ }
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* @implements A-SPEC-597
|
|
137
|
+
* As `posixOnlyNonRoot`, and additionally skipped when `command` is not on PATH.
|
|
138
|
+
*
|
|
139
|
+
* `chflags` is macOS/BSD only. Guarded by platform and uid alone, the assertion that uses it RAN on
|
|
140
|
+
* Linux and died with ENOENT — which reads as a defect in the code under test rather than as a tool
|
|
141
|
+
* this machine does not have. Availability of the tool is knowable at definition time, exactly like
|
|
142
|
+
* the platform and the uid, so it belongs in the same choice.
|
|
143
|
+
*/
|
|
144
|
+
function posixOnlyWithCommand(reason, command, platform = process.platform, uid = typeof process.getuid === 'function' ? process.getuid() : undefined, lookup = onPath) {
|
|
145
|
+
const base = posixOnlyNonRoot(reason, platform, uid); // reason is validated here, before anything else
|
|
146
|
+
return lookup(command) ? base : it.skip;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* @implements A-SPEC-255
|
|
150
|
+
* The mirror of `posixOnly`: a test whose SUBJECT only exists on Windows.
|
|
151
|
+
*
|
|
152
|
+
* Without this, the Windows work had nowhere to put such a test and improvised
|
|
153
|
+
* `(process.platform === 'win32' ? it : it.skip)('…')` — a skip with no stated reason, which is the
|
|
154
|
+
* silence `posixOnly` was written to end, in different syntax. NTFS ACLs, PowerShell 5.1 parsing and
|
|
155
|
+
* 8.3 short names are real subjects; they simply cannot be exercised on a POSIX box, and saying so
|
|
156
|
+
* in the report is the honest form. `reason` is required for the same reason it is required there.
|
|
157
|
+
*/
|
|
158
|
+
function win32Only(reason, platform = process.platform) {
|
|
159
|
+
if (typeof reason !== 'string' || reason.trim().length === 0) {
|
|
160
|
+
throw new Error('win32Only(reason): 사유 없는 스킵은 문법만 바뀐 침묵입니다 — 왜 이 단언이 POSIX에서 무의미한지 적으십시오.');
|
|
161
|
+
}
|
|
162
|
+
return platform === 'win32' ? it : it.skip;
|
|
163
|
+
}
|