@sabaiway/agent-workflow-kit 14.24.0 → 14.25.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.
package/tools/mcp.mjs CHANGED
@@ -16,7 +16,7 @@
16
16
  // would be a client error on every startup;
17
17
  // • merge-don't-clobber — foreign servers, foreign settings keys and existing allow rules are
18
18
  // preserved; re-apply adds nothing twice; the file's EOL is kept;
19
- // • a same-name entry that STRUCTURALLY DIFFERS is REFUSED unwritten (key order is ignored, so a
19
+ // • without `--replace`, a same-name entry that STRUCTURALLY DIFFERS is REFUSED unwritten (key order is ignored, so a
20
20
  // re-serialized identical entry is the SAME registration) — silently changing what an MCP server
21
21
  // launches is exactly what consent must not slide past; the recovery is named;
22
22
  // • the preflight is READ-ONLY: an absent `.claude/` is a NAMED state, and the dir is created on
@@ -32,11 +32,16 @@
32
32
  // Exit codes: 0 done / dry-run (incl. the hand-apply masked state); 1 precondition STOP; 2 usage.
33
33
  // Dependency-free beyond the kit's own exports, Node >= 22. No side effects on import.
34
34
 
35
- import { lstatSync } from 'node:fs';
35
+ import {
36
+ lstatSync, openSync, closeSync, fstatSync, writeFileSync, ftruncateSync, fsyncSync,
37
+ renameSync, constants,
38
+ } from 'node:fs';
36
39
  import { join, resolve } from 'node:path';
37
40
  import { fileURLToPath } from 'node:url';
38
41
  import { assertCreatableDirSafe, writeContainedFileAtomic } from './atomic-write.mjs';
39
42
  import { isDirectRun } from './direct-run.mjs';
43
+ import { describeNonRegular } from './fs-read-nofollow.mjs';
44
+ import { assertContainedRealPath } from './fs-safe.mjs';
40
45
  import {
41
46
  CLAUDE_DIR_REL,
42
47
  ENABLED_KEY,
@@ -67,16 +72,16 @@ const LF = '\n';
67
72
  const JSON_INDENT = 2;
68
73
 
69
74
  export const MCP_TOOL = fileURLToPath(import.meta.url);
70
- export const applyMcpCommand = (root) => `node ${q(MCP_TOOL)} --apply --cwd ${q(root)}`;
75
+ export const applyMcpCommand = (root, replace = false) => `node ${q(MCP_TOOL)}${replace ? ' --replace' : ''} --apply --cwd ${q(root)}`;
71
76
 
72
- const USAGE = `usage: mcp [--dry-run | --apply] [--cwd <dir>] [--help]
77
+ const USAGE = `usage: mcp [--dry-run | --apply] [--replace] [--cwd <dir>] [--help]
73
78
 
74
79
  Registers this kit's stdio MCP server in ONE project: the "${SERVER_NAME}" entry in
75
80
  ${MCP_JSON_REL}, and "${ENABLED_KEY}" + the two tool allow rules in ${SETTINGS_REL}.
76
81
  Default is --dry-run (a preview that prints the exact entry and writes nothing).
77
- --apply writes: ${MCP_JSON_REL} first, then ${SETTINGS_REL}; merge-don't-clobber, EOL kept.
82
+ --apply writes: ${MCP_JSON_REL} first, then ${SETTINGS_REL}; merge-don't-clobber, EOL kept. An existing target is written in place; --replace replaces a differing entry only with --apply.
78
83
 
79
- An existing "${SERVER_NAME}" entry that STRUCTURALLY DIFFERS is refused unwritten (key
84
+ Without --replace, an existing "${SERVER_NAME}" entry that STRUCTURALLY DIFFERS is refused unwritten (key
80
85
  order is ignored). Where ${MCP_JSON_REL} is a device node, FIFO or socket — an OS sandbox
81
86
  mask is the usual cause — the entry to merge is printed and nothing is written.
82
87
  Never writes settings.local.json; never commits.`;
@@ -122,7 +127,7 @@ const assertTargetUsable = (target, { maskedAllowed = false } = {}) => {
122
127
  }
123
128
  };
124
129
 
125
- export const preflightMcp = ({ cwd }, deps = {}) => {
130
+ export const preflightMcp = ({ cwd, replace = false }, deps = {}) => {
126
131
  const root = resolve(cwd ?? deps.cwd ?? process.cwd());
127
132
  // The ROOT is judged BEFORE the registration is read, not merely before the first write: reading
128
133
  // first would follow the very link this refuses, which is exactly what the shipped claim denies.
@@ -152,7 +157,7 @@ export const preflightMcp = ({ cwd }, deps = {}) => {
152
157
  }
153
158
  assertTargetUsable(registration.mcpJson, { maskedAllowed: true });
154
159
  assertTargetUsable(registration.settings);
155
- if (registration.mcpJson.differs) {
160
+ if (registration.mcpJson.differs && !replace) {
156
161
  throw makeMcpError(
157
162
  MCP_DIFFERS,
158
163
  `${MCP_JSON_REL} already carries an "${SERVER_NAME}" server entry that STRUCTURALLY DIFFERS from this kit copy's registration — refusing to change what it launches; review that entry and remove or rename it, then re-run`,
@@ -173,8 +178,53 @@ export const planMcp = (registration) => ({
173
178
 
174
179
  // ── the writer ─────────────────────────────────────────────────────────────────────────
175
180
 
176
- export const writeMcp = ({ cwd, dryRun = true } = {}, deps = {}) => {
177
- const preflight = preflightMcp({ cwd: cwd ?? deps.cwd ?? process.cwd() }, deps);
181
+ const writeTarget = (root, target, body, deps, stop) => {
182
+ const leaf = lstatNoFollow(target.abs, deps.lstat);
183
+ if (target.state === STATE.ABSENT) {
184
+ const assertAbsent = (dst) => {
185
+ if (lstatNoFollow(dst, deps.lstat) !== null) {
186
+ throw Object.assign(new Error('EEXIST'), { code: 'EEXIST' });
187
+ }
188
+ };
189
+ assertAbsent(target.abs);
190
+ const rename = (tmp, dst) => {
191
+ assertAbsent(dst);
192
+ (deps.rename ?? renameSync)(tmp, dst);
193
+ };
194
+ return writeContainedFileAtomic(root, target.abs, body, { ...deps, rename }, { stop, label: target.rel });
195
+ }
196
+ if (leaf?.isSymbolicLink()) {
197
+ throw stop(`${target.rel} is a symlink — refusing to replace it (a write would clobber the link target)`);
198
+ }
199
+ try {
200
+ assertContainedRealPath(root, target.abs, deps);
201
+ } catch (err) {
202
+ throw stop(String(err?.message ?? err).replace(/^\[agent-workflow-kit\] /, ''));
203
+ }
204
+ const flags = deps.constants ?? constants;
205
+ if (!flags.O_NOFOLLOW || !flags.O_NONBLOCK) {
206
+ throw stop('this platform exposes no usable O_NOFOLLOW|O_NONBLOCK open flags — refusing to open (fail closed)');
207
+ }
208
+ const fd = (deps.open ?? openSync)(target.abs, flags.O_WRONLY | flags.O_NOFOLLOW | flags.O_NONBLOCK);
209
+ try {
210
+ const stat = (deps.fstat ?? fstatSync)(fd);
211
+ if (!stat.isFile()) {
212
+ throw stop(`${target.rel} is a ${describeNonRegular(stat)} — refusing to write through it`);
213
+ }
214
+ if (leaf === null || stat.dev !== leaf.dev || stat.ino !== leaf.ino) {
215
+ throw stop('the leaf changed identity between lstat and open (fail closed)');
216
+ }
217
+ const bytes = Buffer.from(body, 'utf8');
218
+ (deps.writeFile ?? writeFileSync)(fd, bytes);
219
+ (deps.ftruncate ?? ftruncateSync)(fd, bytes.length);
220
+ (deps.fsync ?? fsyncSync)(fd);
221
+ } finally {
222
+ (deps.close ?? closeSync)(fd);
223
+ }
224
+ };
225
+
226
+ export const writeMcp = ({ cwd, dryRun = true, replace = false } = {}, deps = {}) => {
227
+ const preflight = preflightMcp({ cwd: cwd ?? deps.cwd ?? process.cwd(), replace }, deps);
178
228
  const base = { ...preflight, dryRun, wrote: false };
179
229
  if (preflight.masked) return { ...base, fragments: renderFragments(preflight.registration) };
180
230
  if (dryRun) return base;
@@ -182,7 +232,7 @@ export const writeMcp = ({ cwd, dryRun = true } = {}, deps = {}) => {
182
232
  const { root, registration, plan } = preflight;
183
233
  const stop = (message) => makeMcpError(MCP_SYMLINK, message);
184
234
  if (plan.writeMcpJson) {
185
- writeContainedFileAtomic(root, registration.mcpJson.abs, plan.mcpBody, deps, { stop, label: MCP_JSON_REL });
235
+ writeTarget(root, registration.mcpJson, plan.mcpBody, deps, stop);
186
236
  }
187
237
  if (plan.writeSettings) {
188
238
  // A settings failure AFTER the entry landed leaves a STANDING registration on disk that never
@@ -194,12 +244,12 @@ export const writeMcp = ({ cwd, dryRun = true } = {}, deps = {}) => {
194
244
  // The ONE write the preflight deliberately does not do: creating `.claude/` is a mutation, so
195
245
  // it belongs on the apply lane only — a preview that made a directory would not be a preview.
196
246
  assertCreatableDirSafe(join(root, CLAUDE_DIR_REL), deps, { stop, noun: SETTINGS_REL });
197
- writeContainedFileAtomic(root, registration.settings.abs, plan.settingsBody, deps, { stop, label: SETTINGS_REL });
247
+ writeTarget(root, registration.settings, plan.settingsBody, deps, stop);
198
248
  } catch (err) {
199
249
  // Reaching here always leaves a STANDING registration on disk — either this run wrote the
200
250
  // entry (`writeMcpJson`) or the preflight found it already current (`matches`), and those two
201
251
  // are exhaustive, so the note is unconditional rather than guarded by a branch nothing can
202
- // take. A differing entry never reaches the writer at all; it STOPs in the preflight.
252
+ // take. Without --replace, a differing entry STOPs in the preflight.
203
253
  throw Object.assign(err, { message: `${err.message}\n${HIDDEN_MODE_LINE}` });
204
254
  }
205
255
  }
@@ -221,7 +271,9 @@ const indented = (text) => text.trimEnd().split(LF).map((line) => ` ${line}`)
221
271
 
222
272
  const mcpJsonLine = (result) => {
223
273
  if (!result.plan.writeMcpJson) return ` - ${MCP_JSON_REL}: already current`;
224
- const verb = result.dryRun ? 'would add' : 'added';
274
+ const verb = result.registration.mcpJson.differs
275
+ ? (result.dryRun ? 'would replace' : 'replaced')
276
+ : (result.dryRun ? 'would add' : 'added');
225
277
  return ` - ${MCP_JSON_REL}: ${verb} the "${SERVER_NAME}" stdio entry`;
226
278
  };
227
279
 
@@ -264,24 +316,31 @@ export const formatResult = (result) => {
264
316
  : 'agent-workflow MCP registration — APPLY',
265
317
  mcpJsonLine(result),
266
318
  settingsLine(result),
267
- ' the entry this registration declares (re-serialized here; the same structured value goes into the file):',
319
+ ...(result.registration.mcpJson.differs ? [
320
+ result.dryRun ? ' the entry standing in .mcp.json now:' : ' the entry this run replaced in .mcp.json:',
321
+ indented(JSON.stringify(result.registration.mcpJson.existing, null, JSON_INDENT)),
322
+ ] : []),
323
+ result.dryRun
324
+ ? ' the entry this registration declares (re-serialized here; the same structured value goes into the file):'
325
+ : ' the entry now in .mcp.json (re-serialized here; the file holds the same structured value):',
268
326
  indented(JSON.stringify(result.registration.entry, null, JSON_INDENT)),
269
327
  POSTURE_LINE,
270
328
  HIDDEN_MODE_LINE,
271
329
  ];
272
- if (result.dryRun) lines.push(` to apply: ${applyMcpCommand(result.root)}`);
330
+ if (result.dryRun) lines.push(` to apply: ${applyMcpCommand(result.root, result.registration.mcpJson.differs)}`);
273
331
  return lines.join(LF);
274
332
  };
275
333
 
276
334
  // ── CLI ────────────────────────────────────────────────────────────────────────────────
277
335
 
278
336
  export const parseArgs = (argv) => {
279
- const opts = { dryRunFlag: false, apply: false, cwd: undefined, help: false };
337
+ const opts = { dryRunFlag: false, apply: false, replace: false, cwd: undefined, help: false };
280
338
  for (let i = 0; i < argv.length; i += 1) {
281
339
  const arg = argv[i];
282
340
  if (arg === '--help' || arg === '-h') opts.help = true;
283
341
  else if (arg === '--dry-run') opts.dryRunFlag = true;
284
342
  else if (arg === '--apply') opts.apply = true;
343
+ else if (arg === '--replace') opts.replace = true;
285
344
  else if (arg === '--cwd') {
286
345
  i += 1;
287
346
  // An EMPTY (or whitespace) value passes both guards above, and `resolve('')` silently means the
@@ -296,7 +355,7 @@ export const parseArgs = (argv) => {
296
355
  }
297
356
  }
298
357
  if (opts.dryRunFlag && opts.apply) throw fail(EXIT_USAGE, '--dry-run and --apply cannot be used together');
299
- return { help: opts.help, dryRun: !opts.apply, cwd: opts.cwd };
358
+ return { help: opts.help, dryRun: !opts.apply, replace: opts.replace, cwd: opts.cwd };
300
359
  };
301
360
 
302
361
  export const main = (argv = process.argv.slice(2), deps = {}) => {
@@ -308,10 +367,11 @@ export const main = (argv = process.argv.slice(2), deps = {}) => {
308
367
  log(USAGE);
309
368
  return EXIT_OK;
310
369
  }
311
- log(formatResult(writeMcp({ cwd: args.cwd ?? deps.cwd ?? process.cwd(), dryRun: args.dryRun }, deps)));
370
+ log(formatResult(writeMcp({ cwd: args.cwd ?? deps.cwd ?? process.cwd(), dryRun: args.dryRun, replace: args.replace }, deps)));
312
371
  return EXIT_OK;
313
372
  } catch (err) {
314
- errlog(err?.message ?? String(err));
373
+ const message = err?.message ?? String(err);
374
+ errlog(err?.code && err.name !== 'McpError' && !message.includes(err.code) ? `${err.code}: ${message}` : message);
315
375
  if (err?.exitCode === EXIT_USAGE) errlog(USAGE);
316
376
  return err?.exitCode ?? EXIT_PRECONDITION;
317
377
  }
@@ -107,7 +107,9 @@ const readTree = (root, entry, reports, directoryOnly = false) => {
107
107
  return entries;
108
108
  };
109
109
 
110
+ // The walk lists a directory before its children, so a kept directory's line covers the directories beneath it.
110
111
  const planSubtree = (carried, installed, orphans, reports) => {
112
+ const keptDirectories = [];
111
113
  for (const [path, kind] of installed) {
112
114
  if (carried.has(path)) {
113
115
  const packageKind = carried.get(path);
@@ -119,7 +121,10 @@ const planSubtree = (carried, installed, orphans, reports) => {
119
121
  }
120
122
  } else if (kind === 'file') {
121
123
  orphans.push(path);
124
+ } else if (kind === 'directory' && keptDirectories.some((kept) => path.startsWith(`${kept}/`))) {
125
+ continue;
122
126
  } else {
127
+ if (kind === 'directory') keptDirectories.push(path);
123
128
  reports.push({
124
129
  ok: true,
125
130
  line: `${PREFIX} kept ${path} (a ${kind} the package does not carry — yours to remove by hand)`,
@@ -48,7 +48,8 @@ import {
48
48
  SETTINGS_LOCAL_FILE,
49
49
  } from './velocity-profile.mjs';
50
50
  import { loadAutonomy, isSparseSeedConfig, AUTONOMY_REL } from './autonomy-config.mjs';
51
- import { deriveDoctorPlan } from './autonomy-doctor.mjs';
51
+ import { deriveDoctorPlan, TRUSTED_DIRS } from './autonomy-doctor.mjs';
52
+ import { laneOf, routeLine } from './write-lanes.mjs';
52
53
  import { detectBackends, findOnPath } from './detect-backends.mjs';
53
54
  import { isDirectRun } from './direct-run.mjs';
54
55
  import { ACTIVITIES, resolveActivityRecipe, composeReadiness, safeLine } from './recipes.mjs';
@@ -60,7 +61,6 @@ import { loadDeclaration, canonicalCheckerGates, coverageProducerPrecedes, isKit
60
61
  import { readRegularFileNoFollow } from './fs-read-nofollow.mjs';
61
62
  // The typed channel's READ-ONLY leaf only — never tools/mcp.mjs, which reaches the write core.
62
63
  import {
63
- MCP_JSON_REL,
64
64
  SERVER_NAME as MCP_SERVER_NAME,
65
65
  STATE as MCP_STATE,
66
66
  readRegistration,
@@ -103,7 +103,7 @@ import {
103
103
  import { ADOPTION, STORE_DIR_REL as SPEC_STORE_DIR_REL, SPEC_ADOPTION_LANE, declineFingerprint, readDeclineAck, surveySpecAdoption } from './spec-adoption.mjs';
104
104
  import { ENSURE_OPS } from './ensure-vocabulary.mjs';
105
105
  import { composeProfileGapScreen } from './profile-gap-screen.mjs';
106
- import { NO_APPLY_LINE, SKILL_PINS, claudeDirOf, connectLine, keySet, placeOf, printable, skillLine, skillState, skillTargets } from './jev-facts.mjs';
106
+ import { NO_APPLY_LINE, SKILL_PINS, claudeDirOf, keySet, placeOf, printable, skillLine, skillState, skillTargets } from './jev-facts.mjs';
107
107
 
108
108
  // The upgrade ensure that seeds the spec store — the not-adopted item's apply; pinned to the vocabulary.
109
109
  const SPEC_LAYER_ENSURE = ENSURE_OPS.includes('specs') ? 'specs' : null;
@@ -117,7 +117,7 @@ export { ACKS_FILE, ACKS_LANE_KEY, ACKS_WORKTREES_DIR_KEY, ACKS_COVERAGE_DOMAIN_
117
117
  const HERE = dirname(fileURLToPath(import.meta.url));
118
118
  const toolPath = (rel) => join(HERE, rel);
119
119
  const q = shellQuoteArg;
120
- const ENFORCEMENT_ABSENT_CODE = 'ENOENT';
120
+ const ABSENT_CODE = 'ENOENT';
121
121
  const ENFORCEMENT_KIT_ROOT = resolve(HERE, '..');
122
122
  const ENFORCEMENT_BUNDLE_ROOT = join(ENFORCEMENT_KIT_ROOT, 'references', 'scripts');
123
123
  const ENFORCEMENT_WHAT = 'the enforcement scripts are not deployed beside docs/ai (no Node evidence) \u2014 the offer copies them and installs the pre-commit hook';
@@ -155,6 +155,7 @@ export const SEVERITIES = Object.freeze({
155
155
  'autonomy-policy': SEVERITY_OPTIONAL,
156
156
  'autonomy-render': SEVERITY_ATTENTION,
157
157
  'sandbox-provision': SEVERITY_OPTIONAL,
158
+ 'sandbox-provision.installable': SEVERITY_OPTIONAL,
158
159
  'review-recipe': SEVERITY_ATTENTION,
159
160
  'gates-declaration': SEVERITY_OPTIONAL,
160
161
  'gates-inert': SEVERITY_ATTENTION,
@@ -185,12 +186,7 @@ export const SEVERITIES = Object.freeze({
185
186
  'read-lane.stale': SEVERITY_ATTENTION,
186
187
  'read-lane.missing': SEVERITY_ATTENTION,
187
188
  'state-block': SEVERITY_OPTIONAL,
188
- // The typed channel. The base arm is an ordinary offer; `.masked` stays an offer too (nothing is
189
- // broken — the kit simply cannot write through a device node, so the remedy is handed over);
190
- // `.differing` reports a CONFIGURED declaration that would launch something else, which is the
191
- // one state here a maintainer must actually look at.
192
189
  'mcp-channel': SEVERITY_OPTIONAL,
193
- 'mcp-channel.masked': SEVERITY_OPTIONAL,
194
190
  'mcp-channel.differing': SEVERITY_ATTENTION,
195
191
  agents: SEVERITY_OPTIONAL,
196
192
  // The `agents` row above is an OFFER to place vehicles. This one reports a config that ALREADY
@@ -276,7 +272,6 @@ export const WHATS = Object.freeze({
276
272
  'read-lane.missing': 'the gate hook is wired but its placed file is missing — every Bash call errors and the read-lane is dark; re-place it',
277
273
  'state-block': 'nothing checks the closing state block — a turn that ends on «nothing needed from you», or on a promise it never started, passes unseen',
278
274
  'mcp-channel': "the kit's read-only MCP server is not registered here — path questions and literal searches stay shell strings",
279
- 'mcp-channel.masked': '{rel} is a {className} here (a sandbox device mask is the usual cause), so the entry to merge is printed instead',
280
275
  'mcp-channel.differing': 'an "{server}" MCP entry is already declared here and DIFFERS from the registration this kit copy would write',
281
276
  agents: '{n} bundled subagent vehicle(s) not placed (Claude Code) — {ro} read-only, {ex} the full-tool executor; the apply PREVIEWS first',
282
277
  'executor-vehicle': '{n} slot(s) configured subagent but the executor vehicle is {state}{reason} — every such slot runs solo until it is usable',
@@ -507,12 +502,18 @@ const probeSandboxProvision = ({ root, deps, add, skip }) => {
507
502
  try {
508
503
  const p = probeSandboxAvailability(deps);
509
504
  if (p.available) return;
510
- const plan = deriveDoctorPlan({ probeResult: p, env: deps.env ?? process.env, isExec: deps.isExecutable ?? isExecutableFile });
511
- const variant = plan.tuple ? 'sandbox-provision.installable' : 'sandbox-provision';
512
- const reason = truncatedTo(oneLineOf(p.reason), templateBudget(WHATS[variant]) - (plan.tuple ? String(plan.tuple).length : 0));
505
+ const isExec = deps.isExecutable ?? isExecutableFile;
506
+ const plan = deriveDoctorPlan({ probeResult: p, env: deps.env ?? process.env, isExec });
507
+ const installable = plan.tuple !== null && plan.untrusted.length === 0 && !plan.envMissing
508
+ && ((deps.euid ?? (() => process.geteuid?.()))() === 0 || TRUSTED_DIRS.some((dir) => isExec(join(dir, 'sudo'))));
509
+ const variant = installable ? 'sandbox-provision.installable' : 'sandbox-provision';
510
+ const reason = truncatedTo(oneLineOf(p.reason), templateBudget(WHATS[variant]) - (installable ? plan.tuple.length : 0));
513
511
  // The doctor reads process.cwd() (deployment-gated) and takes no --cwd flag — the one-liner
514
512
  // pins the target project via a cd prefix (Segment B).
515
- add('sandbox-provision', fillTemplate(WHATS[variant], { reason, tuple: plan.tuple }), `cd ${q(root)} && node ${q(toolPath('autonomy-doctor.mjs'))}`);
513
+ add('sandbox-provision', fillTemplate(WHATS[variant], { reason, tuple: plan.tuple }),
514
+ `cd ${q(root)} && node ${q(toolPath('autonomy-doctor.mjs'))}`, variant,
515
+ installable ? null : 'on native Windows Claude Code runs no sandbox: this item takes no effect there',
516
+ );
516
517
  } catch (err) {
517
518
  skip('sandbox-provision', err);
518
519
  }
@@ -884,6 +885,7 @@ const probeCommitGuard = ({ root, deps, add, skip }) => {
884
885
  if (hooksPath === null) return; // not a git tree — nothing to hook
885
886
  const hook = (() => {
886
887
  try {
888
+ if (readsAbsent(deps, join(hooksPath, 'pre-commit'))) return null;
887
889
  return String(read(join(hooksPath, 'pre-commit'), 'utf8'));
888
890
  } catch {
889
891
  return null;
@@ -904,7 +906,7 @@ const readEnforcementNode = (path, lstat) => {
904
906
  try {
905
907
  return lstat(path);
906
908
  } catch (err) {
907
- if (err?.code === ENFORCEMENT_ABSENT_CODE) return null;
909
+ if (err?.code === ABSENT_CODE) return null;
908
910
  throw new Error(`${path}: ${err?.code ?? err?.message ?? err}`);
909
911
  }
910
912
  };
@@ -954,7 +956,7 @@ const probeReadLane = ({ root, deps, add, skip, shared }) => {
954
956
  // is silently dark; surface it as attention with a place-first recovery (council R2-M2).
955
957
  const placeRecovery = () =>
956
958
  add('read-lane', fillTemplate(WHATS['read-lane.missing'], {}), `node ${q(toolPath('gate-hook.mjs'))} --apply --cwd ${q(root)}`, 'read-lane.missing');
957
- if (!sg.filePlaced) {
959
+ if (!sg.filePlaced || readsAbsent(deps, join(root, GATE_HOOK_REL))) {
958
960
  placeRecovery();
959
961
  return;
960
962
  }
@@ -1099,9 +1101,9 @@ const probeExecutorVehicle = ({ root, deps, add, skip }) => {
1099
1101
  }),
1100
1102
  // The writer places a missing vehicle and re-derives a placed executor whose bytes differ from the derived body (an invalid
1101
1103
  // frontmatter or read-only tools included), so those states need no precondition; every precondition above is a refusal the apply would meet.
1102
- `${preconditions.length ? `HAND-APPLY: ${preconditions.join('; ')}, then run: ` : ''}node ${q(toolPath('cheap-agents.mjs'))} --apply --cwd ${q(root)}`,
1104
+ `node ${q(toolPath('cheap-agents.mjs'))} --apply --cwd ${q(root)}`,
1103
1105
  'executor-vehicle',
1104
- `hidden-mode deployments only: after the apply, run node ${q(toolPath('hide-footprint.mjs'))} --dir ${q(root)} --reconcile so the placed .claude/agents/ stays invisible to git status`,
1106
+ `${preconditions.length ? `precondition: ${preconditions.join('; ')}; ` : ''}hidden-mode deployments only: after the apply, run node ${q(toolPath('hide-footprint.mjs'))} --dir ${q(root)} --reconcile so the placed .claude/agents/ stays invisible to git status`,
1105
1107
  );
1106
1108
  } catch (err) {
1107
1109
  skip('executor-vehicle', err);
@@ -1234,7 +1236,17 @@ const isStateBlockGuardWired = (data) => {
1234
1236
  // itself (a hook that is not there belongs to the place offers, never to a refresh arm); a symlink,
1235
1237
  // a directory and an unreadable target all THROW into the probe's stated-skip lane.
1236
1238
  const HOOK_CURRENCY = Object.freeze({ CURRENT: 'current', STALE: 'stale', ABSENT: 'absent' });
1239
+ const readsAbsent = (deps, path) => {
1240
+ try {
1241
+ (deps.lstat ?? lstatSync)(path);
1242
+ return false;
1243
+ } catch (err) {
1244
+ return err?.code === ABSENT_CODE;
1245
+ }
1246
+ };
1247
+
1237
1248
  const readPlacedHookCurrency = (root, deps) => {
1249
+ if (readsAbsent(deps, join(root, GATE_HOOK_REL))) return HOOK_CURRENCY.ABSENT;
1238
1250
  const read = deps.readRegularFileNoFollow ?? readRegularFileNoFollow;
1239
1251
  const placed = read(join(root, GATE_HOOK_REL));
1240
1252
  if (placed.outcome === 'absent') return HOOK_CURRENCY.ABSENT;
@@ -1521,9 +1533,6 @@ export const probeAdrStore = ({ root, deps, add, skip }) => {
1521
1533
 
1522
1534
  // The typed-channel item. It asks the READ-ONLY registration leaf and nothing else — importing the
1523
1535
  // writer would pull the atomic-write core into the advisor's graph (read-graph-purity.test.mjs).
1524
- // The rendered apply is the mode's FLAGLESS preview on purpose: registering an MCP server means the
1525
- // client will RUN that command, so the entry is read before it is declared and the `--apply` stays
1526
- // the maintainer's separate step.
1527
1536
  const probeMcpChannel = ({ root, deps, add, skip }) => {
1528
1537
  try {
1529
1538
  const registration = readRegistration(root, deps);
@@ -1538,49 +1547,16 @@ const probeMcpChannel = ({ root, deps, add, skip }) => {
1538
1547
  // settings half can end the probe. A differing entry is fully observable, and an unreadable
1539
1548
  // settings file says nothing about it — returning on the settings mask first hid it.
1540
1549
  assertReadable(registration.mcpJson);
1541
- const settingsUsable = registration.settings.state !== MCP_STATE.MASKED
1542
- && registration.settings.state !== MCP_STATE.FOREIGN
1543
- && registration.settings.state !== MCP_STATE.UNREADABLE
1544
- && registration.settings.state !== MCP_STATE.MALFORMED;
1545
1550
  if (registration.mcpJson.differs) {
1546
- // The remedy is the maintainer's edit either way; the "then run" tail is dropped where that
1547
- // command could not succeed, so the item never hands over a line that exits 1.
1548
- const tail = settingsUsable ? `, then run ${preview}` : '';
1549
1551
  add(
1550
1552
  'mcp-channel',
1551
1553
  fillTemplate(WHATS['mcp-channel.differing'], { server: MCP_SERVER_NAME }),
1552
- `HAND-APPLY: edit ${q(join(root, MCP_JSON_REL))} → remove or rename the "${MCP_SERVER_NAME}" entry${tail}`,
1554
+ preview,
1553
1555
  'mcp-channel.differing',
1554
1556
  );
1555
1557
  return;
1556
1558
  }
1557
1559
  assertReadable(registration.settings);
1558
- // The HAND-APPLY arm renders the MODE's own preview, so it may fire only where that command can
1559
- // actually run — and the writer refuses a masked settings.json outright (it can neither write it
1560
- // nor merge into what it cannot read). A masked settings half is therefore a stated SKIP: the
1561
- // completeness this item decides on is unknowable, and offering a command that exits 1 is worse
1562
- // than saying nothing.
1563
- if (registration.settings.state === MCP_STATE.MASKED) {
1564
- skip('mcp-channel', new Error(`${registration.settings.rel} is a ${registration.settings.className} here (a sandbox device mask is the usual cause) — the registration cannot be judged or written from in here; verify it outside the sandbox`));
1565
- return;
1566
- }
1567
- if (registration.mcpJson.state === MCP_STATE.MASKED) {
1568
- // The kit cannot write through the mask either, so the remedy is HAND-APPLY. But when the
1569
- // settings half is already complete, the registration was almost certainly made from outside
1570
- // the sandbox: what cannot be observed becomes a stated SKIP (optimality withheld), never the
1571
- // same offer again on every single upgrade.
1572
- if (registration.settings.complete) {
1573
- skip('mcp-channel', new Error(`${registration.mcpJson.rel} is a ${registration.mcpJson.className} here (a sandbox device mask is the usual cause) and the settings half is already complete — verify the entry outside the sandbox`));
1574
- return;
1575
- }
1576
- add(
1577
- 'mcp-channel',
1578
- fillTemplate(WHATS['mcp-channel.masked'], { rel: registration.mcpJson.rel, className: registration.mcpJson.className }),
1579
- `HAND-APPLY: ${preview}`,
1580
- 'mcp-channel.masked',
1581
- );
1582
- return;
1583
- }
1584
1560
  if (registration.registered) return;
1585
1561
  add('mcp-channel', fillTemplate(WHATS['mcp-channel'], {}), preview);
1586
1562
  } catch (err) {
@@ -1589,16 +1565,16 @@ const probeMcpChannel = ({ root, deps, add, skip }) => {
1589
1565
  };
1590
1566
 
1591
1567
  // The Jev offer (contract: kit/jev-guide, part jev-offer). The key is judged first, through the facts leaf's one rule over
1592
- // the injected environment; the apply is the connect command the USER runs in a terminal of their own (HAND-APPLY), never a guide.
1568
+ // the injected environment.
1593
1569
  export const probeJevConnect = ({ root, deps, add, skip }) => {
1594
1570
  try {
1595
1571
  const env = deps.getenv ?? {};
1596
- if (keySet(env)) return;
1572
+ if (deps.keySet === true || keySet(env)) return;
1597
1573
  if (readAckValue(root, deps, ACKS_JEV_CONNECT_KEY) === JEV_CONNECT_DECLINE) return;
1598
1574
  const host = deps.jevHost ?? {};
1599
1575
  const place = placeOf({ env, platform: host.platform, hostname: host.hostname, container: host.container });
1600
1576
  const decline = `node ${q(toolPath('ack-write.mjs'))} --lane jev-connect --fingerprint ${JEV_CONNECT_DECLINE} --cwd ${q(root)}`;
1601
- add('jev-connect', WHATS['jev-connect'], `HAND-APPLY: ${connectLine(HERE, host.platform)}`, 'jev-connect',
1577
+ add('jev-connect', WHATS['jev-connect'], routeLine('jev-connect'), 'jev-connect',
1602
1578
  `${place ? `run the apply in ${place}; ` : ''}HAND-APPLY alternative (instead of the apply, never after it): decline the offer by recording it — ${decline}`);
1603
1579
  } catch (err) {
1604
1580
  skip('jev-connect', err);
@@ -1610,7 +1586,7 @@ export const probeJevConnect = ({ root, deps, add, skip }) => {
1610
1586
  export const probeJevSkill = ({ root, deps, add, skip }) => {
1611
1587
  try {
1612
1588
  const env = deps.getenv ?? {};
1613
- if (!keySet(env)) return;
1589
+ if (deps.keySet !== true && !keySet(env)) return;
1614
1590
  const host = deps.jevHost ?? {};
1615
1591
  if (typeof host.home !== 'string' || host.home === '') return;
1616
1592
  const pins = deps.jevPins ?? SKILL_PINS;
@@ -1710,14 +1686,29 @@ export const buildRecommendations = ({ cwd, deps = {} } = {}) => {
1710
1686
  skip(key, new Error(`item shape violation — ${problems.join('; ')}`));
1711
1687
  return false;
1712
1688
  }
1713
- items.push({ key, variant, severity: SEVERITIES[variant], what, benefit: BENEFITS[key], apply, detail });
1689
+ items.push({ key, variant, severity: SEVERITIES[variant], lane: laneOf(variant), what, benefit: BENEFITS[key], apply: routeLine(variant) ?? apply, detail });
1714
1690
  return true;
1715
1691
  };
1716
1692
  // The per-run scratch a probe uses to tell a LATER probe what it actually did. Written by exactly
1717
1693
  // one pair today (the marker-stale ⟷ read-lane.stale precedence) and read in the frozen PROBES
1718
1694
  // order, so the reader can never run first.
1719
1695
  const shared = {};
1720
- for (const probe of deps.probes ?? PROBES) probe({ root, deps, add, skip, shared });
1696
+ const contentDeps = Object.create(Object.getPrototypeOf(deps), {
1697
+ ...Object.getOwnPropertyDescriptors(deps),
1698
+ lstat: {
1699
+ enumerable: true,
1700
+ value: (path, ...args) => {
1701
+ const stat = (deps.lstat ?? deps.lstatSync ?? lstatSync)(path, ...args);
1702
+ if (stat.isCharacterDevice?.() || stat.isBlockDevice?.()) {
1703
+ throw Object.assign(new Error(`${ABSENT_CODE}: ${path}`), { code: ABSENT_CODE });
1704
+ }
1705
+ return stat;
1706
+ },
1707
+ },
1708
+ });
1709
+ for (const probe of deps.probes ?? PROBES) {
1710
+ probe({ root, deps: probe === probeMasksItem ? deps : contentDeps, add, skip, shared });
1711
+ }
1721
1712
  return { root, items, skips };
1722
1713
  };
1723
1714
 
@@ -1743,6 +1734,7 @@ export const formatRecommendations = ({ items, skips }) => {
1743
1734
  ordered.forEach((item, i) => {
1744
1735
  lines.push(`${i + 1}. ${SEVERITY_LABELS[item.severity] ?? SEVERITY_LABELS[SEVERITY_OPTIONAL]}: ${item.what}`);
1745
1736
  lines.push(` benefit: ${item.benefit}`);
1737
+ lines.push(` lane: ${item.lane}`);
1746
1738
  if (item.detail) lines.push(` recipe: ${item.detail}`);
1747
1739
  lines.push(` apply: ${item.apply}`);
1748
1740
  });
@@ -1776,7 +1768,7 @@ Read-only: never writes, never commits, never runs a subscription CLI. Exit code
1776
1768
  rendered (items or empty); 1 error; 2 usage.`;
1777
1769
 
1778
1770
  // The host facts of the key and skill offers: the platform, the hostname (a throw reads as none), /.dockerenv a regular file, the home.
1779
- const hostFacts = () => ({ platform: process.platform, hostname: (() => { try { return hostname(); } catch { return ''; } })(),
1771
+ export const hostFacts = () => ({ platform: process.platform, hostname: (() => { try { return hostname(); } catch { return ''; } })(),
1780
1772
  container: (() => { try { return statSync('/.dockerenv').isFile(); } catch { return false; } })(),
1781
1773
  home: (() => { try { return homedir(); } catch { return ''; } })() });
1782
1774