@sabaiway/agent-workflow-kit 14.25.1 → 14.26.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 +41 -0
- package/README.md +8 -7
- package/SKILL.md +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 +15 -15
- package/references/modes/upgrade.md +3 -3
- package/references/scripts/migrate-gates.mjs +6 -2
- package/references/scripts/migrate-gates.test.mjs +4 -0
- package/tools/apply-danger-check.mjs +364 -0
- package/tools/init-project.mjs +27 -29
- package/tools/jev-connect.mjs +2 -2
- package/tools/jev-guide.mjs +2 -2
- package/tools/recommendations.mjs +14 -7
- package/tools/write-lanes.mjs +56 -32
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, lstatSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, win32 } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import {
|
|
9
|
+
BRIDGE_REVIEW_WRAPPERS, CLAUDE_DIR, KIT_GROUNDING_TOOL, RENDER_OWNED_REDLINE_RULES,
|
|
10
|
+
SAFE_DEFAULT_MODES, SETTINGS_FILE, SETTINGS_LOCAL_FILE, UNIVERSAL_READONLY_ALLOWLIST,
|
|
11
|
+
deriveBridgeTierAllowlist, deriveKitToolsAllowlist, mergeAutonomySettings,
|
|
12
|
+
writeAutonomyProfile, writeVelocityProfile,
|
|
13
|
+
} from './velocity-profile.mjs';
|
|
14
|
+
import { bundledSandboxRecipe } from './bridge-sandbox-recipe.mjs';
|
|
15
|
+
import { HOOK_FILE_REL, HOOKS_DIR, buildHookSettingsEntry, mergeHookEntry, writeGateHook } from './gate-hook.mjs';
|
|
16
|
+
import { writeMcp } from './mcp.mjs';
|
|
17
|
+
import { ENABLED_KEY, MCP_JSON_REL, SERVERS_KEY, SERVER_NAME, allowRulesFor, buildServerEntry } from './mcp-registration.mjs';
|
|
18
|
+
import { AGENTS_DIR, writeCheapAgents } from './cheap-agents.mjs';
|
|
19
|
+
import { planFor } from './jev-skill.mjs';
|
|
20
|
+
import { SKILL_FILES, claudeDirOf, skillTargets } from './jev-facts.mjs';
|
|
21
|
+
import { findOnPath } from './detect-backends.mjs';
|
|
22
|
+
import { consoleArgv } from './write-lanes.mjs';
|
|
23
|
+
import { isDirectRun } from './direct-run.mjs';
|
|
24
|
+
import { dirCovers as covers } from './declared-paths.mjs';
|
|
25
|
+
|
|
26
|
+
const TOOLS = dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
const SKILL_VARIANTS = ['jev-skill', 'jev-skill.earlier'];
|
|
28
|
+
const VELOCITY_VARIANTS = ['velocity-core', 'kit-tools-tier', 'bridge-tier'];
|
|
29
|
+
const HOOK_VARIANTS = ['gate-hook', 'read-lane.missing'];
|
|
30
|
+
const MCP_VARIANTS = ['mcp-channel', 'mcp-channel.differing'];
|
|
31
|
+
const AGENT_VARIANTS = ['agents', 'executor-vehicle'];
|
|
32
|
+
const VARIANTS = [...VELOCITY_VARIANTS, 'autonomy-render', ...HOOK_VARIANTS, ...MCP_VARIANTS, ...AGENT_VARIANTS, ...SKILL_VARIANTS];
|
|
33
|
+
const CREDENTIALS = 'sandbox.credentials.envVars';
|
|
34
|
+
const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
35
|
+
const stable = (value) => object(value)
|
|
36
|
+
? `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stable(value[key])}`).join(',')}}`
|
|
37
|
+
: Array.isArray(value) ? `[${value.map(stable).join(',')}]` : JSON.stringify(value);
|
|
38
|
+
const equal = (left, right) => stable(left) === stable(right);
|
|
39
|
+
const sha = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
40
|
+
const textValue = (value) => typeof value === 'string' ? value : JSON.stringify(value);
|
|
41
|
+
|
|
42
|
+
export const CHANGE_CLASSES = Object.freeze([
|
|
43
|
+
'hosts reachable', 'paths writable', 'commands outside the sandbox',
|
|
44
|
+
'prompts removed', 'commands the harness runs', 'nothing widened',
|
|
45
|
+
]);
|
|
46
|
+
export const KEY_TABLE = Object.freeze({
|
|
47
|
+
'sandbox.network.allowedDomains': 0,
|
|
48
|
+
'sandbox.filesystem.allowWrite': 1,
|
|
49
|
+
'sandbox.excludedCommands': 2,
|
|
50
|
+
'permissions.allow': 3,
|
|
51
|
+
'permissions.defaultMode': 3,
|
|
52
|
+
'sandbox.autoAllowBashIfSandboxed': 3,
|
|
53
|
+
'permissions.ask': 3,
|
|
54
|
+
'permissions.deny': 3,
|
|
55
|
+
'sandbox.enabled': 3,
|
|
56
|
+
'hooks.PreToolUse': 4,
|
|
57
|
+
[ENABLED_KEY]: 4,
|
|
58
|
+
[SERVERS_KEY]: 4,
|
|
59
|
+
[CREDENTIALS]: 5,
|
|
60
|
+
});
|
|
61
|
+
const SCALARS = ['permissions.defaultMode', 'sandbox.enabled', 'sandbox.autoAllowBashIfSandboxed'];
|
|
62
|
+
const valid = (key, value, element = false) => {
|
|
63
|
+
if (key.startsWith(`${SERVERS_KEY}.`) || key === SERVERS_KEY) return object(value);
|
|
64
|
+
if (key === 'permissions.defaultMode') return typeof value === 'string';
|
|
65
|
+
if (SCALARS.includes(key)) return typeof value === 'boolean';
|
|
66
|
+
if (!element) return Array.isArray(value) && value.every((entry) => valid(key, entry, true));
|
|
67
|
+
if (key === CREDENTIALS) return object(value) && typeof value.name === 'string' && typeof value.mode === 'string';
|
|
68
|
+
return key === 'hooks.PreToolUse' ? object(value) : typeof value === 'string';
|
|
69
|
+
};
|
|
70
|
+
const keyOf = (key) => key.startsWith(`${SERVERS_KEY}.`) ? SERVERS_KEY : key;
|
|
71
|
+
|
|
72
|
+
export const diffFiles = (before, after) => {
|
|
73
|
+
const entries = [];
|
|
74
|
+
const scalar = (key, left, right) => {
|
|
75
|
+
if (equal(left, right)) return;
|
|
76
|
+
if (left === undefined) entries.push({ key, change: 'added', value: right });
|
|
77
|
+
else if (right === undefined) entries.push({ key, change: 'removed', value: left });
|
|
78
|
+
else entries.push({ key, change: 'altered', value: right, previous: left });
|
|
79
|
+
};
|
|
80
|
+
const walk = (left, right, key) => {
|
|
81
|
+
if (equal(left, right)) return;
|
|
82
|
+
const known = Object.hasOwn(KEY_TABLE, key);
|
|
83
|
+
if (known && [left, right].some((value) => value !== undefined && !valid(key, value))) {
|
|
84
|
+
scalar(key, left, right);
|
|
85
|
+
} else if (key === SERVERS_KEY) {
|
|
86
|
+
for (const name of new Set([...Object.keys(left ?? {}), ...Object.keys(right ?? {})])) {
|
|
87
|
+
scalar(`${key}.${name}`, left?.[name], right?.[name]);
|
|
88
|
+
}
|
|
89
|
+
} else if (known && !SCALARS.includes(key)) {
|
|
90
|
+
const old = left ?? [];
|
|
91
|
+
const next = right ?? [];
|
|
92
|
+
const unpaired = old.filter((value) => !next.some((entry) => equal(value, entry)));
|
|
93
|
+
const changes = next.filter((value) => !old.some((entry) => equal(value, entry))).map((value) => {
|
|
94
|
+
const at = key === CREDENTIALS ? unpaired.findIndex((entry) => entry.name === value.name) : -1;
|
|
95
|
+
return at < 0 ? { key, change: 'added', value } : { key, change: 'altered', value, previous: unpaired.splice(at, 1)[0] };
|
|
96
|
+
});
|
|
97
|
+
for (const value of unpaired) entries.push({ key, change: 'removed', value });
|
|
98
|
+
entries.push(...changes);
|
|
99
|
+
} else if (!known && (key === '' || Object.keys(KEY_TABLE).some((path) => path.startsWith(`${key}.`)))
|
|
100
|
+
&& [left, right].every((value) => value === undefined || object(value))) {
|
|
101
|
+
for (const name of new Set([...Object.keys(left ?? {}), ...Object.keys(right ?? {})])) {
|
|
102
|
+
walk(left?.[name], right?.[name], key ? `${key}.${name}` : name);
|
|
103
|
+
}
|
|
104
|
+
} else scalar(key, left, right);
|
|
105
|
+
};
|
|
106
|
+
walk(before ?? {}, after ?? {}, '');
|
|
107
|
+
const order = Object.keys(KEY_TABLE);
|
|
108
|
+
const rank = (key) => order.includes(keyOf(key)) ? order.indexOf(keyOf(key)) : order.length;
|
|
109
|
+
return entries.sort((a, b) => rank(a.key) - rank(b.key));
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const classOf = ({ key, change, value }) => {
|
|
113
|
+
const base = keyOf(key);
|
|
114
|
+
if (!Object.hasOwn(KEY_TABLE, base)) return null;
|
|
115
|
+
let index = KEY_TABLE[base];
|
|
116
|
+
if (change === 'removed') index = ['permissions.ask', 'permissions.deny', CREDENTIALS].includes(key) ? 3 : 5;
|
|
117
|
+
else if (['permissions.ask', 'permissions.deny'].includes(key)) index = 5;
|
|
118
|
+
else if (key === 'permissions.defaultMode') index = value === 'acceptEdits' ? 3 : 5;
|
|
119
|
+
else if (key === 'sandbox.enabled') index = value === false ? 3 : 5;
|
|
120
|
+
else if (key === 'sandbox.autoAllowBashIfSandboxed') index = value === true ? 3 : 5;
|
|
121
|
+
return CHANGE_CLASSES[index];
|
|
122
|
+
};
|
|
123
|
+
export const judgeChange = ({ variant, files, scope }) => {
|
|
124
|
+
const entries = [];
|
|
125
|
+
const refused = [];
|
|
126
|
+
for (const { path, before, after } of files) {
|
|
127
|
+
for (const diff of diffFiles(before, after)) {
|
|
128
|
+
const entry = { ...diff, path, class: classOf(diff) };
|
|
129
|
+
entries.push(entry);
|
|
130
|
+
const { key, change } = diff;
|
|
131
|
+
const atKey = (data) => key.split('.').reduce((value, name) => value?.[name], data);
|
|
132
|
+
const wrongType = [atKey(before), atKey(after)].some((value) => value !== undefined && !valid(key, value));
|
|
133
|
+
const reason = entry.class === null ? 'unknown key' : wrongType ? 'wrong type'
|
|
134
|
+
: key === CREDENTIALS && change !== 'added' ? 'credentials entry removed or altered'
|
|
135
|
+
: !scope.admits(entry) ? `beyond the declared scope of ${variant}` : null;
|
|
136
|
+
if (reason) refused.push({ path, key, reason });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return { entries, refused };
|
|
140
|
+
};
|
|
141
|
+
const writerFacts = (facts, deps) => ({
|
|
142
|
+
...deps, ...facts,
|
|
143
|
+
findWrapper: (name) => findOnPath(name, { getenv: facts.env }).state === 'present',
|
|
144
|
+
findOnPath: (name) => findOnPath(name, { getenv: facts.env }),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
export const scopeOf = (variant, facts) => {
|
|
148
|
+
const allowed = {};
|
|
149
|
+
const set = (key, values) => {
|
|
150
|
+
allowed[key] = (value) => values.some((entry) => equal(entry, value));
|
|
151
|
+
};
|
|
152
|
+
if (VELOCITY_VARIANTS.includes(variant)) {
|
|
153
|
+
const allow = [...UNIVERSAL_READONLY_ALLOWLIST];
|
|
154
|
+
if (variant === 'kit-tools-tier') allow.push(...deriveKitToolsAllowlist({ projectDir: facts.root }));
|
|
155
|
+
if (variant === 'bridge-tier') {
|
|
156
|
+
const deps = writerFacts(facts, {});
|
|
157
|
+
const placed = BRIDGE_REVIEW_WRAPPERS.filter(deps.findWrapper);
|
|
158
|
+
allow.push(...deriveBridgeTierAllowlist({ findWrapper: deps.findWrapper,
|
|
159
|
+
groundingAbsPath: join(facts.toolsDir, '..', KIT_GROUNDING_TOOL) }).allow);
|
|
160
|
+
set('sandbox.excludedCommands', placed);
|
|
161
|
+
set('sandbox.network.allowedDomains', bundledSandboxRecipe(placed, deps).hosts);
|
|
162
|
+
}
|
|
163
|
+
set('permissions.allow', allow);
|
|
164
|
+
} else if (variant === 'autonomy-render') {
|
|
165
|
+
set('permissions.defaultMode', SAFE_DEFAULT_MODES);
|
|
166
|
+
set('sandbox.enabled', [true]);
|
|
167
|
+
set('sandbox.autoAllowBashIfSandboxed', [true, false]);
|
|
168
|
+
set('permissions.ask', RENDER_OWNED_REDLINE_RULES);
|
|
169
|
+
set('permissions.deny', RENDER_OWNED_REDLINE_RULES);
|
|
170
|
+
allowed[CREDENTIALS] = (value) => value.mode === 'deny';
|
|
171
|
+
} else if (HOOK_VARIANTS.includes(variant)) set('hooks.PreToolUse', [buildHookSettingsEntry()]);
|
|
172
|
+
else if (MCP_VARIANTS.includes(variant)) {
|
|
173
|
+
set(`${SERVERS_KEY}.${SERVER_NAME}`, [buildServerEntry(join(facts.toolsDir, 'mcp-server.mjs'))]);
|
|
174
|
+
set(ENABLED_KEY, [SERVER_NAME]);
|
|
175
|
+
set('permissions.allow', allowRulesFor());
|
|
176
|
+
}
|
|
177
|
+
const targets = SKILL_VARIANTS.includes(variant) ? skillTargets({ home: facts.home,
|
|
178
|
+
claudeDir: facts.claudeDir ?? claudeDirOf(facts.env, facts.home, facts.platform).dir }) : [];
|
|
179
|
+
const filePaths = HOOK_VARIANTS.includes(variant) ? [join(facts.root, HOOK_FILE_REL)] : [];
|
|
180
|
+
const directories = VELOCITY_VARIANTS.includes(variant) || variant === 'autonomy-render'
|
|
181
|
+
|| HOOK_VARIANTS.includes(variant) || MCP_VARIANTS.includes(variant) || AGENT_VARIANTS.includes(variant)
|
|
182
|
+
? [join(facts.root, CLAUDE_DIR)] : [];
|
|
183
|
+
if (HOOK_VARIANTS.includes(variant)) directories.push(join(facts.root, HOOKS_DIR));
|
|
184
|
+
if (AGENT_VARIANTS.includes(variant)) directories.push(join(facts.root, AGENTS_DIR));
|
|
185
|
+
for (const target of targets) {
|
|
186
|
+
for (let at = target.root; at !== target.base && covers(target.base, at); at = dirname(at)) directories.push(at);
|
|
187
|
+
filePaths.push(target.path);
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
admits: ({ path, key, change, value }) => {
|
|
191
|
+
const correctFile = key.startsWith(`${SERVERS_KEY}.`) ? path === MCP_JSON_REL : path === SETTINGS_FILE;
|
|
192
|
+
const gains = key === 'hooks.PreToolUse' || key === ENABLED_KEY || key === CREDENTIALS;
|
|
193
|
+
return correctFile && (!gains || change === 'added') && Boolean(allowed[key]?.(value));
|
|
194
|
+
},
|
|
195
|
+
admitsPath: (path, directory = false) => directory ? directories.includes(path)
|
|
196
|
+
: filePaths.includes(path) || (AGENT_VARIANTS.includes(variant) && covers(join(facts.root, AGENTS_DIR), path)),
|
|
197
|
+
};
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const jsonAt = (path) => existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : null;
|
|
201
|
+
const halted = (lines) => Object.assign(new Error(lines.join('\n')), { code: 'CHECK_HALTED', lines });
|
|
202
|
+
const maskedFiles = (variant, facts) => {
|
|
203
|
+
const paths = VELOCITY_VARIANTS.includes(variant) || variant === 'autonomy-render' || HOOK_VARIANTS.includes(variant)
|
|
204
|
+
? [SETTINGS_FILE, SETTINGS_LOCAL_FILE] : MCP_VARIANTS.includes(variant) ? [MCP_JSON_REL, SETTINGS_FILE] : [];
|
|
205
|
+
return paths.flatMap((path) => lstatSync(join(facts.root, path), { throwIfNoEntry: false })?.isCharacterDevice()
|
|
206
|
+
? [`masked: ${path}`] : []);
|
|
207
|
+
};
|
|
208
|
+
export const plannedChange = (variant, facts, deps = {}) => {
|
|
209
|
+
const files = [];
|
|
210
|
+
const writes = [];
|
|
211
|
+
const temporaries = [];
|
|
212
|
+
const writerDeps = writerFacts(facts, deps);
|
|
213
|
+
const addFile = (path, before, after) => {
|
|
214
|
+
files.push({ path, before: before ?? null, after });
|
|
215
|
+
};
|
|
216
|
+
try {
|
|
217
|
+
const masks = maskedFiles(variant, facts);
|
|
218
|
+
if (masks.length) throw halted(masks);
|
|
219
|
+
if (VELOCITY_VARIANTS.includes(variant)) {
|
|
220
|
+
const plan = writeVelocityProfile({ cwd: facts.root, dryRun: true,
|
|
221
|
+
kitTools: variant === 'kit-tools-tier', bridgeTier: variant === 'bridge-tier' }, writerDeps);
|
|
222
|
+
const before = plan.projectSettings.data ?? {};
|
|
223
|
+
const after = structuredClone(before);
|
|
224
|
+
after.permissions = { ...before.permissions,
|
|
225
|
+
allow: [...(before.permissions?.allow ?? []), ...plan.toAdd, ...plan.tierToAdd, ...plan.bridgeToAdd] };
|
|
226
|
+
if (plan.excludedToAdd.length || plan.hostsToAdd.length) {
|
|
227
|
+
after.sandbox = { ...before.sandbox };
|
|
228
|
+
if (plan.excludedToAdd.length) after.sandbox.excludedCommands = [...(before.sandbox?.excludedCommands ?? []), ...plan.excludedToAdd];
|
|
229
|
+
if (plan.hostsToAdd.length) after.sandbox.network = { ...before.sandbox?.network,
|
|
230
|
+
allowedDomains: [...(before.sandbox?.network?.allowedDomains ?? []), ...plan.hostsToAdd] };
|
|
231
|
+
}
|
|
232
|
+
addFile(SETTINGS_FILE, plan.projectSettings.data, after);
|
|
233
|
+
} else if (variant === 'autonomy-render') {
|
|
234
|
+
const render = writeAutonomyProfile({ cwd: facts.root, apply: false }, writerDeps);
|
|
235
|
+
const before = jsonAt(join(facts.root, SETTINGS_FILE));
|
|
236
|
+
addFile(SETTINGS_FILE, before, mergeAutonomySettings(before, render));
|
|
237
|
+
} else if (HOOK_VARIANTS.includes(variant)) {
|
|
238
|
+
const plan = writeGateHook({ cwd: facts.root, dryRun: true }, writerDeps);
|
|
239
|
+
if (plan.wirePlanned) addFile(SETTINGS_FILE, plan.projectSettings.data, mergeHookEntry(plan.projectSettings.data));
|
|
240
|
+
if (plan.placePlanned) writes.push({ path: join(facts.root, HOOK_FILE_REL), bytes: plan.bundleContent });
|
|
241
|
+
} else if (MCP_VARIANTS.includes(variant)) {
|
|
242
|
+
const result = writeMcp({ cwd: facts.root, dryRun: true, replace: variant === 'mcp-channel.differing' }, writerDeps);
|
|
243
|
+
const { plan, registration } = result;
|
|
244
|
+
if (plan.writeMcpJson) addFile(MCP_JSON_REL, registration.mcpJson.data, JSON.parse(plan.mcpBody));
|
|
245
|
+
if (plan.writeSettings) addFile(SETTINGS_FILE, registration.settings.data, JSON.parse(plan.settingsBody));
|
|
246
|
+
} else if (AGENT_VARIANTS.includes(variant)) {
|
|
247
|
+
const plan = writeCheapAgents({ cwd: facts.root, dryRun: true }, writerDeps);
|
|
248
|
+
for (const item of plan.plan.filter(({ action }) => ['place', 're-derive'].includes(action))) {
|
|
249
|
+
writes.push({ path: item.abs, bytes: item.content });
|
|
250
|
+
}
|
|
251
|
+
} else if (SKILL_VARIANTS.includes(variant)) {
|
|
252
|
+
const plan = planFor({ ...writerDeps, lstat: lstatSync, readdir: readdirSync, readFile: readFileSync });
|
|
253
|
+
for (const target of plan.targets.filter(({ action }) => ['install', 'replace'].includes(action))) {
|
|
254
|
+
const stem = basename(target.path);
|
|
255
|
+
const bytes = Object.keys(SKILL_FILES).sort().map((name) => [name,
|
|
256
|
+
readFileSync(join(facts.toolsDir, '..', 'references', 'vendor', stem, SKILL_FILES[name]))]);
|
|
257
|
+
writes.push({ path: target.path, bytes: Buffer.concat(bytes.flatMap(([name, body]) => [Buffer.from(`${name}\0`), body])), directory: true });
|
|
258
|
+
temporaries.push(`temporary: ${dirname(target.root)}/.${stem}.kit-tmp-*`);
|
|
259
|
+
if (target.action === 'replace') temporaries.push(`temporary: ${dirname(target.root)}/.${stem}.kit-old-*`);
|
|
260
|
+
}
|
|
261
|
+
} else throw new Error(`unsupported variant: ${variant}`);
|
|
262
|
+
} catch (error) {
|
|
263
|
+
throw error.code === 'CHECK_HALTED' ? error : halted([error.message]);
|
|
264
|
+
}
|
|
265
|
+
const directories = new Set();
|
|
266
|
+
const changedFiles = files.filter(({ before, after }) => diffFiles(before, after).length);
|
|
267
|
+
for (const path of [...changedFiles.map(({ path }) => join(facts.root, path)), ...writes.map(({ path }) => path)]) {
|
|
268
|
+
for (let parent = dirname(path); !existsSync(parent); parent = dirname(parent)) directories.add(parent);
|
|
269
|
+
}
|
|
270
|
+
return { files, writes, temporaries, directories: [...directories] };
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
export const checkApply = (variant, facts, deps = {}) => {
|
|
274
|
+
const plan = plannedChange(variant, facts, deps);
|
|
275
|
+
const scope = scopeOf(variant, facts);
|
|
276
|
+
const { entries, refused } = judgeChange({ variant, files: plan.files, scope });
|
|
277
|
+
const lines = [];
|
|
278
|
+
const display = (path) => facts.root && covers(facts.root, path) ? relative(facts.root, path) : path;
|
|
279
|
+
for (const file of plan.files) {
|
|
280
|
+
const changes = entries.filter(({ path }) => path === file.path);
|
|
281
|
+
if (!changes.length) continue;
|
|
282
|
+
lines.push(`${file.path}:`);
|
|
283
|
+
for (const entry of changes) {
|
|
284
|
+
const old = entry.change === 'altered' ? `${textValue(entry.previous)} → ` : '';
|
|
285
|
+
lines.push(` ${entry.key}: ${entry.change} ${old}${textValue(entry.value)} — ${entry.class ?? 'unknown'}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const pathLine = (path, directory, line) => {
|
|
289
|
+
lines.push(line);
|
|
290
|
+
if (!scope.admitsPath(path, directory)) refused.push({ path: display(path), key: display(path),
|
|
291
|
+
reason: `beyond the declared scope of ${variant}` });
|
|
292
|
+
};
|
|
293
|
+
for (const write of plan.writes) pathLine(write.path, false, `writes: ${display(write.path)} sha256:${sha(write.bytes)}`);
|
|
294
|
+
for (const path of plan.directories) pathLine(path, true, `writes: ${display(path)}/ created`);
|
|
295
|
+
lines.push(...plan.temporaries);
|
|
296
|
+
if (refused.length) {
|
|
297
|
+
for (const entry of refused) lines.push(`refused: ${entry.path} ${entry.key} — ${entry.reason}`);
|
|
298
|
+
lines.push('nothing written');
|
|
299
|
+
} else lines.push(lines.length ? `scope: within the declared scope of ${variant}` : `nothing to change: ${variant} is already applied`);
|
|
300
|
+
const digest = sha(lines.join('\n'));
|
|
301
|
+
return { lines: [...lines, `digest: ${digest}`], digest, refused };
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const USAGE = 'usage: apply-danger-check --variant <v> --cwd <root> [--claude-dir <abs>] [--apply --expect <digest>]';
|
|
305
|
+
const parse = (argv, platform) => {
|
|
306
|
+
if (argv.length === 1 && ['--help', '-h'].includes(argv[0])) return { help: true };
|
|
307
|
+
const args = {};
|
|
308
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
309
|
+
const flag = argv[index];
|
|
310
|
+
if (flag === '--apply') args.apply = true;
|
|
311
|
+
else if (['--variant', '--cwd', '--claude-dir', '--expect'].includes(flag)) {
|
|
312
|
+
const value = argv[index + 1];
|
|
313
|
+
if (!value || value.startsWith('-')) throw new Error(`${flag} takes a value`);
|
|
314
|
+
args[flag.slice(2)] = value;
|
|
315
|
+
index += 1;
|
|
316
|
+
} else throw new Error(`unknown argument: ${flag}`);
|
|
317
|
+
}
|
|
318
|
+
if (!VARIANTS.includes(args.variant) || !args.cwd) throw new Error('--variant and --cwd are required for a harness variant');
|
|
319
|
+
if (args['claude-dir'] && (!SKILL_VARIANTS.includes(args.variant)
|
|
320
|
+
|| !(platform === 'win32' ? win32.isAbsolute : isAbsolute)(args['claude-dir']))) throw new Error('--claude-dir requires a skill variant and an absolute path');
|
|
321
|
+
if (Boolean(args.apply) !== Boolean(args.expect) || (args.expect && !/^[0-9a-f]{64}$/.test(args.expect))) {
|
|
322
|
+
throw new Error('--apply requires --expect with 64 lowercase hex characters');
|
|
323
|
+
}
|
|
324
|
+
args.root = resolve(args.cwd);
|
|
325
|
+
if (!statSync(args.root).isDirectory()) throw new Error('--cwd must be a directory');
|
|
326
|
+
return args;
|
|
327
|
+
};
|
|
328
|
+
export const main = (argv, io = {}) => {
|
|
329
|
+
const resolved = { log: console.log, error: console.error, env: process.env, home: homedir(),
|
|
330
|
+
platform: process.platform, toolsDir: TOOLS,
|
|
331
|
+
spawn: (argv, options) => spawnSync(argv[0], argv.slice(1), options), ...io };
|
|
332
|
+
let args;
|
|
333
|
+
try {
|
|
334
|
+
args = parse(argv, resolved.platform);
|
|
335
|
+
} catch (error) {
|
|
336
|
+
resolved.error(error.message);
|
|
337
|
+
return 2;
|
|
338
|
+
}
|
|
339
|
+
if (args.help) {
|
|
340
|
+
resolved.log(USAGE);
|
|
341
|
+
return 0;
|
|
342
|
+
}
|
|
343
|
+
const facts = { toolsDir: resolved.toolsDir, root: args.root, env: resolved.env, home: resolved.home,
|
|
344
|
+
platform: resolved.platform, claudeDir: args['claude-dir'] };
|
|
345
|
+
let result;
|
|
346
|
+
try {
|
|
347
|
+
result = checkApply(args.variant, facts);
|
|
348
|
+
} catch (error) {
|
|
349
|
+
for (const line of error.lines ?? [error.message]) resolved.log(line);
|
|
350
|
+
return 3;
|
|
351
|
+
}
|
|
352
|
+
for (const line of result.lines) resolved.log(line);
|
|
353
|
+
if (result.refused.length) return 1;
|
|
354
|
+
if (!args.apply) return 0;
|
|
355
|
+
if (args.expect !== result.digest) return 5;
|
|
356
|
+
const spawnFacts = facts.claudeDir ? { ...facts, env: { ...facts.env, CLAUDE_CONFIG_DIR: facts.claudeDir } } : facts;
|
|
357
|
+
const child = resolved.spawn(consoleArgv(args.variant, spawnFacts).apply,
|
|
358
|
+
{ cwd: facts.root, env: resolved.env, stdio: 'inherit' });
|
|
359
|
+
if (child.status === 0) return 0;
|
|
360
|
+
resolved.error(`apply exited ${child.status}`);
|
|
361
|
+
return 6;
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
if (isDirectRun(import.meta.url)) process.exitCode = main(process.argv.slice(2));
|
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:',
|
|
@@ -49,7 +49,7 @@ import {
|
|
|
49
49
|
} from './velocity-profile.mjs';
|
|
50
50
|
import { loadAutonomy, isSparseSeedConfig, AUTONOMY_REL } from './autonomy-config.mjs';
|
|
51
51
|
import { deriveDoctorPlan, TRUSTED_DIRS } from './autonomy-doctor.mjs';
|
|
52
|
-
import { laneOf,
|
|
52
|
+
import { laneOf, applySlot } from './write-lanes.mjs';
|
|
53
53
|
import { detectBackends, findOnPath } from './detect-backends.mjs';
|
|
54
54
|
import { isDirectRun } from './direct-run.mjs';
|
|
55
55
|
import { ACTIVITIES, resolveActivityRecipe, composeReadiness, safeLine } from './recipes.mjs';
|
|
@@ -1574,7 +1574,7 @@ export const probeJevConnect = ({ root, deps, add, skip }) => {
|
|
|
1574
1574
|
const host = deps.jevHost ?? {};
|
|
1575
1575
|
const place = placeOf({ env, platform: host.platform, hostname: host.hostname, container: host.container });
|
|
1576
1576
|
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'],
|
|
1577
|
+
add('jev-connect', WHATS['jev-connect'], applySlot('jev-connect', { toolsDir: HERE }).apply, 'jev-connect',
|
|
1578
1578
|
`${place ? `run the apply in ${place}; ` : ''}HAND-APPLY alternative (instead of the apply, never after it): decline the offer by recording it — ${decline}`);
|
|
1579
1579
|
} catch (err) {
|
|
1580
1580
|
skip('jev-connect', err);
|
|
@@ -1582,7 +1582,7 @@ export const probeJevConnect = ({ root, deps, add, skip }) => {
|
|
|
1582
1582
|
};
|
|
1583
1583
|
|
|
1584
1584
|
// 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
|
|
1585
|
+
// 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
1586
|
export const probeJevSkill = ({ root, deps, add, skip }) => {
|
|
1587
1587
|
try {
|
|
1588
1588
|
const env = deps.getenv ?? {};
|
|
@@ -1608,7 +1608,7 @@ export const probeJevSkill = ({ root, deps, add, skip }) => {
|
|
|
1608
1608
|
const untouched = judged.filter(({ state }) => state === 'foreign')
|
|
1609
1609
|
.map(({ path, reason }) => `left untouched: ${printable(path)} — ${printable(reason)}; `).join('');
|
|
1610
1610
|
const record = `node ${q(toolPath('ack-write.mjs'))} --lane jev-skill --fingerprint ${decline} --cwd ${q(root)}`;
|
|
1611
|
-
add('jev-skill', what,
|
|
1611
|
+
add('jev-skill', what, line, behind.length ? 'jev-skill.earlier' : 'jev-skill',
|
|
1612
1612
|
`${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
1613
|
} catch (err) {
|
|
1614
1614
|
skip('jev-skill', err);
|
|
@@ -1686,7 +1686,13 @@ export const buildRecommendations = ({ cwd, deps = {} } = {}) => {
|
|
|
1686
1686
|
skip(key, new Error(`item shape violation — ${problems.join('; ')}`));
|
|
1687
1687
|
return false;
|
|
1688
1688
|
}
|
|
1689
|
-
|
|
1689
|
+
const slot = applySlot(variant, {
|
|
1690
|
+
toolsDir: HERE, root, env: deps.getenv ?? {},
|
|
1691
|
+
home: deps.jevHost?.home ?? deps.home ?? '',
|
|
1692
|
+
platform: deps.jevHost?.platform ?? deps.platform,
|
|
1693
|
+
});
|
|
1694
|
+
items.push({ key, variant, severity: SEVERITIES[variant], lane: laneOf(variant), what, benefit: BENEFITS[key],
|
|
1695
|
+
check: slot?.check ?? null, apply: slot?.apply ?? apply, detail });
|
|
1690
1696
|
return true;
|
|
1691
1697
|
};
|
|
1692
1698
|
// The per-run scratch a probe uses to tell a LATER probe what it actually did. Written by exactly
|
|
@@ -1736,6 +1742,7 @@ export const formatRecommendations = ({ items, skips }) => {
|
|
|
1736
1742
|
lines.push(` benefit: ${item.benefit}`);
|
|
1737
1743
|
lines.push(` lane: ${item.lane}`);
|
|
1738
1744
|
if (item.detail) lines.push(` recipe: ${item.detail}`);
|
|
1745
|
+
if (item.check != null) lines.push(` check: ${item.check}`);
|
|
1739
1746
|
lines.push(` apply: ${item.apply}`);
|
|
1740
1747
|
});
|
|
1741
1748
|
}
|
|
@@ -1752,9 +1759,9 @@ Usage:
|
|
|
1752
1759
|
|
|
1753
1760
|
Computes the deterministic Recommendations section every kit upgrade ends with — VERDICT-FIRST:
|
|
1754
1761
|
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
|
|
1762
|
+
sub-optimal · the benefit in one plain line · the lane · an optional \`recipe:\` line (the sandbox-lane live
|
|
1756
1763
|
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
|
|
1764
|
+
follow-up) · the harness check line · the exact consent-gated apply one-liner}. --cwd is
|
|
1758
1765
|
REQUIRED (the target project is explicit, never inferred from the shell's current directory). The
|
|
1759
1766
|
section renders present-even-when-empty ("${RECOMMENDATIONS_EMPTY_LINE}"); a probe failure is a
|
|
1760
1767
|
stated skipped-item line. Apply lines are cwd-independent (absolute tool paths, a pinned --cwd;
|