@vodmal/vdx-cli 0.22.1 → 0.23.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/README.md +1 -1
- package/package.json +1 -1
- package/rubric/vdx-rubric.yaml +1 -1
- package/src/ai.ts +110 -22
- package/src/index.ts +10 -3
- package/src/init.ts +1 -1
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ vdx doctor --check [project-path] # one line per hook declared but not running
|
|
|
51
51
|
vdx doctor --fix # write the profile's personal hooks (git.hooks) into ~/.gitconfig
|
|
52
52
|
|
|
53
53
|
# Start your agent in a project per your profile (~/.vdx-environment.yaml or $VDX_ENVIRONMENT)
|
|
54
|
-
vdx ai[@host] [project-path] [--new | --conversation <id>] [--restart] [--detach] [--dry-run] # default: cwd; @host: over ssh, in tmux there
|
|
54
|
+
vdx ai[@host] [project-path] [--new | --conversation <id>] [--focused] [--restart] [--detach] [--dry-run] # default: cwd; @host: over ssh, in tmux there
|
|
55
55
|
vdx ai --check [project-path] # this session against the profile — what the plugin's SessionStart hook tells the agent
|
|
56
56
|
|
|
57
57
|
# What a command does and takes; nothing runs (ai: also where launch flags live)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vodmal/vdx-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "vdx — unified lifecycle interface (up/down/build/test/check/fix) + versioned maturity audit with drift detection. Pairs with the vdx Claude Code plugin.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"vdx",
|
package/rubric/vdx-rubric.yaml
CHANGED
package/src/ai.ts
CHANGED
|
@@ -57,6 +57,8 @@ export interface WhenRule {
|
|
|
57
57
|
if: Predicate;
|
|
58
58
|
args?: string[];
|
|
59
59
|
confirm?: ConfirmRule[];
|
|
60
|
+
/** The rule is there to wake the agent: a focused session (`vdx ai --focused`) leaves it out. */
|
|
61
|
+
wakes?: boolean;
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
export interface AgentProfile {
|
|
@@ -118,6 +120,11 @@ export const ENVIRONMENT_ENV_VAR = 'VDX_ENVIRONMENT';
|
|
|
118
120
|
export const HOST_ENV_VAR = 'VDX_HOST';
|
|
119
121
|
/** Set by `vdx ai@<host>` for the vdx it starts there: the machine the person sits at. */
|
|
120
122
|
export const SEAT_ENV_VAR = 'VDX_AI_FROM';
|
|
123
|
+
/**
|
|
124
|
+
* `1` in the environment of an agent started with `vdx ai --focused`, and so of
|
|
125
|
+
* its hooks and MCP servers: nothing wakes this session or reads its inbox for it.
|
|
126
|
+
*/
|
|
127
|
+
export const FOCUSED_ENV_VAR = 'VDX_FOCUSED';
|
|
121
128
|
export const USER_ENVIRONMENT_FILE = '.vdx-environment.yaml';
|
|
122
129
|
/** Tried in order when no profile names an agent. Started plain, with no flags. */
|
|
123
130
|
export const FALLBACK_AGENTS = ['claude', 'codex'] as const;
|
|
@@ -170,6 +177,7 @@ export function parseEnvironment(text: string, source: string): Environment {
|
|
|
170
177
|
if (rule.args !== undefined && !isStringList(rule.args)) {
|
|
171
178
|
fail(`${at}.args`, 'must be a list of strings');
|
|
172
179
|
}
|
|
180
|
+
if (rule.wakes !== undefined && typeof rule.wakes !== 'boolean') fail(`${at}.wakes`, 'must be true or false');
|
|
173
181
|
if (rule.confirm !== undefined) {
|
|
174
182
|
if (!Array.isArray(rule.confirm)) fail(`${at}.confirm`, 'must be a list');
|
|
175
183
|
rule.confirm.forEach((c, j) => {
|
|
@@ -289,6 +297,10 @@ export interface LaunchPlan {
|
|
|
289
297
|
args: string[];
|
|
290
298
|
resumeArgs: string[];
|
|
291
299
|
matched: string[];
|
|
300
|
+
/** `vdx ai --focused`: the agent gets VDX_FOCUSED=1, and the rules that wake it are left out. */
|
|
301
|
+
focused: boolean;
|
|
302
|
+
/** Matched rules a focused session leaves out (`wakes: true`). */
|
|
303
|
+
skipped: string[];
|
|
292
304
|
confirm: ConfirmRule[];
|
|
293
305
|
multiplexer: Multiplexer;
|
|
294
306
|
sessionName: string;
|
|
@@ -305,14 +317,21 @@ export function planLaunch(input: {
|
|
|
305
317
|
projectSource?: string;
|
|
306
318
|
host: string;
|
|
307
319
|
seat?: string;
|
|
320
|
+
focused?: boolean;
|
|
308
321
|
}): LaunchPlan {
|
|
309
322
|
const { environment, agent, projectRoot } = input;
|
|
323
|
+
const focused = input.focused === true;
|
|
310
324
|
const ctx: Ctx = { projectRoot, stack: autoDetectStack(projectRoot), cache: new Map() };
|
|
311
325
|
const args = [...(agent.args ?? [])];
|
|
312
326
|
const matched: string[] = [];
|
|
327
|
+
const skipped: string[] = [];
|
|
313
328
|
const confirm: ConfirmRule[] = [];
|
|
314
329
|
for (const rule of agent.when ?? []) {
|
|
315
330
|
if (!evalPredicate(rule.if, ctx)) continue;
|
|
331
|
+
if (focused && rule.wakes) {
|
|
332
|
+
skipped.push(rule.id);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
316
335
|
matched.push(rule.id);
|
|
317
336
|
args.push(...(rule.args ?? []));
|
|
318
337
|
confirm.push(...(rule.confirm ?? []));
|
|
@@ -328,6 +347,8 @@ export function planLaunch(input: {
|
|
|
328
347
|
args,
|
|
329
348
|
resumeArgs: [...(agent.resume_args ?? [])],
|
|
330
349
|
matched,
|
|
350
|
+
focused,
|
|
351
|
+
skipped,
|
|
331
352
|
confirm,
|
|
332
353
|
multiplexer: environment.session?.multiplexer ?? 'none',
|
|
333
354
|
sessionName: renderSessionName(environment.session?.name ?? DEFAULT_SESSION_NAME, {
|
|
@@ -670,6 +691,8 @@ export interface AiOptions {
|
|
|
670
691
|
conversation?: string;
|
|
671
692
|
detach: boolean;
|
|
672
693
|
dryRun: boolean;
|
|
694
|
+
/** `--focused`: a session nothing wakes — VDX_FOCUSED=1, the profile's `wakes` rules left out. */
|
|
695
|
+
focused?: boolean;
|
|
673
696
|
}
|
|
674
697
|
|
|
675
698
|
export interface AiDeps {
|
|
@@ -683,6 +706,8 @@ export interface AiDeps {
|
|
|
683
706
|
out: (text: string) => void;
|
|
684
707
|
onPath: (bin: string) => boolean;
|
|
685
708
|
processes: () => ProcInfo[];
|
|
709
|
+
/** Was this agent process started focused; default: its environment (`processFocused`). */
|
|
710
|
+
agentFocused?: (pid: number) => boolean;
|
|
686
711
|
sleep: (ms: number) => void;
|
|
687
712
|
confirmTimeoutMs: number;
|
|
688
713
|
/** One line from the person at the terminal, or null when there is none. */
|
|
@@ -758,15 +783,21 @@ export const EXIT_LAUNCH_FAILED = 4;
|
|
|
758
783
|
*/
|
|
759
784
|
export const NOTHING_TO_RESUME_MS = 30_000;
|
|
760
785
|
|
|
761
|
-
export function renderPlan(
|
|
786
|
+
export function renderPlan(
|
|
787
|
+
plan: LaunchPlan,
|
|
788
|
+
multiplexer: Multiplexer,
|
|
789
|
+
running: AgentPane[],
|
|
790
|
+
focusedOf: (pid: number) => boolean = () => false,
|
|
791
|
+
): string {
|
|
762
792
|
const lines = [
|
|
763
793
|
`vdx ai — ${plan.projectRoot}`,
|
|
764
794
|
` name: ${plan.project} (${plan.projectSource})`,
|
|
765
795
|
` profile: ${plan.profilePath ?? `none — built-in default (${FALLBACK_AGENTS.join(' or ')}, no flags, no tmux)`}`,
|
|
766
|
-
` agent: ${
|
|
796
|
+
` agent: ${agentLine(plan, plan.args)}`,
|
|
767
797
|
` resume: ${plan.resumeArgs.length ? plan.resumeArgs.join(' ') : '—'}`,
|
|
768
798
|
` matched: ${plan.matched.length ? plan.matched.join(', ') : '—'}`,
|
|
769
799
|
];
|
|
800
|
+
if (plan.focused) lines.push(` focused: nothing wakes it; left out: ${plan.skipped.length ? plan.skipped.join(', ') : '—'}`);
|
|
770
801
|
for (const c of plan.confirm) lines.push(` confirm: "${c.screen}" → ${c.keys.join(' ')}`);
|
|
771
802
|
lines.push(
|
|
772
803
|
multiplexer === 'tmux'
|
|
@@ -777,10 +808,11 @@ export function renderPlan(plan: LaunchPlan, multiplexer: Multiplexer, running:
|
|
|
777
808
|
if (running.length === 0) lines.push(' running: —');
|
|
778
809
|
for (const r of running) {
|
|
779
810
|
const missing = missingArgs(r.proc.args, plan.args);
|
|
811
|
+
const focused = focusedOf(r.proc.pid);
|
|
780
812
|
lines.push(
|
|
781
813
|
` running: ${r.pane.session} ${r.pane.paneId} — ${
|
|
782
814
|
missing.length ? `missing ${missing.join(' ')}` : 'matches the profile'
|
|
783
|
-
}`,
|
|
815
|
+
}${focused ? ', focused' : plan.focused ? ', not focused' : ''}`,
|
|
784
816
|
);
|
|
785
817
|
}
|
|
786
818
|
}
|
|
@@ -880,7 +912,7 @@ function settle(plan: LaunchPlan, paneId: string, deps: AiDeps): SettleResult {
|
|
|
880
912
|
function settleResumed(plan: LaunchPlan, paneId: string, deps: AiDeps): boolean {
|
|
881
913
|
const result = settle(plan, paneId, deps);
|
|
882
914
|
if (result !== 'not-running' || plan.resumeArgs.length === 0) return result === 'ok';
|
|
883
|
-
const line =
|
|
915
|
+
const line = agentLine(plan, plan.args);
|
|
884
916
|
deps.log(`↻ nothing to resume — starting without ${plan.resumeArgs.join(' ')}: ${line}`);
|
|
885
917
|
deps.tmux.respawn(paneId, plan.projectRoot, tmuxShellCommand(line, deps.shell));
|
|
886
918
|
return settle(plan, paneId, deps) === 'ok';
|
|
@@ -926,6 +958,7 @@ export function remoteAiArgs(input: {
|
|
|
926
958
|
const { host, opts, version } = input;
|
|
927
959
|
const flags = [
|
|
928
960
|
opts.fresh && '--new',
|
|
961
|
+
opts.focused && '--focused',
|
|
929
962
|
opts.restart && '--restart',
|
|
930
963
|
opts.detach && '--detach',
|
|
931
964
|
opts.dryRun && '--dry-run',
|
|
@@ -1194,10 +1227,12 @@ interface Prepared {
|
|
|
1194
1227
|
profilePath: string | null;
|
|
1195
1228
|
profileDir: string;
|
|
1196
1229
|
plan: LaunchPlan;
|
|
1230
|
+
/** The same plan for a focused session or a plain one. */
|
|
1231
|
+
replan: (focused: boolean) => LaunchPlan;
|
|
1197
1232
|
}
|
|
1198
1233
|
|
|
1199
1234
|
/** The project, the profile and the launch plan for `pathArg`; an exit code when there is none. */
|
|
1200
|
-
function prepare(pathArg: string, deps: AiDeps): Prepared | number {
|
|
1235
|
+
function prepare(pathArg: string, deps: AiDeps, focused = false): Prepared | number {
|
|
1201
1236
|
const { log } = deps;
|
|
1202
1237
|
|
|
1203
1238
|
const projectRoot = resolveProjectRoot(pathArg);
|
|
@@ -1235,9 +1270,8 @@ function prepare(pathArg: string, deps: AiDeps): Prepared | number {
|
|
|
1235
1270
|
profileDir,
|
|
1236
1271
|
});
|
|
1237
1272
|
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
plan = planLaunch({
|
|
1273
|
+
const replan = (asFocused: boolean) =>
|
|
1274
|
+
planLaunch({
|
|
1241
1275
|
environment,
|
|
1242
1276
|
profilePath,
|
|
1243
1277
|
agent,
|
|
@@ -1246,12 +1280,37 @@ function prepare(pathArg: string, deps: AiDeps): Prepared | number {
|
|
|
1246
1280
|
projectSource: named.source,
|
|
1247
1281
|
host: hostLabel(deps.env),
|
|
1248
1282
|
seat: seatLabel(deps.env),
|
|
1283
|
+
focused: asFocused,
|
|
1249
1284
|
});
|
|
1285
|
+
let plan: LaunchPlan;
|
|
1286
|
+
try {
|
|
1287
|
+
plan = replan(focused);
|
|
1250
1288
|
} catch (e: any) {
|
|
1251
1289
|
log(`vdx ai: ${profilePath ?? 'profile'}: ${e?.message ?? e}`);
|
|
1252
1290
|
return EXIT_USAGE;
|
|
1253
1291
|
}
|
|
1254
|
-
return { projectRoot, environment, profilePath, profileDir, plan };
|
|
1292
|
+
return { projectRoot, environment, profilePath, profileDir, plan, replan };
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
/** Was the process started focused — VDX_FOCUSED=1 in its environment (`ps -E` on macOS, /proc on Linux)? */
|
|
1296
|
+
export function processFocused(pid: number): boolean {
|
|
1297
|
+
const mark = `${FOCUSED_ENV_VAR}=1`;
|
|
1298
|
+
try {
|
|
1299
|
+
const environ = `/proc/${pid}/environ`;
|
|
1300
|
+
if (fs.existsSync(environ)) return fs.readFileSync(environ, 'utf8').split('\0').includes(mark);
|
|
1301
|
+
const out = execFileSync('ps', ['-E', '-ww', '-o', 'command=', '-p', String(pid)], {
|
|
1302
|
+
encoding: 'utf8',
|
|
1303
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1304
|
+
});
|
|
1305
|
+
return out.split(/\s+/).includes(mark);
|
|
1306
|
+
} catch {
|
|
1307
|
+
return false;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
/** The agent's command line; a focused one carries VDX_FOCUSED=1 in front, for the shell in its tmux pane. */
|
|
1312
|
+
function agentLine(plan: LaunchPlan, args: string[]): string {
|
|
1313
|
+
return `${plan.focused ? `${FOCUSED_ENV_VAR}=1 ` : ''}${commandLine(plan.command, args)}`;
|
|
1255
1314
|
}
|
|
1256
1315
|
|
|
1257
1316
|
/** The nearest ancestor of `pid` that is the agent: the session a hook or a shell tool runs in. */
|
|
@@ -1272,9 +1331,12 @@ export function findOwnAgent(procs: ProcInfo[], pid: number, command: string): P
|
|
|
1272
1331
|
*/
|
|
1273
1332
|
export function runAiCheck(pathArg: string, deps: AiDeps, selfPid: number = process.pid): number {
|
|
1274
1333
|
if (!resolveEnvironmentPath(deps.env, deps.home)) return EXIT_OK;
|
|
1275
|
-
|
|
1334
|
+
// The hook runs in the agent's environment: a focused session's carries VDX_FOCUSED=1.
|
|
1335
|
+
const focused = deps.env[FOCUSED_ENV_VAR] === '1';
|
|
1336
|
+
const prepared = prepare(pathArg, deps, focused);
|
|
1276
1337
|
if (typeof prepared === 'number') return prepared;
|
|
1277
1338
|
const { projectRoot, profilePath, plan } = prepared;
|
|
1339
|
+
const vdxAi = `vdx ai${focused ? ' --focused' : ''}`;
|
|
1278
1340
|
|
|
1279
1341
|
const lines = [
|
|
1280
1342
|
`${machineLine(plan.host)}; the agent in this project is started with \`vdx ai\`, and its flags come from the profile ` +
|
|
@@ -1283,6 +1345,14 @@ export function runAiCheck(pathArg: string, deps: AiDeps, selfPid: number = proc
|
|
|
1283
1345
|
`condition in the profile, not an instruction to type. A project's agent on another machine: ` +
|
|
1284
1346
|
`\`vdx ai@<machine>\`. More: \`vdx ai --help\`.`,
|
|
1285
1347
|
];
|
|
1348
|
+
if (focused) {
|
|
1349
|
+
lines.push(
|
|
1350
|
+
`· this session is focused (\`vdx ai --focused\`): it works on what the user gave it — nothing should wake it, ` +
|
|
1351
|
+
`and it does not read its inbox unless the user asks` +
|
|
1352
|
+
(plan.skipped.length ? `; the profile's ${plan.skipped.join(', ')} is left out` : '') +
|
|
1353
|
+
'.',
|
|
1354
|
+
);
|
|
1355
|
+
}
|
|
1286
1356
|
const own = findOwnAgent(deps.processes(), selfPid, plan.command);
|
|
1287
1357
|
if (!own) {
|
|
1288
1358
|
lines.push(`· no ${plan.command} among the ancestors of this process — no session to compare with the profile.`);
|
|
@@ -1300,10 +1370,10 @@ export function runAiCheck(pathArg: string, deps: AiDeps, selfPid: number = proc
|
|
|
1300
1370
|
lines.push(
|
|
1301
1371
|
`✗ this session runs without ${missing.join(' ')}${why}. Hand the user the fix — ` +
|
|
1302
1372
|
(deps.env['TMUX']
|
|
1303
|
-
?
|
|
1373
|
+
? `\`${vdxAi} --restart ${root}\`: it stops this session and starts it again with the profile's flags` +
|
|
1304
1374
|
(plan.resumeArgs.length ? `, resuming the conversation (${plan.resumeArgs.join(' ')})` : '') +
|
|
1305
1375
|
`. Do not run it yourself: it ends this session.`
|
|
1306
|
-
: `this session runs outside tmux, so vdx cannot restart it: the user exits it and runs
|
|
1376
|
+
: `this session runs outside tmux, so vdx cannot restart it: the user exits it and runs \`${vdxAi} ${root}\`.`),
|
|
1307
1377
|
);
|
|
1308
1378
|
deps.out(lines.join('\n') + '\n');
|
|
1309
1379
|
return EXIT_DRIFT;
|
|
@@ -1312,12 +1382,14 @@ export function runAiCheck(pathArg: string, deps: AiDeps, selfPid: number = proc
|
|
|
1312
1382
|
export function runAi(opts: AiOptions, deps: AiDeps): number {
|
|
1313
1383
|
const { log, tmux } = deps;
|
|
1314
1384
|
|
|
1315
|
-
const prepared = prepare(opts.path, deps);
|
|
1385
|
+
const prepared = prepare(opts.path, deps, opts.focused === true);
|
|
1316
1386
|
if (typeof prepared === 'number') return prepared;
|
|
1317
1387
|
const { projectRoot, environment, profileDir } = prepared;
|
|
1318
1388
|
let { plan } = prepared;
|
|
1319
|
-
// The agent started here runs on this machine: it does not inherit where the person sat
|
|
1389
|
+
// The agent started here runs on this machine: it does not inherit where the person sat,
|
|
1390
|
+
// and it is focused by this start's flag, not by the shell vdx runs in.
|
|
1320
1391
|
delete deps.env[SEAT_ENV_VAR];
|
|
1392
|
+
delete deps.env[FOCUSED_ENV_VAR];
|
|
1321
1393
|
// Under `vdx ai@<host>` the person is not here: this machine goes by its name.
|
|
1322
1394
|
const atSeat = sameMachine(plan.seat, plan.host);
|
|
1323
1395
|
const here = atSeat ? `here, on ${plan.host}` : `on ${plan.host}`;
|
|
@@ -1340,6 +1412,13 @@ export function runAi(opts: AiOptions, deps: AiDeps): number {
|
|
|
1340
1412
|
? findAgentPanes(tmux.panes(), deps.processes(), projectRoot, plan.command)
|
|
1341
1413
|
: [];
|
|
1342
1414
|
const current = running[0];
|
|
1415
|
+
// A running agent keeps its mode: plain `vdx ai` takes a focused one as it is,
|
|
1416
|
+
// and the rules it leaves out are not missing from it.
|
|
1417
|
+
const focusedOf = deps.agentFocused ?? processFocused;
|
|
1418
|
+
const runningFocused = current ? focusedOf(current.proc.pid) : false;
|
|
1419
|
+
if (current && !opts.restart && runningFocused && !plan.focused) {
|
|
1420
|
+
plan = { ...prepared.replan(true), resumeArgs: plan.resumeArgs };
|
|
1421
|
+
}
|
|
1343
1422
|
|
|
1344
1423
|
// What a start continues. A restart keeps the conversation the agent is in.
|
|
1345
1424
|
let choice: ConversationChoice | null = null;
|
|
@@ -1358,7 +1437,7 @@ export function runAi(opts: AiOptions, deps: AiDeps): number {
|
|
|
1358
1437
|
|
|
1359
1438
|
const poolDirs = authorPoolDirs(environment, projectRoot, deps.home, profileDir);
|
|
1360
1439
|
if (opts.dryRun) {
|
|
1361
|
-
deps.out(renderPlan(plan, multiplexer, running));
|
|
1440
|
+
deps.out(renderPlan(plan, multiplexer, running, focusedOf));
|
|
1362
1441
|
for (const l of choice?.lines ?? []) deps.out(` conversation: ${l}\n`);
|
|
1363
1442
|
ensureAuthor(projectRoot, poolDirs, deps, true);
|
|
1364
1443
|
return EXIT_OK;
|
|
@@ -1404,16 +1483,17 @@ export function runAi(opts: AiOptions, deps: AiDeps): number {
|
|
|
1404
1483
|
return EXIT_USAGE;
|
|
1405
1484
|
}
|
|
1406
1485
|
for (const c of plan.confirm) log(`note: the agent will ask "${c.screen}" — answer it with ${c.keys.join(' ')}`);
|
|
1486
|
+
const env = plan.focused ? { ...process.env, [FOCUSED_ENV_VAR]: '1' } : undefined;
|
|
1407
1487
|
const startedAt = Date.now();
|
|
1408
|
-
let res = spawnSync(plan.command, [...plan.args, ...plan.resumeArgs], { cwd: projectRoot, stdio: 'inherit' });
|
|
1488
|
+
let res = spawnSync(plan.command, [...plan.args, ...plan.resumeArgs], { cwd: projectRoot, stdio: 'inherit', env });
|
|
1409
1489
|
if ((res.error as NodeJS.ErrnoException | undefined)?.code === 'ENOENT') {
|
|
1410
1490
|
log(`vdx ai: \`${plan.command}\` not found on PATH`);
|
|
1411
1491
|
return 127;
|
|
1412
1492
|
}
|
|
1413
1493
|
const failedAtOnce = res.status !== null && res.status !== 0 && Date.now() - startedAt < NOTHING_TO_RESUME_MS;
|
|
1414
1494
|
if (plan.resumeArgs.length > 0 && failedAtOnce) {
|
|
1415
|
-
log(`↻ nothing to resume — starting without ${plan.resumeArgs.join(' ')}: ${
|
|
1416
|
-
res = spawnSync(plan.command, plan.args, { cwd: projectRoot, stdio: 'inherit' });
|
|
1495
|
+
log(`↻ nothing to resume — starting without ${plan.resumeArgs.join(' ')}: ${agentLine(plan, plan.args)}`);
|
|
1496
|
+
res = spawnSync(plan.command, plan.args, { cwd: projectRoot, stdio: 'inherit', env });
|
|
1417
1497
|
}
|
|
1418
1498
|
return res.status ?? 1;
|
|
1419
1499
|
}
|
|
@@ -1436,11 +1516,19 @@ export function runAi(opts: AiOptions, deps: AiDeps): number {
|
|
|
1436
1516
|
);
|
|
1437
1517
|
}
|
|
1438
1518
|
}
|
|
1519
|
+
if (plan.focused && !runningFocused) {
|
|
1520
|
+
log(`✗ ${current.pane.session}: ${plan.command} is running, not focused`);
|
|
1521
|
+
log(
|
|
1522
|
+
` to focus it: vdx ai --restart --focused ${shellQuote(projectRoot)}` +
|
|
1523
|
+
(plan.resumeArgs.length ? ' (stops the running agent, then resumes its conversation)' : ' (stops the running agent)'),
|
|
1524
|
+
);
|
|
1525
|
+
return EXIT_DRIFT;
|
|
1526
|
+
}
|
|
1439
1527
|
const missing = missingArgs(current.proc.args, plan.args);
|
|
1440
1528
|
if (missing.length > 0) {
|
|
1441
1529
|
log(`✗ ${current.pane.session}: ${plan.command} is running without ${missing.join(' ')}`);
|
|
1442
1530
|
log(
|
|
1443
|
-
` to apply the profile: vdx ai --restart${opts.fresh ? ' --new' : ''} ${shellQuote(projectRoot)}` +
|
|
1531
|
+
` to apply the profile: vdx ai --restart${plan.focused ? ' --focused' : ''}${opts.fresh ? ' --new' : ''} ${shellQuote(projectRoot)}` +
|
|
1444
1532
|
(plan.resumeArgs.length
|
|
1445
1533
|
? ` (stops the running agent, then resumes its conversation)`
|
|
1446
1534
|
: opts.fresh
|
|
@@ -1449,19 +1537,19 @@ export function runAi(opts: AiOptions, deps: AiDeps): number {
|
|
|
1449
1537
|
);
|
|
1450
1538
|
return EXIT_DRIFT;
|
|
1451
1539
|
}
|
|
1452
|
-
log(`✓ ${current.pane.session}: already running with the profile — ${current.proc.args}`);
|
|
1540
|
+
log(`✓ ${current.pane.session}: already running with the profile${runningFocused ? ', focused' : ''} — ${current.proc.args}`);
|
|
1453
1541
|
return finish(true, current.pane.session, current.pane.paneId, opts, deps);
|
|
1454
1542
|
}
|
|
1455
1543
|
|
|
1456
1544
|
if (current) {
|
|
1457
|
-
const line =
|
|
1545
|
+
const line = agentLine(plan, [...plan.args, ...plan.resumeArgs]);
|
|
1458
1546
|
log(`↻ ${current.pane.session} ${current.pane.paneId}: restarting — ${line}`);
|
|
1459
1547
|
tmux.respawn(current.pane.paneId, projectRoot, tmuxShellCommand(line, deps.shell));
|
|
1460
1548
|
return finish(settleResumed(plan, current.pane.paneId, deps), current.pane.session, current.pane.paneId, opts, deps);
|
|
1461
1549
|
}
|
|
1462
1550
|
|
|
1463
1551
|
if (opts.restart) log(`note: no running ${plan.command} in this project — starting one`);
|
|
1464
|
-
const line =
|
|
1552
|
+
const line = agentLine(plan, [...plan.args, ...plan.resumeArgs]);
|
|
1465
1553
|
const cmd = tmuxShellCommand(line, deps.shell);
|
|
1466
1554
|
const reuse = tmux.hasSession(plan.sessionName);
|
|
1467
1555
|
const paneId = reuse
|
package/src/index.ts
CHANGED
|
@@ -76,6 +76,11 @@ the project's files). Hand a person \`vdx ai\`, not \`<agent> --<flag>\`.
|
|
|
76
76
|
project_path the project (default: cwd); its git toplevel is used
|
|
77
77
|
@host the same command on <host> over ssh: its vdx, profile and tmux
|
|
78
78
|
--new a new conversation instead of continuing the last one
|
|
79
|
+
--focused a session nothing wakes: the agent, its hooks and MCP servers get
|
|
80
|
+
VDX_FOCUSED=1 — for the plugins that wake sessions to leave it
|
|
81
|
+
alone — and the profile's rules marked \`wakes: true\` (a channel
|
|
82
|
+
flag) are left out. A running agent keeps its mode; --restart
|
|
83
|
+
applies this one
|
|
79
84
|
--conversation <id>
|
|
80
85
|
continue this Claude Code conversation
|
|
81
86
|
--restart restart the running agent in its pane — applies a changed profile
|
|
@@ -97,7 +102,8 @@ machine's. A new conversation (--new, or none to continue) while an agent of
|
|
|
97
102
|
the project runs on another machine starts only after a yes.
|
|
98
103
|
|
|
99
104
|
Exit codes: 0 the agent runs per the profile; 2 profile or usage error;
|
|
100
|
-
3 the running agent lacks profile flags
|
|
105
|
+
3 the running agent lacks profile flags, or is not focused under --focused
|
|
106
|
+
(rerun with --restart); 4 the start
|
|
101
107
|
was not confirmed.
|
|
102
108
|
Profile format: https://github.com/VoDmAl/vdx/blob/main/docs/specs/environment-format.md
|
|
103
109
|
`;
|
|
@@ -156,10 +162,10 @@ Exit 2 when something is missing.
|
|
|
156
162
|
positionals: 1,
|
|
157
163
|
},
|
|
158
164
|
ai: {
|
|
159
|
-
usage: 'vdx ai[@host] [project_path] [--new | --conversation <id>] [--restart] [--detach] [--dry-run]',
|
|
165
|
+
usage: 'vdx ai[@host] [project_path] [--new | --conversation <id>] [--focused] [--restart] [--detach] [--dry-run]',
|
|
160
166
|
help: AI_HELP,
|
|
161
167
|
// `--resume` asked to continue in 0.12; continuing is the default now.
|
|
162
|
-
bools: ['new', 'restart', 'resume', 'detach', 'dry-run', 'check'],
|
|
168
|
+
bools: ['new', 'focused', 'restart', 'resume', 'detach', 'dry-run', 'check'],
|
|
163
169
|
values: ['conversation'],
|
|
164
170
|
positionals: 1,
|
|
165
171
|
},
|
|
@@ -500,6 +506,7 @@ function cmdAi(opts: ParsedArgs): void {
|
|
|
500
506
|
conversation,
|
|
501
507
|
detach: opts.flags.detach === true,
|
|
502
508
|
dryRun: opts.flags['dry-run'] === true,
|
|
509
|
+
focused: opts.flags.focused === true,
|
|
503
510
|
};
|
|
504
511
|
const deps = { ...defaultDeps(), version: readCliVersion() };
|
|
505
512
|
// `ai@m3`: the same command on another machine. Its own label runs here.
|
package/src/init.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
export const STANDARD_VERBS = ['up', 'down', 'build', 'test', 'check', 'fix'] as const;
|
|
13
13
|
export type Verb = (typeof STANDARD_VERBS)[number];
|
|
14
14
|
|
|
15
|
-
export const DEFAULT_BASELINE = 'github.com/VoDmAl/vdx-rubric-vodmal@v1.
|
|
15
|
+
export const DEFAULT_BASELINE = 'github.com/VoDmAl/vdx-rubric-vodmal@v1.2.0';
|
|
16
16
|
|
|
17
17
|
const VERB_ALIASES: Record<Verb, string[]> = {
|
|
18
18
|
up: ['docker:up', 'docker-up', 'docker:up:detached', 'start', 'dev', 'serve'],
|