@phnx-labs/agents-cli 1.21.3 → 1.22.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.
Files changed (73) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +32 -3
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/computer-actions.js +1 -0
  5. package/dist/commands/exec.d.ts +27 -0
  6. package/dist/commands/exec.js +123 -6
  7. package/dist/commands/models.js +36 -1
  8. package/dist/commands/projects.js +22 -2
  9. package/dist/commands/sessions-backfill.d.ts +32 -0
  10. package/dist/commands/sessions-backfill.js +186 -0
  11. package/dist/commands/sessions.d.ts +17 -1
  12. package/dist/commands/sessions.js +317 -18
  13. package/dist/commands/teams.js +1 -1
  14. package/dist/commands/worktree.d.ts +3 -3
  15. package/dist/commands/worktree.js +35 -4
  16. package/dist/lib/daemon.d.ts +5 -1
  17. package/dist/lib/daemon.js +63 -14
  18. package/dist/lib/devices/resolve-target.d.ts +6 -0
  19. package/dist/lib/devices/resolve-target.js +9 -3
  20. package/dist/lib/exec.js +39 -8
  21. package/dist/lib/hosts/dispatch.d.ts +12 -0
  22. package/dist/lib/hosts/dispatch.js +23 -6
  23. package/dist/lib/hosts/reconnect.d.ts +38 -0
  24. package/dist/lib/hosts/reconnect.js +85 -4
  25. package/dist/lib/hosts/run-target.js +14 -2
  26. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  27. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  28. package/dist/lib/model-tiers.d.ts +54 -0
  29. package/dist/lib/model-tiers.js +229 -0
  30. package/dist/lib/models.d.ts +3 -0
  31. package/dist/lib/models.js +44 -7
  32. package/dist/lib/pricing/prices.json +16 -1
  33. package/dist/lib/project-focus.d.ts +42 -0
  34. package/dist/lib/project-focus.js +80 -0
  35. package/dist/lib/project-schedule.d.ts +75 -0
  36. package/dist/lib/project-schedule.js +110 -0
  37. package/dist/lib/redact.d.ts +2 -0
  38. package/dist/lib/redact.js +22 -0
  39. package/dist/lib/remote-agents-json.d.ts +2 -0
  40. package/dist/lib/remote-agents-json.js +3 -3
  41. package/dist/lib/rotate.d.ts +84 -1
  42. package/dist/lib/rotate.js +155 -5
  43. package/dist/lib/runner.d.ts +4 -2
  44. package/dist/lib/runner.js +13 -4
  45. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  46. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  47. package/dist/lib/session/bash-command.js +60 -9
  48. package/dist/lib/session/db.d.ts +7 -1
  49. package/dist/lib/session/db.js +301 -32
  50. package/dist/lib/session/discover.d.ts +40 -7
  51. package/dist/lib/session/discover.js +144 -83
  52. package/dist/lib/session/parse.d.ts +8 -1
  53. package/dist/lib/session/parse.js +83 -32
  54. package/dist/lib/session/remote-list.d.ts +71 -0
  55. package/dist/lib/session/remote-list.js +410 -2
  56. package/dist/lib/session/shell-programs.d.ts +15 -0
  57. package/dist/lib/session/shell-programs.js +359 -0
  58. package/dist/lib/session/tool-calls.d.ts +88 -0
  59. package/dist/lib/session/tool-calls.js +612 -0
  60. package/dist/lib/session/tool-index.d.ts +100 -0
  61. package/dist/lib/session/tool-index.js +773 -0
  62. package/dist/lib/session/tool-store.d.ts +15 -0
  63. package/dist/lib/session/tool-store.js +198 -0
  64. package/dist/lib/session/types.d.ts +7 -0
  65. package/dist/lib/state.d.ts +10 -1
  66. package/dist/lib/state.js +11 -2
  67. package/dist/lib/teams/remoteWorktree.d.ts +3 -4
  68. package/dist/lib/teams/remoteWorktree.js +3 -4
  69. package/dist/lib/teams/worktree.d.ts +11 -1
  70. package/dist/lib/teams/worktree.js +42 -4
  71. package/dist/lib/types.d.ts +17 -0
  72. package/dist/lib/types.js +17 -0
  73. package/package.json +3 -1
@@ -14,6 +14,7 @@ import { spawn } from 'child_process';
14
14
  import { Option } from 'commander';
15
15
  import chalk from 'chalk';
16
16
  import { truncate, padRight } from '../lib/format.js';
17
+ import { sanitizeForTerminal } from '../lib/redact.js';
17
18
  import { resolveProjectKey } from '../lib/project-key.js';
18
19
  import { listProjectDefs, resolveProjectNameForCwd } from '../lib/projects.js';
19
20
  import ora from 'ora';
@@ -26,10 +27,10 @@ import { mapPanesToTargets, listClients } from '../lib/tmux/session.js';
26
27
  import { resolveViewingIn, viewingInLabel } from '../lib/session/viewing-in.js';
27
28
  import { machineId, normalizeHost } from '../lib/session/sync/config.js';
28
29
  import { gatherRemoteActive, NO_FANOUT_ENV } from '../lib/session/remote-active.js';
29
- import { gatherRemoteList, runOnPeer } from '../lib/session/remote-list.js';
30
+ import { gatherRemoteList, gatherRemoteToolProgramCounts, gatherRemoteToolSearch, runOnPeer } from '../lib/session/remote-list.js';
30
31
  import { stringWidth, truncateToWidth, padToWidth, terminalWidth } from '../lib/session/width.js';
31
32
  import { inferSessionState } from '../lib/session/state.js';
32
- import { discoverSessions, countSessionsInScope, resolveSessionById, isCompleteSessionId, looksLikeSessionId, searchContentIndex, getSessionRoots } from '../lib/session/discover.js';
33
+ import { discoverSessions, queryIndexedSessions, countSessionsInScope, resolveSessionById, isCompleteSessionId, looksLikeSessionId, searchContentIndex, getSessionRoots, scopeToManaged } from '../lib/session/discover.js';
33
34
  import { findSessionsById, querySessions } from '../lib/session/db.js';
34
35
  import { filterTeamSessions, safeTeamText } from '../lib/session/team-filter.js';
35
36
  import { parseSession } from '../lib/session/parse.js';
@@ -57,8 +58,13 @@ import { registerSessionsInjectCommand } from './sessions-inject.js';
57
58
  import { registerSessionsExportCommand } from './sessions-export.js';
58
59
  import { registerSessionsImportCommand } from './sessions-import.js';
59
60
  import { registerSessionsMigrateCommand, registerSessionsMigrationsCommand } from './sessions-migrate.js';
61
+ import { registerSessionsBackfillCommand } from './sessions-backfill.js';
60
62
  import { runBrowserSessions } from '../lib/browser/sessions-list.js';
63
+ import { countToolProgramOccurrences, parseToolProgramCountClause, readToolIndexCoverage, searchToolCalls, TOOL_QUERY_MAX_CLAUSE_BYTES, TOOL_QUERY_MAX_CLAUSES, TOOL_QUERY_MAX_RESULT_SESSIONS, serializeToolSearchEnvelope, toolSearchRemoteReceiveBudget, } from '../lib/session/tool-index.js';
61
64
  const SESSION_AGENT_FILTER_HELP = `Filter by agent, e.g. claude, codex, claude@2.0.65`;
65
+ function collectQueryClause(value, previous) {
66
+ return [...previous, value];
67
+ }
62
68
  /**
63
69
  * The prioritized harnesses that get a boolean shorthand flag (e.g. `--claude`
64
70
  * === `--agent claude`). The rest stay reachable via `--agent <name>`, which
@@ -1296,6 +1302,114 @@ export function formatLiveStatusHeadline(live, favorite = false) {
1296
1302
  }
1297
1303
  return `${star}${glyph} ${statusColor(live.status)(word)}${suffix}`;
1298
1304
  }
1305
+ /** Merge local and peer envelopes without changing the versioned JSON shape. */
1306
+ export function mergeToolSearchEnvelopes(local, remotes) {
1307
+ const all = [local, ...remotes];
1308
+ const sessions = new Map();
1309
+ for (const envelope of all) {
1310
+ for (const session of envelope.sessions) {
1311
+ sessions.set(`${session.machine ?? 'local'}\0${session.id}`, session);
1312
+ }
1313
+ }
1314
+ return {
1315
+ schemaVersion: 1,
1316
+ generatedAt: new Date().toISOString(),
1317
+ query: local.query,
1318
+ coverage: {
1319
+ indexedFiles: all.reduce((n, envelope) => n + envelope.coverage.indexedFiles, 0),
1320
+ indexedCalls: all.reduce((n, envelope) => n + envelope.coverage.indexedCalls, 0),
1321
+ skippedFiles: all.reduce((n, envelope) => n + envelope.coverage.skippedFiles, 0),
1322
+ limitedFiles: all.reduce((n, envelope) => n + envelope.coverage.limitedFiles, 0),
1323
+ remainingFiles: all.reduce((n, envelope) => n + envelope.coverage.remainingFiles, 0),
1324
+ complete: all.every((envelope) => envelope.coverage.complete),
1325
+ },
1326
+ sessions: [...sessions.values()].sort((a, b) => b.timestamp.localeCompare(a.timestamp)),
1327
+ };
1328
+ }
1329
+ export function mergeToolProgramCountEnvelopes(local, remotes) {
1330
+ const all = [local, ...remotes];
1331
+ return {
1332
+ schemaVersion: 1,
1333
+ kind: 'tool-program-count',
1334
+ generatedAt: new Date().toISOString(),
1335
+ query: local.query,
1336
+ coverage: {
1337
+ indexedFiles: all.reduce((sum, envelope) => sum + envelope.coverage.indexedFiles, 0),
1338
+ indexedCalls: all.reduce((sum, envelope) => sum + envelope.coverage.indexedCalls, 0),
1339
+ skippedFiles: all.reduce((sum, envelope) => sum + envelope.coverage.skippedFiles, 0),
1340
+ limitedFiles: all.reduce((sum, envelope) => sum + envelope.coverage.limitedFiles, 0),
1341
+ remainingFiles: all.reduce((sum, envelope) => sum + envelope.coverage.remainingFiles, 0),
1342
+ complete: all.every((envelope) => envelope.coverage.complete),
1343
+ },
1344
+ totals: {
1345
+ occurrences: all.reduce((sum, envelope) => sum + envelope.totals.occurrences, 0),
1346
+ toolCalls: all.reduce((sum, envelope) => sum + envelope.totals.toolCalls, 0),
1347
+ sessions: all.reduce((sum, envelope) => sum + envelope.totals.sessions, 0),
1348
+ },
1349
+ machines: all.flatMap((envelope) => envelope.machines),
1350
+ };
1351
+ }
1352
+ /** Partition a fleet query by transcript origin so synced mirrors cannot duplicate results. */
1353
+ export function toolOriginSessions(sessions, machine, originOnly) {
1354
+ return originOnly
1355
+ ? sessions.filter((session) => (session.machine ?? machine) === machine)
1356
+ : sessions;
1357
+ }
1358
+ export function printToolProgramCount(envelope) {
1359
+ const { totals } = envelope;
1360
+ const qualifier = envelope.coverage.complete ? '' : 'at least ';
1361
+ console.log(`${envelope.query.program}: ${qualifier}${totals.occurrences.toLocaleString()} static occurrence${totals.occurrences === 1 ? '' : 's'} `
1362
+ + `in ${totals.toolCalls.toLocaleString()} tool call${totals.toolCalls === 1 ? '' : 's'} `
1363
+ + `across ${totals.sessions.toLocaleString()} session${totals.sessions === 1 ? '' : 's'}.`);
1364
+ if (!envelope.coverage.complete) {
1365
+ console.log(chalk.yellow(`Partial tool index: ${envelope.coverage.remainingFiles.toLocaleString()} transcript${envelope.coverage.remainingFiles === 1 ? '' : 's'} still need `
1366
+ + '`agents sessions backfill tools`.'));
1367
+ }
1368
+ }
1369
+ export function toolSearchFleetSortError(sort, spansDevices) {
1370
+ if (!spansDevices || !sort || sort === 'recent')
1371
+ return undefined;
1372
+ return 'Tool search across devices supports only --sort recent; cost and duration are local-only.';
1373
+ }
1374
+ /** Compact grouped evidence for humans; JSON retains every bounded field. */
1375
+ export function printToolSearch(envelope) {
1376
+ for (const session of envelope.sessions) {
1377
+ const machineName = session.machine
1378
+ ? truncate(sanitizeForTerminal(session.machine).replace(/\s+/g, ' '), 80)
1379
+ : '';
1380
+ const machine = machineName ? ` @ ${machineName}` : '';
1381
+ const rawHeading = session.label || session.topic || session.project || session.shortId;
1382
+ const heading = truncate(sanitizeForTerminal(rawHeading).replace(/\s+/g, ' '), Math.max(30, terminalWidth() - 20));
1383
+ console.log(`${chalk.cyan(session.shortId)}${chalk.gray(machine)} ${heading}`);
1384
+ for (const call of session.calls) {
1385
+ const tool = truncate(sanitizeForTerminal(call.tool).replace(/\s+/g, ' '), 80);
1386
+ const safePrograms = call.programs.map((program) => truncate(sanitizeForTerminal(program).replace(/\s+/g, ' '), 80));
1387
+ const programs = safePrograms.length > 0 ? ` [${safePrograms.join(', ')}]` : '';
1388
+ const status = call.outcome === 'unknown' ? '' : ` ${call.outcome}`;
1389
+ const input = truncate(sanitizeForTerminal(call.input).replace(/\s+/g, ' '), Math.max(30, terminalWidth() - 26));
1390
+ console.log(` ${chalk.gray(`#${call.ordinal + 1}`)} ${tool}${programs}${status} ${input}`);
1391
+ const snippet = call.error || call.output;
1392
+ if (snippet) {
1393
+ console.log(` ${chalk.gray(truncate(sanitizeForTerminal(snippet).replace(/\s+/g, ' '), Math.max(30, terminalWidth() - 8)))}`);
1394
+ }
1395
+ }
1396
+ console.log();
1397
+ }
1398
+ const count = envelope.sessions.length;
1399
+ console.log(chalk.gray(`${count} matching session${count === 1 ? '' : 's'}.`));
1400
+ if (!envelope.coverage.complete) {
1401
+ const skipped = envelope.coverage.skippedFiles > 0
1402
+ ? ` ${envelope.coverage.skippedFiles} transcript${envelope.coverage.skippedFiles === 1 ? ' was' : 's were'} skipped.`
1403
+ : '';
1404
+ const limited = envelope.coverage.limitedFiles > 0
1405
+ ? ` ${envelope.coverage.limitedFiles} transcript${envelope.coverage.limitedFiles === 1 ? ' has' : 's have'} incomplete evidence because a safety limit was reached.`
1406
+ : '';
1407
+ const retry = envelope.coverage.remainingFiles > 0
1408
+ ? ' Run `agents sessions backfill tools` to index historical transcripts.'
1409
+ : '';
1410
+ console.log(chalk.yellow(`Tool index coverage is partial: ${envelope.coverage.remainingFiles} transcript${envelope.coverage.remainingFiles === 1 ? '' : 's'} remain.${skipped}${limited}${retry}`));
1411
+ }
1412
+ }
1299
1413
  /** Main action handler for `agents sessions`. Routes to picker, table, or single-session render. */
1300
1414
  async function sessionsAction(query, options,
1301
1415
  /**
@@ -1304,9 +1418,70 @@ async function sessionsAction(query, options,
1304
1418
  * because the default arrives as a string like any typed value.
1305
1419
  */
1306
1420
  limitSource) {
1307
- // Explicit --query is interchangeable with the positional; it's how you search
1308
- // for text that collides with a subcommand name (e.g. `sessions --query go`).
1309
- query = query ?? options.query;
1421
+ const queryClauses = options.query ?? [];
1422
+ const toolOnly = options.include?.split(',').map((role) => role.trim()).filter(Boolean).join(',') === 'tools';
1423
+ const toolEvidenceMode = toolOnly;
1424
+ if (options.count && !toolOnly) {
1425
+ console.error(chalk.red('--count requires --include tools.'));
1426
+ process.exitCode = 1;
1427
+ return;
1428
+ }
1429
+ if (!toolEvidenceMode) {
1430
+ if (queryClauses.length > 1) {
1431
+ console.error(chalk.red('Repeated --query clauses require --include tools.'));
1432
+ process.exitCode = 1;
1433
+ return;
1434
+ }
1435
+ // Outside tool evidence mode, --query retains its original positional-search
1436
+ // meaning. The collector only makes it repeatable for tool clauses.
1437
+ query = query ?? queryClauses[0];
1438
+ }
1439
+ if (options.fleet && !toolEvidenceMode) {
1440
+ console.error(chalk.red('--fleet applies to tool-call queries: add --include tools.'));
1441
+ process.exitCode = 1;
1442
+ return;
1443
+ }
1444
+ if (toolEvidenceMode && (options.markdown || options.redact === false)) {
1445
+ const incompatible = [
1446
+ options.markdown ? '--markdown' : undefined,
1447
+ options.redact === false ? '--no-redact' : undefined,
1448
+ ].filter((flag) => flag !== undefined);
1449
+ console.error(chalk.red(`${incompatible.join(' and ')} cannot be used with --include tools.`));
1450
+ console.error(chalk.gray('Tool evidence is always redacted and byte-bounded; drop the conflicting render flag.'));
1451
+ process.exitCode = 1;
1452
+ return;
1453
+ }
1454
+ if (toolEvidenceMode && queryClauses.length > TOOL_QUERY_MAX_CLAUSES) {
1455
+ console.error(chalk.red(`Tool search accepts at most ${TOOL_QUERY_MAX_CLAUSES} --query clauses.`));
1456
+ process.exitCode = 1;
1457
+ return;
1458
+ }
1459
+ if (toolEvidenceMode && queryClauses.some((clause) => Buffer.byteLength(clause) > TOOL_QUERY_MAX_CLAUSE_BYTES)) {
1460
+ console.error(chalk.red(`Each tool --query clause is limited to ${TOOL_QUERY_MAX_CLAUSE_BYTES} bytes.`));
1461
+ process.exitCode = 1;
1462
+ return;
1463
+ }
1464
+ if (options.count && (queryClauses.length !== 1 || query !== undefined)) {
1465
+ console.error(chalk.red('--count requires exactly one --query program:<name> clause and no positional query.'));
1466
+ process.exitCode = 1;
1467
+ return;
1468
+ }
1469
+ if (options.count && (limitSource === 'cli' || limitSource === 'env')) {
1470
+ console.error(chalk.red('--count covers the complete filtered scope and cannot be combined with --limit.'));
1471
+ process.exitCode = 1;
1472
+ return;
1473
+ }
1474
+ let countProgram;
1475
+ if (options.count) {
1476
+ try {
1477
+ countProgram = parseToolProgramCountClause(queryClauses[0]);
1478
+ }
1479
+ catch (error) {
1480
+ console.error(chalk.red(error instanceof Error ? error.message : String(error)));
1481
+ process.exitCode = 1;
1482
+ return;
1483
+ }
1484
+ }
1310
1485
  // Normalize convenience flags before any routing reads them: per-agent
1311
1486
  // shorthands fold into --agent, and --device is an alias for --host (both
1312
1487
  // resolve against the same device registry).
@@ -1362,13 +1537,19 @@ limitSource) {
1362
1537
  });
1363
1538
  return;
1364
1539
  }
1540
+ if (toolEvidenceMode && options.local === true
1541
+ && options.host && !shouldIncludeLocal(options.host, machineId())) {
1542
+ console.error(chalk.red('--local and --device name opposite scopes: --local skips the SSH fan-out that --device needs.'));
1543
+ console.error(chalk.gray('Drop one — `--device <box>` to read that machine, `--local` to stay on this one.'));
1544
+ process.exit(1);
1545
+ }
1365
1546
  // --host WITHOUT --active. `--json` fans the recent listing out and emits ONE
1366
1547
  // clean merged SessionMeta[] array (same shape as the local --json path), for
1367
1548
  // scripts/extensions that JSON.parse a remote's history. Without --json it
1368
1549
  // keeps the legacy per-host stream (each remote's raw stdout under a
1369
1550
  // `── host ──` banner). With --active, the hosts are folded into the merged
1370
1551
  // machine-grouped view instead (handled below).
1371
- if (options.host && options.host.length > 0 && !options.active) {
1552
+ if (options.host && options.host.length > 0 && !options.active && !toolEvidenceMode) {
1372
1553
  // --local means "skip the SSH fan-out"; --host means "look only over there".
1373
1554
  // Together they ask for a peer's sessions without dialing the peer, which can
1374
1555
  // only ever be empty — so say that instead of rendering a blank list.
@@ -1503,7 +1684,7 @@ limitSource) {
1503
1684
  }
1504
1685
  const mode = resolveViewMode(options, filterOpts);
1505
1686
  // --markdown or any filter flag forces single-session render.
1506
- const wantsRender = mode === 'markdown' || hasAnyFilter(filterOpts);
1687
+ const wantsRender = !toolEvidenceMode && (mode === 'markdown' || hasAnyFilter(filterOpts));
1507
1688
  // Artifact-list or artifact-read paths: widen scope and resolve session globally.
1508
1689
  if ((options.artifacts || options.artifact !== undefined) && searchQuery) {
1509
1690
  await renderArtifactsGlobal(searchQuery, options.artifacts ?? false, options.artifact, { agent: options.agent, project: options.project });
@@ -1536,11 +1717,24 @@ limitSource) {
1536
1717
  const limit = wantsOverview
1537
1718
  ? OVERVIEW_POOL_LIMIT
1538
1719
  : parseInt(userSetLimit ? options.limit : wantsWholeTeam ? String(WHOLE_TEAM_POOL_LIMIT) : DEFAULT_LIMIT, 10);
1720
+ if (toolEvidenceMode && (!Number.isSafeInteger(limit) || limit < 1 || limit > TOOL_QUERY_MAX_RESULT_SESSIONS)) {
1721
+ console.error(chalk.red(`Tool search --limit must be from 1 to ${TOOL_QUERY_MAX_RESULT_SESSIONS}.`));
1722
+ process.exitCode = 1;
1723
+ return;
1724
+ }
1539
1725
  // Overview: recency order across the whole index, no default window; an explicit
1540
1726
  // --since still narrows. Non-overview keeps the prior interactive-30d default.
1541
1727
  const since = wantsOverview
1542
1728
  ? options.since
1543
1729
  : (options.since ?? (isInteractive && !options.all && !wantsWholeTeam ? '30d' : undefined));
1730
+ const toolSpansDevices = toolEvidenceMode
1731
+ && (options.fleet || (options.host?.length ?? 0) > 0);
1732
+ const toolSortError = toolSearchFleetSortError(options.sort, toolSpansDevices);
1733
+ if (toolSortError) {
1734
+ console.error(chalk.red(toolSortError));
1735
+ process.exitCode = 1;
1736
+ return;
1737
+ }
1544
1738
  const spinner = options.json ? null : ora().start();
1545
1739
  const tracker = createScanProgressTracker(LOAD_VERBS, 'sessions', spinner);
1546
1740
  try {
@@ -1556,29 +1750,53 @@ limitSource) {
1556
1750
  // --in-team spans directories by construction: a team's teammates run in
1557
1751
  // their own worktrees, so scoping to the current cwd hides most of the
1558
1752
  // lineage the flag exists to show.
1559
- all: pathFilter ? undefined : options.all || wantsWholeTeam,
1753
+ all: pathFilter ? undefined : options.all || wantsWholeTeam || toolSpansDevices,
1560
1754
  cwd: process.cwd(),
1561
1755
  // Default overview scopes to the current repo SUBTREE (prefix match), so a
1562
1756
  // monorepo shows its sub-projects grouped instead of collapsing to the one
1563
1757
  // exact-cwd project. `--all` clears the prefix and spans the whole index.
1564
- cwdPrefix: pathFilter ?? (wantsOverview && !options.all && !wantsWholeTeam ? process.cwd() : undefined),
1758
+ cwdPrefix: pathFilter ?? (wantsOverview && !options.all && !wantsWholeTeam && !toolSpansDevices ? process.cwd() : undefined),
1565
1759
  project: options.project,
1566
1760
  since,
1567
1761
  until: options.until,
1568
1762
  sortBy,
1569
1763
  origin: options.routine ? 'routine' : undefined,
1764
+ skipExistenceCheck: toolEvidenceMode,
1765
+ unbounded: toolEvidenceMode,
1570
1766
  skill: options.skill,
1571
1767
  plugin: options.plugin,
1572
1768
  };
1573
1769
  let hiddenUnmanaged = 0;
1574
- let sessions = await discoverSessions({
1575
- ...scope,
1576
- limit,
1577
- excludeTeamOrigin: !options.teams,
1578
- onProgress: tracker.onProgress,
1579
- includeUnmanaged: options.unmanaged,
1580
- onHiddenUnmanaged: (n) => { hiddenUnmanaged = n; },
1581
- });
1770
+ const toolSelf = toolEvidenceMode ? machineId() : undefined;
1771
+ const toolIncludesLocal = !toolEvidenceMode
1772
+ || process.env[NO_FANOUT_ENV] === '1'
1773
+ || shouldIncludeLocal(options.host, toolSelf);
1774
+ const indexedIdMatches = toolIncludesLocal && toolEvidenceMode && searchQuery && looksLikeSessionId(searchQuery)
1775
+ ? scopeToManaged(findSessionsById(searchQuery, { agent, version, project: options.project }), agent ? [agent] : SESSION_AGENTS, { agent, includeUnmanaged: options.unmanaged })
1776
+ : [];
1777
+ let sessions;
1778
+ if (!toolIncludesLocal) {
1779
+ sessions = [];
1780
+ }
1781
+ else if (indexedIdMatches.length > 0) {
1782
+ sessions = indexedIdMatches.map((session) => ({
1783
+ ...session,
1784
+ machine: session.machine ?? toolSelf,
1785
+ }));
1786
+ }
1787
+ else {
1788
+ const readOptions = {
1789
+ ...scope,
1790
+ limit,
1791
+ excludeTeamOrigin: !options.teams,
1792
+ onProgress: tracker.onProgress,
1793
+ includeUnmanaged: options.unmanaged,
1794
+ onHiddenUnmanaged: (n) => { hiddenUnmanaged = n; },
1795
+ };
1796
+ sessions = toolEvidenceMode
1797
+ ? await queryIndexedSessions(readOptions, { resolveLinear: false })
1798
+ : await discoverSessions(readOptions);
1799
+ }
1582
1800
  tracker.stop();
1583
1801
  spinner?.stop();
1584
1802
  // Version filter is pushed down to SQL via scope.version above; no
@@ -1601,6 +1819,58 @@ limitSource) {
1601
1819
  const starred = listFavorites();
1602
1820
  sessions = sessions.filter((s) => starred.has(s.id));
1603
1821
  }
1822
+ if (toolEvidenceMode) {
1823
+ const self = toolSelf;
1824
+ const selectedSessions = searchQuery
1825
+ ? filterSessionsByQuery(sessions, searchQuery)
1826
+ : sessions;
1827
+ const localSessions = selectedSessions;
1828
+ const mayFanOut = options.local !== true && process.env[NO_FANOUT_ENV] !== '1';
1829
+ const hosts = remoteHostsToDial(options.host, self);
1830
+ const originOnly = process.env[NO_FANOUT_ENV] === '1'
1831
+ || (mayFanOut && (options.fleet || (options.host?.length ?? 0) > 0));
1832
+ const querySessions = toolOriginSessions(localSessions, self, originOnly);
1833
+ if (countProgram) {
1834
+ const countCoverage = readToolIndexCoverage(querySessions);
1835
+ let countEnvelope = countToolProgramOccurrences(querySessions, countProgram, countCoverage, self);
1836
+ if (!toolIncludesLocal)
1837
+ countEnvelope.machines = [];
1838
+ if (mayFanOut && (options.fleet || (options.host?.length ?? 0) > 0)
1839
+ && (!options.host?.length || (hosts && hosts.length > 0))) {
1840
+ const stripped = toolSearchForwardedArgs(process.argv, options.host ?? []);
1841
+ const remote = await gatherRemoteToolProgramCounts(stripped, options.host?.length ? hosts : undefined, countProgram);
1842
+ countEnvelope = mergeToolProgramCountEnvelopes(countEnvelope, remote.envelopes.map((item) => item.envelope));
1843
+ if (remote.unreachable.length > 0)
1844
+ countEnvelope.coverage.complete = false;
1845
+ }
1846
+ if (options.json)
1847
+ process.stdout.write(JSON.stringify(countEnvelope, null, 2) + '\n');
1848
+ else
1849
+ printToolProgramCount(countEnvelope);
1850
+ return;
1851
+ }
1852
+ const coverage = readToolIndexCoverage(querySessions);
1853
+ let envelope = searchToolCalls(querySessions, queryClauses, coverage, limit);
1854
+ if (mayFanOut && (options.fleet || (options.host?.length ?? 0) > 0)) {
1855
+ if (!options.host?.length || (hosts && hosts.length > 0)) {
1856
+ const stripped = toolSearchForwardedArgs(process.argv, options.host ?? []);
1857
+ const remote = await gatherRemoteToolSearch(stripped, options.host?.length ? hosts : undefined, toolSearchRemoteReceiveBudget(envelope), queryClauses);
1858
+ envelope = mergeToolSearchEnvelopes(envelope, remote.envelopes.map((item) => item.envelope));
1859
+ if (remote.truncated.length > 0 || remote.unreachable.length > 0) {
1860
+ envelope.coverage.complete = false;
1861
+ }
1862
+ }
1863
+ }
1864
+ envelope.sessions = envelope.sessions.slice(0, limit);
1865
+ const serializedEnvelope = serializeToolSearchEnvelope(envelope);
1866
+ if (options.json) {
1867
+ process.stdout.write(serializedEnvelope);
1868
+ }
1869
+ else {
1870
+ printToolSearch(envelope);
1871
+ }
1872
+ return;
1873
+ }
1604
1874
  // Under --in-team the visible list is one team, so the whole-index team-origin
1605
1875
  // count would be a non-sequitur next to it.
1606
1876
  const hiddenCount = options.teams || options.inTeam
@@ -3166,6 +3436,15 @@ export function metadataResolveForwardedArgs(selector, scope) {
3166
3436
  args.push('--project', scope.project);
3167
3437
  return args;
3168
3438
  }
3439
+ /** Build the local-only peer argv for a distributed tool search. */
3440
+ export function toolSearchForwardedArgs(argv, hosts) {
3441
+ const args = ensureWholeIndex(buildForwardedArgs(argv, new Set(hosts)).filter((arg) => arg !== '--fleet'));
3442
+ if (!args.includes('--json'))
3443
+ args.push('--json');
3444
+ if (!args.includes('--local'))
3445
+ args.push('--local');
3446
+ return args;
3447
+ }
3169
3448
  /** Resolution must fail closed when any selected peer did not answer. Choosing
3170
3449
  * from a partial fleet can turn an unseen candidate into a false unique match. */
3171
3450
  export function metadataResolveOutcome(localMatches, remote, selector) {
@@ -3301,7 +3580,7 @@ export function registerSessionsCommands(program) {
3301
3580
  const sessionsCmd = program
3302
3581
  .command('sessions')
3303
3582
  .argument('[query]', 'Session ID, search query, or path (., ../, /path) to filter by project')
3304
- .option('--query <text>', 'Search text — use when the term collides with a subcommand name (e.g. "go")')
3583
+ .option('--query <clause>', 'Search text; repeat with --include tools to require distinct matching calls', collectQueryClause, [])
3305
3584
  .option('--resolve <selector>', 'Resolve one full ID, unique prefix, or keyword query to safe session metadata (requires --json; searches the fleet unless --local)')
3306
3585
  .addOption(new Option('--resolve-safe-v1 <selector>').hideHelp())
3307
3586
  .description('Find, browse, and read agent conversation transcripts across Claude, Codex, Gemini, and OpenCode.')
@@ -3344,6 +3623,8 @@ export function registerSessionsCommands(program) {
3344
3623
  .option('--cloud', 'Source sessions from Rush Cloud (captured runs) instead of local disk')
3345
3624
  .option('-H, --host <target...>', 'Run this query on remote machine(s) over SSH (host alias or user@host; repeatable)')
3346
3625
  .option('--device <target...>', 'Alias for --host (device alias from `agents devices`; repeatable)')
3626
+ .option('--fleet', 'With --include tools: query every registered online compute device and merge compact matches')
3627
+ .option('--count', 'With one program:<name> tool query: count static occurrences, containing calls, and sessions')
3347
3628
  .option('--browser', 'List browser-profile captures (screenshots, PDFs, recordings, downloads) instead of agent transcripts — alias of `agents browser sessions`')
3348
3629
  .option('--no-interactive', 'Print the listing instead of opening the interactive browser (default on a TTY for the bare listing and --active)')
3349
3630
  .option('--print-cmd', 'Print the canonical `ag sessions …` command for the given flags and exit (the twin of the browser’s `y` hotkey)')
@@ -3380,6 +3661,18 @@ export function registerSessionsCommands(program) {
3380
3661
  # Export for analysis
3381
3662
  agents sessions --since 30d --limit 200 --json > sessions.json
3382
3663
 
3664
+ # List indexed tool calls in recent Codex sessions on one device
3665
+ agents sessions --include tools --agent codex --device mac-mini --since 7d
3666
+
3667
+ # Each repeated clause must match a different call in the same session
3668
+ agents sessions --include tools --query 'program:git input:merge' --query 'program:gh output:CONFLICT' --fleet --json
3669
+
3670
+ # Count every pre-indexed static git site without reparsing transcripts
3671
+ agents sessions --include tools --query 'program:git' --count --fleet --json
3672
+
3673
+ # Explicitly populate historical tool rows once on every device
3674
+ agents sessions backfill tools --fleet
3675
+
3383
3676
  # Resolve one historical selector to metadata only, across the fleet
3384
3677
  agents sessions --resolve d3470b57 --json
3385
3678
 
@@ -3394,6 +3687,11 @@ export function registerSessionsCommands(program) {
3394
3687
  - --host runs the query on the remote's own index over SSH (host alias or user@host); repeat or pass several to fan out. SSH access is the only auth.
3395
3688
  - --in-team matches both ends of the lineage: the session that ran 'agents teams create/add', and (with --teams) that team's teammates. In the interactive list, 't' cycles the same filter over the teams in view.
3396
3689
  - --include and --exclude are mutually exclusive.
3690
+ - With --include tools, repeat --query for same-session AND across distinct calls. Fields: tool, program, input, output, status, exit, error.
3691
+ - --count accepts exactly one program:<name> clause and reports static source occurrences, containing tool calls, and sessions.
3692
+ - Tool queries read SQLite only. Run 'agents sessions backfill tools' once for historical transcripts; normal scans index new and changed sessions.
3693
+ - Tool evidence is redacted and bounded before it reaches SQLite. --markdown and --no-redact conflict with --include tools.
3694
+ - Tool queries accept 32 clauses (4 KiB each), --limit 1–1,000, and at most 8 MiB of materialized evidence.
3397
3695
  - --first and --last are mutually exclusive.
3398
3696
  - A filter flag (--include/--exclude/--first/--last) without --markdown/--json defaults to --markdown output.
3399
3697
  - --cloud sources from Rush Cloud captured runs instead of local disk.
@@ -3421,6 +3719,7 @@ export function registerSessionsCommands(program) {
3421
3719
  registerSessionsImportCommand(sessionsCmd);
3422
3720
  registerSessionsMigrateCommand(sessionsCmd);
3423
3721
  registerSessionsMigrationsCommand(sessionsCmd);
3722
+ registerSessionsBackfillCommand(sessionsCmd);
3424
3723
  }
3425
3724
  function formatNoSessionsMessage(showAll, project) {
3426
3725
  const projectQuery = project?.trim();
@@ -1279,7 +1279,7 @@ export function registerTeamsCommands(program) {
1279
1279
  .option('-n, --name <name>', 'Friendly name for this teammate (e.g. alice). Required if using --after. Unique within team.')
1280
1280
  .option('-m, --mode <mode>', `Permissions: plan (read-only) | edit (can write files) | auto (smart classifier auto-approves safe ops) | skip (bypass all permission prompts). 'full' accepted as alias for skip. Teammates run headless: plan works headless on claude/codex/droid/opencode; kimi/grok/cursor/antigravity have no headless plan mode and auto-downgrade a plan request to auto.`, 'edit')
1281
1281
  .option('-e, --effort <effort>', `Reasoning intensity: ${VALID_EFFORTS.join('|')}`, 'medium')
1282
- .option('--model <model>', 'Override the effort tier and use this specific model (e.g. claude-opus-4-6)')
1282
+ .option('--model <model>', 'Cost tier (cheap|default|best|ultra) or a concrete id (e.g. claude-opus-4-8); tiers resolve per harness+version to a supported model')
1283
1283
  .option('--env <key=value>', 'Set an environment variable for this teammate (repeatable for multiple vars)', (val, prev) => [...prev, val], [])
1284
1284
  .option('--cwd <dir>', 'Working directory for this teammate (default: current directory)')
1285
1285
  .option('--worktree <name>', 'Run this teammate in a dedicated git worktree (required when team has --enable-worktrees)')
@@ -11,9 +11,9 @@
11
11
  * agents worktree prune -> removes every clean+merged one
12
12
  *
13
13
  * Worktrees live at <repo>/.agents/worktrees/<terminal-id>, on a branch
14
- * named agents/<terminal-id>. The branch starts at HEAD of the parent repo.
15
- * This matches the agent-system rule that keeps all coding-agent worktrees
16
- * under the repo-local .agents/ state directory.
14
+ * named agents/<terminal-id>. New branches start at the freshly-fetched
15
+ * origin/<default> (not local HEAD), matching lib/teams/worktree.ts and the
16
+ * agent-system rule that keeps coding-agent worktrees under .agents/.
17
17
  */
18
18
  import type { Command } from 'commander';
19
19
  export declare function registerWorktreeCommands(program: Command): void;
@@ -67,6 +67,25 @@ async function inspect(root, terminalId) {
67
67
  }
68
68
  return { exists, dirty, aheadOfUpstream, hasUpstream, branchMerged };
69
69
  }
70
+ /** Resolve origin/HEAD → default branch name; falls back to `main`. */
71
+ async function defaultBranch(root) {
72
+ try {
73
+ await execFileAsync('git', ['remote', 'set-head', 'origin', '--auto'], { cwd: root });
74
+ }
75
+ catch {
76
+ // offline / no origin
77
+ }
78
+ try {
79
+ const { stdout } = await execFileAsync('git', ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD'], { cwd: root });
80
+ const base = stdout.trim().replace(/^origin\//, '');
81
+ if (base)
82
+ return base;
83
+ }
84
+ catch {
85
+ // no origin/HEAD
86
+ }
87
+ return 'main';
88
+ }
70
89
  async function provision(root, terminalId) {
71
90
  const wt = worktreePathFor(root, terminalId);
72
91
  const branch = branchNameFor(terminalId);
@@ -84,10 +103,22 @@ async function provision(root, terminalId) {
84
103
  catch {
85
104
  branchExists = false;
86
105
  }
87
- const args = branchExists
88
- ? ['worktree', 'add', wt, branch]
89
- : ['worktree', 'add', '-b', branch, wt, 'HEAD'];
90
- await execFileAsync('git', args, { cwd: root });
106
+ if (branchExists) {
107
+ await execFileAsync('git', ['worktree', 'add', wt, branch], { cwd: root });
108
+ return wt;
109
+ }
110
+ // New branch: fetch + base on origin/<default>, never local HEAD (stale trap).
111
+ const base = await defaultBranch(root);
112
+ try {
113
+ await execFileAsync('git', ['fetch', 'origin'], { cwd: root });
114
+ }
115
+ catch (err) {
116
+ const detail = (err?.stderr || err?.message || String(err)).toString().trim();
117
+ die(`git fetch origin failed in ${root}` +
118
+ (detail ? `: ${detail}` : '') +
119
+ `. Cannot provision a worktree from a stale remote-tracking ref.`);
120
+ }
121
+ await execFileAsync('git', ['worktree', 'add', '-b', branch, wt, `origin/${base}`], { cwd: root });
91
122
  return wt;
92
123
  }
93
124
  async function release(root, terminalId, force) {
@@ -43,7 +43,11 @@ export declare function writeHeartbeat(pid?: number): void;
43
43
  export declare function readHeartbeat(): DaemonHeartbeat | null;
44
44
  export declare function removeHeartbeat(): void;
45
45
  export declare function isDaemonWedged(): boolean;
46
- /** Check if the daemon process is alive by sending signal 0 to the stored PID. */
46
+ /**
47
+ * Check whether a daemon is alive — via the pid file, or a fresh heartbeat when
48
+ * the pid file has been lost (see resolveLiveDaemonPid). Heals the pid file as a
49
+ * side effect so a subsequent read is consistent.
50
+ */
47
51
  export declare function isDaemonRunning(): boolean;
48
52
  /**
49
53
  * Single-instance claim for the daemon foreground entrypoint.