@phnx-labs/agents-cli 1.20.49 → 1.20.51

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.
Files changed (100) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +3 -0
  3. package/dist/commands/browser-picker.js +1 -18
  4. package/dist/commands/cloud.js +1 -25
  5. package/dist/commands/computer.d.ts +1 -0
  6. package/dist/commands/computer.js +129 -8
  7. package/dist/commands/doctor.js +133 -2
  8. package/dist/commands/exec.js +49 -6
  9. package/dist/commands/factory.js +1 -4
  10. package/dist/commands/inspect.js +1 -11
  11. package/dist/commands/mcp.js +2 -6
  12. package/dist/commands/message.js +1 -4
  13. package/dist/commands/profiles.js +1 -18
  14. package/dist/commands/repo.js +33 -14
  15. package/dist/commands/resource-view.d.ts +1 -0
  16. package/dist/commands/resource-view.js +5 -17
  17. package/dist/commands/secrets.d.ts +1 -0
  18. package/dist/commands/secrets.js +1 -28
  19. package/dist/commands/sessions-picker.js +1 -18
  20. package/dist/commands/sessions.js +6 -8
  21. package/dist/commands/teams-picker.js +1 -32
  22. package/dist/commands/teams.js +218 -97
  23. package/dist/commands/tmux.js +1 -3
  24. package/dist/commands/view.js +1 -9
  25. package/dist/commands/worktree.js +1 -4
  26. package/dist/lib/agents.d.ts +0 -4
  27. package/dist/lib/agents.js +20 -33
  28. package/dist/lib/auto-dispatch-linear.d.ts +18 -0
  29. package/dist/lib/auto-dispatch-linear.js +107 -0
  30. package/dist/lib/auto-dispatch-provider.d.ts +10 -0
  31. package/dist/lib/auto-dispatch-provider.js +25 -0
  32. package/dist/lib/auto-dispatch.d.ts +87 -0
  33. package/dist/lib/auto-dispatch.js +142 -0
  34. package/dist/lib/browser/cdp.js +11 -2
  35. package/dist/lib/browser/drivers/ssh.d.ts +28 -10
  36. package/dist/lib/browser/drivers/ssh.js +57 -18
  37. package/dist/lib/browser/refs.js +1 -5
  38. package/dist/lib/cli-resources.d.ts +0 -2
  39. package/dist/lib/cli-resources.js +30 -13
  40. package/dist/lib/cloud/rush.d.ts +0 -24
  41. package/dist/lib/cloud/rush.js +0 -31
  42. package/dist/lib/crabbox/cli.js +4 -1
  43. package/dist/lib/crabbox/lease.js +29 -1
  44. package/dist/lib/daemon.js +41 -0
  45. package/dist/lib/exec.js +43 -18
  46. package/dist/lib/format.d.ts +38 -0
  47. package/dist/lib/format.js +108 -0
  48. package/dist/lib/git.d.ts +21 -0
  49. package/dist/lib/git.js +92 -0
  50. package/dist/lib/hooks/cache.d.ts +9 -2
  51. package/dist/lib/hooks/cache.js +220 -8
  52. package/dist/lib/hooks.js +17 -8
  53. package/dist/lib/hosts/passthrough.js +30 -1
  54. package/dist/lib/hosts/progress.d.ts +31 -0
  55. package/dist/lib/hosts/progress.js +35 -0
  56. package/dist/lib/hosts/remote-cmd.d.ts +1 -1
  57. package/dist/lib/hosts/remote-cmd.js +13 -3
  58. package/dist/lib/platform/exec.d.ts +4 -1
  59. package/dist/lib/platform/exec.js +8 -2
  60. package/dist/lib/resources.d.ts +0 -8
  61. package/dist/lib/resources.js +0 -10
  62. package/dist/lib/runner.js +10 -2
  63. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  64. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  65. package/dist/lib/session/active.d.ts +11 -1
  66. package/dist/lib/session/active.js +3 -0
  67. package/dist/lib/session/db.d.ts +1 -4
  68. package/dist/lib/session/db.js +20 -25
  69. package/dist/lib/session/discover.d.ts +2 -2
  70. package/dist/lib/session/discover.js +61 -48
  71. package/dist/lib/session/parse.js +35 -34
  72. package/dist/lib/session/render.d.ts +7 -3
  73. package/dist/lib/session/render.js +15 -9
  74. package/dist/lib/session/state.d.ts +55 -0
  75. package/dist/lib/session/state.js +87 -10
  76. package/dist/lib/session/types.d.ts +9 -0
  77. package/dist/lib/shims.d.ts +25 -2
  78. package/dist/lib/shims.js +73 -8
  79. package/dist/lib/ssh-tunnel.d.ts +33 -2
  80. package/dist/lib/ssh-tunnel.js +94 -7
  81. package/dist/lib/staleness/types.d.ts +0 -1
  82. package/dist/lib/teams/agents.d.ts +108 -1
  83. package/dist/lib/teams/agents.js +511 -11
  84. package/dist/lib/teams/api.d.ts +7 -1
  85. package/dist/lib/teams/api.js +5 -2
  86. package/dist/lib/teams/registry.d.ts +17 -0
  87. package/dist/lib/teams/registry.js +2 -0
  88. package/dist/lib/teams/remoteWorktree.d.ts +57 -0
  89. package/dist/lib/teams/remoteWorktree.js +213 -0
  90. package/dist/lib/teams/scheduler.d.ts +29 -0
  91. package/dist/lib/teams/scheduler.js +78 -0
  92. package/dist/lib/teams/supervisor.js +7 -0
  93. package/dist/lib/types.d.ts +14 -1
  94. package/dist/lib/versions.d.ts +7 -26
  95. package/dist/lib/versions.js +44 -146
  96. package/dist/lib/warn-unpushed.d.ts +40 -0
  97. package/dist/lib/warn-unpushed.js +128 -0
  98. package/package.json +3 -1
  99. package/dist/lib/resources/index.d.ts +0 -53
  100. package/dist/lib/resources/index.js +0 -76
@@ -11,6 +11,7 @@ import { setHelpSections } from '../lib/help.js';
11
11
  import { parseLoopInterval } from '../lib/loop.js';
12
12
  import { AGENTS } from '../lib/agents.js';
13
13
  import { recordDispatchedRun } from '../lib/audit/log.js';
14
+ import { warnUnpushedWork, shouldWarnUnpushed } from '../lib/warn-unpushed.js';
14
15
  import * as fs from 'fs';
15
16
  import * as path from 'path';
16
17
  import * as os from 'os';
@@ -211,7 +212,7 @@ export function registerRunCommand(program) {
211
212
  .option('--secrets-keys <keys>', 'Inject only this comma-separated subset of keys from --secrets bundles (e.g. KEY1,KEY2). Missing keys are an error. Applies to all --secrets bundles on this run.')
212
213
  .option('--allow-expired', 'Inject secrets even if their expiry date has passed (overrides the pre-run expiry abort).')
213
214
  .option('--cwd <dir>', 'Working directory for the agent (defaults to current directory)')
214
- .option('--add-dir <dir>', 'Grant access to an additional directory outside the project (Claude only, repeatable)', (val, prev) => [...prev, val], [])
215
+ .option('--add-dir <dir>', 'Grant access to an additional directory outside the project (Claude and Codex, repeatable)', (val, prev) => [...prev, val], [])
215
216
  .option('--json', 'Stream events as JSON lines (for parsing by other tools)')
216
217
  .option('--quiet', 'Suppress preamble (rotation banner, "Running:" line). Useful when piping JSON events to a parser.', false)
217
218
  .option('--headless', 'Force headless mode. Auto-enabled when a prompt is provided; pass explicitly to stay headless with no prompt (reads the prompt from stdin).', false)
@@ -245,6 +246,12 @@ export function registerRunCommand(program) {
245
246
  // `--on` and `--computer` are hidden aliases of `--host` — same behavior.
246
247
  runCmd.addOption(new Option('--on <name>', 'Alias of --host.').hideHelp());
247
248
  runCmd.addOption(new Option('--computer <name>', 'Alias of --host.').hideHelp());
249
+ // Required for the documented `agents run <agent> [prompt] -- <native flags>`
250
+ // passthrough: commander >=13 rejects excess operands by default, so any
251
+ // post-`--` token died with "too many arguments" before the action ran. The
252
+ // action re-derives the `--` boundary from rawArgs and still errors on excess
253
+ // operands that are NOT behind `--`.
254
+ runCmd.allowExcessArguments(true);
248
255
  setHelpSections(runCmd, {
249
256
  examples: `
250
257
  # Headless, read-only: investigate or summarize without writing files
@@ -293,10 +300,25 @@ export function registerRunCommand(program) {
293
300
  `,
294
301
  });
295
302
  runCmd.action(async (agentSpec, prompt, options, command) => {
296
- // Capture everything after -- as passthrough args forwarded verbatim to the underlying CLI.
297
- // Use command.args (all positional strings) and strip the declared positional args from the front.
298
- const declaredArgCount = prompt !== undefined ? 2 : 1;
299
- const passthroughArgs = command.args.slice(declaredArgCount);
303
+ // Capture everything after -- as passthrough args forwarded verbatim to the
304
+ // underlying CLI. Commander strips the literal `--` and folds what follows
305
+ // into the positional operands (so `agents run codex -- --yolo` would parse
306
+ // `--yolo` as the PROMPT) — recover the boundary from rawArgs instead of
307
+ // operand counts. Excess operands not behind `--` are still an error (an
308
+ // unquoted prompt must not silently become agent flags).
309
+ const rawArgs = process.argv;
310
+ const separatorIdx = rawArgs.indexOf('--');
311
+ const passthroughArgs = separatorIdx === -1 ? [] : rawArgs.slice(separatorIdx + 1);
312
+ const operandsBeforeSeparator = command.args.length - passthroughArgs.length;
313
+ if (operandsBeforeSeparator > 2) {
314
+ console.error(chalk.red(`Too many arguments for 'run'. Quote the prompt ("fix the bug"), and put agent-native flags after -- (agents run codex -- --yolo).`));
315
+ process.exit(1);
316
+ }
317
+ if (prompt !== undefined && operandsBeforeSeparator < 2) {
318
+ // The token commander assigned to [prompt] came from behind `--` — it is
319
+ // a native flag, not a prompt. Run interactively.
320
+ prompt = undefined;
321
+ }
300
322
  // --lease: invent a disposable cloud box for this run (via crabbox), run
301
323
  // the agent there, then tear it down. Unlike --host, nothing is registered.
302
324
  if (options.lease) {
@@ -342,7 +364,7 @@ export function registerRunCommand(program) {
342
364
  secretsBundle: process.env.AGENTS_LEASE_SECRETS_BUNDLE,
343
365
  keep: options.keepBox,
344
366
  });
345
- console.error(chalk.gray(toreDown ? `Box ${box.slug} destroyed.` : `Box ${box.slug} kept${box.ip ? ` (${box.ip})` : ''}. Stop it: crabbox stop --id ${box.slug}`));
367
+ console.error(chalk.gray(toreDown ? `Box ${box.slug} destroyed.` : `Box ${box.slug} kept${box.ip ? ` (${box.ip})` : ''}. Stop it: crabbox stop ${box.slug}`));
346
368
  process.exit(exitCode === null ? 1 : exitCode);
347
369
  }
348
370
  catch (err) {
@@ -568,6 +590,9 @@ export function registerRunCommand(program) {
568
590
  cwd: resumeExec.cwd ?? process.cwd(),
569
591
  exitCode: resumeExit,
570
592
  });
593
+ // A resumed loop is always headless; surface any commits it left unpushed.
594
+ if (shouldWarnUnpushed(resumeExec.mode ?? 'auto', false))
595
+ await warnUnpushedWork(resumeExec.cwd ?? process.cwd());
571
596
  process.exit(resumeExit);
572
597
  }
573
598
  const [{ buildExecCommand, parseExecEnv, execAgent, runWithFallback, normalizeMode, resolveMode, headlessPlanStallCommand, nativeResume, resolveInteractive }, { ALL_AGENT_IDS }, { profileExists, resolveProfileForRun }, { readAndResolveBundleEnv, describeBundle, assertRemoteBundleFlagsUnsupported }, { splitBundleRef, resolveSshTarget, remoteResolveEnv }, { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, rotationFailoverChain, shouldArmRotationFailover, RUN_STRATEGIES }, { getGlobalDefault, getVersionHomePath, resolveVersion, resolveVersionAlias, ensureAgentRunnable }, { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion }, { parseWorkflowFrontmatter, resolveWorkflowRef, resolveAllowedSubagents, pruneStaleWorkflowSubagents }, { resolveRunDefaults }, { getMcpServersByName, buildWorkflowMcpConfig }, { supports },] = await Promise.all([
@@ -1279,6 +1304,9 @@ export function registerRunCommand(program) {
1279
1304
  // Governance chokepoint (#347): the --acp path exits here, bypassing
1280
1305
  // the normal finalize below — record its one audit entry.
1281
1306
  recordDispatchedRun({ agent, version: defaultVersion ?? 'unknown', mode, cwd, exitCode });
1307
+ // ACP headless run always has a prompt; surface any unpushed commits.
1308
+ if (shouldWarnUnpushed(mode, false))
1309
+ await warnUnpushedWork(cwd);
1282
1310
  process.exit(exitCode);
1283
1311
  }
1284
1312
  catch (err) {
@@ -1435,6 +1463,9 @@ export function registerRunCommand(program) {
1435
1463
  // the normal finalize below — record its one audit entry.
1436
1464
  const loopExit = loopExitCode(result.stoppedBy);
1437
1465
  recordDispatchedRun({ agent, version: defaultVersion ?? 'unknown', mode, cwd, exitCode: loopExit });
1466
+ // A loop is always headless; surface any commits it left unpushed.
1467
+ if (shouldWarnUnpushed(mode, false))
1468
+ await warnUnpushedWork(cwd);
1438
1469
  process.exit(loopExit);
1439
1470
  }
1440
1471
  catch (err) {
@@ -1462,6 +1493,13 @@ export function registerRunCommand(program) {
1462
1493
  }
1463
1494
  cleanupWorkflowMcpConfig();
1464
1495
  cleanupWorkflowSubagents();
1496
+ // Surface committed-but-unpushed work a headless writable run left
1497
+ // behind, so it isn't silently stranded in a worktree. Advisory only,
1498
+ // never throws; skipped for interactive runs (the human sees the shell)
1499
+ // and read-only plan mode (can't commit).
1500
+ if (shouldWarnUnpushed(mode, resolveInteractive(execOptions))) {
1501
+ await warnUnpushedWork(cwd);
1502
+ }
1465
1503
  // Governance chokepoint (#347): every dispatched run finalizes here.
1466
1504
  // ONE tamper-evident audit record per run — non-fatal by contract.
1467
1505
  recordDispatchedRun({ agent: ranAgent, version: ranVersion ?? 'unknown', mode, cwd, exitCode });
@@ -1470,6 +1508,11 @@ export function registerRunCommand(program) {
1470
1508
  catch (err) {
1471
1509
  cleanupWorkflowMcpConfig();
1472
1510
  cleanupWorkflowSubagents();
1511
+ // An agent that committed then crashed is the case where stranded work
1512
+ // matters most — warn before exiting the error path too.
1513
+ if (shouldWarnUnpushed(mode, resolveInteractive(execOptions))) {
1514
+ await warnUnpushedWork(cwd);
1515
+ }
1473
1516
  console.error(chalk.red(`Failed to execute ${agent}: ${err.message}`));
1474
1517
  process.exit(1);
1475
1518
  }
@@ -1,13 +1,10 @@
1
1
  import chalk from 'chalk';
2
+ import { die } from '../lib/format.js';
2
3
  import * as fs from 'fs';
3
4
  import * as path from 'path';
4
5
  import { homedir } from 'os';
5
6
  import { betaEnableHint, isBetaEnabled } from '../lib/beta.js';
6
7
  import { insertTask } from '../lib/cloud/store.js';
7
- function die(msg, code = 1) {
8
- console.error(chalk.red(msg));
9
- process.exit(code);
10
- }
11
8
  function requireFactoryUrl() {
12
9
  const url = process.env.FACTORY_FLOOR_URL;
13
10
  if (!url) {
@@ -17,6 +17,7 @@ import * as fs from 'fs';
17
17
  import * as os from 'os';
18
18
  import * as path from 'path';
19
19
  import chalk from 'chalk';
20
+ import { truncate, termLink } from '../lib/format.js';
20
21
  import * as yaml from 'yaml';
21
22
  import { AGENTS, getCliState, resolveAgentName } from '../lib/agents.js';
22
23
  import { supports } from '../lib/capabilities.js';
@@ -1216,12 +1217,6 @@ function readFirstProseLine(p) {
1216
1217
  return '';
1217
1218
  }
1218
1219
  // ─── OSC-8 + path helpers ────────────────────────────────────────────────────
1219
- function termLink(text, filePath) {
1220
- if (!filePath || !process.stdout.isTTY)
1221
- return text;
1222
- const url = `file://${filePath}`;
1223
- return `\x1b]8;;${url}\x1b\\${text}\x1b]8;;\x1b\\`;
1224
- }
1225
1220
  function linkTarget(p) {
1226
1221
  if (!p)
1227
1222
  return '';
@@ -1271,8 +1266,3 @@ function safeCountSessions(agent) {
1271
1266
  return 0;
1272
1267
  }
1273
1268
  }
1274
- function truncate(s, n) {
1275
- if (s.length <= n)
1276
- return s;
1277
- return s.slice(0, n - 1) + '…';
1278
- }
@@ -1,4 +1,5 @@
1
1
  import chalk from 'chalk';
2
+ import { truncate } from '../lib/format.js';
2
3
  import ora from 'ora';
3
4
  import { checkbox } from '@inquirer/prompts';
4
5
  import { capableAgents, isCapable } from '../lib/capabilities.js';
@@ -764,7 +765,7 @@ function buildMcpRows(opts) {
764
765
  const displayCommand = declaredCommand || firstCommand;
765
766
  rows.push({
766
767
  name,
767
- description: displayCommand ? truncateString(displayCommand, 60) : '',
768
+ description: displayCommand ? truncate(displayCommand, 60) : '',
768
769
  extra: source,
769
770
  targets,
770
771
  buildDetail: () => formatMcpDetail(name, source, centralConfig, manifestConfig, displayCommand, targets),
@@ -811,8 +812,3 @@ function formatMcpDetail(name, source, centralConfig, manifestConfig, command, t
811
812
  lines.push(buildTargetsSection(targets));
812
813
  return lines.join('\n');
813
814
  }
814
- function truncateString(s, max) {
815
- if (s.length <= max)
816
- return s;
817
- return s.slice(0, max - 1) + '…';
818
- }
@@ -1,13 +1,10 @@
1
1
  import chalk from 'chalk';
2
+ import { die } from '../lib/format.js';
2
3
  import { getActiveSessions } from '../lib/session/active.js';
3
4
  import { getTaskById, updateTaskStatus } from '../lib/cloud/store.js';
4
5
  import { resolveProvider } from '../lib/cloud/registry.js';
5
6
  import { mailboxDir, enqueue } from '../lib/mailbox.js';
6
7
  import { resolveMessageTarget } from '../lib/mailbox-target.js';
7
- function die(msg, code = 1) {
8
- console.error(chalk.red(msg));
9
- process.exit(code);
10
- }
11
8
  export function registerMessageCommand(program) {
12
9
  program
13
10
  .command('message <target> <text>')
@@ -7,7 +7,7 @@
7
7
  * through a standard agent CLI with no local proxy.
8
8
  */
9
9
  import chalk from 'chalk';
10
- import * as fs from 'fs';
10
+ import { readStdinSync } from '../lib/format.js';
11
11
  import { spawn } from 'child_process';
12
12
  import { listProfiles, readProfile, writeProfile, deleteProfile, profileExists, profileFromPreset, validateProfileName, getPresetForProfile, } from '../lib/profiles.js';
13
13
  import { getPreset, listPresets, expandPreset } from '../lib/profiles-presets.js';
@@ -41,23 +41,6 @@ async function promptForSecret(message) {
41
41
  return await password({ message, mask: true });
42
42
  }
43
43
  /** Read all available data from stdin synchronously, trimmed. */
44
- function readStdinSync() {
45
- const chunks = [];
46
- const buf = Buffer.alloc(65536);
47
- while (true) {
48
- let bytesRead;
49
- try {
50
- bytesRead = fs.readSync(0, buf, 0, buf.length, null);
51
- }
52
- catch {
53
- break;
54
- }
55
- if (bytesRead === 0)
56
- break;
57
- chunks.push(Buffer.from(buf.subarray(0, bytesRead)));
58
- }
59
- return Buffer.concat(chunks).toString('utf-8').trim();
60
- }
61
44
  /** Ensure a provider API key exists in keychain, prompting or reading stdin if missing. */
62
45
  async function ensureProviderToken(provider, signupUrl, fromStdin) {
63
46
  const item = keychainItemName(provider);
@@ -1,4 +1,5 @@
1
1
  import chalk from 'chalk';
2
+ import { visibleWidth, padVisible } from '../lib/format.js';
2
3
  import ora from 'ora';
3
4
  import * as fs from 'fs';
4
5
  import * as path from 'path';
@@ -26,7 +27,7 @@ function resolveRepoPath(target) {
26
27
  return path.join(HOME, `.agents-${trimmed}`);
27
28
  }
28
29
  import { applyExtraAliasToVersions, ensureAgentsDir, getExtraRepoDir, getSystemAgentsDir, getUserAgentsDir, readMeta, resolveExtraRepoDir, updateMeta, } from '../lib/state.js';
29
- import { parseSource, pullRepo, commitAndPush, isGitRepo, isSystemRepoOrigin } from '../lib/git.js';
30
+ import { parseSource, pullRepo, commitAndPush, isGitRepo, isSystemRepoOrigin, adoptRepo } from '../lib/git.js';
30
31
  import { DEFAULT_SYSTEM_REPO } from '../lib/types.js';
31
32
  import { ALL_AGENT_IDS, isAgentName, resolveAgentName } from '../lib/agents.js';
32
33
  import { refresh } from '../lib/refresh.js';
@@ -218,14 +219,6 @@ async function diffResourceEntries(git, range) {
218
219
  }
219
220
  return out;
220
221
  }
221
- /** Visible character width of a string with embedded ANSI color codes. */
222
- function visibleWidth(s) {
223
- return s.replace(/\[[0-9;]*m/g, '').length;
224
- }
225
- /** Pad string with trailing spaces to a target visible column width. */
226
- function padVisible(s, width) {
227
- return s + ' '.repeat(Math.max(0, width - visibleWidth(s)));
228
- }
229
222
  /**
230
223
  * Read one repo's status into structured data: branch, resource-level sync (what
231
224
  * a pull/push would move), resource-level local edits, and remote URL + commit.
@@ -752,9 +745,9 @@ export function registerRepoCommands(program) {
752
745
  await toggle(alias, false);
753
746
  });
754
747
  repoCmd
755
- .command('pull [alias]')
756
- .description('Pull updates. Aliases: "system" (~/.agents/.system/), "user" (~/.agents/), or any registered extra. No arg pulls all.')
757
- .action(async (alias) => {
748
+ .command('pull [alias] [url]')
749
+ .description('Pull updates. Aliases: "system" (~/.agents/.system/), "user" (~/.agents/), or any registered extra. No arg pulls all. Pass a git URL to git-back a not-yet-cloned user repo: "agents repo pull user <url>".')
750
+ .action(async (alias, url) => {
758
751
  const targets = collectRepoTargets(alias);
759
752
  if (!targets) {
760
753
  process.exitCode = 1;
@@ -766,8 +759,34 @@ export function registerRepoCommands(program) {
766
759
  }
767
760
  for (const t of targets) {
768
761
  if (!fs.existsSync(t.dir) || !isGitRepo(t.dir)) {
769
- console.log(chalk.yellow(` ${t.alias}: not a git repo, skipping`));
770
- continue;
762
+ // A plain (never-cloned) user repo — setup makes ~/.agents a bare dir and
763
+ // never git-clones it (state.ts ensureAgentsDir), so a fresh/Windows box
764
+ // silently falls out of config sync. If a URL was passed, git-back it in
765
+ // place (keeping local files, backing up any that differ); else guide the
766
+ // user. Only the USER repo gets this — system is cloned by setup, extras
767
+ // by `repo add`.
768
+ if (t.alias === 'user' && url) {
769
+ const spinner = ora(`Git-backing ${t.dir} from ${url}...`).start();
770
+ const res = await adoptRepo(url, t.dir);
771
+ if (!res.success) {
772
+ spinner.fail(`user: could not git-back — ${res.error}`);
773
+ process.exitCode = 1;
774
+ continue;
775
+ }
776
+ spinner.succeed(`user: git-backed -> ${res.commit}`);
777
+ if (res.backedUp.length > 0) {
778
+ console.log(chalk.yellow(` backed up ${res.backedUp.length} locally-modified file(s) to ${res.backupDir}`));
779
+ console.log(chalk.gray(` (${res.backedUp.slice(0, 5).join(', ')}${res.backedUp.length > 5 ? ', …' : ''})`));
780
+ }
781
+ // Now a git repo — fall through to the normal pull below.
782
+ }
783
+ else {
784
+ console.log(chalk.yellow(` ${t.alias}: not a git repo, skipping`));
785
+ if (t.alias === 'user') {
786
+ console.log(chalk.gray(' git-back it from your config remote: agents repo pull user <git-url>'));
787
+ }
788
+ continue;
789
+ }
771
790
  }
772
791
  if (t.alias === 'system') {
773
792
  // Skip system repo unless explicitly requested
@@ -58,3 +58,4 @@ export declare function resourceLayout(cols: number, o: {
58
58
  }): ResourceLayout;
59
59
  /** Build the sync targets section showing version pills grouped by agent. */
60
60
  export declare function buildTargetsSection(targets: SyncTarget[]): string;
61
+ /** Pad a string to a fixed width, accounting for ANSI escape codes in length calculation. */
@@ -7,6 +7,7 @@
7
7
  * filtering, and paging.
8
8
  */
9
9
  import chalk from 'chalk';
10
+ import { truncate, padVisible } from '../lib/format.js';
10
11
  import { agentLabel } from '../lib/agents.js';
11
12
  import { itemPicker } from '../lib/picker.js';
12
13
  import { isInteractiveTerminal, isPromptCancelled, printWithPager } from './utils.js';
@@ -65,15 +66,15 @@ function filterRows(rows, query) {
65
66
  }
66
67
  /** Row label rendered inside the picker list. */
67
68
  function formatPickerRow(row, opts) {
68
- const name = chalk.cyan(padRight(row.name, 22));
69
+ const name = chalk.cyan(padVisible(row.name, 22));
69
70
  const extra = opts.extraLabel
70
- ? chalk.gray(padRight(row.extra ?? '-', 10))
71
+ ? chalk.gray(padVisible(row.extra ?? '-', 10))
71
72
  : '';
72
73
  const extra2 = opts.extra2Label
73
- ? chalk.gray(padRight(row.extra2 ?? '-', 16))
74
+ ? chalk.gray(padVisible(row.extra2 ?? '-', 16))
74
75
  : '';
75
76
  const descRaw = row.description ? truncate(row.description, 40) : '';
76
- const desc = padRight(chalk.gray(descRaw), 42);
77
+ const desc = padVisible(chalk.gray(descRaw), 42);
77
78
  const sync = formatSyncSummary(row.targets, opts);
78
79
  return `${name} ${extra}${extra2}${desc} ${sync}`;
79
80
  }
@@ -252,16 +253,3 @@ function formatVersionPill(t) {
252
253
  }
253
254
  }
254
255
  /** Pad a string to a fixed width, accounting for ANSI escape codes in length calculation. */
255
- function padRight(s, width) {
256
- // Strip ANSI for length calc
257
- const raw = s.replace(/\x1b\[[0-9;]*m/g, '');
258
- if (raw.length >= width)
259
- return s;
260
- return s + ' '.repeat(width - raw.length);
261
- }
262
- /** Truncate a string to a maximum length, appending an ellipsis if needed. */
263
- function truncate(s, max) {
264
- if (s.length <= max)
265
- return s;
266
- return s.slice(0, max - 1) + '…';
267
- }
@@ -9,6 +9,7 @@ import { type Command } from 'commander';
9
9
  import { SSH_TARGET_RE, assertValidSshTarget } from '../lib/ssh-exec.js';
10
10
  import { quoteWin32ExecArg } from '../lib/platform/index.js';
11
11
  import { type SecretsBundle, type SecretsPolicy } from '../lib/secrets/bundles.js';
12
+ /** Read all available data from stdin synchronously, trimmed. */
12
13
  /**
13
14
  * Read the raw `.env` text for `import --from <path|->`. A `-` reads the .env
14
15
  * from stdin (the SSH push path: `export --host` pipes the resolved dotenv over
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { Option } from 'commander';
9
9
  import chalk from 'chalk';
10
+ import { padVisible, readStdinSync } from '../lib/format.js';
10
11
  import { terminalWidth, truncateToWidth, stringWidth } from '../lib/session/width.js';
11
12
  import * as fs from 'fs';
12
13
  import { SSH_TARGET_RE, assertValidSshTarget, sshExec } from '../lib/ssh-exec.js';
@@ -125,23 +126,6 @@ async function resolveVault(vaultOpt) {
125
126
  });
126
127
  }
127
128
  /** Read all available data from stdin synchronously, trimmed. */
128
- function readStdinSync() {
129
- const chunks = [];
130
- const buf = Buffer.alloc(65536);
131
- while (true) {
132
- let bytesRead;
133
- try {
134
- bytesRead = fs.readSync(0, buf, 0, buf.length, null);
135
- }
136
- catch {
137
- break;
138
- }
139
- if (bytesRead === 0)
140
- break;
141
- chunks.push(Buffer.from(buf.subarray(0, bytesRead)));
142
- }
143
- return Buffer.concat(chunks).toString('utf-8').trim();
144
- }
145
129
  /**
146
130
  * Read the raw `.env` text for `import --from <path|->`. A `-` reads the .env
147
131
  * from stdin (the SSH push path: `export --host` pipes the resolved dotenv over
@@ -257,17 +241,6 @@ async function browseRemote(targets, args, tty) {
257
241
  process.exit(1);
258
242
  }
259
243
  /** Strip ANSI escape sequences so padding can be computed on visible width. */
260
- function visibleWidth(s) {
261
- // eslint-disable-next-line no-control-regex
262
- return s.replace(/\x1b\[[0-9;]*m/g, '').length;
263
- }
264
- /** padEnd that respects ANSI color codes (chalk-wrapped strings have invisible bytes). */
265
- function padVisible(s, n) {
266
- const w = visibleWidth(s);
267
- if (w >= n)
268
- return s;
269
- return s + ' '.repeat(n - w);
270
- }
271
244
  /** Render an ISO-8601 timestamp as a compact relative age: "now", "5m", "1h", "3d", "2w", "4mo", "1y". */
272
245
  function relativeAge(iso) {
273
246
  const t = Date.parse(iso);
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import fs from 'node:fs';
9
9
  import chalk from 'chalk';
10
+ import { truncate, humanDuration } from '../lib/format.js';
10
11
  import { parseSession, sanitizeForTerminal } from '../lib/session/parse.js';
11
12
  import { cleanSessionPrompt, extractSessionTopic } from '../lib/session/prompt.js';
12
13
  import { linkPath, relativeToCwd } from '../lib/session/render.js';
@@ -154,21 +155,6 @@ function extractTiming(events) {
154
155
  const dur = Number.isNaN(lastMs) ? undefined : humanDuration(Math.max(0, lastMs - firstMs));
155
156
  return { startedAgo: ago, duration: dur };
156
157
  }
157
- function humanDuration(ms) {
158
- const s = Math.floor(ms / 1000);
159
- if (s < 60)
160
- return `${s}s`;
161
- const m = Math.floor(s / 60);
162
- if (m < 60)
163
- return `${m}m`;
164
- const h = Math.floor(m / 60);
165
- const mm = m % 60;
166
- if (h < 24)
167
- return mm ? `${h}h ${mm}m` : `${h}h`;
168
- const d = Math.floor(h / 24);
169
- const hh = h % 24;
170
- return hh ? `${d}d ${hh}h` : `${d}d`;
171
- }
172
158
  function countMessages(events) {
173
159
  return events.filter(e => e.type === 'message').length;
174
160
  }
@@ -364,9 +350,6 @@ function renderTodos(todos, termWidth) {
364
350
  }
365
351
  return out;
366
352
  }
367
- function truncate(s, max) {
368
- return s.length > max ? s.slice(0, max - 1) + '…' : s;
369
- }
370
353
  /** Show an interactive session picker and return the selected session with its action (resume or view). */
371
354
  export async function sessionPicker(config) {
372
355
  const picked = await itemPicker({
@@ -12,6 +12,7 @@ import * as os from 'os';
12
12
  import * as path from 'path';
13
13
  import { spawn } from 'child_process';
14
14
  import chalk from 'chalk';
15
+ import { truncate, padRight } from '../lib/format.js';
15
16
  import ora from 'ora';
16
17
  import { SESSION_AGENTS } from '../lib/session/types.js';
17
18
  import { discoverArtifacts, readArtifact, resolveArtifact } from '../lib/session/artifacts.js';
@@ -1349,8 +1350,11 @@ async function renderSession(session, mode, filters, options = {}) {
1349
1350
  process.stdout.write(renderMarkdown(renderConversationMarkdown(events, { redact: options.noRedact !== true })));
1350
1351
  return;
1351
1352
  }
1352
- // json — no header, raw events only (pipeable)
1353
- process.stdout.write(renderJson(events));
1353
+ // json — normalized events plus the durable session signals from the state
1354
+ // engine (plan text, PR, worktree, ticket). Pre-1.20.51 emitted a bare event
1355
+ // array; consumers that JSON.parse this now read `output.events` for the
1356
+ // array. See issue #743 (plan surfaced) and CHANGELOG for the shape change.
1357
+ process.stdout.write(renderJson(events, session));
1354
1358
  }
1355
1359
  function renderTopicCell(label, topic, query, visibleWidth, paddedWidth) {
1356
1360
  const lbl = (label ?? '').trim();
@@ -2289,9 +2293,3 @@ function formatAbsoluteTime(isoTimestamp) {
2289
2293
  const mm = String(d.getMinutes()).padStart(2, '0');
2290
2294
  return `${months[d.getMonth()]} ${d.getDate()} ${hh}:${mm}`;
2291
2295
  }
2292
- function padRight(s, width) {
2293
- return s.length >= width ? s : s + ' '.repeat(width - s.length);
2294
- }
2295
- function truncate(s, max) {
2296
- return s.length > max ? s.slice(0, max - 1) + '.' : s;
2297
- }
@@ -7,6 +7,7 @@
7
7
  * activity for each team.
8
8
  */
9
9
  import chalk from 'chalk';
10
+ import { relTime, truncate, humanDuration } from '../lib/format.js';
10
11
  import { itemPicker } from '../lib/picker.js';
11
12
  const AGENT_LABEL = {
12
13
  claude: 'Claude',
@@ -26,23 +27,6 @@ function statusColor(status) {
26
27
  default: return chalk.white;
27
28
  }
28
29
  }
29
- function relTime(iso) {
30
- const secs = Math.floor((Date.now() - new Date(iso).getTime()) / 1000);
31
- if (secs < 10)
32
- return 'just now';
33
- if (secs < 60)
34
- return `${secs}s ago`;
35
- if (secs < 3600)
36
- return `${Math.floor(secs / 60)}m ago`;
37
- if (secs < 86400)
38
- return `${Math.floor(secs / 3600)}h ago`;
39
- return `${Math.floor(secs / 86400)}d ago`;
40
- }
41
- function truncate(s, n) {
42
- if (s.length <= n)
43
- return s;
44
- return s.slice(0, n - 1) + '…';
45
- }
46
30
  function firstLine(s) {
47
31
  // Collapse to the first non-empty line so multi-line messages (markdown,
48
32
  // code blocks) render cleanly inside a one-line preview slot.
@@ -120,21 +104,6 @@ function formatComposition(agents) {
120
104
  .map(([type, n]) => (n === 1 ? type : `${type}×${n}`));
121
105
  return parts.join(' ');
122
106
  }
123
- function humanDuration(ms) {
124
- const s = Math.floor(ms / 1000);
125
- if (s < 60)
126
- return `${s}s`;
127
- const m = Math.floor(s / 60);
128
- if (m < 60)
129
- return `${m}m`;
130
- const h = Math.floor(m / 60);
131
- const mm = m % 60;
132
- if (h < 24)
133
- return mm ? `${h}h${mm}m` : `${h}h`;
134
- const d = Math.floor(h / 24);
135
- const hh = h % 24;
136
- return hh ? `${d}d${hh}h` : `${d}d`;
137
- }
138
107
  function runtimeSpan(t) {
139
108
  const startMs = new Date(t.created_at).getTime();
140
109
  const endMs = new Date(t.modified_at).getTime();