@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
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// bridge-state-dirs.mjs — the one leaf that turns the bundled bridges' declared writable state dirs
|
|
2
|
+
// (`writableDirs`) into `sandbox.filesystem.allowWrite` entries (contract: bridges/velocity-profile).
|
|
3
|
+
// The bridge tier writes them, the advisor counts them, the danger check admits them, the autonomy
|
|
4
|
+
// preview names them: one resolution, one home-relative spelling, one merge, so the four cannot drift.
|
|
5
|
+
//
|
|
6
|
+
// A LEAF: imports declared-paths.mjs and node:path only. No side effects on import. Dependency-free,
|
|
7
|
+
// Node >= 22.
|
|
8
|
+
|
|
9
|
+
import { dirname, relative, resolve } from 'node:path';
|
|
10
|
+
import { dirCovers, isResolvableDeclaredEntry, resolveDeclaredDir } from './declared-paths.mjs';
|
|
11
|
+
|
|
12
|
+
// D6 resolution, mirroring the wrappers' byte-semantics (`${VAR:-default}` + the exact case-arms:
|
|
13
|
+
// `~` / `~/…` / `/…` ride as-given; EVERY other form — including `~user/…`, which the wrappers
|
|
14
|
+
// never resolve as a home path — anchors like a relative path). Anchored to the TARGET PROJECT ROOT,
|
|
15
|
+
// matching what a wrapper invoked from the project root resolves.
|
|
16
|
+
export const resolveWritableDir = (entry, { env, root }) => {
|
|
17
|
+
const value = entry.env == null ? '' : (env[entry.env] ?? '');
|
|
18
|
+
if (value === '') return entry.default;
|
|
19
|
+
if (value === '~' || value.startsWith('~/') || value.startsWith('/')) return value;
|
|
20
|
+
return resolve(root, value);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// The HOME-SYMBOLIC spelling: tilde forms stay symbolic, an absolute dir under the home (by path
|
|
24
|
+
// segment, never a string prefix) folds back to `~/…`, anything outside the home stays absolute.
|
|
25
|
+
export const homeRelative = (dir, home) => {
|
|
26
|
+
if (dir === '~') return '~';
|
|
27
|
+
if (dir.startsWith('~/')) return `~/${dir.slice(2)}`;
|
|
28
|
+
const homeAbs = resolve(home);
|
|
29
|
+
const abs = resolve(dir);
|
|
30
|
+
if (abs === homeAbs) return '~';
|
|
31
|
+
return dirCovers(homeAbs, abs) ? `~/${relative(homeAbs, abs)}` : abs;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const ancestorsOf = (dir) => {
|
|
35
|
+
const out = [];
|
|
36
|
+
for (let at = dirname(dir); at !== dir; dir = at, at = dirname(at)) out.push(at);
|
|
37
|
+
return out;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// A grant on `/`, on the home, or on an ancestor of the home or of the root would hand every
|
|
41
|
+
// sandboxed command the user's whole tree — never a bridge's state dir. The override that produced
|
|
42
|
+
// it is named, so the user knows which variable to fix.
|
|
43
|
+
const dangerOf = (abs, { home, root }) => {
|
|
44
|
+
const homeAbs = resolve(home);
|
|
45
|
+
if (abs === '/') return 'the filesystem root';
|
|
46
|
+
if (abs === homeAbs) return 'the home directory';
|
|
47
|
+
if (ancestorsOf(homeAbs).includes(abs)) return 'an ancestor of the home directory';
|
|
48
|
+
if (ancestorsOf(resolve(root)).includes(abs)) return 'an ancestor of the project root';
|
|
49
|
+
return null;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const stateDirsOf = (dirEntries, { env, root, home }) => {
|
|
53
|
+
const dirs = [];
|
|
54
|
+
for (const entry of dirEntries) {
|
|
55
|
+
const raw = resolveWritableDir(entry, { env, root });
|
|
56
|
+
const danger = dangerOf(resolveDeclaredDir(raw, { home, root }), { home, root });
|
|
57
|
+
if (danger !== null) {
|
|
58
|
+
const source = entry.env == null ? 'its declared default' : entry.env;
|
|
59
|
+
throw new Error(`writable state dir "${raw}" from ${source} resolves to ${danger} — refusing to grant it; point ${source} at the bridge's own state dir`);
|
|
60
|
+
}
|
|
61
|
+
const rendered = homeRelative(raw, home);
|
|
62
|
+
if (!dirs.includes(rendered)) dirs.push(rendered);
|
|
63
|
+
}
|
|
64
|
+
return dirs;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// The dirs no existing allowWrite entry covers (resolved both sides, containment by path segment).
|
|
68
|
+
export const missingStateDirs = (dirs, allowWrite, { home, root }) => {
|
|
69
|
+
const existing = (Array.isArray(allowWrite) ? allowWrite : [])
|
|
70
|
+
.filter(isResolvableDeclaredEntry)
|
|
71
|
+
.map((entry) => resolveDeclaredDir(entry, { home, root }));
|
|
72
|
+
return dirs.filter((dir) => {
|
|
73
|
+
const abs = resolveDeclaredDir(dir, { home, root });
|
|
74
|
+
return !existing.some((container) => dirCovers(container, abs));
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// The ONE writer of `sandbox.filesystem.allowWrite`: the other sandbox and filesystem sub-keys stay
|
|
79
|
+
// where they are, each dir not already present appends once. Pure: returns a new object, and with
|
|
80
|
+
// nothing to add returns the input unchanged (no sandbox key is created for nothing).
|
|
81
|
+
export const mergeAllowWrite = (settings, dirs) => {
|
|
82
|
+
const base = settings ?? {};
|
|
83
|
+
const existing = Array.isArray(base.sandbox?.filesystem?.allowWrite) ? base.sandbox.filesystem.allowWrite : [];
|
|
84
|
+
const toAppend = [];
|
|
85
|
+
for (const dir of dirs) if (!existing.includes(dir) && !toAppend.includes(dir)) toAppend.push(dir);
|
|
86
|
+
if (toAppend.length === 0) return base;
|
|
87
|
+
return {
|
|
88
|
+
...base,
|
|
89
|
+
sandbox: {
|
|
90
|
+
...base.sandbox,
|
|
91
|
+
filesystem: { ...base.sandbox?.filesystem, allowWrite: [...existing, ...toAppend] },
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
};
|
package/tools/init-project.mjs
CHANGED
|
@@ -4,15 +4,16 @@ import { homedir } from 'node:os';
|
|
|
4
4
|
import { basename, dirname, join, resolve, sep } from 'node:path';
|
|
5
5
|
import { createInterface } from 'node:readline/promises';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { checkApply } from './apply-danger-check.mjs';
|
|
7
8
|
import { resolveGitHooksPath } from './commit-guard.mjs';
|
|
8
9
|
import { dirCovers } from './declared-paths.mjs';
|
|
9
10
|
import { isDirectRun } from './direct-run.mjs';
|
|
10
|
-
import { GIT_MAX_BUFFER } from './git-env.mjs';
|
|
11
|
+
import { GIT_MAX_BUFFER, stripGitLocationEnv } from './git-env.mjs';
|
|
11
12
|
import { preflightInit } from './init-preflight.mjs';
|
|
12
13
|
import { buildRecommendations, hostFacts } from './recommendations.mjs';
|
|
13
14
|
import { compareSemver } from './semver-lite.mjs';
|
|
14
15
|
import { EXPECTED_WORKFLOW_VERSION } from './velocity-profile.mjs';
|
|
15
|
-
import {
|
|
16
|
+
import { LANE_HARNESS, VARIANT_LANES, consoleArgv, isEnvDependent, isProtectedPath, laneOf, writesOf } from './write-lanes.mjs';
|
|
16
17
|
|
|
17
18
|
const INSIDE_LINE = 'you are inside an agent; run this in your own terminal';
|
|
18
19
|
const APPLY_PROMPT = 'apply? y/N: ';
|
|
@@ -96,29 +97,21 @@ const readState = (path, io) => {
|
|
|
96
97
|
if (stat.isSymbolicLink()) return `link:${io.readlink(path)}:${followedState(path, io)}`;
|
|
97
98
|
return statState(path, stat, io);
|
|
98
99
|
};
|
|
99
|
-
const
|
|
100
|
+
const readStateOrElse = (path, io, onError) => {
|
|
100
101
|
try {
|
|
101
102
|
return readState(path, io);
|
|
102
103
|
} catch (error) {
|
|
103
|
-
|
|
104
|
+
return onError(error);
|
|
104
105
|
}
|
|
105
106
|
};
|
|
107
|
+
const stateOf = (path, io) => readStateOrElse(path, io, (error) => {
|
|
108
|
+
throw new Error(`${path} could not be read: ${error.code ?? error.message}`);
|
|
109
|
+
});
|
|
106
110
|
const recordPaths = (paths, io) => new Map([...new Set(paths)].map((path) => [path, stateOf(path, io)]));
|
|
107
|
-
const findChanged = (record, io) => [...record].find(([path, state]) =>
|
|
108
|
-
|
|
109
|
-
return readState(path, io) !== state;
|
|
110
|
-
} catch {
|
|
111
|
-
return true;
|
|
112
|
-
}
|
|
113
|
-
})?.[0];
|
|
111
|
+
const findChanged = (record, io) => [...record].find(([path, state]) =>
|
|
112
|
+
readStateOrElse(path, io, () => null) !== state)?.[0];
|
|
114
113
|
// After an apply a path that cannot be read is recorded as such, so the next comparison stops the batch on it.
|
|
115
|
-
const refreshState = (path, io) => {
|
|
116
|
-
try {
|
|
117
|
-
return readState(path, io);
|
|
118
|
-
} catch (error) {
|
|
119
|
-
return `unreadable:${error.code ?? error.message}`;
|
|
120
|
-
}
|
|
121
|
-
};
|
|
114
|
+
const refreshState = (path, io) => readStateOrElse(path, io, (error) => `unreadable:${error.code ?? error.message}`);
|
|
122
115
|
const DIRECTORY = 'directory:';
|
|
123
116
|
const entriesOf = (state) => (state === 'absent' ? [] : JSON.parse(state.slice(DIRECTORY.length)));
|
|
124
117
|
// A recorded parent moves only by the written entry, so an entry created or removed beside it still stops the batch.
|
|
@@ -179,16 +172,13 @@ const reachesQueue = (variant, facts) => {
|
|
|
179
172
|
const isMasked = (variant, context) => (writesOf(variant, context.facts) ?? [])
|
|
180
173
|
.some((write) => [...context.masks].some((mask) => dirCovers(write, mask)));
|
|
181
174
|
|
|
182
|
-
// context.items keeps the console items of the advisor's latest return, before the queue's filters.
|
|
183
175
|
const getQueue = async (context) => {
|
|
184
|
-
|
|
185
|
-
context.items = items.filter(({ variant }) => laneOf(variant) === LANE_CONSOLE);
|
|
176
|
+
context.items = (await context.recommend(context.facts.root, { keySet: context.keySet })).items;
|
|
186
177
|
return context.items.filter(({ variant }) => reachesQueue(variant, context.facts) && !isMasked(variant, context));
|
|
187
178
|
};
|
|
188
179
|
|
|
189
180
|
const printNotPending = (context) => {
|
|
190
|
-
const
|
|
191
|
-
const variants = Object.keys(VARIANT_LANES).filter((variant) => laneOf(variant) === LANE_CONSOLE && routeLine(variant)?.length > plain.length);
|
|
181
|
+
const variants = Object.keys(VARIANT_LANES).filter(isEnvDependent);
|
|
192
182
|
const keys = new Set();
|
|
193
183
|
for (const variant of variants) {
|
|
194
184
|
const key = variant.split('.')[0];
|
|
@@ -227,6 +217,16 @@ const previewQueue = async (context, queue) => {
|
|
|
227
217
|
context.print(result.stdout);
|
|
228
218
|
const tuple = result.stdout.match(/--apply\s+(\S+)/)?.[1];
|
|
229
219
|
const facts = { ...context.facts, tuple };
|
|
220
|
+
if (laneOf(item.variant) === LANE_HARNESS) {
|
|
221
|
+
try {
|
|
222
|
+
const { lines, refused } = context.checkApply(item.variant, facts, {});
|
|
223
|
+
for (const line of lines) context.print(line);
|
|
224
|
+
if (refused.length > 0) throw new Error(refused.map(({ key }) => key).join(', '));
|
|
225
|
+
} catch (error) {
|
|
226
|
+
context.print(`not applied: ${item.variant} — the danger check refused: ${error.message}`);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
230
|
batch.push({ ...item, applyArgv: consoleArgv(item.variant, facts).apply, writes: writesOf(item.variant, facts) });
|
|
231
231
|
printNote(context, item.key);
|
|
232
232
|
}
|
|
@@ -295,7 +295,7 @@ const runPass = async (context, queue) => {
|
|
|
295
295
|
const selectRoot = async (context) => {
|
|
296
296
|
const { candidates } = findProject({ cwd: context.cwd, lstat: context.io.lstat });
|
|
297
297
|
if (candidates.length === 0) {
|
|
298
|
-
context.print(
|
|
298
|
+
context.print('no agent-workflow project here — run /agent-workflow-kit upgrade in your agent from that project, or deploy it there first: its yes applies the project items');
|
|
299
299
|
return { root: null };
|
|
300
300
|
}
|
|
301
301
|
if (candidates.length === 1) return candidates[0];
|
|
@@ -317,7 +317,7 @@ const gateStamp = (candidate, context) => {
|
|
|
317
317
|
if (compareSemver(stamp, EXPECTED_WORKFLOW_VERSION) === 1) {
|
|
318
318
|
context.print(`this project's stamp ${stamp} is ahead of this kit's lineage ${EXPECTED_WORKFLOW_VERSION} — update the kit, then init again`);
|
|
319
319
|
} else {
|
|
320
|
-
context.print('run /agent-workflow-kit upgrade
|
|
320
|
+
context.print('run /agent-workflow-kit upgrade in your agent: its yes applies the items');
|
|
321
321
|
}
|
|
322
322
|
return null;
|
|
323
323
|
};
|
|
@@ -334,10 +334,8 @@ export const runInitProject = async (deps) => {
|
|
|
334
334
|
print(INSIDE_LINE);
|
|
335
335
|
return { code: 0 };
|
|
336
336
|
}
|
|
337
|
-
const spawnEnv =
|
|
338
|
-
|
|
339
|
-
delete spawnEnv.GIT_WORK_TREE;
|
|
340
|
-
const context = { ...deps, io, print, spawnEnv, masks: new Set(), keySet: false, restart: false };
|
|
337
|
+
const spawnEnv = stripGitLocationEnv(deps.env);
|
|
338
|
+
const context = { ...deps, checkApply: deps.checkApply ?? checkApply, io, print, spawnEnv, masks: new Set(), keySet: false, restart: false };
|
|
341
339
|
const candidate = await selectRoot(context);
|
|
342
340
|
if (candidate === null) return { code: 0 };
|
|
343
341
|
if (!preflightInit({ ...deps, root: candidate.root, io }).ok) {
|
package/tools/jev-connect.mjs
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
// their own: it reads the key with no echo, verifies it with one request, saves it where the shell
|
|
5
5
|
// reads it (bash, zsh, fish) or in the Windows user environment, and prints the restart step. It never
|
|
6
6
|
// prints the key, never puts it in an argv, and refuses when stdin is not a terminal, so a
|
|
7
|
-
// non-interactive run
|
|
8
|
-
//
|
|
7
|
+
// non-interactive run cannot reach the prompt. The key never reaches the agent or the chat.
|
|
8
|
+
// It claims no connection: the guide's key mark, read after the restart, is the proof.
|
|
9
9
|
import { spawnSync } from 'node:child_process';
|
|
10
10
|
import { lstatSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
11
11
|
import { homedir } from 'node:os';
|
package/tools/jev-guide.mjs
CHANGED
|
@@ -45,11 +45,11 @@ export const JEV_TEXT = Object.freeze([
|
|
|
45
45
|
]);
|
|
46
46
|
|
|
47
47
|
const KEY_GET = 'Get a key at console.typesafe.ai and set it on every host the agent runs on.';
|
|
48
|
-
const KEY_CONNECT = 'Connect it from a terminal of your own
|
|
48
|
+
const KEY_CONNECT = 'Connect it from a terminal of your own — the key never reaches the agent or the chat: the command asks for the key with no echo, checks it with one request and saves it for bash, zsh or fish in your shell\'s startup files, or on Windows as a user environment variable.';
|
|
49
49
|
const KEY_RESTART = `Then ${RESTART_STEP}. Run /agent-workflow-kit jev again: the key mark should read set.`;
|
|
50
50
|
const KEY_NEVER = 'Never paste the key into the chat or into a project file.';
|
|
51
51
|
const KEY_NOT_SET = `key: ${KEY_VARIABLE} not set. A host setting that filters the environment of the agent's commands also hides it. If it still reads not set after that restart and no such setting applies, run the connect line again and follow its last line.`;
|
|
52
|
-
const SKILL_OPTIONAL = 'Optional, for your own code and prompts — the vendor skill. The kit installs it pinned, one verified copy per agent skill root (Codex, Claude Code, Antigravity CLI);
|
|
52
|
+
const SKILL_OPTIONAL = 'Optional, for your own code and prompts — the vendor skill. The kit installs it pinned, one verified copy per agent skill root (Codex, Claude Code, Antigravity CLI); if Recommendations offers this item, the agent applies it on your chat yes after its danger check; you can also run this yourself in a terminal of your own:';
|
|
53
53
|
const NO_HOME = 'no home directory found: the kit\'s install needs one';
|
|
54
54
|
const INSTALL = [
|
|
55
55
|
'Or the vendor\'s own routes:',
|
package/tools/manifest/schema.md
CHANGED
|
@@ -100,15 +100,16 @@ drift-guarded set-equal to this block by the bridge `bin/*.test.mjs` suites (hel
|
|
|
100
100
|
`networkHosts` declares the hosts the backend CLI is **observed** to contact (synthetic examples:
|
|
101
101
|
`*.api.backend.example`, `accounts.backend.example`; the real observed lists live in each bridge's
|
|
102
102
|
`capability.json`) — the **single source** of the hosts `velocity-profile --bridge-tier` seeds
|
|
103
|
-
into the project's `sandbox.network.allowedDomains` for every
|
|
103
|
+
into the project's `sandbox.network.allowedDomains` for every used bridge, and of a hand-applied
|
|
104
104
|
session sandbox allowlist. Rules:
|
|
105
105
|
|
|
106
106
|
- Each entry is a bare dotted hostname or a `*.family` wildcard — never a scheme/path/port.
|
|
107
107
|
Entries must be unique. A malformed list **fails** `--strict` (the entries are pasted verbatim
|
|
108
108
|
into allowlist lines by the Recommendations advisor).
|
|
109
|
-
- **The `--bridge-tier` seed is the primary lane** (AD-172):
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
- **The `--bridge-tier` seed is the primary lane** (AD-172): for every used bridge it merges
|
|
110
|
+
hosts into the project's `sandbox.network.allowedDomains` beside the wrapper's `<wrapper> *`
|
|
111
|
+
exclusion (`codex-exec *` for the execution wrapper). A settings-native harness that drops the
|
|
112
|
+
exclusion still admits them; that pre-allow widens egress
|
|
112
113
|
for **every** sandboxed command, which the tier's consent states. The **hand-apply** fallback
|
|
113
114
|
stays for a harness-managed sandbox that ignores the settings keys.
|
|
114
115
|
- Observed-minimal, honestly incomplete: a blocked host names itself at run time — extend the
|
|
@@ -132,8 +133,9 @@ declared path is only the **default under no override**. Rules:
|
|
|
132
133
|
relative path. The advisor anchors those to the **target project root** (its pinned `--cwd`),
|
|
133
134
|
matching what a wrapper invoked from the project root resolves — the documented dispatch form
|
|
134
135
|
(the wrapper itself anchors to its invocation `$PWD`); else the `default` applies.
|
|
135
|
-
-
|
|
136
|
-
`sandbox.filesystem.allowWrite`
|
|
136
|
+
- **The `--bridge-tier` seeds every used bridge's state dirs**, home-relative under the home
|
|
137
|
+
(absolute outside it), into `sandbox.filesystem.allowWrite` beside the wrapper's `<wrapper> *`
|
|
138
|
+
exclusion (`codex-exec *` for the execution wrapper) — the entries feed the
|
|
137
139
|
sandbox-lane discoverability item (session/host sandbox config is hand-applied territory).
|
|
138
140
|
|
|
139
141
|
## Mode catalog (BRIDGE-MODES-CATALOG)
|
|
@@ -19,9 +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
|
-
//
|
|
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; the sandbox-lane item's convergence is a NEUTRAL
|
|
25
26
|
// fingerprint-bound acknowledgement recorded by the consent-gated ack writer into the family-owned
|
|
26
27
|
// docs/ai/acks.json (AD-055 relocated it off the host settings schema), never a security key (D4).
|
|
27
28
|
//
|
|
@@ -49,7 +50,7 @@ import {
|
|
|
49
50
|
} from './velocity-profile.mjs';
|
|
50
51
|
import { loadAutonomy, isSparseSeedConfig, AUTONOMY_REL } from './autonomy-config.mjs';
|
|
51
52
|
import { deriveDoctorPlan, TRUSTED_DIRS } from './autonomy-doctor.mjs';
|
|
52
|
-
import { laneOf,
|
|
53
|
+
import { laneOf, applySlot } from './write-lanes.mjs';
|
|
53
54
|
import { detectBackends, findOnPath } from './detect-backends.mjs';
|
|
54
55
|
import { isDirectRun } from './direct-run.mjs';
|
|
55
56
|
import { ACTIVITIES, resolveActivityRecipe, composeReadiness, safeLine } from './recipes.mjs';
|
|
@@ -80,6 +81,7 @@ import { resolveDeclaredDir, dirCovers, isResolvableDeclaredEntry } from './decl
|
|
|
80
81
|
import { resolveGitHooksPath } from './commit-guard.mjs';
|
|
81
82
|
import { loadConfig } from './orchestration-config.mjs';
|
|
82
83
|
import { bundledSandboxRecipe } from './bridge-sandbox-recipe.mjs';
|
|
84
|
+
import { homeRelative, resolveWritableDir } from './bridge-state-dirs.mjs';
|
|
83
85
|
import { assertContainedRealPath } from './fs-safe.mjs';
|
|
84
86
|
import { loadWorktreesConfig, resolveProbeDir } from './worktrees.mjs';
|
|
85
87
|
import { preflightCheapAgents, EXECUTOR_VEHICLE } from './cheap-agents.mjs';
|
|
@@ -248,7 +250,7 @@ export const composeVerdict = ({ attention, optional, skipped }) => {
|
|
|
248
250
|
export const WHATS = Object.freeze({
|
|
249
251
|
'velocity-core': 'routine read-only commands still prompt — {n} audited read-only allowlist entr(ies) not seeded',
|
|
250
252
|
'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,
|
|
253
|
+
'bridge-tier': 'bridge-wrappers tier incomplete — {n} entr(ies) missing (allow rules, exclusions, hosts or state dirs of used bridges)',
|
|
252
254
|
'autonomy-policy': 'no {path} — the computed defaults apply implicitly (red-lines ask/deny; every activity floors at prompt)',
|
|
253
255
|
'autonomy-render': 'the declared autonomy policy is not rendered into .claude/settings.json — drift: {drift}',
|
|
254
256
|
'sandbox-provision': 'the OS sandbox is unavailable: {reason}',
|
|
@@ -324,7 +326,7 @@ export const BENEFITS = Object.freeze({
|
|
|
324
326
|
'velocity-core': 'velocity — routine read-only commands stop prompting while the maintainer is away',
|
|
325
327
|
'kit-tools-tier': "velocity — the kit's own read-only tools run promptless (audited, resolved-absolute tier)",
|
|
326
328
|
'bridge-tier':
|
|
327
|
-
'velocity —
|
|
329
|
+
'velocity — used bridges get <wrapper> * exclusions (codex-exec * if delegated), hosts, state dirs (allowWrite); reviews: code-mode allows',
|
|
328
330
|
'autonomy-policy': 'clarity — the per-activity autonomy policy becomes an explicit, versioned declaration instead of implicit computed defaults',
|
|
329
331
|
'autonomy-render': `velocity — confined commands auto-allow per your declared policy; ${DUAL_SECURITY_BENEFIT}`,
|
|
330
332
|
'sandbox-provision': `velocity — confined ad-hoc commands stop prompting; ${DUAL_SECURITY_BENEFIT}`,
|
|
@@ -409,9 +411,9 @@ export const OPT_IN_CAPABILITIES = Object.freeze([
|
|
|
409
411
|
// state that makes a configured subagent carrier run solo.
|
|
410
412
|
{ id: 'executor-vehicle', mode: 'agents', advisorKey: 'executor-vehicle' },
|
|
411
413
|
// Exempt, not un-audited. `acceptEdits` auto-applies Edit/Write and auto-runs mkdir/touch/mv/cp:
|
|
412
|
-
// a TRUST-POSTURE change.
|
|
413
|
-
//
|
|
414
|
-
//
|
|
414
|
+
// a TRUST-POSTURE change. Velocity presents its full posture at its own consent moment;
|
|
415
|
+
// the bridge tier separately seeds used bridges' allow rules, <wrapper> * exclusions
|
|
416
|
+
// (codex-exec * where delegated), hosts and state dirs (allowWrite).
|
|
415
417
|
{
|
|
416
418
|
id: 'accept-edits',
|
|
417
419
|
mode: 'velocity',
|
|
@@ -460,8 +462,12 @@ const probeVelocityItems = ({ root, deps, add, skip }) => {
|
|
|
460
462
|
skip('kit-tools-tier', err);
|
|
461
463
|
}
|
|
462
464
|
try {
|
|
463
|
-
const bt = planVelocityProfile(preflight, {
|
|
464
|
-
|
|
465
|
+
const bt = planVelocityProfile(preflight, {
|
|
466
|
+
bridgeTier: true, findWrapper: deps.findWrapper,
|
|
467
|
+
bundleRoot: deps.bundleRoot, readdir: deps.readdir, readFile: deps.readFile,
|
|
468
|
+
detect: deps.detect, env: deps.getenv ?? deps.env, home: deps.home,
|
|
469
|
+
});
|
|
470
|
+
const delta = bt.bridgeToAdd.length + bt.excludedToAdd.length + bt.hostsToAdd.length + bt.dirsToAdd.length;
|
|
465
471
|
if (delta > 0) {
|
|
466
472
|
add('bridge-tier', fillTemplate(WHATS['bridge-tier'], { n: delta }), applyLine(' --bridge-tier'));
|
|
467
473
|
}
|
|
@@ -1159,18 +1165,6 @@ const probeMasksItem = ({ root, deps, add, skip }) => {
|
|
|
1159
1165
|
// knob is retired in the wrappers (recognized, arms nothing) and an oversized code review is now a
|
|
1160
1166
|
// chunked feed with a per-part delivery proof, so there is nothing left to offer.
|
|
1161
1167
|
|
|
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
1168
|
// The NEUTRAL recipe fingerprint (D4): a hash over the resolved hosts ∪ dirs data — an
|
|
1175
1169
|
// acknowledgement token, never a security key. Canonical form is HOME-SYMBOLIC: an
|
|
1176
1170
|
// absolute dir under the resolved home canonicalizes BACK to its `~/…` form and tilde forms stay
|
|
@@ -1179,14 +1173,7 @@ const resolveWritableDir = (entry, { env, root }) => {
|
|
|
1179
1173
|
// churns between them); only a genuinely-outside-home absolute override stays absolute
|
|
1180
1174
|
// (machine-specific by nature). Any change to the recipe re-fires the item.
|
|
1181
1175
|
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
|
-
};
|
|
1176
|
+
const norm = (dir) => homeRelative(dir, home);
|
|
1190
1177
|
return factFingerprint(JSON.stringify({ hosts: [...hosts].sort(), dirs: [...new Set(dirs.map(norm))].sort() }));
|
|
1191
1178
|
};
|
|
1192
1179
|
|
|
@@ -1346,14 +1333,14 @@ const probeSandboxLane = ({ root, deps, add, skip }) => {
|
|
|
1346
1333
|
// Wired = the two-surface proof the tier wrote before AD-172 (the wrapper in the PROJECT
|
|
1347
1334
|
// excludedCommands + its code-mode allow rule, either scope) — surfacing the recipe before those
|
|
1348
1335
|
// exist would front-run the bridge-tier item (codex terminal). The hosts surface (AD-172) is
|
|
1349
|
-
// deliberately not required:
|
|
1350
|
-
// render beside a bridge-tier item offering the
|
|
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.
|
|
1351
1338
|
const allowRules = [
|
|
1352
1339
|
...(Array.isArray(settings.data?.permissions?.allow) ? settings.data.permissions.allow : []),
|
|
1353
1340
|
...(Array.isArray(localSettings.data?.permissions?.allow) ? localSettings.data.permissions.allow : []),
|
|
1354
1341
|
];
|
|
1355
1342
|
const wired = BRIDGE_REVIEW_WRAPPERS.filter(
|
|
1356
|
-
(w) => excluded.includes(w) && probePlaced(w) && allowRules.includes(`Bash(${w} ${BRIDGE_REVIEW_MODE}:*)`),
|
|
1343
|
+
(w) => (excluded.includes(w) || excluded.includes(`${w} *`)) && probePlaced(w) && allowRules.includes(`Bash(${w} ${BRIDGE_REVIEW_MODE}:*)`),
|
|
1357
1344
|
);
|
|
1358
1345
|
if (wired.length === 0) return; // the tier's rules and exclusions are not wired — the bridge-tier item covers first
|
|
1359
1346
|
const { hosts, dirEntries } = bundledSandboxRecipe(wired, deps);
|
|
@@ -1574,7 +1561,7 @@ export const probeJevConnect = ({ root, deps, add, skip }) => {
|
|
|
1574
1561
|
const host = deps.jevHost ?? {};
|
|
1575
1562
|
const place = placeOf({ env, platform: host.platform, hostname: host.hostname, container: host.container });
|
|
1576
1563
|
const decline = `node ${q(toolPath('ack-write.mjs'))} --lane jev-connect --fingerprint ${JEV_CONNECT_DECLINE} --cwd ${q(root)}`;
|
|
1577
|
-
add('jev-connect', WHATS['jev-connect'],
|
|
1564
|
+
add('jev-connect', WHATS['jev-connect'], applySlot('jev-connect', { toolsDir: HERE }).apply, 'jev-connect',
|
|
1578
1565
|
`${place ? `run the apply in ${place}; ` : ''}HAND-APPLY alternative (instead of the apply, never after it): decline the offer by recording it — ${decline}`);
|
|
1579
1566
|
} catch (err) {
|
|
1580
1567
|
skip('jev-connect', err);
|
|
@@ -1582,7 +1569,7 @@ export const probeJevConnect = ({ root, deps, add, skip }) => {
|
|
|
1582
1569
|
};
|
|
1583
1570
|
|
|
1584
1571
|
// The Jev skill offer (contract: kit/jev-guide, part jev-offer): only once the key is set, so one jev item shows at a time.
|
|
1585
|
-
// Each target is judged by the facts leaf's one state rule; the apply
|
|
1572
|
+
// Each target is judged by the facts leaf's one state rule; the item's apply comes from the lane leaf's checked slot.
|
|
1586
1573
|
export const probeJevSkill = ({ root, deps, add, skip }) => {
|
|
1587
1574
|
try {
|
|
1588
1575
|
const env = deps.getenv ?? {};
|
|
@@ -1608,7 +1595,7 @@ export const probeJevSkill = ({ root, deps, add, skip }) => {
|
|
|
1608
1595
|
const untouched = judged.filter(({ state }) => state === 'foreign')
|
|
1609
1596
|
.map(({ path, reason }) => `left untouched: ${printable(path)} — ${printable(reason)}; `).join('');
|
|
1610
1597
|
const record = `node ${q(toolPath('ack-write.mjs'))} --lane jev-skill --fingerprint ${decline} --cwd ${q(root)}`;
|
|
1611
|
-
add('jev-skill', what,
|
|
1598
|
+
add('jev-skill', what, line, behind.length ? 'jev-skill.earlier' : 'jev-skill',
|
|
1612
1599
|
`${place ? `run the apply in ${place}; ` : ''}${untouched}${note ? `${note}; ` : ''}HAND-APPLY alternative (instead of the apply, never after it): decline the offer by recording it — ${record}`);
|
|
1613
1600
|
} catch (err) {
|
|
1614
1601
|
skip('jev-skill', err);
|
|
@@ -1686,7 +1673,13 @@ export const buildRecommendations = ({ cwd, deps = {} } = {}) => {
|
|
|
1686
1673
|
skip(key, new Error(`item shape violation — ${problems.join('; ')}`));
|
|
1687
1674
|
return false;
|
|
1688
1675
|
}
|
|
1689
|
-
|
|
1676
|
+
const slot = applySlot(variant, {
|
|
1677
|
+
toolsDir: HERE, root, env: deps.getenv ?? {},
|
|
1678
|
+
home: deps.jevHost?.home ?? deps.home ?? '',
|
|
1679
|
+
platform: deps.jevHost?.platform ?? deps.platform,
|
|
1680
|
+
});
|
|
1681
|
+
items.push({ key, variant, severity: SEVERITIES[variant], lane: laneOf(variant), what, benefit: BENEFITS[key],
|
|
1682
|
+
check: slot?.check ?? null, apply: slot?.apply ?? apply, detail });
|
|
1690
1683
|
return true;
|
|
1691
1684
|
};
|
|
1692
1685
|
// The per-run scratch a probe uses to tell a LATER probe what it actually did. Written by exactly
|
|
@@ -1736,6 +1729,7 @@ export const formatRecommendations = ({ items, skips }) => {
|
|
|
1736
1729
|
lines.push(` benefit: ${item.benefit}`);
|
|
1737
1730
|
lines.push(` lane: ${item.lane}`);
|
|
1738
1731
|
if (item.detail) lines.push(` recipe: ${item.detail}`);
|
|
1732
|
+
if (item.check != null) lines.push(` check: ${item.check}`);
|
|
1739
1733
|
lines.push(` apply: ${item.apply}`);
|
|
1740
1734
|
});
|
|
1741
1735
|
}
|
|
@@ -1752,16 +1746,21 @@ Usage:
|
|
|
1752
1746
|
|
|
1753
1747
|
Computes the deterministic Recommendations section every kit upgrade ends with — VERDICT-FIRST:
|
|
1754
1748
|
one composed verdict line opens every non-optimal render, then per item {severity · what is
|
|
1755
|
-
sub-optimal · the benefit in one plain line · an optional \`recipe:\` line (the sandbox-lane live
|
|
1749
|
+
sub-optimal · the benefit in one plain line · the lane · an optional \`recipe:\` line (the sandbox-lane live
|
|
1756
1750
|
recipe, the worktrees-dir hand-apply-first grant advice, or the agents hidden-mode reconcile
|
|
1757
|
-
follow-up) · the exact consent-gated apply one-liner}. --cwd is
|
|
1751
|
+
follow-up) · the harness check line · the exact consent-gated apply one-liner}. --cwd is
|
|
1758
1752
|
REQUIRED (the target project is explicit, never inferred from the shell's current directory). The
|
|
1759
1753
|
section renders present-even-when-empty ("${RECOMMENDATIONS_EMPTY_LINE}"); a probe failure is a
|
|
1760
1754
|
stated skipped-item line. Apply lines are cwd-independent (absolute tool paths, a pinned --cwd;
|
|
1761
1755
|
the doctor item pins via a cd prefix; the ONE exception is the set-autonomy item — a
|
|
1762
1756
|
conversational skill invocation labeled "run IN the target project") and preserve each writer's
|
|
1763
|
-
own consent semantics
|
|
1764
|
-
|
|
1757
|
+
own consent semantics. The consented velocity-profile --bridge-tier seeds used bridges' code-mode
|
|
1758
|
+
allow rules, <wrapper> * exclusions (codex-exec * where an execute slot delegates), hosts in
|
|
1759
|
+
sandbox.network.allowedDomains and state dirs in sandbox.filesystem.allowWrite. Where the host
|
|
1760
|
+
honors the settings sandbox keys, an excluded wrapper runs outside the sandbox, every sandboxed
|
|
1761
|
+
command of the project can reach each host, and every sandboxed command can write each state dir;
|
|
1762
|
+
plan/diff review modes and delegated execution keep their prompt. Items marked HAND-APPLY stay
|
|
1763
|
+
the user's own step. The sandbox-lane convergence is a neutral fingerprint acknowledgement recorded by
|
|
1765
1764
|
the consent-gated ack writer into docs/ai/acks.json (never a security key).
|
|
1766
1765
|
|
|
1767
1766
|
Read-only: never writes, never commits, never runs a subscription CLI. Exit codes: 0 report
|