@sabaiway/agent-workflow-kit 14.27.0 → 14.29.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 +64 -0
- package/README.md +2 -2
- package/SKILL.md +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 +4 -5
- package/references/shared/report-footer.md +29 -32
- package/tools/ack-write.mjs +11 -11
- package/tools/apply-danger-check.mjs +5 -2
- package/tools/bridge-sandbox-recipe.mjs +1 -1
- package/tools/bridge-wiring.mjs +196 -0
- package/tools/manifest/schema.md +12 -11
- package/tools/manifest/validate.mjs +11 -11
- package/tools/recipes.mjs +75 -15
- package/tools/recommendations.mjs +50 -92
- package/tools/velocity-profile.mjs +45 -16
- package/tools/write-lanes.mjs +0 -1
|
@@ -22,9 +22,7 @@
|
|
|
22
22
|
// skipped-item line — never a crash, never a fabricated item. The consented
|
|
23
23
|
// `velocity-profile --bridge-tier` seeds used bridges' allow rules, <wrapper> * exclusions
|
|
24
24
|
// (codex-exec * where an execute slot delegates), hosts in sandbox.network.allowedDomains and
|
|
25
|
-
// state dirs in sandbox.filesystem.allowWrite;
|
|
26
|
-
// fingerprint-bound acknowledgement recorded by the consent-gated ack writer into the family-owned
|
|
27
|
-
// docs/ai/acks.json (AD-055 relocated it off the host settings schema), never a security key (D4).
|
|
25
|
+
// state dirs in sandbox.filesystem.allowWrite; its recipe: line names the missing entries.
|
|
28
26
|
//
|
|
29
27
|
// Read-only: never writes, never commits, never runs a subscription CLI. The reused probes are all
|
|
30
28
|
// exported read-only surfaces of their owning tools (velocity/autonomy/doctor/backends/recipes/
|
|
@@ -43,15 +41,13 @@ import {
|
|
|
43
41
|
probeSandboxAvailability,
|
|
44
42
|
isExecutableFile,
|
|
45
43
|
readSettingsFile,
|
|
46
|
-
BRIDGE_REVIEW_WRAPPERS,
|
|
47
|
-
BRIDGE_REVIEW_MODE,
|
|
48
44
|
SETTINGS_FILE,
|
|
49
45
|
SETTINGS_LOCAL_FILE,
|
|
50
46
|
} from './velocity-profile.mjs';
|
|
51
47
|
import { loadAutonomy, isSparseSeedConfig, AUTONOMY_REL } from './autonomy-config.mjs';
|
|
52
48
|
import { deriveDoctorPlan, TRUSTED_DIRS } from './autonomy-doctor.mjs';
|
|
53
49
|
import { laneOf, applySlot } from './write-lanes.mjs';
|
|
54
|
-
import { detectBackends
|
|
50
|
+
import { detectBackends } from './detect-backends.mjs';
|
|
55
51
|
import { isDirectRun } from './direct-run.mjs';
|
|
56
52
|
import { ACTIVITIES, resolveActivityRecipe, composeReadiness, safeLine } from './recipes.mjs';
|
|
57
53
|
import { surveyFamily, surveyGateHook, surveyAdrLayoutStrict } from './family-registry.mjs';
|
|
@@ -80,8 +76,8 @@ import { CHECKER_CLAIM, INITIAL_ADOPTION_REASON, SOURCE_SIZE_GATE_ID, classifySo
|
|
|
80
76
|
import { resolveDeclaredDir, dirCovers, isResolvableDeclaredEntry } from './declared-paths.mjs';
|
|
81
77
|
import { resolveGitHooksPath } from './commit-guard.mjs';
|
|
82
78
|
import { loadConfig } from './orchestration-config.mjs';
|
|
83
|
-
import {
|
|
84
|
-
import {
|
|
79
|
+
import { homeRelative } from './bridge-state-dirs.mjs';
|
|
80
|
+
import { sandboxEnabled, HOST_HONORS_QUALIFIER } from './bridge-wiring.mjs';
|
|
85
81
|
import { assertContainedRealPath } from './fs-safe.mjs';
|
|
86
82
|
import { loadWorktreesConfig, resolveProbeDir } from './worktrees.mjs';
|
|
87
83
|
import { preflightCheapAgents, EXECUTOR_VEHICLE } from './cheap-agents.mjs';
|
|
@@ -199,7 +195,6 @@ export const SEVERITIES = Object.freeze({
|
|
|
199
195
|
'adr-store-migration': SEVERITY_ATTENTION,
|
|
200
196
|
'sandbox-masks': SEVERITY_OPTIONAL,
|
|
201
197
|
'sandbox-masks.unfenced-mount': SEVERITY_ATTENTION,
|
|
202
|
-
'sandbox-lane': SEVERITY_OPTIONAL,
|
|
203
198
|
'worktrees-dir': SEVERITY_OPTIONAL,
|
|
204
199
|
// The layer is opt-in, so both arms are OFFERS under the frozen registry (attention is a CONFIGURED
|
|
205
200
|
// declaration that is broken): an absent store offers the seed, a store with no live contract offers
|
|
@@ -249,6 +244,8 @@ export const composeVerdict = ({ attention, optional, skipped }) => {
|
|
|
249
244
|
// WHAT is never truncated: the leaf's suite pins every composed WHAT under the cap.
|
|
250
245
|
export const WHATS = Object.freeze({
|
|
251
246
|
'velocity-core': 'routine read-only commands still prompt — {n} audited read-only allowlist entr(ies) not seeded',
|
|
247
|
+
'velocity-core.rule': 'in auto mode a harness apply is judged instead of asked — the apply ask rule is not seeded',
|
|
248
|
+
'velocity-core.both': 'routine read-only commands still prompt, and in auto mode a harness apply is judged instead of asked — {n} entr(ies) not seeded',
|
|
252
249
|
'kit-tools-tier': "the kit's own read-only tools still prompt — {n} kit-tools tier entr(ies) not seeded",
|
|
253
250
|
'bridge-tier': 'bridge-wrappers tier incomplete — {n} entr(ies) missing (allow rules, exclusions, hosts or state dirs of used bridges)',
|
|
254
251
|
'autonomy-policy': 'no {path} — the computed defaults apply implicitly (red-lines ask/deny; every activity floors at prompt)',
|
|
@@ -281,7 +278,6 @@ export const WHATS = Object.freeze({
|
|
|
281
278
|
'adr-store-migration': 'still on the retired 3-tier ADR layout — {shape}',
|
|
282
279
|
'sandbox-masks': '{n} sandbox mask(s) clutter git status — the managed exclude block is absent or stale',
|
|
283
280
|
'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)',
|
|
284
|
-
'sandbox-lane': 'the wired review wrappers declare a session-sandbox recipe (egress hosts + writable state dirs) not yet acknowledged for this project',
|
|
285
281
|
'worktrees-dir': 'write access to the worktrees parent dir {dir} is not confirmed — provision may still stop',
|
|
286
282
|
'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',
|
|
287
283
|
'profile-gap': '{what}',
|
|
@@ -323,7 +319,7 @@ const templateBudget = (template) => ITEM_LINE_CAP - template.replace(/\{[A-Za-z
|
|
|
323
319
|
|
|
324
320
|
// ── the frozen benefit registry (fact-true; pinned by tests) ────────────────────────────────────
|
|
325
321
|
export const BENEFITS = Object.freeze({
|
|
326
|
-
'velocity-core': 'velocity — routine read-only commands stop prompting while the maintainer is away',
|
|
322
|
+
'velocity-core': 'velocity — routine read-only commands stop prompting while the maintainer is away; the harness asks before each harness apply',
|
|
327
323
|
'kit-tools-tier': "velocity — the kit's own read-only tools run promptless (audited, resolved-absolute tier)",
|
|
328
324
|
'bridge-tier':
|
|
329
325
|
'velocity — used bridges get <wrapper> * exclusions (codex-exec * if delegated), hosts, state dirs (allowWrite); reviews: code-mode allows',
|
|
@@ -345,7 +341,6 @@ export const BENEFITS = Object.freeze({
|
|
|
345
341
|
'family-freshness': 'currency — placed family members carry the latest shipped fixes and features',
|
|
346
342
|
'adr-store-migration': 'durability — every decision becomes its own file with a generated navigator, instead of one hand-rotated pile',
|
|
347
343
|
'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',
|
|
348
|
-
'sandbox-lane': 'discoverability — the manifest-declared observed sandbox recipe for bridge runs surfaces itself instead of waiting to be asked',
|
|
349
344
|
'worktrees-dir': 'parallel features — the host-specific write allowance or terminal fallback is surfaced before provision',
|
|
350
345
|
'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',
|
|
351
346
|
'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',
|
|
@@ -373,7 +368,6 @@ export const OPT_IN_CAPABILITIES = Object.freeze([
|
|
|
373
368
|
{ id: 'kit-tools-tier', mode: 'velocity', advisorKey: 'kit-tools-tier' },
|
|
374
369
|
{ id: 'bridge-tier', mode: 'velocity', advisorKey: 'bridge-tier' },
|
|
375
370
|
{ id: 'autonomy-render', mode: 'velocity', advisorKey: 'autonomy-render' },
|
|
376
|
-
{ id: 'sandbox-lane', mode: 'velocity', advisorKey: 'sandbox-lane' },
|
|
377
371
|
{ id: 'autonomy-policy', mode: 'set-autonomy', advisorKey: 'autonomy-policy' },
|
|
378
372
|
{ id: 'sandbox-provision', mode: 'autonomy-doctor', advisorKey: 'sandbox-provision' },
|
|
379
373
|
{ id: 'gates-declaration', mode: 'gates', advisorKey: 'gates-declaration' },
|
|
@@ -450,8 +444,10 @@ const probeVelocityItems = ({ root, deps, add, skip }) => {
|
|
|
450
444
|
// deliberately carries NO defensive catch (a dead branch is not honesty; the preflight catch
|
|
451
445
|
// above owns the real failure modes).
|
|
452
446
|
const core = planVelocityProfile(preflight, {});
|
|
453
|
-
if (core.toAdd.length > 0) {
|
|
454
|
-
|
|
447
|
+
if (core.toAdd.length > 0 || core.askToAdd.length > 0) {
|
|
448
|
+
const template = core.toAdd.length === 0 ? WHATS['velocity-core.rule']
|
|
449
|
+
: core.askToAdd.length === 0 ? WHATS['velocity-core'] : WHATS['velocity-core.both'];
|
|
450
|
+
add('velocity-core', fillTemplate(template, { n: core.toAdd.length + core.askToAdd.length }), applyLine(''));
|
|
455
451
|
}
|
|
456
452
|
try {
|
|
457
453
|
const kt = planVelocityProfile(preflight, { kitTools: true });
|
|
@@ -469,7 +465,34 @@ const probeVelocityItems = ({ root, deps, add, skip }) => {
|
|
|
469
465
|
});
|
|
470
466
|
const delta = bt.bridgeToAdd.length + bt.excludedToAdd.length + bt.hostsToAdd.length + bt.dirsToAdd.length;
|
|
471
467
|
if (delta > 0) {
|
|
472
|
-
|
|
468
|
+
const entrySeparator = ', ';
|
|
469
|
+
const missing = [
|
|
470
|
+
['allow rules', bt.bridgeToAdd],
|
|
471
|
+
['exclusions', bt.excludedToAdd],
|
|
472
|
+
['hosts', bt.hostsToAdd],
|
|
473
|
+
['state dirs', bt.dirsToAdd],
|
|
474
|
+
].filter(([, entries]) => entries.length > 0)
|
|
475
|
+
.map(([kind, entries]) => `${kind}: ${entries.join(entrySeparator)}`).join('; ');
|
|
476
|
+
const executeMissing = [...bt.executeToAdd.excludedCommands, ...bt.executeToAdd.hosts, ...bt.executeToAdd.dirs];
|
|
477
|
+
const executeDetail = executeMissing.length > 0
|
|
478
|
+
? `; the execution wrapper codex-exec needs: ${executeMissing.join(entrySeparator)}`
|
|
479
|
+
: '';
|
|
480
|
+
const hostCondition = (() => {
|
|
481
|
+
try {
|
|
482
|
+
return sandboxEnabled(preflight.projectSettings.data, preflight.localSettings.data)
|
|
483
|
+
? ` — once added, wired ${HOST_HONORS_QUALIFIER}`
|
|
484
|
+
: '';
|
|
485
|
+
} catch (err) {
|
|
486
|
+
return ` — host condition unchecked: ${oneLineOf(err?.message ?? String(err))}`;
|
|
487
|
+
}
|
|
488
|
+
})();
|
|
489
|
+
add(
|
|
490
|
+
'bridge-tier',
|
|
491
|
+
fillTemplate(WHATS['bridge-tier'], { n: delta }),
|
|
492
|
+
applyLine(' --bridge-tier'),
|
|
493
|
+
'bridge-tier',
|
|
494
|
+
`missing — ${missing}${executeDetail}${hostCondition}`,
|
|
495
|
+
);
|
|
473
496
|
}
|
|
474
497
|
} catch (err) {
|
|
475
498
|
skip('bridge-tier', err);
|
|
@@ -1177,10 +1200,9 @@ export const recipeFingerprint = ({ hosts, dirs, home }) => {
|
|
|
1177
1200
|
return factFingerprint(JSON.stringify({ hosts: [...hosts].sort(), dirs: [...new Set(dirs.map(norm))].sort() }));
|
|
1178
1201
|
};
|
|
1179
1202
|
|
|
1180
|
-
// The ack store (D4; AD-055 Part I) is the kit-owned
|
|
1181
|
-
//
|
|
1182
|
-
//
|
|
1183
|
-
// names it. The store's path, keys, lane registry and reader are ack-store.mjs (re-exported above).
|
|
1203
|
+
// The ack store (D4; AD-055 Part I) is the kit-owned ack channel. An ack lane exists for a state the
|
|
1204
|
+
// maintainer can only ANSWER, never converge — a dead checker/producer pair is fixed, not acknowledged,
|
|
1205
|
+
// so no lane names it. The store's path, keys, lane registry and reader are ack-store.mjs.
|
|
1184
1206
|
|
|
1185
1207
|
// The opt-in read-lane toggle file (AD-055 Part II) — the SAME kit-owned docs/ai/lanes.json the
|
|
1186
1208
|
// placed hook reads live. The read-lane item offers to enable it once the hook is placed+wired.
|
|
@@ -1255,11 +1277,6 @@ const declarationCarriesMarker = (root, deps) => {
|
|
|
1255
1277
|
return declaration.outcome === 'loaded' && declaration.gates.some((gate) => Object.hasOwn(gate, LCOV_PRODUCER_KEY));
|
|
1256
1278
|
};
|
|
1257
1279
|
|
|
1258
|
-
// The LEGACY neutral ack namespace (pre-AD-055): read from BOTH settings scopes until the next kit
|
|
1259
|
-
// MAJOR (2.0.0) so a never-migrated host stays converged across the deprecation window (Decisions 3).
|
|
1260
|
-
export const SANDBOX_LANE_ACK_PARENT = 'agentWorkflow';
|
|
1261
|
-
export const SANDBOX_LANE_ACK_KEY = 'sandboxLaneAck';
|
|
1262
|
-
|
|
1263
1280
|
// Read the opt-in read-lane toggle for the read-lane item. An ABSENT file (or absent docs/ai) →
|
|
1264
1281
|
// false (the lane is off — offer it). `readLane === true` → enabled (converged). A parse/IO error on
|
|
1265
1282
|
// an EXISTING file, a symlinked ancestor/leaf, an escape, or a non-object root THROWS — the probe
|
|
@@ -1290,7 +1307,7 @@ const readReadLaneToggle = (root, deps) => {
|
|
|
1290
1307
|
// D3: the risk-marked keys — every key here has a per-item posture note in the mode doc, surfaced
|
|
1291
1308
|
// at the consent moment; the static contract test asserts EXACT bidirectional coverage
|
|
1292
1309
|
// (risk-marked keys == mode-doc note keys — a dropped note goes red, not silent).
|
|
1293
|
-
export const RISK_NOTED_KEYS = Object.freeze(['bridge-tier', '
|
|
1310
|
+
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']);
|
|
1294
1311
|
|
|
1295
1312
|
// The feature-spec layer's adoption state (contract: kit/spec-adoption). The canon lets a plan cite
|
|
1296
1313
|
// zero governing specs while a project adopts the layer, and nothing ever said whether adoption had
|
|
@@ -1323,63 +1340,6 @@ export const probeSpecAdoption = ({ root, deps, add, skip }) => {
|
|
|
1323
1340
|
}
|
|
1324
1341
|
};
|
|
1325
1342
|
|
|
1326
|
-
const probeSandboxLane = ({ root, deps, add, skip }) => {
|
|
1327
|
-
try {
|
|
1328
|
-
const settings = readSettingsFile(join(root, SETTINGS_FILE), { ...deps, cwd: root });
|
|
1329
|
-
const localSettings = readSettingsFile(join(root, SETTINGS_LOCAL_FILE), { ...deps, cwd: root });
|
|
1330
|
-
const sandbox = settings.data?.sandbox;
|
|
1331
|
-
const excluded = Array.isArray(sandbox?.excludedCommands) ? sandbox.excludedCommands : [];
|
|
1332
|
-
const probePlaced = deps.findWrapper ?? ((cmd) => findOnPath(cmd, deps).state === 'present');
|
|
1333
|
-
// Wired = the two-surface proof the tier wrote before AD-172 (the wrapper in the PROJECT
|
|
1334
|
-
// excludedCommands + its code-mode allow rule, either scope) — surfacing the recipe before those
|
|
1335
|
-
// exist would front-run the bridge-tier item (codex terminal). The hosts surface (AD-172) is
|
|
1336
|
-
// deliberately not required: hosts and state dirs may still be missing, so the recipe may
|
|
1337
|
-
// render beside a bridge-tier item offering the re-run. Byte-form from the tier's own constants.
|
|
1338
|
-
const allowRules = [
|
|
1339
|
-
...(Array.isArray(settings.data?.permissions?.allow) ? settings.data.permissions.allow : []),
|
|
1340
|
-
...(Array.isArray(localSettings.data?.permissions?.allow) ? localSettings.data.permissions.allow : []),
|
|
1341
|
-
];
|
|
1342
|
-
const wired = BRIDGE_REVIEW_WRAPPERS.filter(
|
|
1343
|
-
(w) => (excluded.includes(w) || excluded.includes(`${w} *`)) && probePlaced(w) && allowRules.includes(`Bash(${w} ${BRIDGE_REVIEW_MODE}:*)`),
|
|
1344
|
-
);
|
|
1345
|
-
if (wired.length === 0) return; // the tier's rules and exclusions are not wired — the bridge-tier item covers first
|
|
1346
|
-
const { hosts, dirEntries } = bundledSandboxRecipe(wired, deps);
|
|
1347
|
-
const env = deps.getenv ?? process.env;
|
|
1348
|
-
const home = deps.home ?? homedir();
|
|
1349
|
-
const dirs = [];
|
|
1350
|
-
for (const entry of dirEntries) {
|
|
1351
|
-
const resolved = resolveWritableDir(entry, { env, root });
|
|
1352
|
-
if (!dirs.includes(resolved)) dirs.push(resolved);
|
|
1353
|
-
}
|
|
1354
|
-
const fingerprint = recipeFingerprint({ hosts, dirs, home });
|
|
1355
|
-
// Convergence is the NEUTRAL fingerprint-bound acknowledgement: the item converges iff the
|
|
1356
|
-
// CURRENT fingerprint equals the ack in ANY consulted store — the family-owned acks.json FIRST,
|
|
1357
|
-
// then the legacy settings scopes; a stale value in one store is ignored when another matches
|
|
1358
|
-
// (Decisions 2). A changed recipe (hosts, dirs, or an env override) re-fires the item (D4).
|
|
1359
|
-
const acks = [
|
|
1360
|
-
readAckValue(root, deps, ACKS_LANE_KEY),
|
|
1361
|
-
settings.data?.[SANDBOX_LANE_ACK_PARENT]?.[SANDBOX_LANE_ACK_KEY],
|
|
1362
|
-
localSettings.data?.[SANDBOX_LANE_ACK_PARENT]?.[SANDBOX_LANE_ACK_KEY],
|
|
1363
|
-
];
|
|
1364
|
-
if (acks.includes(fingerprint)) return; // the acknowledged recipe — the item converged
|
|
1365
|
-
// The item joins the CONSENT-GATED WRITER class (Decisions 4): the apply is the ack writer's
|
|
1366
|
-
// PREVIEW one-liner (pure executable, cwd-independent), carrying the neutral fingerprint — never
|
|
1367
|
-
// a security key. The LIVE recipe (egress hosts + resolved writable dirs) rides a separate
|
|
1368
|
-
// rendered `recipe:` line (the fill source for the mode doc's lane-(2) hand-apply block); the
|
|
1369
|
-
// fingerprint encodes it, so a changed recipe re-fires with a fresh command.
|
|
1370
|
-
const recipe = `egress hosts [${hosts.join(', ')}]; writable state dirs [${dirs.join(', ')}] (observed-minimal; a blocked host names itself at run time)`;
|
|
1371
|
-
add(
|
|
1372
|
-
'sandbox-lane',
|
|
1373
|
-
fillTemplate(WHATS['sandbox-lane'], {}),
|
|
1374
|
-
`node ${q(toolPath('ack-write.mjs'))} --fingerprint ${fingerprint} --cwd ${q(root)}`,
|
|
1375
|
-
'sandbox-lane',
|
|
1376
|
-
recipe,
|
|
1377
|
-
);
|
|
1378
|
-
} catch (err) {
|
|
1379
|
-
skip('sandbox-lane', err);
|
|
1380
|
-
}
|
|
1381
|
-
};
|
|
1382
|
-
|
|
1383
1343
|
// D7 lane 1 — the DECLARATION confirmation. A settings entry is not proof of writable CAPABILITY
|
|
1384
1344
|
// (runtime truth stays with the provision preflight's real create+delete probe); it is proof the
|
|
1385
1345
|
// maintainer applied this item's own advice, which is what the item may converge on. Both scopes
|
|
@@ -1433,8 +1393,8 @@ const declaresWritableDir = (root, probeDir, deps) => {
|
|
|
1433
1393
|
};
|
|
1434
1394
|
|
|
1435
1395
|
// D7 lane 2 — the ack fallback for a host that ignores the settings key, bound to the PROBED DIR
|
|
1436
|
-
// through the
|
|
1437
|
-
//
|
|
1396
|
+
// through the worktrees-dir fingerprint's home-symbolic canonicalization, so a home-anchored dir
|
|
1397
|
+
// acks portably. The binding is to the RESOLVED probe dir, so the
|
|
1438
1398
|
// item re-fires only when that resolved dir changes — two ABSENT parentDir values sharing an
|
|
1439
1399
|
// existing ancestor resolve to the same dir and keep the same ack.
|
|
1440
1400
|
export const worktreesDirFingerprint = (probeDir, home) => recipeFingerprint({ hosts: [], dirs: [probeDir], home });
|
|
@@ -1630,7 +1590,6 @@ const PROBES = Object.freeze([
|
|
|
1630
1590
|
probeFamilyFreshness,
|
|
1631
1591
|
probeAdrStore,
|
|
1632
1592
|
probeMasksItem,
|
|
1633
|
-
probeSandboxLane,
|
|
1634
1593
|
probeWorktreesDir,
|
|
1635
1594
|
probeMcpChannel,
|
|
1636
1595
|
probeSpecAdoption,
|
|
@@ -1654,8 +1613,8 @@ export const buildRecommendations = ({ cwd, deps = {} } = {}) => {
|
|
|
1654
1613
|
// smoke asserting a specific false-green never returns) can assert the exact outcome instead of
|
|
1655
1614
|
// pattern-matching prose that is free to be reworded.
|
|
1656
1615
|
// `detail` (optional) is an extra rendered `recipe:` line — factual context that is TOO LONG for
|
|
1657
|
-
// the capped WHAT and does NOT belong in the pure-command apply (the
|
|
1658
|
-
//
|
|
1616
|
+
// the capped WHAT and does NOT belong in the pure-command apply (the bridge-tier missing-entries
|
|
1617
|
+
// line; the worktrees-dir hand-apply-first grant advice; the
|
|
1659
1618
|
// agents hidden-mode reconcile follow-up). Single-line like apply; absent for every other item.
|
|
1660
1619
|
// Returns whether the item really RENDERED. One probe's disposition depends on another's having
|
|
1661
1620
|
// spoken (the marker-stale ⟷ read-lane.stale precedence), and "the conditions still look right"
|
|
@@ -1746,8 +1705,8 @@ Usage:
|
|
|
1746
1705
|
|
|
1747
1706
|
Computes the deterministic Recommendations section every kit upgrade ends with — VERDICT-FIRST:
|
|
1748
1707
|
one composed verdict line opens every non-optimal render, then per item {severity · what is
|
|
1749
|
-
sub-optimal · the benefit in one plain line · the lane · an optional \`recipe:\` line (the
|
|
1750
|
-
|
|
1708
|
+
sub-optimal · the benefit in one plain line · the lane · an optional \`recipe:\` line (the bridge-tier
|
|
1709
|
+
missing-entries line, the worktrees-dir hand-apply-first grant advice, or the agents hidden-mode reconcile
|
|
1751
1710
|
follow-up) · the harness check line · the exact consent-gated apply one-liner}. --cwd is
|
|
1752
1711
|
REQUIRED (the target project is explicit, never inferred from the shell's current directory). The
|
|
1753
1712
|
section renders present-even-when-empty ("${RECOMMENDATIONS_EMPTY_LINE}"); a probe failure is a
|
|
@@ -1760,8 +1719,7 @@ sandbox.network.allowedDomains and state dirs in sandbox.filesystem.allowWrite.
|
|
|
1760
1719
|
honors the settings sandbox keys, an excluded wrapper runs outside the sandbox, every sandboxed
|
|
1761
1720
|
command of the project can reach each host, and every sandboxed command can write each state dir;
|
|
1762
1721
|
plan/diff review modes and delegated execution keep their prompt. Items marked HAND-APPLY stay
|
|
1763
|
-
the user's own step.
|
|
1764
|
-
the consent-gated ack writer into docs/ai/acks.json (never a security key).
|
|
1722
|
+
the user's own step.
|
|
1765
1723
|
|
|
1766
1724
|
Read-only: never writes, never commits, never runs a subscription CLI. Exit codes: 0 report
|
|
1767
1725
|
rendered (items or empty); 1 error; 2 usage.`;
|
|
@@ -13,10 +13,12 @@ import { SHELL_METACHARACTERS, hasShellMetacharacter, isSeedablePathToken } from
|
|
|
13
13
|
import { isDirectRun } from './direct-run.mjs';
|
|
14
14
|
import { compareSemver } from './semver-lite.mjs';
|
|
15
15
|
import { BRIDGE_REVIEW_WRAPPERS, BRIDGE_REVIEW_MODE, KIT_GROUNDING_TOOL, usedBridges, bundledSandboxRecipe, exclusionOf } from './bridge-sandbox-recipe.mjs';
|
|
16
|
-
import { stateDirsOf,
|
|
16
|
+
import { stateDirsOf, mergeAllowWrite } from './bridge-state-dirs.mjs';
|
|
17
|
+
import { HOST_HONORS_QUALIFIER, sandboxSurfaceDelta } from './bridge-wiring.mjs';
|
|
17
18
|
import { loadConfig } from './orchestration-config.mjs';
|
|
18
19
|
import { validateManifest } from './manifest/validate.mjs';
|
|
19
20
|
export { BRIDGE_REVIEW_WRAPPERS, BRIDGE_REVIEW_MODE, KIT_GROUNDING_TOOL };
|
|
21
|
+
export { HOST_HONORS_QUALIFIER };
|
|
20
22
|
// The declared-path resolution + segment containment the allowWrite degrade shares with the
|
|
21
23
|
// advisor's worktrees-dir convergence lane — ONE leaf, so the two readings cannot drift.
|
|
22
24
|
import { resolveDeclaredDir, dirCovers, isResolvableDeclaredEntry } from './declared-paths.mjs';
|
|
@@ -37,6 +39,8 @@ export const WORKFLOW_STAMP = 'docs/ai/.workflow-version';
|
|
|
37
39
|
export const SAFE_DEFAULT_MODES = Object.freeze(['default', 'acceptEdits', 'plan']);
|
|
38
40
|
export const UNSAFE_BYPASS_MODE = 'bypassPermissions';
|
|
39
41
|
export const ACCEPT_EDITS_MODE = 'acceptEdits';
|
|
42
|
+
// The harness asks before every harness apply; an ask rule allows nothing.
|
|
43
|
+
export const APPLY_ASK_RULE = 'Bash(node *apply-danger-check.mjs*--apply*)';
|
|
40
44
|
|
|
41
45
|
// The audited read-only core. Every entry is a Claude Code Bash allow pattern whose command runs NO
|
|
42
46
|
// mutating operation and NO inline arbitrary code execution through its OWN flags (verified
|
|
@@ -303,18 +307,19 @@ const DO_NOT_ADD_WARNING = 'do not add';
|
|
|
303
307
|
const ADD_BY_HAND = true;
|
|
304
308
|
const MUTATING_SCRIPT_NAME_PATTERN = /(release|publish|deploy|push|version|commit|tag)/iu;
|
|
305
309
|
const MUTATING_SCRIPT_HOOK_PATTERN = /^(pre|post)/iu;
|
|
310
|
+
const CODEX_BRIDGE = 'codex-cli-bridge';
|
|
311
|
+
const EXECUTE_ROLE = 'execute';
|
|
306
312
|
// The one thing velocity must never seed — an explicit, greppable expression of the load-bearing
|
|
307
313
|
// invariant (kept deliberately even though the read-only screen already rejects these, so the
|
|
308
314
|
// refusal is named, tested, and produces a clear message).
|
|
309
315
|
const MUTATING_ALLOW_COMMAND_PATTERN = /^(?:git\s+(?:commit|push)|npm\s+publish)(?:\s|$)/iu;
|
|
310
316
|
// Whether a host APPLIES a `sandbox.*` settings key is not knowable from here: a settings-native
|
|
311
|
-
// host honors it, while an IDE/session-imposed sandbox was observed ignoring the
|
|
312
|
-
//
|
|
317
|
+
// host honors it, while an IDE/session-imposed sandbox was observed ignoring the security keys
|
|
318
|
+
// in BOTH scopes (the advisor records the same limit in
|
|
313
319
|
// references/modes/recommendations.md). So every claim about the RUNTIME EFFECT of a settings key —
|
|
314
320
|
// the tier's exclusion routing and every weakening detail alike — carries this qualifier. The
|
|
315
321
|
// CLASSIFICATION is untouched: which red-line a key would weaken, and that a proven tier exclusion
|
|
316
322
|
// is a note rather than a weakening, are properties of the DECLARATION, not of a host.
|
|
317
|
-
export const HOST_HONORS_QUALIFIER = 'where the host honors the settings sandbox keys';
|
|
318
323
|
export const HOST_HONORS_NOTICE =
|
|
319
324
|
`host-conditional: whether a host applies the sandbox.* settings keys is not knowable from here — a settings-native host honors them, a harness-managed session sandbox may ignore them in BOTH scopes, so each runtime effect above is stated "${HOST_HONORS_QUALIFIER}" rather than promised; what a key would weaken is a property of the declaration and is stated flat.`;
|
|
320
325
|
|
|
@@ -332,6 +337,8 @@ absolute path (args wildcard), run-gates.mjs as ONE exact project-root-pinned by
|
|
|
332
337
|
(project-exec - it runs YOUR declared gates.json), source-size-check.mjs as ONE exact --check
|
|
333
338
|
byte-string (its read-only mode only - its writers keep prompting), and the writers' exact arg-free
|
|
334
339
|
dry-run preview byte-strings. Never touches settings.local.json.
|
|
340
|
+
Every apply also appends the apply ask rule ${APPLY_ASK_RULE} to permissions.ask
|
|
341
|
+
once, so the harness asks before every harness apply; it allows nothing.
|
|
335
342
|
--bridge-tier (own consent) derives each entry for a used bridge from the effective project recipes:
|
|
336
343
|
review wrappers' CODE mode (codex-review code, agy-review code), the quoted grounding pre-step rule,
|
|
337
344
|
and <wrapper> * exclusions in sandbox.excludedCommands. An execute slot delegating to codex adds
|
|
@@ -405,8 +412,8 @@ const validateSettingsShape = (data, relPath) => {
|
|
|
405
412
|
}
|
|
406
413
|
// The --autonomy render merges into permissions.ask / permissions.deny (arrays) and the top-level
|
|
407
414
|
// `sandbox` object — a malformed one of these is a STOP with ZERO writes, never a merge-through-clobber
|
|
408
|
-
// (the same precondition posture as permissions.allow above). The allowlist mode
|
|
409
|
-
// so
|
|
415
|
+
// (the same precondition posture as permissions.allow above). The allowlist mode also appends the
|
|
416
|
+
// apply ask rule to permissions.ask, so its shape is a precondition for both paths.
|
|
410
417
|
if (data.permissions?.ask !== undefined && !Array.isArray(data.permissions.ask)) {
|
|
411
418
|
throw makeVelocityProfileError(VELOCITY_MALFORMED, `${relPath}: permissions.ask must be an array`);
|
|
412
419
|
}
|
|
@@ -584,14 +591,17 @@ const assertTargetWritable = (absPath, deps = {}) => {
|
|
|
584
591
|
|
|
585
592
|
const formatJson = (data, eol) => `${JSON.stringify(data, null, SETTINGS_JSON_INDENT).replace(JSON_NEWLINE_PATTERN, eol)}${eol}`;
|
|
586
593
|
|
|
587
|
-
const mergeProjectSettings = (projectData, toAdd, acceptEdits, excludedToAdd = [], hostsToAdd = []) => {
|
|
594
|
+
const mergeProjectSettings = (projectData, toAdd, acceptEdits, excludedToAdd = [], hostsToAdd = [], askToAdd = []) => {
|
|
588
595
|
const base = projectData ?? {};
|
|
589
596
|
const permissions = getPermissions(base);
|
|
590
597
|
const allow = getAllowEntries(base);
|
|
591
598
|
const mergedAllow = [...allow, ...toAdd.filter((entry) => !allow.includes(entry))];
|
|
599
|
+
const ask = permissions.ask ?? [];
|
|
600
|
+
const newAsk = askToAdd.filter((entry) => !ask.includes(entry));
|
|
592
601
|
const mergedPermissions = {
|
|
593
602
|
...permissions,
|
|
594
603
|
allow: mergedAllow,
|
|
604
|
+
...(newAsk.length ? { ask: [...ask, ...newAsk] } : {}),
|
|
595
605
|
...(acceptEdits === true ? { defaultMode: ACCEPT_EDITS_MODE } : {}),
|
|
596
606
|
};
|
|
597
607
|
// The bridge tier's second and third surfaces: sandbox.excludedCommands and
|
|
@@ -623,6 +633,9 @@ const formatAllowlist = (result) => [
|
|
|
623
633
|
`${result.wrote ? 'added' : 'would add'} read-only core entries: ${result.toAdd.length}`,
|
|
624
634
|
...formatEntryList(result.toAdd),
|
|
625
635
|
`already present: ${result.alreadyPresent.length}`,
|
|
636
|
+
`${result.wrote ? 'added' : 'would add'} permissions.ask entries: ${result.askToAdd.length}`,
|
|
637
|
+
...formatEntryList(result.askToAdd),
|
|
638
|
+
`already present (ask): ${result.askAlreadyPresent.length}`,
|
|
626
639
|
];
|
|
627
640
|
|
|
628
641
|
// The tier's honest posture, printed on every --kit-tools run: run-gates is project-exec (never
|
|
@@ -976,6 +989,9 @@ const deriveUsedBridgeTier = (root, deps = {}) => {
|
|
|
976
989
|
export const planVelocityProfile = (preflight, opts = {}) => {
|
|
977
990
|
const { acceptEdits, kitTools, bridgeTier } = opts;
|
|
978
991
|
const projectAllow = getAllowEntries(preflight.projectSettings?.data);
|
|
992
|
+
const projectAsk = getPermissions(preflight.projectSettings?.data).ask ?? [];
|
|
993
|
+
const askToAdd = projectAsk.includes(APPLY_ASK_RULE) ? [] : [APPLY_ASK_RULE];
|
|
994
|
+
const askAlreadyPresent = projectAsk.includes(APPLY_ASK_RULE) ? [APPLY_ASK_RULE] : [];
|
|
979
995
|
const toAdd = UNIVERSAL_READONLY_ALLOWLIST.filter((entry) => !projectAllow.includes(entry));
|
|
980
996
|
const alreadyPresent = UNIVERSAL_READONLY_ALLOWLIST.filter((entry) => projectAllow.includes(entry));
|
|
981
997
|
const tier = kitTools === true ? deriveKitToolsAllowlist({ projectDir: preflight.cwd }) : [];
|
|
@@ -986,19 +1002,30 @@ export const planVelocityProfile = (preflight, opts = {}) => {
|
|
|
986
1002
|
const bridgeToAdd = bridge.allow.filter((entry) => !projectAllow.includes(entry));
|
|
987
1003
|
const bridgeAlreadyPresent = bridge.allow.filter((entry) => projectAllow.includes(entry));
|
|
988
1004
|
const existingSandbox = isJsonObject(preflight.projectSettings?.data?.sandbox) ? preflight.projectSettings.data.sandbox : {};
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
const hostsToAdd = bridge.hosts.filter((host) => !existingHosts.includes(host));
|
|
994
|
-
const hostsAlreadyPresent = bridge.hosts.filter((host) => existingHosts.includes(host));
|
|
995
|
-
const existingDirs = bridgeTier === true ? assertFilesystemMergeable(existingSandbox) : [];
|
|
1005
|
+
if (bridgeTier === true) {
|
|
1006
|
+
assertNetworkMergeable(existingSandbox);
|
|
1007
|
+
assertFilesystemMergeable(existingSandbox);
|
|
1008
|
+
}
|
|
996
1009
|
const dirs = derived?.dirs ?? [];
|
|
997
|
-
const
|
|
1010
|
+
const delta = derived
|
|
1011
|
+
? sandboxSurfaceDelta({ excludedCommands: bridge.excludedCommands, hosts: bridge.hosts, dirs }, existingSandbox, { home: derived.home, root: preflight.cwd })
|
|
1012
|
+
: { excludedCommands: [], hosts: [], dirs: [] };
|
|
1013
|
+
const { excludedCommands: excludedToAdd, hosts: hostsToAdd, dirs: dirsToAdd } = delta;
|
|
1014
|
+
const excludedAlreadyPresent = bridge.excludedCommands.filter((cmd) => !excludedToAdd.includes(cmd));
|
|
1015
|
+
const hostsAlreadyPresent = bridge.hosts.filter((host) => !hostsToAdd.includes(host));
|
|
998
1016
|
const dirsAlreadyPresent = dirs.filter((dir) => !dirsToAdd.includes(dir));
|
|
1017
|
+
const executeRecipe = derived?.used.some((entry) => entry.bridge === CODEX_BRIDGE && entry.roles.includes(EXECUTE_ROLE))
|
|
1018
|
+
? bundledSandboxRecipe([{ bridge: CODEX_BRIDGE, roles: [EXECUTE_ROLE] }], opts)
|
|
1019
|
+
: null;
|
|
1020
|
+
const executeDirs = executeRecipe ? stateDirsOf(executeRecipe.dirEntries, { env: derived.env, root: preflight.cwd, home: derived.home }) : [];
|
|
1021
|
+
const executeToAdd = executeRecipe
|
|
1022
|
+
? sandboxSurfaceDelta({ excludedCommands: executeRecipe.excludedCommands, hosts: executeRecipe.hosts, dirs: executeDirs }, existingSandbox, { home: derived.home, root: preflight.cwd })
|
|
1023
|
+
: { excludedCommands: [], hosts: [], dirs: [] };
|
|
999
1024
|
return {
|
|
1000
1025
|
toAdd,
|
|
1001
1026
|
alreadyPresent,
|
|
1027
|
+
askToAdd,
|
|
1028
|
+
askAlreadyPresent,
|
|
1002
1029
|
tierToAdd,
|
|
1003
1030
|
tierAlreadyPresent,
|
|
1004
1031
|
kitTools: kitTools === true,
|
|
@@ -1012,6 +1039,7 @@ export const planVelocityProfile = (preflight, opts = {}) => {
|
|
|
1012
1039
|
hostsAlreadyPresent,
|
|
1013
1040
|
dirsToAdd,
|
|
1014
1041
|
dirsAlreadyPresent,
|
|
1042
|
+
executeToAdd,
|
|
1015
1043
|
setsDefaultMode: acceptEdits === true,
|
|
1016
1044
|
};
|
|
1017
1045
|
};
|
|
@@ -1061,6 +1089,7 @@ export const writeVelocityProfile = ({ cwd, acceptEdits = false, dryRun = true,
|
|
|
1061
1089
|
acceptEdits,
|
|
1062
1090
|
plan.excludedToAdd,
|
|
1063
1091
|
plan.hostsToAdd,
|
|
1092
|
+
plan.askToAdd,
|
|
1064
1093
|
), plan.dirsToAdd);
|
|
1065
1094
|
fs.writeFile(settingsPath, formatJson(merged, preflight.projectSettings.eol ?? LF), UTF8);
|
|
1066
1095
|
return { wrote: true, dryRun: false, settingsPath, ...resultBase };
|
|
@@ -1568,7 +1597,7 @@ const collectSandboxWeakenings = (sources, { root, home, tmp }, tier) => {
|
|
|
1568
1597
|
const foreign = fsb.allowWrite.filter((entry) => !known.includes(entry));
|
|
1569
1598
|
addNotes(`${SANDBOX_KEY}.filesystem.allowWrite`, known, 'every sandboxed command can write it');
|
|
1570
1599
|
// Only the entries that RESOLVE outside the repo and $TMPDIR are a weakening at all — an entry
|
|
1571
|
-
// pointing INSIDE the repo grants nothing the red-line
|
|
1600
|
+
// pointing INSIDE the repo grants nothing beyond the red-line's own bound, so reporting it would be an
|
|
1572
1601
|
// over-report. All-contained ⇒ no line at all.
|
|
1573
1602
|
// TWO records, never one: a resolved external path IS a weakening, an unresolvable entry is
|
|
1574
1603
|
// UNVERIFIABLE, and a single line carrying both would assert an effect for entries it could
|
package/tools/write-lanes.mjs
CHANGED
|
@@ -41,7 +41,6 @@ export const VARIANT_LANES = Object.freeze({
|
|
|
41
41
|
'adr-store-migration': LANE_CHAT,
|
|
42
42
|
'sandbox-masks': LANE_CHAT,
|
|
43
43
|
'sandbox-masks.unfenced-mount': LANE_CHAT,
|
|
44
|
-
'sandbox-lane': LANE_CHAT,
|
|
45
44
|
'worktrees-dir': LANE_CHAT,
|
|
46
45
|
'spec-adoption': LANE_CHAT,
|
|
47
46
|
'profile-gap': LANE_CHAT,
|