@sabaiway/agent-workflow-kit 14.25.2 → 14.27.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 +9 -8
- 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/bootstrap.md +2 -4
- package/references/modes/jev.md +2 -2
- package/references/modes/mcp.md +1 -1
- package/references/modes/recommendations.md +19 -19
- package/references/modes/upgrade.md +3 -3
- package/references/modes/velocity.md +4 -4
- package/tools/apply-danger-check.mjs +369 -0
- package/tools/bridge-sandbox-recipe.mjs +102 -15
- package/tools/bridge-state-dirs.mjs +94 -0
- package/tools/init-project.mjs +27 -29
- package/tools/jev-connect.mjs +2 -2
- package/tools/jev-guide.mjs +2 -2
- package/tools/manifest/schema.md +8 -6
- package/tools/recommendations.mjs +41 -42
- package/tools/velocity-profile.mjs +174 -111
- package/tools/write-lanes.mjs +56 -32
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, lstatSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { accessSync, constants, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
3
3
|
import { homedir, tmpdir } from 'node:os';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
@@ -7,14 +7,16 @@ import { fileURLToPath } from 'node:url';
|
|
|
7
7
|
// it never imports the policy fs-writer (autonomy-write.mjs) — the render owns the settings file, not
|
|
8
8
|
// the policy file.
|
|
9
9
|
import { AUTONOMY_REL, loadAutonomy, resolveAutonomy, COMMAND_REDLINES } from './autonomy-config.mjs';
|
|
10
|
-
|
|
11
|
-
// for a PLACED bridge wrapper — findOnPath is the same read-only PATH scan the backend detector uses.
|
|
12
|
-
import { findOnPath } from './detect-backends.mjs';
|
|
10
|
+
import { detectBackends, findOnPath, wrapperCmdFor } from './detect-backends.mjs';
|
|
13
11
|
// The seedable-path predicate lives in the pure leaf so the renders spell a path exactly as it is seeded.
|
|
14
12
|
import { SHELL_METACHARACTERS, hasShellMetacharacter, isSeedablePathToken } from './repo-lex.mjs';
|
|
15
13
|
import { isDirectRun } from './direct-run.mjs';
|
|
16
14
|
import { compareSemver } from './semver-lite.mjs';
|
|
17
|
-
import { bundledSandboxRecipe } from './bridge-sandbox-recipe.mjs';
|
|
15
|
+
import { BRIDGE_REVIEW_WRAPPERS, BRIDGE_REVIEW_MODE, KIT_GROUNDING_TOOL, usedBridges, bundledSandboxRecipe, exclusionOf } from './bridge-sandbox-recipe.mjs';
|
|
16
|
+
import { stateDirsOf, missingStateDirs, mergeAllowWrite } from './bridge-state-dirs.mjs';
|
|
17
|
+
import { loadConfig } from './orchestration-config.mjs';
|
|
18
|
+
import { validateManifest } from './manifest/validate.mjs';
|
|
19
|
+
export { BRIDGE_REVIEW_WRAPPERS, BRIDGE_REVIEW_MODE, KIT_GROUNDING_TOOL };
|
|
18
20
|
// The declared-path resolution + segment containment the allowWrite degrade shares with the
|
|
19
21
|
// advisor's worktrees-dir convergence lane — ONE leaf, so the two readings cannot drift.
|
|
20
22
|
import { resolveDeclaredDir, dirCovers, isResolvableDeclaredEntry } from './declared-paths.mjs';
|
|
@@ -197,27 +199,6 @@ const KIT_ROOT = fileURLToPath(new URL('..', import.meta.url));
|
|
|
197
199
|
const KIT_TOOL_INVOKER = 'node';
|
|
198
200
|
const RUN_GATES_CWD_FLAG = '--cwd';
|
|
199
201
|
|
|
200
|
-
// ── the opt-in --bridge-tier (AD-044 Plan 4, Decision 2) ────────────────────────────────
|
|
201
|
-
// The FROZEN membership source: review-role wrapper NAMES only, spelled bare — that is how the
|
|
202
|
-
// wrappers are invoked (setup-backends places them as symlinks in ~/.local/bin). Roles come from
|
|
203
|
-
// THIS constant, never from the detector's wrapperCmds (which carries no role labels): codex-exec
|
|
204
|
-
// (execution role) and agy-run (probe role) are deliberately ABSENT. The velocity pain this tier
|
|
205
|
-
// closes is council REVIEW runs; codex-exec's nested-sandbox recovery is handled the canon way —
|
|
206
|
-
// route it outside ON the OBSERVED failure (orchestration.md §5), NOT a preemptive tier seed (AD-054
|
|
207
|
-
// final council: a blanket exclusion at opt-in contradicts «never a preemptive blanket»), guided by
|
|
208
|
-
// codex-exec.sh's own nested-sandbox detection hint.
|
|
209
|
-
export const BRIDGE_REVIEW_WRAPPERS = Object.freeze(['codex-review', 'agy-review']);
|
|
210
|
-
// Only the `code` review mode is auto-allowed (Segment A): a bare `Bash(<wrapper>:*)`
|
|
211
|
-
// prefix would also cover the plan/diff file-argument modes, whose targets can point OUTSIDE the
|
|
212
|
-
// repo — the tier's stated surface is the unattended council CODE review, so the seeded prefix is
|
|
213
|
-
// `<wrapper> code`. Plan/diff invocations keep their prompt.
|
|
214
|
-
export const BRIDGE_REVIEW_MODE = 'code';
|
|
215
|
-
// The grounding pre-step tool (the reviews' facts assembler). Its tier entry is seeded in the
|
|
216
|
-
// EXACT byte-form the procedures advisor renders — a DOUBLE-QUOTED absolute path (deliberate
|
|
217
|
-
// there: a skill dir with a space must stay copy-pasteable) — so seeded↔rendered byte-parity
|
|
218
|
-
// holds; the screen accepts the quoted form ONLY for this one tool (a tier-scoped acceptance,
|
|
219
|
-
// never a general quote allowance).
|
|
220
|
-
export const KIT_GROUNDING_TOOL = 'tools/grounding.mjs';
|
|
221
202
|
const WRAPPER_PLACED = 'present';
|
|
222
203
|
|
|
223
204
|
// The quoted-grounding token: "<seedable-abs-path>/tools/grounding.mjs" — quotes stripped, the
|
|
@@ -229,41 +210,6 @@ const isQuotedGroundingToken = (token) => {
|
|
|
229
210
|
return isSeedablePathToken(inner) && inner.endsWith(`/${KIT_GROUNDING_TOOL}`);
|
|
230
211
|
};
|
|
231
212
|
|
|
232
|
-
/**
|
|
233
|
-
* Derive the opt-in bridge-wrappers tier: one code-mode allow rule per PLACED review wrapper (the
|
|
234
|
-
* frozen constant is the membership source; placement is a probe, never a role source), the SAME
|
|
235
|
-
* wrapper names for sandbox.excludedCommands (the harness runs an excluded command OUTSIDE the
|
|
236
|
-
* sandbox, so a plain allowlisted invocation needs no sandbox-bypass approval — the zero-prompt
|
|
237
|
-
* wiring), and the grounding pre-step rule in its rendered quoted byte-form (derived only when
|
|
238
|
-
* agy-review is placed; an unseedable kit path is a stated skip). An absent bridge is a stated skip.
|
|
239
|
-
*/
|
|
240
|
-
export const deriveBridgeTierAllowlist = ({ findWrapper, groundingAbsPath } = {}) => {
|
|
241
|
-
const probe = findWrapper ?? ((cmd) => findOnPath(cmd).state === WRAPPER_PLACED);
|
|
242
|
-
const placed = BRIDGE_REVIEW_WRAPPERS.filter((cmd) => probe(cmd));
|
|
243
|
-
const skips = BRIDGE_REVIEW_WRAPPERS.filter((cmd) => !placed.includes(cmd)).map((cmd) => ({
|
|
244
|
-
entry: cmd,
|
|
245
|
-
reason: `bridge wrapper "${cmd}" is not placed on PATH — its allow rule is not seeded (place the bridge with /agent-workflow-kit setup, then re-run)`,
|
|
246
|
-
}));
|
|
247
|
-
const allow = placed.map((cmd) => `Bash(${cmd} ${BRIDGE_REVIEW_MODE}:*)`);
|
|
248
|
-
const excludedCommands = [...placed];
|
|
249
|
-
// The grounding pre-step exists FOR agy (its grounded --facts reviews; codex grounds natively via
|
|
250
|
-
// the AGENTS.md auto-merge) — a codex-only install must not auto-allow an unused writer.
|
|
251
|
-
if (placed.includes('agy-review')) {
|
|
252
|
-
// groundingAbsPath is a TEST seam only (an unseedable kit path — spaces — is not constructible
|
|
253
|
-
// from a test against the real checkout); production callers never pass it.
|
|
254
|
-
const groundingAbs = groundingAbsPath ?? join(KIT_ROOT, KIT_GROUNDING_TOOL);
|
|
255
|
-
if (isSeedablePathToken(groundingAbs)) {
|
|
256
|
-
allow.push(`Bash(${KIT_TOOL_INVOKER} "${groundingAbs}":*)`);
|
|
257
|
-
} else {
|
|
258
|
-
skips.push({
|
|
259
|
-
entry: groundingAbs,
|
|
260
|
-
reason: 'the kit path is not a POSIX absolute path free of spaces/metacharacters/quoting — the grounding pre-step rule is not seeded (its prompt stays); add a hand-picked entry if you accept the spelling',
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
return Object.freeze({ allow: Object.freeze(allow), excludedCommands: Object.freeze(excludedCommands), skips: Object.freeze(skips), placed: Object.freeze(placed) });
|
|
265
|
-
};
|
|
266
|
-
|
|
267
213
|
// Characters that make a pattern NOT a single read-only command, so the screen rejects them.
|
|
268
214
|
// Per current Claude Code permission semantics: the recognized command SEPARATORS are
|
|
269
215
|
// `&& || ; | |& &` plus newline (each segment must be independently permitted, so a chained
|
|
@@ -386,14 +332,15 @@ absolute path (args wildcard), run-gates.mjs as ONE exact project-root-pinned by
|
|
|
386
332
|
(project-exec - it runs YOUR declared gates.json), source-size-check.mjs as ONE exact --check
|
|
387
333
|
byte-string (its read-only mode only - its writers keep prompting), and the writers' exact arg-free
|
|
388
334
|
dry-run preview byte-strings. Never touches settings.local.json.
|
|
389
|
-
--bridge-tier (own consent)
|
|
390
|
-
(codex-review code, agy-review code
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
every sandboxed command of the project can
|
|
395
|
-
|
|
396
|
-
|
|
335
|
+
--bridge-tier (own consent) derives each entry for a used bridge from the effective project recipes:
|
|
336
|
+
review wrappers' CODE mode (codex-review code, agy-review code), the quoted grounding pre-step rule,
|
|
337
|
+
and <wrapper> * exclusions in sandbox.excludedCommands. An execute slot delegating to codex adds
|
|
338
|
+
codex-exec * as an exclusion; execution keeps its prompt, as do the review plan/diff modes.
|
|
339
|
+
${HOST_HONORS_QUALIFIER}, an excluded wrapper runs outside the sandbox;
|
|
340
|
+
sandbox.network.allowedDomains means every sandboxed command of the project can reach those hosts;
|
|
341
|
+
sandbox.filesystem.allowWrite means every sandboxed command can write those state dirs.
|
|
342
|
+
Consented posture: an auto-allowed review wrapper runs UNATTENDED and sends the assembled repo
|
|
343
|
+
payload to its subscription backend (see the printed tier notice).
|
|
397
344
|
|
|
398
345
|
--autonomy renders docs/ai/autonomy.json into the settings blocks it OWNS — the sandbox block +
|
|
399
346
|
permissions.ask/deny red-lines + permissions.defaultMode. POLICY-ONLY: never seeds the allowlist and
|
|
@@ -696,11 +643,9 @@ const formatKitTier = (result) =>
|
|
|
696
643
|
// The bridge tier's honest posture, printed on EVERY --bridge-tier run: the informed-consent
|
|
697
644
|
// resolution states the exfiltration surface, never pretends it away.
|
|
698
645
|
export const KIT_BRIDGE_TIER_NOTICE =
|
|
699
|
-
'bridge-wrappers tier:
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
`${HOST_HONORS_QUALIFIER} the harness runs an excluded command OUTSIDE the sandbox (the wrappers need network), so a plain allowlisted invocation triggers no sandbox-bypass approval — whether a host honors them is not knowable from here, and where it does not the wrapper simply starts sandboxed (fail-safe, never a silent widening). ` +
|
|
703
|
-
`The placed bridges' declared hosts (each bundled manifest's networkHosts) are seeded into sandbox.network.allowedDomains IN THE PROJECT settings.json: ${HOST_HONORS_QUALIFIER}, a wrapper the harness keeps sandboxed still reaches them, and every sandboxed command of the project can then reach those hosts, not only the wrappers — that widening is part of this consent, and the --autonomy preview reports it as a network weakening; the writable state dirs (writableDirs) stay hand-apply. ` +
|
|
646
|
+
'bridge-wrappers tier: each entry derives for a used bridge from the effective project recipes and host readiness. Review allows cover only CODE mode (`codex-review code`, `agy-review code`) plus the grounding pre-step rule in its rendered quoted byte-form; the plan/diff modes keep their prompt because their file arguments can point outside the repo. An execute slot delegating to codex adds codex-exec * to sandbox.excludedCommands and keeps its prompt, with no execution allow rule; agy-run is not seeded. ' +
|
|
647
|
+
'POSTURE (what this consent covers): an auto-allowed review wrapper runs UNATTENDED — it reads any repo file it is pointed at and sends the assembled payload to its subscription backend. Prefix rules cannot inspect arguments, so readable file arguments (agy\'s --facts/--decided) ride the same consent; tier entries get NO PreToolUse-hook coverage. The grounding entry\'s writer surface is bounded by grounding.mjs\'s OWN scratch-destination guard (a tracked or in-repo-not-ignored --out is refused by the tool). ' +
|
|
648
|
+
`The tier writes the PROJECT settings.json: ${HOST_HONORS_QUALIFIER}, an excluded wrapper runs outside the sandbox (<wrapper> * in sandbox.excludedCommands), every sandboxed command of the project can reach the declared hosts (sandbox.network.allowedDomains), and every sandboxed command can write the declared state dirs (sandbox.filesystem.allowWrite). These surfaces are part of the bridge-wrappers consent. Whether a host honors them is not knowable from here; an exclusion only in settings.local.json was live-observed not to route. ` +
|
|
704
649
|
'INVOCATION SHAPE: a prefix rule matches only a PLAIN invocation starting with the wrapper name — an env-var prefix or a compound chain never matches (redirects are fine).';
|
|
705
650
|
|
|
706
651
|
const formatBridgeTier = (result) =>
|
|
@@ -719,6 +664,13 @@ const formatBridgeTier = (result) =>
|
|
|
719
664
|
`already present (allowedDomains): ${result.hostsAlreadyPresent.length}`,
|
|
720
665
|
]
|
|
721
666
|
: []),
|
|
667
|
+
...(result.dirsToAdd.length + result.dirsAlreadyPresent.length
|
|
668
|
+
? [
|
|
669
|
+
`${result.wrote ? 'added' : 'would add'} sandbox.filesystem.allowWrite entries: ${result.dirsToAdd.length}`,
|
|
670
|
+
...formatEntryList(result.dirsToAdd),
|
|
671
|
+
`already present (allowWrite): ${result.dirsAlreadyPresent.length}`,
|
|
672
|
+
]
|
|
673
|
+
: []),
|
|
722
674
|
...result.bridgeSkips.map((s) => ` skipped: ${s.reason}`),
|
|
723
675
|
KIT_BRIDGE_TIER_NOTICE,
|
|
724
676
|
]
|
|
@@ -941,7 +893,8 @@ export const preflightVelocityProfile = ({ cwd }, deps = {}) => {
|
|
|
941
893
|
// bridge-shaped entry — over-flagging is the safe direction for a read-only advisory.
|
|
942
894
|
const derivedBridge = (() => {
|
|
943
895
|
try {
|
|
944
|
-
|
|
896
|
+
const probe = deps.findWrapper ?? ((cmd) => findOnPath(cmd).state === WRAPPER_PLACED);
|
|
897
|
+
return bundledSandboxRecipe(BRIDGE_REVIEW_WRAPPERS.filter(probe), deps).allow;
|
|
945
898
|
} catch {
|
|
946
899
|
return [];
|
|
947
900
|
}
|
|
@@ -982,14 +935,54 @@ const assertNetworkMergeable = (sandbox) => {
|
|
|
982
935
|
return sandbox.network?.allowedDomains ?? [];
|
|
983
936
|
};
|
|
984
937
|
|
|
985
|
-
|
|
938
|
+
const assertFilesystemMergeable = (sandbox) => {
|
|
939
|
+
if (sandbox.filesystem !== undefined && !isJsonObject(sandbox.filesystem)) {
|
|
940
|
+
throw makeVelocityProfileError(VELOCITY_MALFORMED, `${SETTINGS_FILE}: sandbox.filesystem must be a JSON object`);
|
|
941
|
+
}
|
|
942
|
+
const allowWrite = sandbox.filesystem?.allowWrite;
|
|
943
|
+
if (allowWrite !== undefined && !Array.isArray(allowWrite)) {
|
|
944
|
+
throw makeVelocityProfileError(VELOCITY_MALFORMED, `${SETTINGS_FILE}: sandbox.filesystem.allowWrite must be an array`);
|
|
945
|
+
}
|
|
946
|
+
if (allowWrite?.some((entry) => !isResolvableDeclaredEntry(entry))) {
|
|
947
|
+
throw makeVelocityProfileError(VELOCITY_MALFORMED, `${SETTINGS_FILE}: sandbox.filesystem.allowWrite entries must be non-blank strings`);
|
|
948
|
+
}
|
|
949
|
+
return allowWrite ?? [];
|
|
950
|
+
};
|
|
951
|
+
|
|
952
|
+
// Readiness as a wrapper started from the project root sees it: every relative path the detector probes
|
|
953
|
+
// (an env override such as CODEX_HOME=state) resolves against the root, never this process's cwd, so the
|
|
954
|
+
// danger check and the writer it spawns with cwd=root derive one used set.
|
|
955
|
+
export const detectAtRoot = (root, { env, home }) => {
|
|
956
|
+
const at = (path) => resolve(root, path);
|
|
957
|
+
return detectBackends({
|
|
958
|
+
getenv: env, home,
|
|
959
|
+
exists: (path) => existsSync(at(path)), stat: (path) => statSync(at(path)),
|
|
960
|
+
access: (path) => accessSync(at(path), constants.X_OK), realpath: (path) => realpathSync(at(path)),
|
|
961
|
+
validate: (dir) => validateManifest(at(dir)),
|
|
962
|
+
});
|
|
963
|
+
};
|
|
964
|
+
|
|
965
|
+
const deriveUsedBridgeTier = (root, deps = {}) => {
|
|
966
|
+
const env = deps.env ?? process.env;
|
|
967
|
+
const home = deps.home ?? homedir();
|
|
968
|
+
const config = loadConfig(root).config;
|
|
969
|
+
const readiness = (deps.detect ?? (() => detectAtRoot(root, { env, home })))();
|
|
970
|
+
const used = usedBridges(config, readiness);
|
|
971
|
+
const recipe = bundledSandboxRecipe(used, deps);
|
|
972
|
+
const dirs = stateDirsOf(recipe.dirEntries, { env, root, home });
|
|
973
|
+
return { used, recipe, dirs, env, home };
|
|
974
|
+
};
|
|
975
|
+
|
|
976
|
+
export const planVelocityProfile = (preflight, opts = {}) => {
|
|
977
|
+
const { acceptEdits, kitTools, bridgeTier } = opts;
|
|
986
978
|
const projectAllow = getAllowEntries(preflight.projectSettings?.data);
|
|
987
979
|
const toAdd = UNIVERSAL_READONLY_ALLOWLIST.filter((entry) => !projectAllow.includes(entry));
|
|
988
980
|
const alreadyPresent = UNIVERSAL_READONLY_ALLOWLIST.filter((entry) => projectAllow.includes(entry));
|
|
989
981
|
const tier = kitTools === true ? deriveKitToolsAllowlist({ projectDir: preflight.cwd }) : [];
|
|
990
982
|
const tierToAdd = tier.filter((entry) => !projectAllow.includes(entry));
|
|
991
983
|
const tierAlreadyPresent = tier.filter((entry) => projectAllow.includes(entry));
|
|
992
|
-
const
|
|
984
|
+
const derived = bridgeTier === true ? deriveUsedBridgeTier(preflight.cwd, opts) : null;
|
|
985
|
+
const bridge = derived?.recipe ?? { allow: [], excludedCommands: [], hosts: [], skips: [] };
|
|
993
986
|
const bridgeToAdd = bridge.allow.filter((entry) => !projectAllow.includes(entry));
|
|
994
987
|
const bridgeAlreadyPresent = bridge.allow.filter((entry) => projectAllow.includes(entry));
|
|
995
988
|
const existingSandbox = isJsonObject(preflight.projectSettings?.data?.sandbox) ? preflight.projectSettings.data.sandbox : {};
|
|
@@ -997,9 +990,12 @@ export const planVelocityProfile = (preflight, { acceptEdits, kitTools, bridgeTi
|
|
|
997
990
|
const excludedToAdd = bridge.excludedCommands.filter((cmd) => !existingExcluded.includes(cmd));
|
|
998
991
|
const excludedAlreadyPresent = bridge.excludedCommands.filter((cmd) => existingExcluded.includes(cmd));
|
|
999
992
|
const existingHosts = bridgeTier === true ? assertNetworkMergeable(existingSandbox) : [];
|
|
1000
|
-
const
|
|
1001
|
-
const
|
|
1002
|
-
const
|
|
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) : [];
|
|
996
|
+
const dirs = derived?.dirs ?? [];
|
|
997
|
+
const dirsToAdd = derived ? missingStateDirs(dirs, existingDirs, { home: derived.home, root: preflight.cwd }) : [];
|
|
998
|
+
const dirsAlreadyPresent = dirs.filter((dir) => !dirsToAdd.includes(dir));
|
|
1003
999
|
return {
|
|
1004
1000
|
toAdd,
|
|
1005
1001
|
alreadyPresent,
|
|
@@ -1014,6 +1010,8 @@ export const planVelocityProfile = (preflight, { acceptEdits, kitTools, bridgeTi
|
|
|
1014
1010
|
excludedAlreadyPresent,
|
|
1015
1011
|
hostsToAdd,
|
|
1016
1012
|
hostsAlreadyPresent,
|
|
1013
|
+
dirsToAdd,
|
|
1014
|
+
dirsAlreadyPresent,
|
|
1017
1015
|
setsDefaultMode: acceptEdits === true,
|
|
1018
1016
|
};
|
|
1019
1017
|
};
|
|
@@ -1022,13 +1020,10 @@ export const writeVelocityProfile = ({ cwd, acceptEdits = false, dryRun = true,
|
|
|
1022
1020
|
const projectDir = cwd ?? deps.cwd ?? process.cwd();
|
|
1023
1021
|
const preflight = preflightVelocityProfile({ cwd: projectDir }, deps);
|
|
1024
1022
|
const plan = planVelocityProfile(preflight, {
|
|
1023
|
+
...deps,
|
|
1025
1024
|
acceptEdits,
|
|
1026
1025
|
kitTools,
|
|
1027
1026
|
bridgeTier,
|
|
1028
|
-
findWrapper: deps.findWrapper,
|
|
1029
|
-
bundleRoot: deps.bundleRoot,
|
|
1030
|
-
readdir: deps.readdir,
|
|
1031
|
-
readFile: deps.readFile,
|
|
1032
1027
|
});
|
|
1033
1028
|
// Drift guard runs on BOTH dry-run and apply (so a dry-run faithfully predicts the apply) and
|
|
1034
1029
|
// validates the FULL audited core, not just the to-add delta — a drifted core entry is caught even
|
|
@@ -1044,8 +1039,8 @@ export const writeVelocityProfile = ({ cwd, acceptEdits = false, dryRun = true,
|
|
|
1044
1039
|
// (Decision 2): every seeded bridge entry passes the screen AND sits in the audited set, so the
|
|
1045
1040
|
// flagless advisory can never flag an entry the tier itself seeded.
|
|
1046
1041
|
if (bridgeTier === true) {
|
|
1047
|
-
const
|
|
1048
|
-
validateProfile(
|
|
1042
|
+
const bridgeAllow = [...plan.bridgeToAdd, ...plan.bridgeAlreadyPresent];
|
|
1043
|
+
validateProfile(bridgeAllow, [...UNIVERSAL_READONLY_ALLOWLIST, ...bridgeAllow]);
|
|
1049
1044
|
}
|
|
1050
1045
|
const resultBase = { ...preflight, ...plan };
|
|
1051
1046
|
if (dryRun) return { wrote: false, dryRun: true, ...resultBase };
|
|
@@ -1060,13 +1055,13 @@ export const writeVelocityProfile = ({ cwd, acceptEdits = false, dryRun = true,
|
|
|
1060
1055
|
const fs = fsDeps(deps);
|
|
1061
1056
|
const settingsPath = join(projectDir, SETTINGS_FILE);
|
|
1062
1057
|
if (preflight.claudeDirAbsent) fs.mkdir(join(projectDir, CLAUDE_DIR), { recursive: true });
|
|
1063
|
-
const merged = mergeProjectSettings(
|
|
1058
|
+
const merged = mergeAllowWrite(mergeProjectSettings(
|
|
1064
1059
|
preflight.projectSettings.data,
|
|
1065
1060
|
[...plan.toAdd, ...plan.tierToAdd, ...plan.bridgeToAdd],
|
|
1066
1061
|
acceptEdits,
|
|
1067
1062
|
plan.excludedToAdd,
|
|
1068
1063
|
plan.hostsToAdd,
|
|
1069
|
-
);
|
|
1064
|
+
), plan.dirsToAdd);
|
|
1070
1065
|
fs.writeFile(settingsPath, formatJson(merged, preflight.projectSettings.eol ?? LF), UTF8);
|
|
1071
1066
|
return { wrote: true, dryRun: false, settingsPath, ...resultBase };
|
|
1072
1067
|
};
|
|
@@ -1489,23 +1484,76 @@ const externalWriteEntries = (entries, { root, home, tmp }) => {
|
|
|
1489
1484
|
};
|
|
1490
1485
|
};
|
|
1491
1486
|
|
|
1492
|
-
const
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1487
|
+
const cacheBundleReader = (read) => {
|
|
1488
|
+
const entries = new Map();
|
|
1489
|
+
return (path, ...args) => {
|
|
1490
|
+
if (!entries.has(path)) entries.set(path, read(path, ...args));
|
|
1491
|
+
return entries.get(path);
|
|
1492
|
+
};
|
|
1493
|
+
};
|
|
1494
|
+
|
|
1495
|
+
// The --autonomy preview's tier-known set (spec velocity-profile): only a used role whose consent
|
|
1496
|
+
// proof the PROJECT settings carry — a review role's derived code-mode allow rule, the execute
|
|
1497
|
+
// role's `codex-exec *` exclusion — makes its derived surfaces notes; any derivation failure is
|
|
1498
|
+
// returned as `error`, and the caller then judges every entry as revision 3 did.
|
|
1499
|
+
const deriveBridgeConsent = (data, root, deps) => {
|
|
1500
|
+
try {
|
|
1501
|
+
const bundleDeps = {
|
|
1502
|
+
...deps,
|
|
1503
|
+
readdir: cacheBundleReader(deps.readdir ?? readdirSync),
|
|
1504
|
+
readFile: cacheBundleReader(deps.readFile ?? readFileSync),
|
|
1505
|
+
};
|
|
1506
|
+
const derived = deriveUsedBridgeTier(root, bundleDeps);
|
|
1507
|
+
const allow = getAllowEntries(data);
|
|
1508
|
+
const excluded = Array.isArray(data?.sandbox?.excludedCommands) ? data.sandbox.excludedCommands : [];
|
|
1509
|
+
const proven = derived.used.map(({ bridge, roles }) => ({
|
|
1510
|
+
bridge,
|
|
1511
|
+
roles: roles.filter((role) => {
|
|
1512
|
+
const wrapper = wrapperCmdFor(bridge, role);
|
|
1513
|
+
const exclusion = exclusionOf(wrapper);
|
|
1514
|
+
if (role === 'execute') return derived.recipe.excludedCommands.includes(exclusion) && excluded.includes(exclusion);
|
|
1515
|
+
const rule = `Bash(${wrapper} ${BRIDGE_REVIEW_MODE}:*)`;
|
|
1516
|
+
return derived.recipe.allow.includes(rule) && allow.includes(rule);
|
|
1517
|
+
}),
|
|
1518
|
+
})).filter(({ roles }) => roles.length);
|
|
1519
|
+
const allProven = JSON.stringify(proven) === JSON.stringify(derived.used);
|
|
1520
|
+
const recipe = allProven
|
|
1521
|
+
? derived.recipe
|
|
1522
|
+
: bundledSandboxRecipe(proven, bundleDeps);
|
|
1523
|
+
const knownDirs = allProven ? derived.dirs : stateDirsOf(recipe.dirEntries, { env: derived.env, root, home: derived.home });
|
|
1524
|
+
const bareReview = recipe.excludedCommands
|
|
1525
|
+
.map((entry) => entry.slice(0, -2))
|
|
1526
|
+
.filter((cmd) => BRIDGE_REVIEW_WRAPPERS.includes(cmd));
|
|
1527
|
+
return { dirs: derived.dirs, knownDirs, hosts: recipe.hosts, excluded: [...recipe.excludedCommands, ...bareReview] };
|
|
1528
|
+
} catch (err) {
|
|
1529
|
+
return { error: err?.message ?? String(err) };
|
|
1530
|
+
}
|
|
1531
|
+
};
|
|
1532
|
+
|
|
1533
|
+
const collectSandboxWeakenings = (sources, { root, home, tmp }, tier) => {
|
|
1497
1534
|
const project = sources.find((s) => s.source === SETTINGS_FILE);
|
|
1498
1535
|
const projectAllow = getAllowEntries(project?.data);
|
|
1536
|
+
const judged = tier && !tier.error;
|
|
1537
|
+
const isTierKnown = (source, entry, entries) => source === SETTINGS_FILE && judged && entries.includes(entry);
|
|
1499
1538
|
const isTierKnownExclusion = (source, cmd) =>
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1539
|
+
judged
|
|
1540
|
+
? isTierKnown(source, cmd, tier.excluded)
|
|
1541
|
+
: source === SETTINGS_FILE && BRIDGE_REVIEW_WRAPPERS.includes(cmd) &&
|
|
1542
|
+
projectAllow.includes(`Bash(${cmd} ${BRIDGE_REVIEW_MODE}:*)`);
|
|
1503
1543
|
return sources.flatMap(({ source, data }) => {
|
|
1504
1544
|
const sb = isJsonObject(data?.[SANDBOX_KEY]) ? data[SANDBOX_KEY] : {};
|
|
1505
1545
|
const out = [];
|
|
1546
|
+
const addNotes = (key, entries, detail) => {
|
|
1547
|
+
for (const entry of entries) out.push({ source, key, entry, tierKnown: true, detail });
|
|
1548
|
+
};
|
|
1506
1549
|
const net = isJsonObject(sb.network) ? sb.network : {};
|
|
1507
1550
|
if (Array.isArray(net.allowedDomains) && net.allowedDomains.length) {
|
|
1508
|
-
|
|
1551
|
+
const known = net.allowedDomains.filter((entry) => isTierKnown(source, entry, tier?.hosts));
|
|
1552
|
+
const foreign = net.allowedDomains.filter((entry) => !known.includes(entry));
|
|
1553
|
+
addNotes(`${SANDBOX_KEY}.network.allowedDomains`, known, 'every sandboxed command of the project can reach it');
|
|
1554
|
+
if (foreign.length) {
|
|
1555
|
+
out.push({ source, key: `${SANDBOX_KEY}.network.allowedDomains`, weakens: 'network', detail: `${foreign.length} pre-allowed domain(s) — ${HOST_HONORS_QUALIFIER}, egress to them is not gated` });
|
|
1556
|
+
}
|
|
1509
1557
|
}
|
|
1510
1558
|
const fsb = isJsonObject(sb.filesystem) ? sb.filesystem : {};
|
|
1511
1559
|
// A PRESENT but unreadable declaration is reported, never assumed empty: what it would make
|
|
@@ -1516,16 +1564,24 @@ const collectSandboxWeakenings = (sources, { root, home, tmp }) => {
|
|
|
1516
1564
|
// instead of asserting an effect it does not know.
|
|
1517
1565
|
out.push({ source, key: `${SANDBOX_KEY}.filesystem.allowWrite`, weakens: 'fs_outside_repo', unverifiable: true, detail: `the declared value is not an array (${typeof fsb.allowWrite}) — it cannot be read, so what it would make writable is UNKNOWN` });
|
|
1518
1566
|
} else if (Array.isArray(fsb.allowWrite) && fsb.allowWrite.length) {
|
|
1567
|
+
const known = fsb.allowWrite.filter((entry) => isTierKnown(source, entry, tier?.knownDirs));
|
|
1568
|
+
const foreign = fsb.allowWrite.filter((entry) => !known.includes(entry));
|
|
1569
|
+
addNotes(`${SANDBOX_KEY}.filesystem.allowWrite`, known, 'every sandboxed command can write it');
|
|
1519
1570
|
// Only the entries that RESOLVE outside the repo and $TMPDIR are a weakening at all — an entry
|
|
1520
1571
|
// pointing INSIDE the repo grants nothing the red-line withholds, so reporting it would be an
|
|
1521
1572
|
// over-report. All-contained ⇒ no line at all.
|
|
1522
1573
|
// TWO records, never one: a resolved external path IS a weakening, an unresolvable entry is
|
|
1523
1574
|
// UNVERIFIABLE, and a single line carrying both would assert an effect for entries it could
|
|
1524
|
-
// not read
|
|
1525
|
-
|
|
1526
|
-
const { external, unresolvable } = externalWriteEntries(fsb.allowWrite, { root, home, tmp });
|
|
1575
|
+
// not read. An array of only-unresolvable entries therefore yields no weakening claim at all.
|
|
1576
|
+
const { external, unresolvable } = externalWriteEntries(foreign, { root, home, tmp });
|
|
1527
1577
|
if (external.length) {
|
|
1528
|
-
|
|
1578
|
+
const covered = source === SETTINGS_FILE && judged
|
|
1579
|
+
? tier.dirs.filter((dir) => foreign.some((entry) => isResolvableDeclaredEntry(entry) && entry !== dir &&
|
|
1580
|
+
external.includes(resolveDeclaredDir(entry, { home, root })) &&
|
|
1581
|
+
dirCovers(resolveDeclaredDir(entry, { home, root }), resolveDeclaredDir(dir, { home, root }))))
|
|
1582
|
+
: [];
|
|
1583
|
+
const coveringDetail = covered.map((dir) => `; covers the bridge state dir ${dir}`).join('');
|
|
1584
|
+
out.push({ source, key: `${SANDBOX_KEY}.filesystem.allowWrite`, weakens: 'fs_outside_repo', detail: `${HOST_HONORS_QUALIFIER}, ${external.length} declared path(s) resolve OUTSIDE the repo and $TMPDIR and are writable: ${external.map((p) => JSON.stringify(p)).join(', ')}${coveringDetail}` });
|
|
1529
1585
|
}
|
|
1530
1586
|
if (unresolvable) {
|
|
1531
1587
|
out.push({ source, key: `${SANDBOX_KEY}.filesystem.allowWrite`, weakens: 'fs_outside_repo', unverifiable: true, detail: `${unresolvable} declared entr(ies) could not be resolved (not a non-empty string) — what they would make writable is UNKNOWN` });
|
|
@@ -1535,17 +1591,20 @@ const collectSandboxWeakenings = (sources, { root, home, tmp }) => {
|
|
|
1535
1591
|
out.push({ source, key: `${SANDBOX_KEY}.allowUnsandboxedCommands`, weakens: 'every sandbox red-line', detail: `${HOST_HONORS_QUALIFIER}, commands may run unsandboxed` });
|
|
1536
1592
|
}
|
|
1537
1593
|
if (Array.isArray(sb.excludedCommands) && sb.excludedCommands.length) {
|
|
1538
|
-
// The bridge tier's OWN
|
|
1539
|
-
//
|
|
1540
|
-
//
|
|
1541
|
-
// (the Decision-2 self-consistency bar). Every OTHER excluded command — hand-added names,
|
|
1542
|
-
// local-file exclusions, tier names without their allow rules — stays a loud weakening.
|
|
1594
|
+
// The bridge tier's OWN exclusions are tier-known ONLY with their consent proof; then they are
|
|
1595
|
+
// a note, never a weakening flag. Every OTHER excluded command — hand-added names, local-file
|
|
1596
|
+
// exclusions, tier names without their proof — stays a loud weakening.
|
|
1543
1597
|
const tierKnown = sb.excludedCommands.filter((c) => isTierKnownExclusion(source, c));
|
|
1544
1598
|
const foreign = sb.excludedCommands.filter((c) => !tierKnown.includes(c));
|
|
1599
|
+
// A bare name matches only the argument-free command, so its note never claims real calls leave the sandbox.
|
|
1600
|
+
if (judged) {
|
|
1601
|
+
addNotes(`${SANDBOX_KEY}.excludedCommands`, tierKnown.filter((c) => c.endsWith(' *')), 'the wrapper runs outside the sandbox');
|
|
1602
|
+
addNotes(`${SANDBOX_KEY}.excludedCommands`, tierKnown.filter((c) => !c.endsWith(' *')), 'only its argument-free invocation runs outside the sandbox');
|
|
1603
|
+
}
|
|
1545
1604
|
if (foreign.length) {
|
|
1546
|
-
out.push({ source, key: `${SANDBOX_KEY}.excludedCommands`, weakens: 'every sandbox red-line', detail: `${foreign.length} command(s) run UNSANDBOXED ${HOST_HONORS_QUALIFIER} (network/fs confinement not applied to them)${tierKnown.length ? `; ${tierKnown.length} bridge-review wrapper exclusion(s) are tier-known and not flagged` : ''}` });
|
|
1547
|
-
} else if (tierKnown.length) {
|
|
1548
|
-
out.push({ source, key: `${SANDBOX_KEY}.excludedCommands`, weakens: null, tierKnown: true, detail: `${tierKnown.length} bridge-review wrapper exclusion(s) (${tierKnown.join(', ')}) — tier-known: ${HOST_HONORS_QUALIFIER},
|
|
1605
|
+
out.push({ source, key: `${SANDBOX_KEY}.excludedCommands`, weakens: 'every sandbox red-line', detail: `${foreign.length} command(s) run UNSANDBOXED ${HOST_HONORS_QUALIFIER} (network/fs confinement not applied to them)${!judged && tierKnown.length ? `; ${tierKnown.length} bridge-review wrapper exclusion(s) are tier-known and not flagged` : ''}` });
|
|
1606
|
+
} else if (!judged && tierKnown.length) {
|
|
1607
|
+
out.push({ source, key: `${SANDBOX_KEY}.excludedCommands`, weakens: null, tierKnown: true, detail: `${tierKnown.length} bridge-review wrapper exclusion(s) (${tierKnown.join(', ')}) — tier-known: ${HOST_HONORS_QUALIFIER}, only their argument-free invocations run outside the sandbox, and their allow rules are present in the project settings` });
|
|
1549
1608
|
}
|
|
1550
1609
|
}
|
|
1551
1610
|
return out;
|
|
@@ -1614,8 +1673,10 @@ export const formatAutonomyResult = (r) => {
|
|
|
1614
1673
|
for (const b of r.redlineBypass ?? []) {
|
|
1615
1674
|
lines.push(` ⚠ DEGRADE: ${b.source} has a pre-existing allow entry ${b.entry} that would BYPASS the rendered red-line(s) ${b.redlines.join('/')} (a matching allow rule AUTO-APPROVES the command, defeating ask/deny) — remove it by hand; this render never touches permissions.allow.`);
|
|
1616
1675
|
}
|
|
1676
|
+
if (r.bridgeTierError) lines.push(` note: bridge-tier entries not judged — ${r.bridgeTierError}`);
|
|
1617
1677
|
for (const w of r.sandboxWeakenings ?? []) {
|
|
1618
|
-
if (w.tierKnown) lines.push(` note: ${w.source} has ${w.key} (${w.detail}
|
|
1678
|
+
if (w.tierKnown && hasOwn(w, 'entry')) lines.push(` note: ${w.source} has ${w.key} entry ${JSON.stringify(w.entry)} — tier-known: part of the bridge-wrappers tier consent; ${HOST_HONORS_QUALIFIER}, ${w.detail}.`);
|
|
1679
|
+
else if (w.tierKnown) lines.push(` note: ${w.source} has ${w.key} (${w.detail}).`);
|
|
1619
1680
|
else if (w.unverifiable) lines.push(` ⚠ DEGRADE: ${w.source} has ${w.key} (${w.detail}) — the rendered ${w.weakens} red-line CANNOT BE VERIFIED against it (no claim either way); fix the declared value by hand, then re-run this preview.`);
|
|
1620
1681
|
else lines.push(` ⚠ DEGRADE: ${w.source} has ${w.key} (${w.detail}), which WEAKENS the rendered ${w.weakens} red-line — the render preserves your sandbox tuning (never clobbers it), so remove it by hand if you want the red-line fully enforced.`);
|
|
1621
1682
|
}
|
|
@@ -1663,11 +1724,12 @@ export const writeAutonomyProfile = ({ cwd, apply = false } = {}, deps = {}) =>
|
|
|
1663
1724
|
const redlineBypass = collectRedlineBypass(settingsSources);
|
|
1664
1725
|
// The allowWrite degrade resolves its entries before judging them, so it needs the same anchors a
|
|
1665
1726
|
// host resolving the key would use: the project root, the resolved home, and $TMPDIR.
|
|
1727
|
+
const bridgeConsent = deriveBridgeConsent(preflight.projectSettings?.data, projectDir, deps);
|
|
1666
1728
|
const sandboxWeakenings = collectSandboxWeakenings(settingsSources, {
|
|
1667
1729
|
root: projectDir,
|
|
1668
1730
|
home: deps.home ?? homedir(),
|
|
1669
1731
|
tmp: (deps.env ?? process.env).TMPDIR ?? tmpdir(),
|
|
1670
|
-
});
|
|
1732
|
+
}, bridgeConsent);
|
|
1671
1733
|
const resultBase = {
|
|
1672
1734
|
autonomy: true,
|
|
1673
1735
|
source,
|
|
@@ -1677,6 +1739,7 @@ export const writeAutonomyProfile = ({ cwd, apply = false } = {}, deps = {}) =>
|
|
|
1677
1739
|
localMasks,
|
|
1678
1740
|
redlineBypass,
|
|
1679
1741
|
sandboxWeakenings,
|
|
1742
|
+
bridgeTierError: bridgeConsent.error,
|
|
1680
1743
|
stamp: preflight.stamp,
|
|
1681
1744
|
stampOk: preflight.stampOk,
|
|
1682
1745
|
};
|
package/tools/write-lanes.mjs
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import { join, resolve } from 'node:path';
|
|
2
2
|
import { dirCovers } from './declared-paths.mjs';
|
|
3
|
-
import { claudeDirOf, skillTargets } from './jev-facts.mjs';
|
|
3
|
+
import { claudeDirOf, quoteArg, skillTargets } from './jev-facts.mjs';
|
|
4
4
|
import { targetsFor } from './jev-connect.mjs';
|
|
5
5
|
|
|
6
|
-
export const LANE_CONSOLE = 'console';
|
|
7
6
|
export const LANE_CHAT = 'chat';
|
|
7
|
+
export const LANE_HARNESS = 'harness';
|
|
8
|
+
export const LANE_USER = 'user';
|
|
8
9
|
|
|
9
10
|
export const VARIANT_LANES = Object.freeze({
|
|
10
|
-
'velocity-core':
|
|
11
|
-
'kit-tools-tier':
|
|
12
|
-
'bridge-tier':
|
|
11
|
+
'velocity-core': LANE_HARNESS,
|
|
12
|
+
'kit-tools-tier': LANE_HARNESS,
|
|
13
|
+
'bridge-tier': LANE_HARNESS,
|
|
13
14
|
'autonomy-policy': LANE_CHAT,
|
|
14
|
-
'autonomy-render':
|
|
15
|
-
'sandbox-provision':
|
|
16
|
-
'sandbox-provision.installable':
|
|
15
|
+
'autonomy-render': LANE_HARNESS,
|
|
16
|
+
'sandbox-provision': LANE_USER,
|
|
17
|
+
'sandbox-provision.installable': LANE_USER,
|
|
17
18
|
'review-recipe': LANE_CHAT,
|
|
18
19
|
'gates-declaration': LANE_CHAT,
|
|
19
20
|
'gates-inert': LANE_CHAT,
|
|
@@ -24,19 +25,19 @@ export const VARIANT_LANES = Object.freeze({
|
|
|
24
25
|
'source-size.unminted': LANE_CHAT,
|
|
25
26
|
'source-size.adopted-elsewhere': LANE_CHAT,
|
|
26
27
|
'source-size.id-squatter': LANE_CHAT,
|
|
27
|
-
'gate-hook':
|
|
28
|
-
'gate-hook.marker-stale':
|
|
29
|
-
'commit-guard':
|
|
30
|
-
enforcement:
|
|
28
|
+
'gate-hook': LANE_HARNESS,
|
|
29
|
+
'gate-hook.marker-stale': LANE_USER,
|
|
30
|
+
'commit-guard': LANE_USER,
|
|
31
|
+
enforcement: LANE_USER,
|
|
31
32
|
'read-lane': LANE_CHAT,
|
|
32
|
-
'read-lane.stale':
|
|
33
|
-
'read-lane.missing':
|
|
34
|
-
'state-block':
|
|
35
|
-
'mcp-channel':
|
|
36
|
-
'mcp-channel.differing':
|
|
37
|
-
agents:
|
|
38
|
-
'executor-vehicle':
|
|
39
|
-
'family-freshness':
|
|
33
|
+
'read-lane.stale': LANE_USER,
|
|
34
|
+
'read-lane.missing': LANE_HARNESS,
|
|
35
|
+
'state-block': LANE_USER,
|
|
36
|
+
'mcp-channel': LANE_HARNESS,
|
|
37
|
+
'mcp-channel.differing': LANE_HARNESS,
|
|
38
|
+
agents: LANE_HARNESS,
|
|
39
|
+
'executor-vehicle': LANE_HARNESS,
|
|
40
|
+
'family-freshness': LANE_USER,
|
|
40
41
|
'adr-store-migration': LANE_CHAT,
|
|
41
42
|
'sandbox-masks': LANE_CHAT,
|
|
42
43
|
'sandbox-masks.unfenced-mount': LANE_CHAT,
|
|
@@ -44,9 +45,9 @@ export const VARIANT_LANES = Object.freeze({
|
|
|
44
45
|
'worktrees-dir': LANE_CHAT,
|
|
45
46
|
'spec-adoption': LANE_CHAT,
|
|
46
47
|
'profile-gap': LANE_CHAT,
|
|
47
|
-
'jev-connect':
|
|
48
|
-
'jev-skill':
|
|
49
|
-
'jev-skill.earlier':
|
|
48
|
+
'jev-connect': LANE_USER,
|
|
49
|
+
'jev-skill': LANE_HARNESS,
|
|
50
|
+
'jev-skill.earlier': LANE_HARNESS,
|
|
50
51
|
'spec-adoption.adopting': LANE_CHAT,
|
|
51
52
|
});
|
|
52
53
|
|
|
@@ -78,8 +79,6 @@ export const isProtectedPath = (abs, { root, home, hooksDir }) => {
|
|
|
78
79
|
|| [...PROTECTED_DIRS.map((dir) => join(project, dir)), hooks].some((dir) => dirCovers(dir, path));
|
|
79
80
|
};
|
|
80
81
|
|
|
81
|
-
const ROUTE = "applies at your next npx @sabaiway/agent-workflow-kit@latest init, run from this project's folder";
|
|
82
|
-
const RESTART = '; if init reports it not pending, restart the agent from that console';
|
|
83
82
|
const SETTINGS_WRITES = [SETTINGS_PATH];
|
|
84
83
|
const HOOK_WRITES = [join('.claude', 'hooks', 'agent-workflow-gates.mjs'), ...SETTINGS_WRITES];
|
|
85
84
|
const MCP_WRITES = ['.mcp.json', ...SETTINGS_WRITES];
|
|
@@ -98,12 +97,16 @@ const getStartupPaths = ({ env, home, platform }) => targetsFor({
|
|
|
98
97
|
platform,
|
|
99
98
|
}).filter(({ path }) => path !== null).map(({ path }) => path);
|
|
100
99
|
|
|
101
|
-
const
|
|
100
|
+
const formatCommand = (argv) => argv.map(quoteArg).join(' ');
|
|
101
|
+
const TERMINAL_STEP = 'run in a terminal of your own: ';
|
|
102
|
+
|
|
103
|
+
const createForm = (file, preview, apply, writes, envDependent = false, userStep = null) => Object.freeze({
|
|
102
104
|
file,
|
|
103
105
|
preview,
|
|
104
106
|
apply,
|
|
105
107
|
writes,
|
|
106
|
-
|
|
108
|
+
envDependent,
|
|
109
|
+
userStep,
|
|
107
110
|
});
|
|
108
111
|
|
|
109
112
|
const CORE_FORM = createForm('velocity-profile.mjs', ['--cwd', '<root>'], ['--apply', '--cwd', '<root>'], SETTINGS_WRITES);
|
|
@@ -126,11 +129,17 @@ const FORMS = Object.freeze({
|
|
|
126
129
|
['--replace', '--cwd', '<root>'], ['--replace', '--apply', '--cwd', '<root>'], MCP_WRITES),
|
|
127
130
|
agents: AGENT_FORM,
|
|
128
131
|
'executor-vehicle': AGENT_FORM,
|
|
129
|
-
'sandbox-provision.installable': createForm('autonomy-doctor.mjs', [], ['--apply', '<tuple>'], [], true
|
|
132
|
+
'sandbox-provision.installable': createForm('autonomy-doctor.mjs', [], ['--apply', '<tuple>'], [], true,
|
|
133
|
+
(facts, argv) => 'your own step (a sudo install; the doctor prints the --apply line to run next) — '
|
|
134
|
+
+ `${TERMINAL_STEP}cd ${quoteArg(facts.root)} && ${formatCommand(argv.preview)}`),
|
|
130
135
|
'jev-skill': SKILL_FORM,
|
|
131
136
|
'jev-skill.earlier': SKILL_FORM,
|
|
132
|
-
'jev-connect': createForm('jev-connect.mjs', null, [], getStartupPaths, true
|
|
133
|
-
|
|
137
|
+
'jev-connect': createForm('jev-connect.mjs', null, [], getStartupPaths, true,
|
|
138
|
+
(facts, argv) => 'your own step — it asks for the key in your terminal and the key never goes into the chat — '
|
|
139
|
+
+ `${TERMINAL_STEP}${formatCommand(argv.apply)}`),
|
|
140
|
+
'family-freshness': Object.freeze({
|
|
141
|
+
userStep: () => `your own step — ${TERMINAL_STEP}${formatCommand(['npx', '@sabaiway/agent-workflow-kit@latest', 'init'])}`,
|
|
142
|
+
}),
|
|
134
143
|
'sandbox-provision': null,
|
|
135
144
|
'commit-guard': null,
|
|
136
145
|
'gate-hook.marker-stale': null,
|
|
@@ -165,7 +174,22 @@ export const writesOf = (variant, facts) => {
|
|
|
165
174
|
return form.writes.map((path) => join(facts.root, path));
|
|
166
175
|
};
|
|
167
176
|
|
|
168
|
-
export const
|
|
177
|
+
export const isEnvDependent = (variant) => {
|
|
169
178
|
laneOf(variant);
|
|
170
|
-
return FORMS[variant]?.
|
|
179
|
+
return FORMS[variant]?.envDependent === true;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
export const applySlot = (variant, facts) => {
|
|
183
|
+
const lane = laneOf(variant);
|
|
184
|
+
const form = FORMS[variant];
|
|
185
|
+
if (lane === LANE_HARNESS) {
|
|
186
|
+
const args = ['node', join(facts.toolsDir, 'apply-danger-check.mjs'), '--variant', variant, '--cwd', facts.root];
|
|
187
|
+
const skillArgs = form === SKILL_FORM ? form.preview.map((arg) => resolveArgument(arg, facts)) : [];
|
|
188
|
+
const check = formatCommand([...args, ...skillArgs]);
|
|
189
|
+
return { check, apply: `${check} --apply --expect <digest>` };
|
|
190
|
+
}
|
|
191
|
+
if (lane === LANE_USER && form?.userStep) {
|
|
192
|
+
return { check: null, apply: form.userStep(facts, consoleArgv(variant, facts)) };
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
171
195
|
};
|