@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 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.22.1",
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",
@@ -11,7 +11,7 @@ schema_version: "0.3"
11
11
 
12
12
  metadata:
13
13
  name: vdx-rubric-vodmal
14
- version: "1.0.0"
14
+ version: "1.2.0"
15
15
  description: "Personal maturity rubric for multi-stack projects (vodmal)"
16
16
  owner: vodmal
17
17
  homepage: "https://github.com/vodmal/vdx-rubric"
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(plan: LaunchPlan, multiplexer: Multiplexer, running: AgentPane[]): string {
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: ${commandLine(plan.command, plan.args)}`,
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 = commandLine(plan.command, plan.args);
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
- let plan: LaunchPlan;
1239
- try {
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
- const prepared = prepare(pathArg, deps);
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
- ? `\`vdx ai --restart ${root}\`: it stops this session and starts it again with the profile's flags` +
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 \`vdx ai ${root}\`.`),
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(' ')}: ${commandLine(plan.command, plan.args)}`);
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 = commandLine(plan.command, [...plan.args, ...plan.resumeArgs]);
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 = commandLine(plan.command, [...plan.args, ...plan.resumeArgs]);
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 (rerun with --restart); 4 the start
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.0.0';
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'],