@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.
- package/CHANGELOG.md +69 -0
- package/README.md +1 -1
- package/SKILL.md +1 -1
- package/bridges/codex-cli-bridge/SKILL.md +1 -1
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +7 -7
- package/bridges/codex-cli-bridge/bin/codex-review.sh +1 -1
- package/bridges/codex-cli-bridge/capability.json +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/recipes.md +2 -0
- package/references/modes/recommendations.md +9 -10
- package/references/modes/set-recipe.md +1 -1
- package/references/modes/velocity.md +5 -6
- package/references/shared/report-footer.md +29 -32
- package/tools/ack-write.mjs +11 -11
- package/tools/apply-danger-check.mjs +19 -14
- package/tools/bridge-sandbox-recipe.mjs +102 -15
- package/tools/bridge-state-dirs.mjs +94 -0
- package/tools/bridge-wiring.mjs +196 -0
- package/tools/manifest/schema.md +18 -15
- package/tools/manifest/validate.mjs +11 -11
- package/tools/recipes.mjs +75 -15
- package/tools/recommendations.mjs +64 -118
- package/tools/velocity-profile.mjs +195 -119
- package/tools/write-lanes.mjs +0 -1
|
@@ -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
|
|
642
|
-
//
|
|
643
|
-
//
|
|
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
|
|
663
|
-
//
|
|
664
|
-
//
|
|
665
|
-
//
|
|
666
|
-
//
|
|
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
|
|
689
|
-
// (newline, CR, NUL, …) would break
|
|
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) =>
|
|
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
|
|
326
|
-
|
|
327
|
-
const
|
|
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}
|
|
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.
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
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
|
|
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 {
|
|
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,
|
|
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 —
|
|
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.
|
|
413
|
-
//
|
|
414
|
-
//
|
|
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, {
|
|
464
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
1194
|
-
//
|
|
1195
|
-
//
|
|
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', '
|
|
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
|
|
1450
|
-
//
|
|
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
|
|
1671
|
-
//
|
|
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
|
|
1763
|
-
|
|
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
|
|
1771
|
-
|
|
1772
|
-
|
|
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.`;
|