@sabaiway/agent-workflow-kit 14.26.0 → 14.28.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.
@@ -638,10 +638,10 @@ export const validateManifest = (skillDir) => {
638
638
  }
639
639
 
640
640
  // `networkHosts` (AD-044 Plan 4, consult-locked): the backend CLI's OBSERVED egress host
641
- // families. `velocity-profile --bridge-tier` seeds them into sandbox.network.allowedDomains for
642
- // placed bridges (AD-172; the pre-allow widens egress for EVERY sandboxed command, stated at that
643
- // consent), and a malformed list FAILS --strict like `settings`: the tier writes it and the
644
- // Recommendations advisor renders it verbatim, so a bad entry would corrupt a seeded line.
641
+ // families. `velocity-profile --bridge-tier` seeds them into sandbox.network.allowedDomains
642
+ // for USED bridges (a recipe slot uses them, revision 4; AD-172); the pre-allow widens egress for EVERY
643
+ // sandboxed command, stated at that consent. A malformed list FAILS --strict like `settings`:
644
+ // the tier writes it and the Recommendations advisor renders it verbatim, so a bad entry would corrupt a seeded line.
645
645
  const networkHosts = manifest.networkHosts;
646
646
  if (networkHosts != null) {
647
647
  if (!Array.isArray(networkHosts) || networkHosts.length === 0) {
@@ -659,11 +659,11 @@ export const validateManifest = (skillDir) => {
659
659
  }
660
660
  }
661
661
 
662
- // `writableDirs` (REC-UX-REWORK, D6): the backend CLI's writable state-dir declarations —
663
- // {env, default} entries the Recommendations advisor RESOLVES at run time (a NON-EMPTY env
664
- // value wins, else the default) and renders into the sandbox-lane recipe. Unlike `networkHosts`
665
- // this is a DOCUMENTATION source (the kit never seeds filesystem allowances), and a malformed
666
- // list FAILS --strict for the same reason: the resolved dir is rendered into a hand-applied line.
662
+ // `writableDirs` (REC-UX-REWORK, D6): {env, default} entries the tier RESOLVES (a NON-EMPTY
663
+ // env value wins, else the default) and seeds home-relative into sandbox.filesystem.allowWrite
664
+ // for USED bridges via `bridge-state-dirs.mjs`; the advisor's `bridge-tier` item counts and names them.
665
+ // A malformed list FAILS --strict: the resolved dir is written into the settings and rendered
666
+ // on the Recommendations advisor's ONE-LINE `recipe:` line.
667
667
  const writableDirs = manifest.writableDirs;
668
668
  if (writableDirs != null) {
669
669
  if (!Array.isArray(writableDirs) || writableDirs.length === 0) {
@@ -685,8 +685,8 @@ export const validateManifest = (skillDir) => {
685
685
  return;
686
686
  }
687
687
  if (/[*?[\]]/.test(dir)) errors.push(`${at}.default must not carry glob characters ("${dir}")`);
688
- // The resolved dir is rendered into a ONE-LINE hand-applied recipe — a control character
689
- // (newline, CR, NUL, …) would break the line or the shell paste.
688
+ // The resolved dir is written into `allowWrite` and rendered on a ONE-LINE advisor line —
689
+ // a control character (newline, CR, NUL, …) would break that line or the settings entry.
690
690
  // eslint-disable-next-line no-control-regex
691
691
  if (/[\x00-\x1f\x7f]/.test(dir)) errors.push(`${at}.default must not carry control characters (${JSON.stringify(dir)})`);
692
692
  if (dir.endsWith('/')) errors.push(`${at}.default must not end with a trailing slash ("${dir}")`);
package/tools/recipes.mjs CHANGED
@@ -40,6 +40,12 @@ export { ACTIVITIES, POLICY_ACTIVITIES, SLOT_RECIPES, isSwitchSlot, EXECUTOR_APP
40
40
 
41
41
  const [CODEX, AGY] = BACKEND_PRIORITY;
42
42
 
43
+ // Mirror bridge-wiring.mjs states: a static import would cycle through bridge-sandbox-recipe.mjs.
44
+ const WIRED = 'wired';
45
+ const UNWIRED = 'not wired';
46
+ const UNCHECKED = 'unchecked';
47
+ const NOT_COVERED = 'not covered';
48
+
43
49
  // Keyed by readiness-array provider name; executor is the only `carry` provider.
44
50
  export const BACKEND_ROLES = {
45
51
  [CODEX]: ['execute', 'review'],
@@ -237,6 +243,20 @@ export const recommendRecipe = (detection) => {
237
243
  return { recipe: 'solo', clause: `Solo — ${DISPLAY_ALIASES[best.name] ?? best.name}: ${remedy} to unlock Reviewed` };
238
244
  };
239
245
 
246
+ const orderedWiringEntries = (wiring) => [...(wiring?.bridges ?? [])]
247
+ .sort((a, b) => priorityIndex(a.bridge) - priorityIndex(b.bridge));
248
+
249
+ export const recommendWiredRecipe = (detection, wiring) => {
250
+ const base = recommendRecipe(detection);
251
+ const entries = orderedWiringEntries(wiring);
252
+ const uncovered = entries.filter((entry) => entry.review === NOT_COVERED)
253
+ .map((entry) => `${DISPLAY_ALIASES[entry.bridge] ?? entry.bridge} (review) — ${entry.reviewRoute}`).join('; ');
254
+ const unchecked = entries.filter((entry) => entry.review === UNCHECKED)
255
+ .map((entry) => DISPLAY_ALIASES[entry.bridge] ?? entry.bridge).join(', ');
256
+ const tail = (uncovered ? ` · ${UNWIRED}: ${uncovered}` : '') + (unchecked ? ` · ${UNCHECKED}: ${unchecked}` : '');
257
+ return tail ? { recipe: base.recipe, clause: base.clause + tail } : base;
258
+ };
259
+
240
260
  // ── activity procedures: per-slot recipe resolution ────────────────────────────────
241
261
  // The computed default for a silent slot. NEVER Council (opt-in: it spends two backends' quota) and
242
262
  // deliberately not recommendRecipe, which drives the status line rather than a per-slot default.
@@ -317,14 +337,26 @@ export const composeConfiguredPosture = (ctx = {}) => {
317
337
  // mode, and only the recipe lattice sees it.
318
338
  export const bridgeEntries = (readiness) => readiness.filter((b) => b.name !== EXECUTOR_PROVIDER);
319
339
 
320
- export const composeStatusLine = (detection, recommendation, settings = null, autonomy = null, posture = null) => {
340
+ export const composeStatusLine = (detection, recommendation, settings = null, autonomy = null, posture = null, wiring = null) => {
341
+ // A raw env value may carry newlines/control chars — collapse them so the one-line contract holds.
342
+ const oneLine = (s) => String(s).replace(/[\s]+/g, ' ').trim();
321
343
  const backends = bridgeEntries(detection)
322
344
  .sort((a, b) => priorityIndex(a.name) - priorityIndex(b.name))
323
- .map((b) => `${DISPLAY_ALIASES[b.name] ?? b.name} ${b.readiness === READY ? '✓' : '✗'} ${b.readiness}`)
345
+ .map((b) => {
346
+ const alias = DISPLAY_ALIASES[b.name] ?? b.name;
347
+ const entry = b.readiness === READY ? wiring?.bridges.find((row) => row.bridge === b.name) : null;
348
+ if (entry && entry.state !== WIRED) {
349
+ if (entry.state === UNWIRED) {
350
+ return `${alias} ✗ ${UNWIRED}${entry.used ? '' : ' (unused)'} — ${entry.count} sandbox entr(ies) missing`;
351
+ }
352
+ if (entry.state === UNCHECKED) return `${alias} ✗ ${UNCHECKED} — ${oneLine(entry.reason)}`;
353
+ }
354
+ return `${alias} ${b.readiness === READY ? '✓' : '✗'} ${b.readiness}`;
355
+ })
324
356
  .join(' · ');
325
- const base = `backends: ${backends} — run /agent-workflow-kit backends · recipes: ${recommendation.clause} — see /agent-workflow-kit recipes`;
326
- // A raw env value may carry newlines/control chars — collapse them so the one-line contract holds.
327
- const oneLine = (s) => String(s).replace(/[\s]+/g, ' ').trim();
357
+ const clause = wiring == null ? recommendation.clause : recommendWiredRecipe(detection, wiring).clause;
358
+ const base = `backends: ${backends} — run /agent-workflow-kit backends · recipes: ${clause} — see /agent-workflow-kit recipes`;
359
+ const sandboxSegment = wiring == null ? '' : ` · sandbox: ${oneLine(wiring.condition)}`;
328
360
  const active = settings?.active ?? [];
329
361
  // A RETIRED knob renders as retired: hiding the user's line would be a silent deletion, reading it
330
362
  // as active would claim a capability the wrapper no longer has.
@@ -332,7 +364,7 @@ export const composeStatusLine = (detection, recommendation, settings = null, au
332
364
  // Each optional segment renders ONLY when its facts are supplied; an omitted param changes nothing.
333
365
  const autonomySegment = autonomy == null ? '' : ` · autonomy: ${oneLine(formatAutonomySegment(autonomy))}`;
334
366
  const postureSegment = posture == null ? '' : ` · posture: ${oneLine(posture)}`;
335
- return base + suffix + autonomySegment + postureSegment;
367
+ return base + sandboxSegment + suffix + autonomySegment + postureSegment;
336
368
  };
337
369
 
338
370
  const formatAutonomySegment = (a) => {
@@ -432,7 +464,7 @@ export const composeActiveRecipeLine = ({ config, source } = {}, detection, auto
432
464
  // ── report + CLI ─────────────────────────────────────────────────────────────────
433
465
 
434
466
  // The structured report behind `--json`, incl. the same one-line status the --status-line mode emits.
435
- export const buildReport = (detection, settings = null, autonomy = null, posture = null) => {
467
+ export const buildReport = (detection, settings = null, autonomy = null, posture = null, wiring = null) => {
436
468
  const recommendation = recommendRecipe(detection);
437
469
  return {
438
470
  recipes: RECIPES.map(({ id, title, role, minBackends, degradesTo, summary }) => ({
@@ -446,19 +478,31 @@ export const buildReport = (detection, settings = null, autonomy = null, posture
446
478
  recommendation,
447
479
  plans: RECIPES.map((r) => planRecipe(r.id, detection)),
448
480
  // The --json envelope must never expose a status line staler than the --status-line surface.
449
- statusLine: composeStatusLine(detection, recommendation, settings, autonomy, posture),
481
+ statusLine: composeStatusLine(detection, recommendation, settings, autonomy, posture, wiring),
482
+ ...(wiring == null ? {} : { wiring }),
450
483
  };
451
484
  };
452
485
 
453
486
  // Deterministic human advisor text: the recipes, the recommendation, and the per-recipe plan here.
454
- export const formatRecipes = (detection) => {
487
+ export const formatRecipes = (detection, wiring = null) => {
455
488
  const lines = [
456
489
  'agent-workflow orchestration recipes (read-only — the orchestrator executes via the bridge skills or the executor vehicle and always commits)',
457
490
  '',
458
491
  ];
459
492
  for (const r of RECIPES) lines.push(` ${r.title} (${r.id}) — ${r.summary}`);
460
- const rec = recommendRecipe(detection);
461
- lines.push('', `recommended here: ${rec.recipe} — ${rec.clause}`, '', 'plan for the current environment:');
493
+ const rec = wiring == null ? recommendRecipe(detection) : recommendWiredRecipe(detection, wiring);
494
+ lines.push('', `recommended here: ${rec.recipe} — ${rec.clause}`);
495
+ for (const entry of orderedWiringEntries(wiring)) {
496
+ const alias = DISPLAY_ALIASES[entry.bridge] ?? entry.bridge;
497
+ if (entry.state === UNWIRED) {
498
+ const missing = [['excludedCommands', entry.missing.excludedCommands], ['allowedDomains', entry.missing.hosts], ['allowWrite', entry.missing.dirs]]
499
+ .filter(([, entries]) => entries.length)
500
+ .map(([key, entries]) => `${key} [${entries.join(', ')}]`).join('; ');
501
+ lines.push(` ${UNWIRED}: ${alias} — missing ${missing}; ${wiring.condition}; ${entry.route}`);
502
+ }
503
+ if (entry.state === UNCHECKED) lines.push(` ${UNCHECKED}: ${alias} — ${entry.reason}`);
504
+ }
505
+ lines.push('', 'plan for the current environment:');
462
506
  for (const r of RECIPES) {
463
507
  const p = planRecipe(r.id, detection);
464
508
  const arrow = p.degraded ? ` → ${p.effective}` : '';
@@ -493,6 +537,15 @@ export const composeReadiness = (cwd, deps = {}) => {
493
537
  return withVehicle(detection, survey);
494
538
  };
495
539
 
540
+ // The wiring the status line, --json and the default output render. Lazy imports: the leaf reaches
541
+ // this module through bridge-sandbox-recipe.mjs. A failed import fails the run loudly.
542
+ const composeWiring = async (cwd, readiness) => {
543
+ const { composeBridgeWiring } = await import('./bridge-wiring.mjs');
544
+ const { preflightVelocityProfile } = await import('./velocity-profile.mjs');
545
+ const root = projectTopOf(cwd);
546
+ return composeBridgeWiring({ root, readiness }, { preflight: () => preflightVelocityProfile({ cwd: root }) });
547
+ };
548
+
496
549
  const main = async (argv, deps = {}) => {
497
550
  if (argv.includes('--help') || argv.includes('-h')) {
498
551
  console.log(`recipes — read-only orchestration-recipe advisor for the agent-workflow family.
@@ -510,7 +563,13 @@ CONFIGURED recipe per activity/slot, resolved from the per-project docs/ai/orche
510
563
  from the current directory) + live readiness, with degradation stated and each activity's autonomy
511
564
  level beside its cells; paste it verbatim at session start / into the handover "Active recipes:" slot.
512
565
  --json emits the structured report (incl. the same line as \`statusLine\`); the three are mutually
513
- exclusive. Detection only — never writes, never commits, never runs a subscription CLI.`);
566
+ exclusive. Detection only — never writes, never commits, never runs a subscription CLI.
567
+
568
+ Where the project settings enable the sandbox, a ready bridge whose sandbox entries they lack
569
+ reads "<alias> ✗ not wired — <n> sandbox entr(ies) missing" ("not wired (unused)" when no slot
570
+ uses it), one that cannot be judged "<alias> ✗ unchecked — <reason>"; the status line carries
571
+ " · sandbox: <condition>" after the recipes pointer, and the default output lists the missing
572
+ entries and the route per bridge.`);
514
573
  return;
515
574
  }
516
575
  const unknown = argv.find((a) => !KNOWN_ARGS.has(a));
@@ -525,6 +584,7 @@ exclusive. Detection only — never writes, never commits, never runs a subscrip
525
584
  }
526
585
  const cwd = process.cwd();
527
586
  const readiness = composeReadiness(cwd, deps);
587
+ const wiring = argv.includes('--active-line') ? null : await composeWiring(cwd, readiness);
528
588
  if (argv.includes('--active-line')) {
529
589
  // Lazy: orchestration-config.mjs statically imports this module — no static cycle.
530
590
  const { loadConfig } = await import('./orchestration-config.mjs');
@@ -541,12 +601,12 @@ exclusive. Detection only — never writes, never commits, never runs a subscrip
541
601
  }
542
602
  } else if (argv.includes('--status-line')) {
543
603
  const snapshot = settingsSnapshot();
544
- console.log(composeStatusLine(readiness, recommendRecipe(readiness), snapshot, await composeAutonomyFacts(cwd), composeConfiguredPosture({ settings: snapshot })));
604
+ console.log(composeStatusLine(readiness, recommendRecipe(readiness), snapshot, await composeAutonomyFacts(cwd), composeConfiguredPosture({ settings: snapshot }), wiring));
545
605
  } else if (argv.includes('--json')) {
546
606
  const snapshot = settingsSnapshot();
547
- console.log(JSON.stringify(buildReport(readiness, snapshot, await composeAutonomyFacts(cwd), composeConfiguredPosture({ settings: snapshot })), null, 2));
607
+ console.log(JSON.stringify(buildReport(readiness, snapshot, await composeAutonomyFacts(cwd), composeConfiguredPosture({ settings: snapshot }), wiring), null, 2));
548
608
  }
549
- else console.log(formatRecipes(readiness));
609
+ else console.log(formatRecipes(readiness, wiring));
550
610
  return 0;
551
611
  };
552
612
 
@@ -19,11 +19,10 @@
19
19
  // below) and VERDICT-FIRST (D1): every non-optimal state opens with ONE composed verdict line.
20
20
  // Registry strings are frozen tool DATA, fact-true, one line under the shape cap (D2); posture/
21
21
  // risk prose lives in the mode doc at the consent moment (D3). A probe failure is a stated
22
- // skipped-item line — never a crash, never a fabricated item. sandbox.filesystem.allowWrite stays
23
- // HAND-APPLY territory; sandbox.network.allowedDomains is seeded only by the consented
24
- // `velocity-profile --bridge-tier` (AD-172); the sandbox-lane item's convergence is a NEUTRAL
25
- // fingerprint-bound acknowledgement recorded by the consent-gated ack writer into the family-owned
26
- // docs/ai/acks.json (AD-055 relocated it off the host settings schema), never a security key (D4).
22
+ // skipped-item line — never a crash, never a fabricated item. The consented
23
+ // `velocity-profile --bridge-tier` seeds used bridges' allow rules, <wrapper> * exclusions
24
+ // (codex-exec * where an execute slot delegates), hosts in sandbox.network.allowedDomains and
25
+ // state dirs in sandbox.filesystem.allowWrite; its recipe: line names the missing entries.
27
26
  //
28
27
  // Read-only: never writes, never commits, never runs a subscription CLI. The reused probes are all
29
28
  // exported read-only surfaces of their owning tools (velocity/autonomy/doctor/backends/recipes/
@@ -42,15 +41,13 @@ import {
42
41
  probeSandboxAvailability,
43
42
  isExecutableFile,
44
43
  readSettingsFile,
45
- BRIDGE_REVIEW_WRAPPERS,
46
- BRIDGE_REVIEW_MODE,
47
44
  SETTINGS_FILE,
48
45
  SETTINGS_LOCAL_FILE,
49
46
  } from './velocity-profile.mjs';
50
47
  import { loadAutonomy, isSparseSeedConfig, AUTONOMY_REL } from './autonomy-config.mjs';
51
48
  import { deriveDoctorPlan, TRUSTED_DIRS } from './autonomy-doctor.mjs';
52
49
  import { laneOf, applySlot } from './write-lanes.mjs';
53
- import { detectBackends, findOnPath } from './detect-backends.mjs';
50
+ import { detectBackends } from './detect-backends.mjs';
54
51
  import { isDirectRun } from './direct-run.mjs';
55
52
  import { ACTIVITIES, resolveActivityRecipe, composeReadiness, safeLine } from './recipes.mjs';
56
53
  import { surveyFamily, surveyGateHook, surveyAdrLayoutStrict } from './family-registry.mjs';
@@ -79,7 +76,8 @@ import { CHECKER_CLAIM, INITIAL_ADOPTION_REASON, SOURCE_SIZE_GATE_ID, classifySo
79
76
  import { resolveDeclaredDir, dirCovers, isResolvableDeclaredEntry } from './declared-paths.mjs';
80
77
  import { resolveGitHooksPath } from './commit-guard.mjs';
81
78
  import { loadConfig } from './orchestration-config.mjs';
82
- import { bundledSandboxRecipe } from './bridge-sandbox-recipe.mjs';
79
+ import { homeRelative } from './bridge-state-dirs.mjs';
80
+ import { sandboxEnabled, HOST_HONORS_QUALIFIER } from './bridge-wiring.mjs';
83
81
  import { assertContainedRealPath } from './fs-safe.mjs';
84
82
  import { loadWorktreesConfig, resolveProbeDir } from './worktrees.mjs';
85
83
  import { preflightCheapAgents, EXECUTOR_VEHICLE } from './cheap-agents.mjs';
@@ -197,7 +195,6 @@ export const SEVERITIES = Object.freeze({
197
195
  'adr-store-migration': SEVERITY_ATTENTION,
198
196
  'sandbox-masks': SEVERITY_OPTIONAL,
199
197
  'sandbox-masks.unfenced-mount': SEVERITY_ATTENTION,
200
- 'sandbox-lane': SEVERITY_OPTIONAL,
201
198
  'worktrees-dir': SEVERITY_OPTIONAL,
202
199
  // The layer is opt-in, so both arms are OFFERS under the frozen registry (attention is a CONFIGURED
203
200
  // declaration that is broken): an absent store offers the seed, a store with no live contract offers
@@ -248,7 +245,7 @@ export const composeVerdict = ({ attention, optional, skipped }) => {
248
245
  export const WHATS = Object.freeze({
249
246
  'velocity-core': 'routine read-only commands still prompt — {n} audited read-only allowlist entr(ies) not seeded',
250
247
  'kit-tools-tier': "the kit's own read-only tools still prompt — {n} kit-tools tier entr(ies) not seeded",
251
- 'bridge-tier': 'bridge-wrappers tier incomplete — {n} entr(ies) missing (allow rules, sandbox exclusions or declared hosts of placed bridges)',
248
+ 'bridge-tier': 'bridge-wrappers tier incomplete — {n} entr(ies) missing (allow rules, exclusions, hosts or state dirs of used bridges)',
252
249
  'autonomy-policy': 'no {path} — the computed defaults apply implicitly (red-lines ask/deny; every activity floors at prompt)',
253
250
  'autonomy-render': 'the declared autonomy policy is not rendered into .claude/settings.json — drift: {drift}',
254
251
  'sandbox-provision': 'the OS sandbox is unavailable: {reason}',
@@ -279,7 +276,6 @@ export const WHATS = Object.freeze({
279
276
  'adr-store-migration': 'still on the retired 3-tier ADR layout — {shape}',
280
277
  'sandbox-masks': '{n} sandbox mask(s) clutter git status — the managed exclude block is absent or stale',
281
278
  'sandbox-masks.stale-real': '{n} sandbox mask(s) clutter git status — the exclude block is stale; {m} fenced entr(ies) are REAL paths (a fresh apply drops them)',
282
- 'sandbox-lane': 'the wired review wrappers declare a session-sandbox recipe (egress hosts + writable state dirs) not yet acknowledged for this project',
283
279
  'worktrees-dir': 'write access to the worktrees parent dir {dir} is not confirmed — provision may still stop',
284
280
  'spec-adoption': 'feature-spec store absent (docs/ai/specs) — no feature contract can govern a plan here yet; seed the store, or record the decline',
285
281
  'profile-gap': '{what}',
@@ -324,7 +320,7 @@ export const BENEFITS = Object.freeze({
324
320
  'velocity-core': 'velocity — routine read-only commands stop prompting while the maintainer is away',
325
321
  'kit-tools-tier': "velocity — the kit's own read-only tools run promptless (audited, resolved-absolute tier)",
326
322
  'bridge-tier':
327
- 'velocity — placed review wrappers run code-mode council reviews promptless (plan/diff modes and delegated execution keep their prompt)',
323
+ 'velocity — used bridges get <wrapper> * exclusions (codex-exec * if delegated), hosts, state dirs (allowWrite); reviews: code-mode allows',
328
324
  'autonomy-policy': 'clarity — the per-activity autonomy policy becomes an explicit, versioned declaration instead of implicit computed defaults',
329
325
  'autonomy-render': `velocity — confined commands auto-allow per your declared policy; ${DUAL_SECURITY_BENEFIT}`,
330
326
  'sandbox-provision': `velocity — confined ad-hoc commands stop prompting; ${DUAL_SECURITY_BENEFIT}`,
@@ -343,7 +339,6 @@ export const BENEFITS = Object.freeze({
343
339
  'family-freshness': 'currency — placed family members carry the latest shipped fixes and features',
344
340
  'adr-store-migration': 'durability — every decision becomes its own file with a generated navigator, instead of one hand-rotated pile',
345
341
  'sandbox-masks': 'zero clutter — a mask git ignores through the block leaves git status; a mount-only one leaves git add -A and the review fingerprint too',
346
- 'sandbox-lane': 'discoverability — the manifest-declared observed sandbox recipe for bridge runs surfaces itself instead of waiting to be asked',
347
342
  'worktrees-dir': 'parallel features — the host-specific write allowance or terminal fallback is surfaced before provision',
348
343
  'spec-adoption': 'contracts — a plan names the contract it builds to, and a change to a governed slice is visible at review instead of after it',
349
344
  'profile-gap': 'full flow — each difference from the reference profile is previewed by its own writer; a declined one returns only at a lineage step',
@@ -371,7 +366,6 @@ export const OPT_IN_CAPABILITIES = Object.freeze([
371
366
  { id: 'kit-tools-tier', mode: 'velocity', advisorKey: 'kit-tools-tier' },
372
367
  { id: 'bridge-tier', mode: 'velocity', advisorKey: 'bridge-tier' },
373
368
  { id: 'autonomy-render', mode: 'velocity', advisorKey: 'autonomy-render' },
374
- { id: 'sandbox-lane', mode: 'velocity', advisorKey: 'sandbox-lane' },
375
369
  { id: 'autonomy-policy', mode: 'set-autonomy', advisorKey: 'autonomy-policy' },
376
370
  { id: 'sandbox-provision', mode: 'autonomy-doctor', advisorKey: 'sandbox-provision' },
377
371
  { id: 'gates-declaration', mode: 'gates', advisorKey: 'gates-declaration' },
@@ -409,9 +403,9 @@ export const OPT_IN_CAPABILITIES = Object.freeze([
409
403
  // state that makes a configured subagent carrier run solo.
410
404
  { id: 'executor-vehicle', mode: 'agents', advisorKey: 'executor-vehicle' },
411
405
  // Exempt, not un-audited. `acceptEdits` auto-applies Edit/Write and auto-runs mkdir/touch/mv/cp:
412
- // a TRUST-POSTURE change. The kit never nudges a user toward weakening their approval posture (the
413
- // same doctrine that keeps sandbox filesystem allowances HAND-APPLY); velocity presents the
414
- // full honest posture at its own consent moment, where the user is already deciding.
406
+ // a TRUST-POSTURE change. Velocity presents its full posture at its own consent moment;
407
+ // the bridge tier separately seeds used bridges' allow rules, <wrapper> * exclusions
408
+ // (codex-exec * where delegated), hosts and state dirs (allowWrite).
415
409
  {
416
410
  id: 'accept-edits',
417
411
  mode: 'velocity',
@@ -460,10 +454,41 @@ const probeVelocityItems = ({ root, deps, add, skip }) => {
460
454
  skip('kit-tools-tier', err);
461
455
  }
462
456
  try {
463
- const bt = planVelocityProfile(preflight, { bridgeTier: true, findWrapper: deps.findWrapper, bundleRoot: deps.bundleRoot, readdir: deps.readdir, readFile: deps.readFile });
464
- const delta = bt.bridgeToAdd.length + bt.excludedToAdd.length + bt.hostsToAdd.length;
457
+ const bt = planVelocityProfile(preflight, {
458
+ bridgeTier: true, findWrapper: deps.findWrapper,
459
+ bundleRoot: deps.bundleRoot, readdir: deps.readdir, readFile: deps.readFile,
460
+ detect: deps.detect, env: deps.getenv ?? deps.env, home: deps.home,
461
+ });
462
+ const delta = bt.bridgeToAdd.length + bt.excludedToAdd.length + bt.hostsToAdd.length + bt.dirsToAdd.length;
465
463
  if (delta > 0) {
466
- add('bridge-tier', fillTemplate(WHATS['bridge-tier'], { n: delta }), applyLine(' --bridge-tier'));
464
+ const entrySeparator = ', ';
465
+ const missing = [
466
+ ['allow rules', bt.bridgeToAdd],
467
+ ['exclusions', bt.excludedToAdd],
468
+ ['hosts', bt.hostsToAdd],
469
+ ['state dirs', bt.dirsToAdd],
470
+ ].filter(([, entries]) => entries.length > 0)
471
+ .map(([kind, entries]) => `${kind}: ${entries.join(entrySeparator)}`).join('; ');
472
+ const executeMissing = [...bt.executeToAdd.excludedCommands, ...bt.executeToAdd.hosts, ...bt.executeToAdd.dirs];
473
+ const executeDetail = executeMissing.length > 0
474
+ ? `; the execution wrapper codex-exec needs: ${executeMissing.join(entrySeparator)}`
475
+ : '';
476
+ const hostCondition = (() => {
477
+ try {
478
+ return sandboxEnabled(preflight.projectSettings.data, preflight.localSettings.data)
479
+ ? ` — once added, wired ${HOST_HONORS_QUALIFIER}`
480
+ : '';
481
+ } catch (err) {
482
+ return ` — host condition unchecked: ${oneLineOf(err?.message ?? String(err))}`;
483
+ }
484
+ })();
485
+ add(
486
+ 'bridge-tier',
487
+ fillTemplate(WHATS['bridge-tier'], { n: delta }),
488
+ applyLine(' --bridge-tier'),
489
+ 'bridge-tier',
490
+ `missing — ${missing}${executeDetail}${hostCondition}`,
491
+ );
467
492
  }
468
493
  } catch (err) {
469
494
  skip('bridge-tier', err);
@@ -1159,18 +1184,6 @@ const probeMasksItem = ({ root, deps, add, skip }) => {
1159
1184
  // knob is retired in the wrappers (recognized, arms nothing) and an oversized code review is now a
1160
1185
  // chunked feed with a per-part delivery proof, so there is nothing left to offer.
1161
1186
 
1162
- // D6 resolution, mirroring the wrappers' byte-semantics (`${VAR:-default}` + the exact case-arms:
1163
- // `~` / `~/…` / `/…` ride as-given; EVERY other form — including `~user/…`, which the wrappers
1164
- // never resolve as a home path — anchors like a relative path). The advisor anchors to the TARGET
1165
- // PROJECT ROOT (the pinned --cwd), matching what a wrapper invoked from the project root resolves
1166
- // (the documented dispatch form; the wrapper itself anchors to its invocation $PWD).
1167
- const resolveWritableDir = (entry, { env, root }) => {
1168
- const value = entry.env == null ? '' : (env[entry.env] ?? '');
1169
- if (value === '') return entry.default;
1170
- if (value === '~' || value.startsWith('~/') || value.startsWith('/')) return value;
1171
- return resolve(root, value);
1172
- };
1173
-
1174
1187
  // The NEUTRAL recipe fingerprint (D4): a hash over the resolved hosts ∪ dirs data — an
1175
1188
  // acknowledgement token, never a security key. Canonical form is HOME-SYMBOLIC: an
1176
1189
  // absolute dir under the resolved home canonicalizes BACK to its `~/…` form and tilde forms stay
@@ -1179,21 +1192,13 @@ const resolveWritableDir = (entry, { env, root }) => {
1179
1192
  // churns between them); only a genuinely-outside-home absolute override stays absolute
1180
1193
  // (machine-specific by nature). Any change to the recipe re-fires the item.
1181
1194
  export const recipeFingerprint = ({ hosts, dirs, home }) => {
1182
- const homeAbs = resolve(home);
1183
- const norm = (d) => {
1184
- if (d === '~') return '~';
1185
- if (d.startsWith('~/')) return `~/${d.slice(2)}`;
1186
- const abs = resolve(d);
1187
- if (abs === homeAbs) return '~';
1188
- return abs.startsWith(`${homeAbs}/`) ? `~/${abs.slice(homeAbs.length + 1)}` : abs;
1189
- };
1195
+ const norm = (dir) => homeRelative(dir, home);
1190
1196
  return factFingerprint(JSON.stringify({ hosts: [...hosts].sort(), dirs: [...new Set(dirs.map(norm))].sort() }));
1191
1197
  };
1192
1198
 
1193
- // The ack store (D4; AD-055 Part I) is the kit-owned PRIMARY ack channel; the legacy settings-scope
1194
- // keys below are read for one deprecation window. An ack lane exists for a state the maintainer can
1195
- // only ANSWER, never converge — a dead checker/producer pair is fixed, not acknowledged, so no lane
1196
- // names it. The store's path, keys, lane registry and reader are ack-store.mjs (re-exported above).
1199
+ // The ack store (D4; AD-055 Part I) is the kit-owned ack channel. An ack lane exists for a state the
1200
+ // maintainer can only ANSWER, never converge — a dead checker/producer pair is fixed, not acknowledged,
1201
+ // so no lane names it. The store's path, keys, lane registry and reader are ack-store.mjs.
1197
1202
 
1198
1203
  // The opt-in read-lane toggle file (AD-055 Part II) — the SAME kit-owned docs/ai/lanes.json the
1199
1204
  // placed hook reads live. The read-lane item offers to enable it once the hook is placed+wired.
@@ -1268,11 +1273,6 @@ const declarationCarriesMarker = (root, deps) => {
1268
1273
  return declaration.outcome === 'loaded' && declaration.gates.some((gate) => Object.hasOwn(gate, LCOV_PRODUCER_KEY));
1269
1274
  };
1270
1275
 
1271
- // The LEGACY neutral ack namespace (pre-AD-055): read from BOTH settings scopes until the next kit
1272
- // MAJOR (2.0.0) so a never-migrated host stays converged across the deprecation window (Decisions 3).
1273
- export const SANDBOX_LANE_ACK_PARENT = 'agentWorkflow';
1274
- export const SANDBOX_LANE_ACK_KEY = 'sandboxLaneAck';
1275
-
1276
1276
  // Read the opt-in read-lane toggle for the read-lane item. An ABSENT file (or absent docs/ai) →
1277
1277
  // false (the lane is off — offer it). `readLane === true` → enabled (converged). A parse/IO error on
1278
1278
  // an EXISTING file, a symlinked ancestor/leaf, an escape, or a non-object root THROWS — the probe
@@ -1303,7 +1303,7 @@ const readReadLaneToggle = (root, deps) => {
1303
1303
  // D3: the risk-marked keys — every key here has a per-item posture note in the mode doc, surfaced
1304
1304
  // at the consent moment; the static contract test asserts EXACT bidirectional coverage
1305
1305
  // (risk-marked keys == mode-doc note keys — a dropped note goes red, not silent).
1306
- export const RISK_NOTED_KEYS = Object.freeze(['bridge-tier', 'sandbox-lane', 'read-lane', 'worktrees-dir', 'adr-store-migration', 'gates-inert', 'source-size', 'gate-hook', 'mcp-channel', 'spec-adoption', 'enforcement', 'profile-gap', 'jev-connect', 'jev-skill']);
1306
+ export const RISK_NOTED_KEYS = Object.freeze(['bridge-tier', 'read-lane', 'worktrees-dir', 'adr-store-migration', 'gates-inert', 'source-size', 'gate-hook', 'mcp-channel', 'spec-adoption', 'enforcement', 'profile-gap', 'jev-connect', 'jev-skill']);
1307
1307
 
1308
1308
  // The feature-spec layer's adoption state (contract: kit/spec-adoption). The canon lets a plan cite
1309
1309
  // zero governing specs while a project adopts the layer, and nothing ever said whether adoption had
@@ -1336,63 +1336,6 @@ export const probeSpecAdoption = ({ root, deps, add, skip }) => {
1336
1336
  }
1337
1337
  };
1338
1338
 
1339
- const probeSandboxLane = ({ root, deps, add, skip }) => {
1340
- try {
1341
- const settings = readSettingsFile(join(root, SETTINGS_FILE), { ...deps, cwd: root });
1342
- const localSettings = readSettingsFile(join(root, SETTINGS_LOCAL_FILE), { ...deps, cwd: root });
1343
- const sandbox = settings.data?.sandbox;
1344
- const excluded = Array.isArray(sandbox?.excludedCommands) ? sandbox.excludedCommands : [];
1345
- const probePlaced = deps.findWrapper ?? ((cmd) => findOnPath(cmd, deps).state === 'present');
1346
- // Wired = the two-surface proof the tier wrote before AD-172 (the wrapper in the PROJECT
1347
- // excludedCommands + its code-mode allow rule, either scope) — surfacing the recipe before those
1348
- // exist would front-run the bridge-tier item (codex terminal). The hosts surface (AD-172) is
1349
- // deliberately not required: the recipe also carries the writable dirs no tier seeds, so it may
1350
- // render beside a bridge-tier item offering the hosts re-run. Byte-form from the tier's own constants.
1351
- const allowRules = [
1352
- ...(Array.isArray(settings.data?.permissions?.allow) ? settings.data.permissions.allow : []),
1353
- ...(Array.isArray(localSettings.data?.permissions?.allow) ? localSettings.data.permissions.allow : []),
1354
- ];
1355
- const wired = BRIDGE_REVIEW_WRAPPERS.filter(
1356
- (w) => excluded.includes(w) && probePlaced(w) && allowRules.includes(`Bash(${w} ${BRIDGE_REVIEW_MODE}:*)`),
1357
- );
1358
- if (wired.length === 0) return; // the tier's rules and exclusions are not wired — the bridge-tier item covers first
1359
- const { hosts, dirEntries } = bundledSandboxRecipe(wired, deps);
1360
- const env = deps.getenv ?? process.env;
1361
- const home = deps.home ?? homedir();
1362
- const dirs = [];
1363
- for (const entry of dirEntries) {
1364
- const resolved = resolveWritableDir(entry, { env, root });
1365
- if (!dirs.includes(resolved)) dirs.push(resolved);
1366
- }
1367
- const fingerprint = recipeFingerprint({ hosts, dirs, home });
1368
- // Convergence is the NEUTRAL fingerprint-bound acknowledgement: the item converges iff the
1369
- // CURRENT fingerprint equals the ack in ANY consulted store — the family-owned acks.json FIRST,
1370
- // then the legacy settings scopes; a stale value in one store is ignored when another matches
1371
- // (Decisions 2). A changed recipe (hosts, dirs, or an env override) re-fires the item (D4).
1372
- const acks = [
1373
- readAckValue(root, deps, ACKS_LANE_KEY),
1374
- settings.data?.[SANDBOX_LANE_ACK_PARENT]?.[SANDBOX_LANE_ACK_KEY],
1375
- localSettings.data?.[SANDBOX_LANE_ACK_PARENT]?.[SANDBOX_LANE_ACK_KEY],
1376
- ];
1377
- if (acks.includes(fingerprint)) return; // the acknowledged recipe — the item converged
1378
- // The item joins the CONSENT-GATED WRITER class (Decisions 4): the apply is the ack writer's
1379
- // PREVIEW one-liner (pure executable, cwd-independent), carrying the neutral fingerprint — never
1380
- // a security key. The LIVE recipe (egress hosts + resolved writable dirs) rides a separate
1381
- // rendered `recipe:` line (the fill source for the mode doc's lane-(2) hand-apply block); the
1382
- // fingerprint encodes it, so a changed recipe re-fires with a fresh command.
1383
- const recipe = `egress hosts [${hosts.join(', ')}]; writable state dirs [${dirs.join(', ')}] (observed-minimal; a blocked host names itself at run time)`;
1384
- add(
1385
- 'sandbox-lane',
1386
- fillTemplate(WHATS['sandbox-lane'], {}),
1387
- `node ${q(toolPath('ack-write.mjs'))} --fingerprint ${fingerprint} --cwd ${q(root)}`,
1388
- 'sandbox-lane',
1389
- recipe,
1390
- );
1391
- } catch (err) {
1392
- skip('sandbox-lane', err);
1393
- }
1394
- };
1395
-
1396
1339
  // D7 lane 1 — the DECLARATION confirmation. A settings entry is not proof of writable CAPABILITY
1397
1340
  // (runtime truth stays with the provision preflight's real create+delete probe); it is proof the
1398
1341
  // maintainer applied this item's own advice, which is what the item may converge on. Both scopes
@@ -1446,8 +1389,8 @@ const declaresWritableDir = (root, probeDir, deps) => {
1446
1389
  };
1447
1390
 
1448
1391
  // D7 lane 2 — the ack fallback for a host that ignores the settings key, bound to the PROBED DIR
1449
- // through the shared neutral fingerprint (the same home-symbolic canonicalization the sandbox-lane
1450
- // ack uses, so a home-anchored dir acks portably). The binding is to the RESOLVED probe dir, so the
1392
+ // through the worktrees-dir fingerprint's home-symbolic canonicalization, so a home-anchored dir
1393
+ // acks portably. The binding is to the RESOLVED probe dir, so the
1451
1394
  // item re-fires only when that resolved dir changes — two ABSENT parentDir values sharing an
1452
1395
  // existing ancestor resolve to the same dir and keep the same ack.
1453
1396
  export const worktreesDirFingerprint = (probeDir, home) => recipeFingerprint({ hosts: [], dirs: [probeDir], home });
@@ -1643,7 +1586,6 @@ const PROBES = Object.freeze([
1643
1586
  probeFamilyFreshness,
1644
1587
  probeAdrStore,
1645
1588
  probeMasksItem,
1646
- probeSandboxLane,
1647
1589
  probeWorktreesDir,
1648
1590
  probeMcpChannel,
1649
1591
  probeSpecAdoption,
@@ -1667,8 +1609,8 @@ export const buildRecommendations = ({ cwd, deps = {} } = {}) => {
1667
1609
  // smoke asserting a specific false-green never returns) can assert the exact outcome instead of
1668
1610
  // pattern-matching prose that is free to be reworded.
1669
1611
  // `detail` (optional) is an extra rendered `recipe:` line — factual context that is TOO LONG for
1670
- // the capped WHAT and does NOT belong in the pure-command apply (the sandbox-lane live recipe:
1671
- // egress hosts + resolved writable dirs; the worktrees-dir hand-apply-first grant advice; the
1612
+ // the capped WHAT and does NOT belong in the pure-command apply (the bridge-tier missing-entries
1613
+ // line; the worktrees-dir hand-apply-first grant advice; the
1672
1614
  // agents hidden-mode reconcile follow-up). Single-line like apply; absent for every other item.
1673
1615
  // Returns whether the item really RENDERED. One probe's disposition depends on another's having
1674
1616
  // spoken (the marker-stale ⟷ read-lane.stale precedence), and "the conditions still look right"
@@ -1759,17 +1701,21 @@ Usage:
1759
1701
 
1760
1702
  Computes the deterministic Recommendations section every kit upgrade ends with — VERDICT-FIRST:
1761
1703
  one composed verdict line opens every non-optimal render, then per item {severity · what is
1762
- sub-optimal · the benefit in one plain line · the lane · an optional \`recipe:\` line (the sandbox-lane live
1763
- recipe, the worktrees-dir hand-apply-first grant advice, or the agents hidden-mode reconcile
1704
+ sub-optimal · the benefit in one plain line · the lane · an optional \`recipe:\` line (the bridge-tier
1705
+ missing-entries line, the worktrees-dir hand-apply-first grant advice, or the agents hidden-mode reconcile
1764
1706
  follow-up) · the harness check line · the exact consent-gated apply one-liner}. --cwd is
1765
1707
  REQUIRED (the target project is explicit, never inferred from the shell's current directory). The
1766
1708
  section renders present-even-when-empty ("${RECOMMENDATIONS_EMPTY_LINE}"); a probe failure is a
1767
1709
  stated skipped-item line. Apply lines are cwd-independent (absolute tool paths, a pinned --cwd;
1768
1710
  the doctor item pins via a cd prefix; the ONE exception is the set-autonomy item — a
1769
1711
  conversational skill invocation labeled "run IN the target project") and preserve each writer's
1770
- own consent semantics; sandbox filesystem allowances stay HAND-APPLY territory, the placed bridges' declared hosts reach sandbox.network.allowedDomains only through the
1771
- consented velocity-profile --bridge-tier, and the sandbox-lane convergence is a neutral fingerprint acknowledgement recorded by
1772
- the consent-gated ack writer into docs/ai/acks.json (never a security key).
1712
+ own consent semantics. The consented velocity-profile --bridge-tier seeds used bridges' code-mode
1713
+ allow rules, <wrapper> * exclusions (codex-exec * where an execute slot delegates), hosts in
1714
+ sandbox.network.allowedDomains and state dirs in sandbox.filesystem.allowWrite. Where the host
1715
+ honors the settings sandbox keys, an excluded wrapper runs outside the sandbox, every sandboxed
1716
+ command of the project can reach each host, and every sandboxed command can write each state dir;
1717
+ plan/diff review modes and delegated execution keep their prompt. Items marked HAND-APPLY stay
1718
+ the user's own step.
1773
1719
 
1774
1720
  Read-only: never writes, never commits, never runs a subscription CLI. Exit codes: 0 report
1775
1721
  rendered (items or empty); 1 error; 2 usage.`;