@vodmal/vdx-cli 0.12.1 → 0.13.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
@@ -48,7 +48,7 @@ vdx publish <patch|minor|major> [--dry-run] [--force]
48
48
  vdx doctor [--format=ansi|markdown|json]
49
49
 
50
50
  # Start your agent in a project per your profile (~/.vdx-environment.yaml or $VDX_ENVIRONMENT)
51
- vdx ai [project-path] [--restart] [--resume] [--detach] [--dry-run] # default: cwd
51
+ vdx ai [project-path] [--new] [--restart] [--detach] [--dry-run] # default: cwd
52
52
 
53
53
  # MCP stdio server consumed by the Claude Code plugin
54
54
  vdx-mcp --project <path>
@@ -67,7 +67,8 @@ vdx test # → mise run test (which calls `vitest run` or whatever wa
67
67
  # 3. Ship a new minor release of a Node lib
68
68
  vdx publish minor # bump → npm publish (OTP prompt) → git commit + tag (no push)
69
69
 
70
- # 4. Start the agent here; re-run to attach, --restart to apply a changed profile
70
+ # 4. Start the agent here, continuing its last conversation (--new: a new one);
71
+ # re-run to attach, --restart to apply a changed profile
71
72
  vdx ai
72
73
  ```
73
74
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vodmal/vdx-cli",
3
- "version": "0.12.1",
3
+ "version": "0.13.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/src/ai.ts CHANGED
@@ -13,6 +13,10 @@ import type { Predicate } from './rubric.ts';
13
13
  * and hands it to tmux, or runs it in this terminal when tmux is not wanted or
14
14
  * not installed.
15
15
  *
16
+ * A start continues the project's last conversation (the profile's
17
+ * `resume_args`) — after a reboot `vdx ai` brings the agent back where it was;
18
+ * `--new` starts a new conversation.
19
+ *
16
20
  * The profile is personal (which agent, which permissions), so it is never
17
21
  * bundled into the CLI: it is read from `$VDX_ENVIRONMENT` or
18
22
  * `~/.vdx-environment.yaml`. Without one, vdx starts a plain `claude` or
@@ -499,7 +503,8 @@ export class Tmux {
499
503
  export interface AiOptions {
500
504
  path: string;
501
505
  restart: boolean;
502
- resume: boolean;
506
+ /** `--new`: start a new conversation — leave out the profile's resume args. */
507
+ fresh: boolean;
503
508
  detach: boolean;
504
509
  dryRun: boolean;
505
510
  }
@@ -544,6 +549,13 @@ export const EXIT_USAGE = 2;
544
549
  export const EXIT_DRIFT = 3;
545
550
  export const EXIT_LAUNCH_FAILED = 4;
546
551
 
552
+ /**
553
+ * Without tmux the agent runs in the foreground. One that fails this soon after
554
+ * a start with the resume args had nothing to resume (`claude --continue` exits
555
+ * 1 at once); the window covers answering the channel prompt that comes first.
556
+ */
557
+ export const NOTHING_TO_RESUME_MS = 30_000;
558
+
547
559
  export function renderPlan(plan: LaunchPlan, multiplexer: Multiplexer, running: AgentPane[]): string {
548
560
  const lines = [
549
561
  `vdx ai — ${plan.projectRoot}`,
@@ -658,9 +670,9 @@ function settle(plan: LaunchPlan, paneId: string, deps: AiDeps): SettleResult {
658
670
  }
659
671
 
660
672
  /**
661
- * Settle a pane that was started with the resume args. An agent with nothing to
662
- * resume exits at once; start it once more without them rather than leave the
663
- * project without an agent.
673
+ * Settle a pane started with the plan's resume args, if it has any. An agent
674
+ * with nothing to resume exits at once; start it once more without them rather
675
+ * than leave the project without an agent.
664
676
  */
665
677
  function settleResumed(plan: LaunchPlan, paneId: string, deps: AiDeps): boolean {
666
678
  const result = settle(plan, paneId, deps);
@@ -724,6 +736,8 @@ export function runAi(opts: AiOptions, deps: AiDeps): number {
724
736
  log(`vdx ai: ${profilePath ?? 'profile'}: ${e?.message ?? e}`);
725
737
  return EXIT_USAGE;
726
738
  }
739
+ // A start continues the last conversation unless asked for a new one.
740
+ if (opts.fresh) plan = { ...plan, resumeArgs: [] };
727
741
 
728
742
  let multiplexer = plan.multiplexer;
729
743
  if (multiplexer === 'tmux' && !tmux.available()) {
@@ -751,12 +765,17 @@ export function runAi(opts: AiOptions, deps: AiDeps): number {
751
765
  return EXIT_USAGE;
752
766
  }
753
767
  for (const c of plan.confirm) log(`note: the agent will ask "${c.screen}" — answer it with ${c.keys.join(' ')}`);
754
- const args = [...plan.args, ...(opts.resume ? plan.resumeArgs : [])];
755
- const res = spawnSync(plan.command, args, { cwd: projectRoot, stdio: 'inherit' });
768
+ const startedAt = Date.now();
769
+ let res = spawnSync(plan.command, [...plan.args, ...plan.resumeArgs], { cwd: projectRoot, stdio: 'inherit' });
756
770
  if ((res.error as NodeJS.ErrnoException | undefined)?.code === 'ENOENT') {
757
771
  log(`vdx ai: \`${plan.command}\` not found on PATH`);
758
772
  return 127;
759
773
  }
774
+ const failedAtOnce = res.status !== null && res.status !== 0 && Date.now() - startedAt < NOTHING_TO_RESUME_MS;
775
+ if (plan.resumeArgs.length > 0 && failedAtOnce) {
776
+ log(`↻ nothing to resume — starting without ${plan.resumeArgs.join(' ')}: ${commandLine(plan.command, plan.args)}`);
777
+ res = spawnSync(plan.command, plan.args, { cwd: projectRoot, stdio: 'inherit' });
778
+ }
760
779
  return res.status ?? 1;
761
780
  }
762
781
 
@@ -769,14 +788,17 @@ export function runAi(opts: AiOptions, deps: AiDeps): number {
769
788
  }
770
789
 
771
790
  if (current && !opts.restart) {
791
+ if (opts.fresh) log(`note: ${current.pane.session} already runs ${plan.command} — --new replaces it only with --restart`);
772
792
  const missing = missingArgs(current.proc.args, plan.args);
773
793
  if (missing.length > 0) {
774
794
  log(`✗ ${current.pane.session}: ${plan.command} is running without ${missing.join(' ')}`);
775
795
  log(
776
- ` to apply the profile: vdx ai --restart ${shellQuote(projectRoot)}` +
796
+ ` to apply the profile: vdx ai --restart${opts.fresh ? ' --new' : ''} ${shellQuote(projectRoot)}` +
777
797
  (plan.resumeArgs.length
778
798
  ? ` (stops the running agent, then resumes the conversation with ${plan.resumeArgs.join(' ')})`
779
- : ' (stops the running agent; the conversation is not resumed — the profile has no resume_args)'),
799
+ : opts.fresh
800
+ ? ' (stops the running agent and starts a new conversation)'
801
+ : ' (stops the running agent; the conversation is not resumed — the profile has no resume_args)'),
780
802
  );
781
803
  return EXIT_DRIFT;
782
804
  }
@@ -791,14 +813,13 @@ export function runAi(opts: AiOptions, deps: AiDeps): number {
791
813
  return finish(settleResumed(plan, current.pane.paneId, deps), current.pane.session, current.pane.paneId, opts, deps);
792
814
  }
793
815
 
794
- if (opts.restart) log(`note: no running ${plan.command} in this project — starting a new one`);
795
- const line = commandLine(plan.command, [...plan.args, ...(opts.resume ? plan.resumeArgs : [])]);
816
+ if (opts.restart) log(`note: no running ${plan.command} in this project — starting one`);
817
+ const line = commandLine(plan.command, [...plan.args, ...plan.resumeArgs]);
796
818
  const cmd = tmuxShellCommand(line, deps.shell);
797
819
  const reuse = tmux.hasSession(plan.sessionName);
798
820
  const paneId = reuse
799
821
  ? tmux.newWindow(plan.sessionName, projectRoot, cmd)
800
822
  : tmux.newSession(plan.sessionName, projectRoot, cmd);
801
823
  log(`▶ ${plan.sessionName}${reuse ? ' (new window)' : ''}: ${line}`);
802
- const ok = opts.resume ? settleResumed(plan, paneId, deps) : settle(plan, paneId, deps) === 'ok';
803
- return finish(ok, plan.sessionName, paneId, opts, deps);
824
+ return finish(settleResumed(plan, paneId, deps), plan.sessionName, paneId, opts, deps);
804
825
  }
package/src/index.ts CHANGED
@@ -41,7 +41,7 @@ function usage(): never {
41
41
  vdx init [project_path] [--stack <id>] [--baseline <ref>] [--dry-run] [--force] (default: cwd)
42
42
  vdx publish <patch|minor|major> [--dry-run] [--force]
43
43
  vdx doctor [--format=ansi|markdown|json] [--json]
44
- vdx ai [project_path] [--restart] [--resume] [--detach] [--dry-run] (default: cwd)
44
+ vdx ai [project_path] [--new] [--restart] [--detach] [--dry-run] (default: cwd)
45
45
  `,
46
46
  );
47
47
  process.exit(1);
@@ -271,11 +271,14 @@ function cmdAi(opts: ParsedArgs): void {
271
271
  return v !== undefined;
272
272
  };
273
273
  const restart = bool('restart');
274
- const resume = bool('resume');
274
+ const fresh = bool('new');
275
+ // `--resume` asked to continue in 0.12; continuing is the default now.
276
+ // Still read, so the path after it is not taken for its value.
277
+ bool('resume');
275
278
  const detach = bool('detach');
276
279
  const dryRun = bool('dry-run');
277
280
  process.exit(
278
- runAi({ path: positionals[0] ?? '.', restart, resume, detach, dryRun }, defaultDeps()),
281
+ runAi({ path: positionals[0] ?? '.', restart, fresh, detach, dryRun }, defaultDeps()),
279
282
  );
280
283
  }
281
284