@phnx-labs/agents-cli 1.20.85 → 1.20.87

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 (53) hide show
  1. package/CHANGELOG.md +168 -0
  2. package/dist/bin/agents +0 -0
  3. package/dist/commands/events.d.ts +16 -0
  4. package/dist/commands/events.js +44 -5
  5. package/dist/commands/models.js +1 -1
  6. package/dist/commands/sessions-browser.d.ts +18 -0
  7. package/dist/commands/sessions-browser.js +126 -24
  8. package/dist/commands/sessions-picker.d.ts +21 -8
  9. package/dist/commands/sessions-picker.js +88 -11
  10. package/dist/commands/sessions.d.ts +19 -0
  11. package/dist/commands/sessions.js +147 -18
  12. package/dist/commands/ssh.js +59 -1
  13. package/dist/commands/teams-picker.d.ts +2 -0
  14. package/dist/commands/teams-picker.js +2 -1
  15. package/dist/commands/teams.d.ts +4 -1
  16. package/dist/commands/teams.js +106 -70
  17. package/dist/commands/view.js +14 -3
  18. package/dist/index.js +31 -1
  19. package/dist/lib/claude-account-token.d.ts +12 -0
  20. package/dist/lib/claude-account-token.js +63 -0
  21. package/dist/lib/devices/registry.d.ts +25 -0
  22. package/dist/lib/devices/registry.js +82 -1
  23. package/dist/lib/events.d.ts +8 -1
  24. package/dist/lib/events.js +13 -0
  25. package/dist/lib/exec.js +10 -1
  26. package/dist/lib/format.d.ts +7 -0
  27. package/dist/lib/format.js +11 -0
  28. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  29. package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
  30. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +2 -2
  31. package/dist/lib/models.d.ts +21 -0
  32. package/dist/lib/models.js +133 -4
  33. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  34. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  35. package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
  36. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +2 -2
  37. package/dist/lib/session/db.d.ts +21 -0
  38. package/dist/lib/session/db.js +45 -4
  39. package/dist/lib/session/parse.d.ts +11 -0
  40. package/dist/lib/session/parse.js +24 -7
  41. package/dist/lib/session/remote-list.d.ts +7 -0
  42. package/dist/lib/session/remote-list.js +8 -4
  43. package/dist/lib/session/state.d.ts +6 -5
  44. package/dist/lib/session/state.js +84 -11
  45. package/dist/lib/session/team-filter.d.ts +22 -3
  46. package/dist/lib/session/team-filter.js +106 -17
  47. package/dist/lib/session/types.d.ts +8 -0
  48. package/dist/lib/signin-badge.d.ts +17 -0
  49. package/dist/lib/signin-badge.js +19 -0
  50. package/dist/lib/state.d.ts +2 -0
  51. package/dist/lib/state.js +2 -0
  52. package/dist/lib/usage.js +1 -60
  53. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  import chalk from 'chalk';
2
- import { die, relTime, truncate, isJsonMode, padRight } from '../lib/format.js';
2
+ import { dieFriction, relTime, truncate, isJsonMode, padRight } from '../lib/format.js';
3
3
  import * as fs from 'fs/promises';
4
4
  import { addHostOption } from '../lib/hosts/option.js';
5
5
  import * as path from 'path';
@@ -32,6 +32,7 @@ import { buildPreview as buildSessionPreview } from './sessions-picker.js';
32
32
  import { parseExecEnv } from '../lib/exec.js';
33
33
  import { checkRunAccountReadiness } from '../lib/rotate.js';
34
34
  import { teamPicker, printTeamTable } from './teams-picker.js';
35
+ import { teamSpawners } from '../lib/session/db.js';
35
36
  import { itemPicker } from '../lib/picker.js';
36
37
  import { profileExists, readProfile } from '../lib/profiles.js';
37
38
  import { isPromptCancelled, isInteractiveTerminal, requireDestructiveArg, requireInteractiveSelection, } from './utils.js';
@@ -187,10 +188,10 @@ function parseTeammate(spec) {
187
188
  };
188
189
  }
189
190
  catch (err) {
190
- die(`Profile '${name}' is malformed: ${err.message}`);
191
+ dieFriction('teams', 'profile-malformed', `Profile '${name}' is malformed: ${err.message}`);
191
192
  }
192
193
  }
193
- die(`Unknown teammate '${spec}'. Available agents: ${VALID_AGENTS.join(', ')}.\n` +
194
+ dieFriction('teams', 'unknown-teammate', `Unknown teammate '${spec}'. Available agents: ${VALID_AGENTS.join(', ')}.\n` +
194
195
  ` Use 'claude', 'kimi@latest', 'kimi@0.19.2' (a version from 'agents view'), or a profile name.`);
195
196
  }
196
197
  function shortId(id) {
@@ -840,7 +841,9 @@ function buildTasksFromSnapshots(agents) {
840
841
  }
841
842
  return tasks.sort((a, b) => new Date(b.modified_at).getTime() - new Date(a.modified_at).getTime());
842
843
  }
843
- export function buildTeamRowsFromSnapshots(registry, agents) {
844
+ export function buildTeamRowsFromSnapshots(registry, agents,
845
+ /** team name -> the session that spawned it (see teamSpawners). */
846
+ spawners) {
844
847
  const byTeam = new Map();
845
848
  for (const agent of agents) {
846
849
  const details = byTeam.get(agent.task_name) || [];
@@ -854,6 +857,7 @@ export function buildTeamRowsFromSnapshots(registry, agents) {
854
857
  team,
855
858
  agents: byTeam.get(team.task_name) || [],
856
859
  description: registry[team.task_name]?.description,
860
+ spawnedBy: spawners?.get(team.task_name)?.shortId,
857
861
  })),
858
862
  names: teams.map((team) => team.task_name),
859
863
  };
@@ -908,7 +912,17 @@ async function loadTeamAgentSnapshots() {
908
912
  // fallbacks for `status` / `start`; full status stays lazy until a team is picked.
909
913
  async function loadTeamRows(_mgr) {
910
914
  const [registry, agents] = await Promise.all([loadTeams(), loadTeamAgentSnapshots()]);
911
- return buildTeamRowsFromSnapshots(registry, agents);
915
+ // Lineage is read off the session index, not the team registry: the registry is
916
+ // per-machine runtime state, while the transcript that ran `teams create` is
917
+ // indexed, survives the teammate cleanup, and covers teams created before this.
918
+ let spawners;
919
+ try {
920
+ spawners = teamSpawners();
921
+ }
922
+ catch {
923
+ // The index is an enrichment here — a missing/locked DB just drops the column.
924
+ }
925
+ return buildTeamRowsFromSnapshots(registry, agents, spawners);
912
926
  }
913
927
  // Picker fallback for `teams logs` when the teammate ref is omitted. Shows a
914
928
  // flat list of every teammate with their team context; Enter picks one.
@@ -1090,7 +1104,16 @@ export function registerTeamsCommands(program) {
1090
1104
  arr.push(a);
1091
1105
  byTeam.set(a.task_name, arr);
1092
1106
  }
1093
- let rows = buildTeamRowsFromSnapshots(registry, everyAgent).rows;
1107
+ // Same lineage enrichment as loadTeamRows — without it this listing is the
1108
+ // one `teams list` surface that silently drops the "spawned by" column.
1109
+ let spawners;
1110
+ try {
1111
+ spawners = teamSpawners();
1112
+ }
1113
+ catch {
1114
+ // The session index is an enrichment here; a missing one drops the column.
1115
+ }
1116
+ let rows = buildTeamRowsFromSnapshots(registry, everyAgent, spawners).rows;
1094
1117
  // --- query: substring match on team name ---
1095
1118
  if (query) {
1096
1119
  const q = query.toLowerCase();
@@ -1112,7 +1135,7 @@ export function registerTeamsCommands(program) {
1112
1135
  const want = opts.status.toLowerCase();
1113
1136
  const validStatuses = ['working', 'done', 'failed', 'empty'];
1114
1137
  if (!validStatuses.includes(want)) {
1115
- die(`Invalid --status '${opts.status}'. Use one of: ${validStatuses.join(', ')}`);
1138
+ dieFriction('teams', 'invalid-status-filter', `Invalid --status '${opts.status}'. Use one of: ${validStatuses.join(', ')}`);
1116
1139
  }
1117
1140
  rows = rows.filter((row) => classifyTeamStatus(row.team) === want);
1118
1141
  }
@@ -1120,13 +1143,13 @@ export function registerTeamsCommands(program) {
1120
1143
  if (opts.since) {
1121
1144
  const cutoff = parseTimeFilter(opts.since);
1122
1145
  if (!cutoff)
1123
- die(`Could not parse --since '${opts.since}'`);
1146
+ dieFriction('teams', 'invalid-since-filter', `Could not parse --since '${opts.since}'`);
1124
1147
  rows = rows.filter((row) => new Date(row.team.modified_at).getTime() >= cutoff);
1125
1148
  }
1126
1149
  if (opts.until) {
1127
1150
  const cutoff = parseTimeFilter(opts.until);
1128
1151
  if (!cutoff)
1129
- die(`Could not parse --until '${opts.until}'`);
1152
+ dieFriction('teams', 'invalid-until-filter', `Could not parse --until '${opts.until}'`);
1130
1153
  rows = rows.filter((row) => new Date(row.team.modified_at).getTime() <= cutoff);
1131
1154
  }
1132
1155
  rows = rows.slice(0, limit);
@@ -1194,11 +1217,11 @@ export function registerTeamsCommands(program) {
1194
1217
  continue;
1195
1218
  const host = await resolveHost(name);
1196
1219
  if (!host) {
1197
- die(`Couldn't resolve pool device "${name}". Register it with \`agents devices\`, ` +
1220
+ dieFriction('teams', 'pool-device-not-resolvable', `Couldn't resolve pool device "${name}". Register it with \`agents devices\`, ` +
1198
1221
  `enroll it with \`agents hosts add ${name}\`, or pass user@host.`);
1199
1222
  }
1200
1223
  if (remoteShellFor(host.os ?? resolveRemoteOsSync(host.name)) === 'powershell') {
1201
- die(`Distributed teams on Windows device "${host.name}" are not supported yet — ` +
1224
+ dieFriction('teams', 'pool-device-windows-unsupported', `Distributed teams on Windows device "${host.name}" are not supported yet — ` +
1202
1225
  `the teams remote monitor is POSIX-only. Use a Linux/macOS device.`);
1203
1226
  }
1204
1227
  }
@@ -1246,7 +1269,7 @@ export function registerTeamsCommands(program) {
1246
1269
  }
1247
1270
  }
1248
1271
  catch (err) {
1249
- die(err.message);
1272
+ dieFriction('teams', 'create-failed', err.message);
1250
1273
  }
1251
1274
  });
1252
1275
  // add
@@ -1273,29 +1296,29 @@ export function registerTeamsCommands(program) {
1273
1296
  // silently ignoring it — see remoteCwdOnAddError. `!== undefined` so even an
1274
1297
  // explicit empty value (`--remote-cwd ""`) is rejected, not silently dropped.
1275
1298
  if (opts.remoteCwd !== undefined) {
1276
- die(remoteCwdOnAddError(team));
1299
+ dieFriction('teams', 'remote-cwd-on-add', remoteCwdOnAddError(team));
1277
1300
  }
1278
1301
  if (!VALID_MODES.includes(opts.mode)) {
1279
- die(`Invalid mode '${opts.mode}'. Use one of: ${VALID_MODES.join(', ')}`);
1302
+ dieFriction('teams', 'invalid-mode', `Invalid mode '${opts.mode}'. Use one of: ${VALID_MODES.join(', ')}`);
1280
1303
  }
1281
1304
  if (!VALID_EFFORTS.includes(opts.effort)) {
1282
- die(`Invalid effort '${opts.effort}'. Use one of: ${VALID_EFFORTS.join(', ')}`);
1305
+ dieFriction('teams', 'invalid-effort', `Invalid effort '${opts.effort}'. Use one of: ${VALID_EFFORTS.join(', ')}`);
1283
1306
  }
1284
1307
  let taskType = null;
1285
1308
  if (opts.taskType) {
1286
1309
  if (!VALID_TASK_TYPES.includes(opts.taskType)) {
1287
- die(`Invalid task-type '${opts.taskType}'. Use one of: ${VALID_TASK_TYPES.join(', ')}`);
1310
+ dieFriction('teams', 'invalid-task-type', `Invalid task-type '${opts.taskType}'. Use one of: ${VALID_TASK_TYPES.join(', ')}`);
1288
1311
  }
1289
1312
  taskType = opts.taskType;
1290
1313
  }
1291
1314
  let cloudProviderId = null;
1292
1315
  if (opts.cloud) {
1293
1316
  if (!VALID_CLOUD_PROVIDERS.includes(opts.cloud)) {
1294
- die(`Invalid cloud provider '${opts.cloud}'. Use one of: ${VALID_CLOUD_PROVIDERS.join(', ')}`);
1317
+ dieFriction('teams', 'invalid-cloud-provider', `Invalid cloud provider '${opts.cloud}'. Use one of: ${VALID_CLOUD_PROVIDERS.join(', ')}`);
1295
1318
  }
1296
1319
  cloudProviderId = opts.cloud;
1297
1320
  if (cloudProviderId === 'rush' && !opts.repo) {
1298
- die(`--cloud rush requires --repo <owner/repo>`);
1321
+ dieFriction('teams', 'cloud-rush-needs-repo', `--cloud rush requires --repo <owner/repo>`);
1299
1322
  }
1300
1323
  }
1301
1324
  // Auto-create the team if it doesn't exist yet (friendlier UX than erroring),
@@ -1310,7 +1333,7 @@ export function registerTeamsCommands(program) {
1310
1333
  const h = opts.host;
1311
1334
  const d = opts.device;
1312
1335
  if (h && d && h !== d) {
1313
- die('Conflicting --host/--device values — pass just one.');
1336
+ dieFriction('teams', 'conflicting-host-device', 'Conflicting --host/--device values — pass just one.');
1314
1337
  }
1315
1338
  return h ?? d ?? null;
1316
1339
  })();
@@ -1324,18 +1347,18 @@ export function registerTeamsCommands(program) {
1324
1347
  let hostRepoPath = null;
1325
1348
  if (explicitDevice && explicitDevice.toLowerCase() !== machineId()) {
1326
1349
  if (cloudProviderId) {
1327
- die(`--device and --cloud are mutually exclusive (two different remote backends). Pick one.`);
1350
+ dieFriction('teams', 'device-cloud-mutually-exclusive', `--device and --cloud are mutually exclusive (two different remote backends). Pick one.`);
1328
1351
  }
1329
1352
  const host = await resolveHost(explicitDevice);
1330
1353
  if (!host) {
1331
- die(`Couldn't resolve --device "${explicitDevice}". Register it with \`agents devices\`, ` +
1354
+ dieFriction('teams', 'device-not-resolvable', `Couldn't resolve --device "${explicitDevice}". Register it with \`agents devices\`, ` +
1332
1355
  `enroll it with \`agents hosts add ${explicitDevice}\`, or pass user@host.`);
1333
1356
  }
1334
1357
  // POSIX-only in v1: the remote follow/monitor layer offset-tails the log
1335
1358
  // with tail/cat/kill, which don't exist under PowerShell. Refuse Windows
1336
1359
  // up front, mirroring dispatch.ts launchDetached.
1337
1360
  if (remoteShellFor(host.os ?? resolveRemoteOsSync(host.name)) === 'powershell') {
1338
- die(`Distributed teammates on Windows host "${host.name}" are not supported yet — ` +
1361
+ dieFriction('teams', 'device-windows-unsupported', `Distributed teammates on Windows host "${host.name}" are not supported yet — ` +
1339
1362
  `the teams remote monitor is POSIX-only (offset-tails the remote log with tail/cat/kill). ` +
1340
1363
  `Use a Linux/macOS host, or run this teammate locally.`);
1341
1364
  }
@@ -1343,7 +1366,7 @@ export function registerTeamsCommands(program) {
1343
1366
  hostTarget = sshTargetFor(host);
1344
1367
  }
1345
1368
  catch (err) {
1346
- die(`Can't resolve an ssh target for "${host.name}": ${err.message}`);
1369
+ dieFriction('teams', 'ssh-target-unresolvable', `Can't resolve an ssh target for "${host.name}": ${err.message}`);
1347
1370
  }
1348
1371
  // Ensure agents-cli is present + version-matched on the host; surface
1349
1372
  // (not fail on) an agent-not-installed warning, like the run --host path.
@@ -1353,7 +1376,7 @@ export function registerTeamsCommands(program) {
1353
1376
  process.stderr.write(chalk.yellow(`[teams] warning: ${w}\n`));
1354
1377
  }
1355
1378
  catch (err) {
1356
- die(`Host "${host.name}" is not ready: ${err.message}`);
1379
+ dieFriction('teams', 'host-not-ready', `Host "${host.name}" is not ready: ${err.message}`);
1357
1380
  }
1358
1381
  // Provision the repo on the host from the team's --repo (clone into
1359
1382
  // ~/.agents/repos/<team> or reuse an existing checkout), resolving to the
@@ -1370,7 +1393,7 @@ export function registerTeamsCommands(program) {
1370
1393
  hostRepoPath = ensureRemoteRepo(hostTarget, effectiveRepo, team);
1371
1394
  }
1372
1395
  catch (err) {
1373
- die(`Couldn't provision the repo on "${host.name}": ${err.message}\n` +
1396
+ dieFriction('teams', 'repo-provision-failed', `Couldn't provision the repo on "${host.name}": ${err.message}\n` +
1374
1397
  ` Set how each device gets the code with: agents teams create ${team} --repo <url|path>`);
1375
1398
  }
1376
1399
  hostName = host.name;
@@ -1380,7 +1403,7 @@ export function registerTeamsCommands(program) {
1380
1403
  // Version-installed check is about the LOCAL machine — a distributed (--on)
1381
1404
  // teammate's agent/version lives on the host, verified by ensureHostReady.
1382
1405
  if (version && !hostName && !isVersionInstalled(agent, version)) {
1383
- die(`${AGENT_NAMES[agent]} ${version} isn't installed.\n` +
1406
+ dieFriction('teams', 'agent-version-not-installed', `${AGENT_NAMES[agent]} ${version} isn't installed.\n` +
1384
1407
  ` Install it: agents add ${agent}@${version}\n` +
1385
1408
  ` Or see what's installed (incl. @latest): agents view ${agent}`);
1386
1409
  }
@@ -1404,26 +1427,26 @@ export function registerTeamsCommands(program) {
1404
1427
  }
1405
1428
  if (opts.name !== undefined) {
1406
1429
  if (!opts.name || !/^[A-Za-z0-9_-]+$/.test(opts.name)) {
1407
- die(`Invalid teammate name '${opts.name}'. Use letters, numbers, '-', or '_'.`);
1430
+ dieFriction('teams', 'invalid-teammate-name', `Invalid teammate name '${opts.name}'. Use letters, numbers, '-', or '_'.`);
1408
1431
  }
1409
1432
  }
1410
1433
  if (opts.worktree !== undefined) {
1411
1434
  if (!opts.worktree || !/^[A-Za-z0-9_-]+$/.test(opts.worktree)) {
1412
- die(`Invalid worktree name '${opts.worktree}'. Use letters, numbers, '-', or '_'.`);
1435
+ dieFriction('teams', 'invalid-worktree-name', `Invalid worktree name '${opts.worktree}'. Use letters, numbers, '-', or '_'.`);
1413
1436
  }
1414
1437
  }
1415
1438
  const after = opts.after
1416
1439
  ? opts.after.split(',').map((s) => s.trim()).filter(Boolean)
1417
1440
  : [];
1418
1441
  if (after.length > 0 && !opts.name) {
1419
- die("--after requires --name (dependencies reference teammates by name).");
1442
+ dieFriction('teams', 'after-requires-name', "--after requires --name (dependencies reference teammates by name).");
1420
1443
  }
1421
1444
  let envOverrides;
1422
1445
  try {
1423
1446
  envOverrides = parseExecEnv(opts.env);
1424
1447
  }
1425
1448
  catch (err) {
1426
- die(err.message);
1449
+ dieFriction('teams', 'invalid-env', err.message);
1427
1450
  }
1428
1451
  // Team already ensured + loaded above (teamMeta) for the --repo provisioning.
1429
1452
  const worktreesEnabled = teamMeta?.enable_worktrees ?? false;
@@ -1436,19 +1459,19 @@ export function registerTeamsCommands(program) {
1436
1459
  // remote teammate; a per-teammate worktree is created ON THE HOST at launch
1437
1460
  // (createRemoteWorktree in launchRemoteProcess) — we just capture its name.
1438
1461
  if (sharedWorktree) {
1439
- die(`Team '${team}' uses a shared local --use-worktree, which can't apply to a --device (remote) teammate.`);
1462
+ dieFriction('teams', 'shared-worktree-remote-conflict', `Team '${team}' uses a shared local --use-worktree, which can't apply to a --device (remote) teammate.`);
1440
1463
  }
1441
1464
  if (worktreesEnabled) {
1442
1465
  if (!opts.worktree) {
1443
- die(`Team '${team}' has worktrees enabled. Use --worktree <name> for the remote teammate (created on ${hostName}).`);
1466
+ dieFriction('teams', 'worktree-required-remote', `Team '${team}' has worktrees enabled. Use --worktree <name> for the remote teammate (created on ${hostName}).`);
1444
1467
  }
1445
1468
  if (!opts.name) {
1446
- die(`Team '${team}' has worktrees enabled. Use --name <name> to identify this teammate.`);
1469
+ dieFriction('teams', 'name-required-remote', `Team '${team}' has worktrees enabled. Use --name <name> to identify this teammate.`);
1447
1470
  }
1448
1471
  worktreeName = opts.worktree;
1449
1472
  }
1450
1473
  else if (opts.worktree) {
1451
- die(`--worktree requires --enable-worktrees on the team. Recreate the team with: agents teams create ${team} --enable-worktrees`);
1474
+ dieFriction('teams', 'worktree-requires-enable-worktrees', `--worktree requires --enable-worktrees on the team. Recreate the team with: agents teams create ${team} --enable-worktrees`);
1452
1475
  }
1453
1476
  // Local cwd stays null — the remote cwd is repoPath / the remote worktree.
1454
1477
  }
@@ -1458,38 +1481,38 @@ export function registerTeamsCommands(program) {
1458
1481
  try {
1459
1482
  const stat = await fsp.stat(sharedWorktree);
1460
1483
  if (!stat.isDirectory()) {
1461
- die(`Shared worktree path is not a directory: ${sharedWorktree}`);
1484
+ dieFriction('teams', 'shared-worktree-not-dir', `Shared worktree path is not a directory: ${sharedWorktree}`);
1462
1485
  }
1463
1486
  }
1464
1487
  catch {
1465
- die(`Shared worktree path does not exist: ${sharedWorktree}`);
1488
+ dieFriction('teams', 'shared-worktree-missing', `Shared worktree path does not exist: ${sharedWorktree}`);
1466
1489
  }
1467
1490
  worktreePath = sharedWorktree;
1468
1491
  if (opts.worktree) {
1469
- die(`Team '${team}' uses --use-worktree (shared). Don't pass --worktree on add.`);
1492
+ dieFriction('teams', 'worktree-on-shared-team', `Team '${team}' uses --use-worktree (shared). Don't pass --worktree on add.`);
1470
1493
  }
1471
1494
  }
1472
1495
  else if (worktreesEnabled) {
1473
1496
  if (!opts.worktree) {
1474
- die(`Team '${team}' has worktrees enabled. Use --worktree <name> to specify a worktree name.`);
1497
+ dieFriction('teams', 'worktree-required', `Team '${team}' has worktrees enabled. Use --worktree <name> to specify a worktree name.`);
1475
1498
  }
1476
1499
  if (!opts.name) {
1477
- die(`Team '${team}' has worktrees enabled. Use --name <name> to identify this teammate.`);
1500
+ dieFriction('teams', 'name-required', `Team '${team}' has worktrees enabled. Use --name <name> to identify this teammate.`);
1478
1501
  }
1479
1502
  const baseCwd = opts.cwd ?? process.cwd();
1480
1503
  if (!(await isGitRepo(baseCwd))) {
1481
- die(`Worktrees require a git repository. ${baseCwd} is not inside a git repo.`);
1504
+ dieFriction('teams', 'worktree-needs-repo', `Worktrees require a git repository. ${baseCwd} is not inside a git repo.`);
1482
1505
  }
1483
1506
  try {
1484
1507
  worktreeName = opts.worktree;
1485
1508
  worktreePath = await createWorktree(baseCwd, worktreeName);
1486
1509
  }
1487
1510
  catch (err) {
1488
- die(`Failed to create worktree '${opts.worktree}': ${err.message}`);
1511
+ dieFriction('teams', 'worktree-create-failed', `Failed to create worktree '${opts.worktree}': ${err.message}`);
1489
1512
  }
1490
1513
  }
1491
1514
  else if (opts.worktree) {
1492
- die(`--worktree requires --enable-worktrees on the team. Recreate the team with: agents teams create ${team} --enable-worktrees`);
1515
+ dieFriction('teams', 'worktree-requires-enable-worktrees', `--worktree requires --enable-worktrees on the team. Recreate the team with: agents teams create ${team} --enable-worktrees`);
1493
1516
  }
1494
1517
  // Distributed teammates have no LOCAL cwd — their working dir lives on the
1495
1518
  // host (repoPath / the remote worktree). Local teammates default to the
@@ -1541,7 +1564,7 @@ export function registerTeamsCommands(program) {
1541
1564
  cloudSessionId = cloudTask.id;
1542
1565
  }
1543
1566
  catch (err) {
1544
- die(`Cloud dispatch failed: ${err.message}`);
1567
+ dieFriction('teams', 'cloud-dispatch-failed', `Cloud dispatch failed: ${err.message}`);
1545
1568
  }
1546
1569
  }
1547
1570
  try {
@@ -1595,7 +1618,7 @@ export function registerTeamsCommands(program) {
1595
1618
  }
1596
1619
  }
1597
1620
  catch (err) {
1598
- die(`Could not add ${fullName(agent, version)} to ${team}: ${err.message}`);
1621
+ dieFriction('teams', 'add-failed', `Could not add ${fullName(agent, version)} to ${team}: ${err.message}`);
1599
1622
  }
1600
1623
  });
1601
1624
  // status
@@ -1605,20 +1628,22 @@ export function registerTeamsCommands(program) {
1605
1628
  .option('-f, --filter <state>', 'Show only teammates in this state: running, completed, failed, stopped, or all (default: all)', 'all')
1606
1629
  .option('-s, --since <iso>', 'Cursor from a previous status call; only show updates after this timestamp (enables efficient polling)')
1607
1630
  .option('--agent-id <id>', 'Show only this one teammate (by UUID or UUID prefix)')
1631
+ .option('--parent-session <id>', 'Show the teammates spawned BY this session, across teams. Resolves only teammates whose record survives the teams cleanup window.')
1608
1632
  .option('-v, --verbose', 'Emit the full per-teammate detail (prompt, all file paths, all messages). Default is a compact summary.')
1609
1633
  .option('--json', 'Output machine-readable JSON')
1610
1634
  .action(async (team, opts) => {
1611
1635
  const filter = opts.filter;
1612
1636
  const mgr = mkManager();
1613
1637
  // No team given → drop into the picker (TTY) or fail clearly (script).
1614
- if (!team) {
1638
+ // --parent-session is its own lookup key, so it needs no team at all.
1639
+ if (!team && !opts.parentSession) {
1615
1640
  const picked = await pickTeamOr(mgr, 'agents teams status');
1616
1641
  if (!picked)
1617
1642
  return;
1618
1643
  team = picked;
1619
1644
  }
1620
1645
  try {
1621
- const result = await handleStatus(mgr, team, filter, opts.since);
1646
+ const result = await handleStatus(mgr, team, filter, opts.since, opts.parentSession);
1622
1647
  const agents = opts.agentId
1623
1648
  ? result.agents.filter((a) => a.agent_id.startsWith(opts.agentId))
1624
1649
  : result.agents;
@@ -1633,20 +1658,31 @@ export function registerTeamsCommands(program) {
1633
1658
  console.log(JSON.stringify(payload, null, 2));
1634
1659
  return;
1635
1660
  }
1636
- const exists = await teamExists(team);
1637
- if (!exists && result.agents.length === 0) {
1638
- console.log(chalk.yellow(`No team called '${team}'. Create it with: agents teams create ${team}`));
1661
+ // A --parent-session lookup spans teams, so there is no single team to
1662
+ // check for existence its label is the session that spawned them.
1663
+ const label = team ?? `session ${opts.parentSession.slice(0, 8)}`;
1664
+ if (team) {
1665
+ const exists = await teamExists(team);
1666
+ if (!exists && result.agents.length === 0) {
1667
+ console.log(chalk.yellow(`No team called '${team}'. Create it with: agents teams create ${team}`));
1668
+ return;
1669
+ }
1670
+ }
1671
+ else if (result.agents.length === 0) {
1672
+ console.log(chalk.yellow(`No teammates recorded for ${label}.`));
1673
+ console.log(chalk.gray('Teammate records are cleaned up after 7 days; an older team may have aged out.'));
1639
1674
  return;
1640
1675
  }
1641
1676
  if (opts.verbose) {
1642
- await printTeamStatus(team, filtered);
1677
+ await printTeamStatus(label, filtered);
1643
1678
  }
1644
1679
  else {
1645
- printTeamSummary(team, toTaskStatusSummary(filtered));
1680
+ printTeamSummary(label, toTaskStatusSummary(filtered));
1646
1681
  }
1647
1682
  }
1648
1683
  catch (err) {
1649
- die(`Could not check on team ${team}: ${err.message}`);
1684
+ // `team` is undefined for a --parent-session lookup, which spans teams.
1685
+ dieFriction('teams', 'status-failed', `Could not check on ${team ? `team ${team}` : 'the requested session'}: ${err.message}`);
1650
1686
  }
1651
1687
  });
1652
1688
  // active — list every live teammate across every team, grouped by team.
@@ -1908,18 +1944,18 @@ export function registerTeamsCommands(program) {
1908
1944
  }
1909
1945
  const lookup = await mgr.resolveAgentIdInTask(team, ref);
1910
1946
  if (lookup.kind === 'none') {
1911
- die(`No teammate matching '${ref}' in team ${team}`, 2);
1947
+ dieFriction('teams', 'teammate-not-found', `No teammate matching '${ref}' in team ${team}`, 2);
1912
1948
  }
1913
1949
  if (lookup.kind === 'ambiguous') {
1914
1950
  const shorts = lookup.matches.map(shortId).join(', ');
1915
- die(`'${ref}' matches multiple teammates: ${shorts}. Use more characters or a name.`, 2);
1951
+ dieFriction('teams', 'teammate-ambiguous', `'${ref}' matches multiple teammates: ${shorts}. Use more characters or a name.`, 2);
1916
1952
  }
1917
1953
  const agentId = lookup.agentId;
1918
1954
  const agent = await mgr.get(agentId);
1919
1955
  const display = agent?.name || shortId(agentId);
1920
1956
  const stopRes = await handleStop(mgr, team, agentId);
1921
1957
  if ('error' in stopRes)
1922
- die(stopRes.error);
1958
+ dieFriction('teams', 'stop-failed', stopRes.error);
1923
1959
  // Clean up worktree if this teammate had one
1924
1960
  let worktreeKept = false;
1925
1961
  if (agent?.worktreeName && agent?.worktreePath) {
@@ -1977,10 +2013,10 @@ export function registerTeamsCommands(program) {
1977
2013
  const mgr = mkManager();
1978
2014
  const lookup = await mgr.resolveAgentIdInTask(team, ref);
1979
2015
  if (lookup.kind === 'none')
1980
- die(`No teammate matching '${ref}' in team ${team}`, 2);
2016
+ dieFriction('teams', 'teammate-not-found', `No teammate matching '${ref}' in team ${team}`, 2);
1981
2017
  if (lookup.kind === 'ambiguous') {
1982
2018
  const shorts = lookup.matches.map(shortId).join(', ');
1983
- die(`'${ref}' matches multiple teammates: ${shorts}. Use more characters or a name.`, 2);
2019
+ dieFriction('teams', 'teammate-ambiguous', `'${ref}' matches multiple teammates: ${shorts}. Use more characters or a name.`, 2);
1984
2020
  }
1985
2021
  const agentId = lookup.agentId;
1986
2022
  // mgr.get reconciles the teammate's status (PID + start-time guard / remote
@@ -1988,20 +2024,20 @@ export function registerTeamsCommands(program) {
1988
2024
  // is a fact, not a guess.
1989
2025
  const agent = await mgr.get(agentId);
1990
2026
  if (!agent)
1991
- die(`Teammate ${shortId(agentId)} vanished from team ${team}.`);
2027
+ dieFriction('teams', 'teammate-vanished', `Teammate ${shortId(agentId)} vanished from team ${team}.`);
1992
2028
  const display = agent.name || shortId(agentId);
1993
2029
  const status = agent.status;
1994
2030
  const hasMessage = message != null && message.trim().length > 0;
1995
2031
  const route = decideTeamMessageRoute(status, hasMessage);
1996
2032
  switch (route.kind) {
1997
2033
  case 'not-started':
1998
- die(`Teammate '${display}' hasn't started yet (waiting on --after deps). Run \`agents teams start ${team}\` to launch it.`);
2034
+ dieFriction('teams', 'teammate-not-started', `Teammate '${display}' hasn't started yet (waiting on --after deps). Run \`agents teams start ${team}\` to launch it.`);
1999
2035
  return;
2000
2036
  case 'need-message':
2001
2037
  if (status === AgentStatus.RUNNING) {
2002
- die(`Teammate '${display}' is running — pass a message to steer it.`);
2038
+ dieFriction('teams', 'steer-needs-message', `Teammate '${display}' is running — pass a message to steer it.`);
2003
2039
  }
2004
- die(`Teammate '${display}' is ${status} — pass a message to resume it: \`agents teams resume ${team} ${display} "<message>"\`.`);
2040
+ dieFriction('teams', 'resume-needs-message', `Teammate '${display}' is ${status} — pass a message to resume it: \`agents teams resume ${team} ${display} "<message>"\`.`);
2005
2041
  return;
2006
2042
  case 'steer': {
2007
2043
  // Running -> steer via mailbox; never re-launch (that forks a 2nd session).
@@ -2020,7 +2056,7 @@ export function registerTeamsCommands(program) {
2020
2056
  await mgr.resumeTeammate(agentId, message);
2021
2057
  }
2022
2058
  catch (err) {
2023
- die(err.message);
2059
+ dieFriction('teams', 'resume-failed', err.message);
2024
2060
  }
2025
2061
  if (isJsonMode(opts)) {
2026
2062
  console.log(JSON.stringify({ team, agent_id: agentId, name: agent.name ?? null, action: 'resume', prior_status: status }, null, 2));
@@ -2079,18 +2115,18 @@ export function registerTeamsCommands(program) {
2079
2115
  }
2080
2116
  const lookup = await mgr.resolveAgentIdInTask(team, ref);
2081
2117
  if (lookup.kind === 'none') {
2082
- die(`No teammate matching '${ref}' in team ${team}`, 2);
2118
+ dieFriction('teams', 'teammate-not-found', `No teammate matching '${ref}' in team ${team}`, 2);
2083
2119
  }
2084
2120
  if (lookup.kind === 'ambiguous') {
2085
2121
  const shorts = lookup.matches.map(shortId).join(', ');
2086
- die(`'${ref}' matches multiple teammates: ${shorts}. Use more characters or a name.`, 2);
2122
+ dieFriction('teams', 'teammate-ambiguous', `'${ref}' matches multiple teammates: ${shorts}. Use more characters or a name.`, 2);
2087
2123
  }
2088
2124
  const agentId = lookup.agentId;
2089
2125
  const agent = await mgr.get(agentId);
2090
2126
  const display = agent?.name || shortId(agentId);
2091
2127
  // Require agent to be stopped first
2092
2128
  if (agent?.status === 'running' || agent?.status === 'pending') {
2093
- die(`Teammate '${display}' is still ${agent.status}. Run 'agents teams stop ${team} ${display}' first.`);
2129
+ dieFriction('teams', 'remove-still-running', `Teammate '${display}' is still ${agent.status}. Run 'agents teams stop ${team} ${display}' first.`);
2094
2130
  }
2095
2131
  if (!opts.keepLogs) {
2096
2132
  try {
@@ -2128,7 +2164,7 @@ export function registerTeamsCommands(program) {
2128
2164
  }
2129
2165
  const stopRes = await handleStop(mgr, team);
2130
2166
  if ('error' in stopRes)
2131
- die(stopRes.error);
2167
+ dieFriction('teams', 'stop-failed', stopRes.error);
2132
2168
  const status = await handleStatus(mgr, team, 'all');
2133
2169
  // Clean up worktrees for all teammates
2134
2170
  const baseCwd = process.cwd();
@@ -2176,7 +2212,7 @@ export function registerTeamsCommands(program) {
2176
2212
  return;
2177
2213
  }
2178
2214
  if (!existed && stopRes.stopped.length === 0 && status.agents.length === 0) {
2179
- die(`No team called '${team}'`, 2);
2215
+ dieFriction('teams', 'team-not-found', `No team called '${team}'`, 2);
2180
2216
  }
2181
2217
  console.log(chalk.green(`Team ${chalk.cyan(team)} disbanded.`));
2182
2218
  if (stopRes.stopped.length)
@@ -2214,11 +2250,11 @@ export function registerTeamsCommands(program) {
2214
2250
  else {
2215
2251
  const resolved = await resolveTeammateAcrossTeams(base, teammateRef, opts.team);
2216
2252
  if (resolved.kind === 'none') {
2217
- die(`No notes on record for teammate '${teammateRef}'`, 2);
2253
+ dieFriction('teams', 'teammate-notes-not-found', `No notes on record for teammate '${teammateRef}'`, 2);
2218
2254
  }
2219
2255
  if (resolved.kind === 'ambiguous') {
2220
2256
  const hints = resolved.candidates.map((c) => `${c.team}/${c.display}`).join(', ');
2221
- die(`'${teammateRef}' matches multiple teammates: ${hints}.\n` +
2257
+ dieFriction('teams', 'teammate-notes-ambiguous', `'${teammateRef}' matches multiple teammates: ${hints}.\n` +
2222
2258
  ` Narrow it with --team <team>, or pass a UUID prefix.`, 2);
2223
2259
  }
2224
2260
  agentId = resolved.agentId;
@@ -2249,7 +2285,7 @@ export function registerTeamsCommands(program) {
2249
2285
  process.stdout.write(lines.slice(-n).join('\n'));
2250
2286
  }
2251
2287
  catch {
2252
- die(`No notes on record for teammate '${teammateRef ?? agentId}' (looked in ${logPath})`, 2);
2288
+ dieFriction('teams', 'teammate-notes-not-found', `No notes on record for teammate '${teammateRef ?? agentId}' (looked in ${logPath})`, 2);
2253
2289
  }
2254
2290
  });
2255
2291
  // doctor
@@ -5,7 +5,7 @@ import ora from 'ora';
5
5
  import * as fs from 'fs';
6
6
  import * as path from 'path';
7
7
  import { AGENTS, ALL_AGENT_IDS, accountDisplayLabel, getAllCliStates, getUnmanagedCliState, getAccountInfo, resolveAgentName, formatAgentError, agentLabel, colorAgent, } from '../lib/agents.js';
8
- import { loginHint } from '../lib/signin-badge.js';
8
+ import { ambientClaudeToken, loginHint } from '../lib/signin-badge.js';
9
9
  import { machineId } from '../lib/machine-id.js';
10
10
  import { authCacheKey, formatCheckedAge, readAuthHealthCache } from '../lib/auth-health.js';
11
11
  import { agentReportsUsage, deriveUsageStatusFromSnapshot, formatUsageSection, formatUsageSummary, formatUsageStatusBadge, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, } from '../lib/usage.js';
@@ -530,8 +530,19 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
530
530
  if (runDefaults.mode)
531
531
  runDefaultBits.push(`mode:${runDefaults.mode}`);
532
532
  if (!hasEmail && !hasUsage && !signedIn) {
533
- // Installed but never signed in
534
- parts.push(chalk.gray('(logged out log in with: ' + loginHint(agentId) + ')'));
533
+ // No per-version credential. That is NOT the same as unusable: Claude
534
+ // Code authenticates from `CLAUDE_CODE_OAUTH_TOKEN` when the
535
+ // environment carries one, and a run then succeeds against whatever
536
+ // account minted that token — while `signedIn` (agents.ts: `!!email`,
537
+ // read from this version home's `.claude.json`) stays false because no
538
+ // account was ever written here. Reporting that as "logged out" reads
539
+ // as a locked-out account and sends people hunting a login that is not
540
+ // missing; naming the ambient token instead points at the real state —
541
+ // every version on this box resolves to the SAME account, so balanced
542
+ // rotation across them is not actually rotating.
543
+ parts.push(chalk.gray(ambientClaudeToken(agentId)
544
+ ? '(no per-version login — using ambient CLAUDE_CODE_OAUTH_TOKEN)'
545
+ : '(logged out — log in with: ' + loginHint(agentId) + ')'));
535
546
  }
536
547
  else {
537
548
  if (hasEmail || hasUsage || hasActive || signedIn) {
package/dist/index.js CHANGED
@@ -98,7 +98,7 @@ import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedbac
98
98
  import { applyGlobalHelpConventions } from './lib/help.js';
99
99
  import { renderWhatsNew } from './lib/whats-new.js';
100
100
  import { getCliLaunch } from './lib/cli-entry.js';
101
- import { emit, redactArgs } from './lib/events.js';
101
+ import { emit, emitFriction, redactArgs } from './lib/events.js';
102
102
  // Transparent shim delegate: the generated Windows `.cmd` shims invoke
103
103
  // `agents __shim <agent>[@version] <raw args>`. Intercept here, before commander
104
104
  // parses anything, so the agent's own flags (`--help`, `--version`, etc.) pass
@@ -691,6 +691,32 @@ function registerResourcesTombstoneCommand(p) {
691
691
  await program.parseAsync(['node', 'agents', ...args]);
692
692
  });
693
693
  }
694
+ /**
695
+ * Hidden `agents _internal <sub>` namespace for machine-to-machine calls that
696
+ * are not user-facing. The first subcommand is `friction`, used by shell guard
697
+ * hooks (git-guard, rm-guard, …) to self-report a block into the event log
698
+ * before they exit 2 — they run before any `agents` process exists, so they
699
+ * cannot emit in-process.
700
+ */
701
+ function registerInternalCommand(p) {
702
+ const internal = p.command('_internal', { hidden: true });
703
+ internal
704
+ .command('friction')
705
+ .option('--surface <surface>', 'Subsystem that hit the failure (e.g. guard, teams)')
706
+ .option('--id <failureId>', 'Stable failure slug (e.g. git.reset-hard)')
707
+ .option('--error <message>', 'Human-readable failure reason')
708
+ .option('--command <command>', 'The command that was blocked')
709
+ .action((opts) => {
710
+ if (!opts.surface || !opts.id) {
711
+ process.exit(0); // fail-open: never break the caller
712
+ }
713
+ emitFriction(opts.surface, opts.id, {
714
+ ...(opts.error ? { error: opts.error } : {}),
715
+ ...(opts.command ? { command: opts.command } : {}),
716
+ });
717
+ process.exit(0);
718
+ });
719
+ }
694
720
  /** Self-upgrade command (`agents upgrade [version]`). */
695
721
  function registerUpgradeCommand(p) {
696
722
  p.command('upgrade')
@@ -784,6 +810,9 @@ async function registerEagerForRequest(name) {
784
810
  registerResourcesTombstoneCommand(program);
785
811
  await reg(loadView);
786
812
  return true;
813
+ case '_internal':
814
+ registerInternalCommand(program);
815
+ return true;
787
816
  case 'upgrade':
788
817
  registerUpgradeCommand(program);
789
818
  return true;
@@ -878,6 +907,7 @@ async function registerAllEagerCommands() {
878
907
  await reg(loadSsh);
879
908
  registerJobsCronAliasCommand(program, 'jobs');
880
909
  registerJobsCronAliasCommand(program, 'cron');
910
+ registerInternalCommand(program);
881
911
  registerUpgradeCommand(program);
882
912
  await reg(loadPull);
883
913
  await reg(loadPush);