@phnx-labs/agents-cli 1.22.26 → 1.22.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +293 -0
- package/README.md +80 -27
- package/dist/bin/agents +0 -0
- package/dist/commands/bench.d.ts +2 -0
- package/dist/commands/bench.js +101 -0
- package/dist/commands/doctor.js +24 -10
- package/dist/commands/exec.js +71 -26
- package/dist/commands/hosts.js +4 -3
- package/dist/commands/insights.js +39 -5
- package/dist/commands/output.js +100 -25
- package/dist/commands/perf.d.ts +10 -0
- package/dist/commands/perf.js +14 -6
- package/dist/commands/projects.js +24 -10
- package/dist/commands/resume.d.ts +11 -0
- package/dist/commands/resume.js +51 -0
- package/dist/commands/routines.js +31 -4
- package/dist/commands/run-account-picker.d.ts +37 -0
- package/dist/commands/run-account-picker.js +101 -17
- package/dist/commands/secrets.js +86 -2
- package/dist/commands/sessions-picker.js +12 -7
- package/dist/commands/sessions.d.ts +73 -7
- package/dist/commands/sessions.js +391 -49
- package/dist/commands/ssh.js +184 -11
- package/dist/commands/teams.js +22 -11
- package/dist/index.js +16 -20
- package/dist/lib/activity.js +45 -99
- package/dist/lib/auth-health.d.ts +47 -3
- package/dist/lib/auth-health.js +74 -15
- package/dist/lib/bench/index.d.ts +4 -0
- package/dist/lib/bench/index.js +4 -0
- package/dist/lib/bench/runner.d.ts +16 -0
- package/dist/lib/bench/runner.js +111 -0
- package/dist/lib/bench/schema.d.ts +5 -0
- package/dist/lib/bench/schema.js +91 -0
- package/dist/lib/bench/storage.d.ts +5 -0
- package/dist/lib/bench/storage.js +32 -0
- package/dist/lib/bench/types.d.ts +40 -0
- package/dist/lib/bench/types.js +1 -0
- package/dist/lib/claude-account-token.d.ts +2 -0
- package/dist/lib/claude-account-token.js +56 -3
- package/dist/lib/cloud/host.js +1 -0
- package/dist/lib/crabbox/cli.d.ts +2 -0
- package/dist/lib/crabbox/cli.js +2 -0
- package/dist/lib/crabbox/lease.js +7 -1
- package/dist/lib/daemon.js +20 -0
- package/dist/lib/devices/connect.d.ts +2 -0
- package/dist/lib/devices/connect.js +7 -0
- package/dist/lib/devices/doctor-findings.d.ts +4 -1
- package/dist/lib/devices/doctor-findings.js +15 -1
- package/dist/lib/devices/harness-inventory.d.ts +97 -0
- package/dist/lib/devices/harness-inventory.js +0 -0
- package/dist/lib/devices/registry.d.ts +2 -0
- package/dist/lib/devices/resolve-target.d.ts +1 -0
- package/dist/lib/devices/resolve-target.js +9 -2
- package/dist/lib/devices/ssh-config.js +3 -0
- package/dist/lib/devices/windows-ssh-enrollment.d.ts +20 -0
- package/dist/lib/devices/windows-ssh-enrollment.js +98 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/events.js +2 -2
- package/dist/lib/exec.js +18 -8
- package/dist/lib/fleet/apply.js +9 -7
- package/dist/lib/fleet/remote-login.d.ts +4 -3
- package/dist/lib/fleet/remote-login.js +11 -9
- package/dist/lib/gemini-settings.d.ts +0 -1
- package/dist/lib/gemini-settings.js +12 -7
- package/dist/lib/git.d.ts +9 -3
- package/dist/lib/git.js +39 -22
- package/dist/lib/hooks/profile.d.ts +12 -1
- package/dist/lib/hooks/profile.js +6 -1
- package/dist/lib/hooks.d.ts +8 -5
- package/dist/lib/hooks.js +14 -7
- package/dist/lib/hosts/dispatch.d.ts +10 -0
- package/dist/lib/hosts/dispatch.js +85 -25
- package/dist/lib/hosts/logs.js +11 -1
- package/dist/lib/hosts/passthrough.d.ts +27 -0
- package/dist/lib/hosts/passthrough.js +19 -10
- package/dist/lib/hosts/progress.d.ts +12 -1
- package/dist/lib/hosts/progress.js +41 -8
- package/dist/lib/hosts/providers/devices.js +1 -0
- package/dist/lib/hosts/ready.d.ts +45 -4
- package/dist/lib/hosts/ready.js +124 -20
- package/dist/lib/hosts/reconcile.d.ts +2 -1
- package/dist/lib/hosts/reconcile.js +24 -7
- package/dist/lib/hosts/reconnect.d.ts +45 -12
- package/dist/lib/hosts/reconnect.js +94 -36
- package/dist/lib/hosts/registry.d.ts +2 -2
- package/dist/lib/hosts/registry.js +4 -5
- package/dist/lib/hosts/remote-cmd.d.ts +17 -0
- package/dist/lib/hosts/remote-cmd.js +29 -0
- package/dist/lib/hosts/tasks.d.ts +3 -0
- package/dist/lib/hosts/types.d.ts +1 -0
- package/dist/lib/hosts/types.js +3 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +9 -6
- package/dist/lib/menubar/install-menubar.js +20 -9
- package/dist/lib/menubar/snapshot.js +15 -2
- package/dist/lib/models.d.ts +42 -6
- package/dist/lib/models.js +55 -153
- package/dist/lib/perf/db.js +16 -3
- package/dist/lib/perf/types.d.ts +12 -1
- package/dist/lib/pricing/cost.d.ts +9 -0
- package/dist/lib/pricing/cost.js +24 -0
- package/dist/lib/pricing/index.d.ts +1 -1
- package/dist/lib/pricing/index.js +1 -1
- package/dist/lib/project-probe.d.ts +22 -3
- package/dist/lib/project-probe.js +105 -17
- package/dist/lib/project-status.d.ts +9 -0
- package/dist/lib/project-status.js +15 -0
- package/dist/lib/redact.js +8 -3
- package/dist/lib/refresh.js +22 -11
- package/dist/lib/remote-agents-json.d.ts +32 -0
- package/dist/lib/remote-agents-json.js +47 -16
- package/dist/lib/resource-profiles.js +1 -2
- package/dist/lib/rotate.d.ts +22 -0
- package/dist/lib/rotate.js +26 -0
- package/dist/lib/routine-notify-owner.d.ts +102 -0
- package/dist/lib/routine-notify-owner.js +232 -0
- package/dist/lib/routines.d.ts +10 -0
- package/dist/lib/runner.d.ts +9 -1
- package/dist/lib/runner.js +182 -17
- package/dist/lib/sandbox.d.ts +0 -2
- package/dist/lib/sandbox.js +2 -19
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.d.ts +19 -3
- package/dist/lib/secrets/agent.js +61 -22
- package/dist/lib/secrets/audit.d.ts +1 -1
- package/dist/lib/secrets/audit.js +2 -0
- package/dist/lib/secrets/bundles.d.ts +3 -3
- package/dist/lib/secrets/bundles.js +15 -5
- package/dist/lib/secrets/filestore.d.ts +2 -0
- package/dist/lib/secrets/filestore.js +4 -0
- package/dist/lib/secrets/lease.d.ts +25 -0
- package/dist/lib/secrets/lease.js +44 -0
- package/dist/lib/secrets/session-store.d.ts +7 -0
- package/dist/lib/secrets/session-store.js +21 -0
- package/dist/lib/self-update.d.ts +20 -5
- package/dist/lib/self-update.js +93 -16
- package/dist/lib/session/active.d.ts +49 -0
- package/dist/lib/session/active.js +175 -11
- package/dist/lib/session/bash-command.d.ts +16 -0
- package/dist/lib/session/bash-command.js +65 -0
- package/dist/lib/session/db.d.ts +23 -2
- package/dist/lib/session/db.js +86 -20
- package/dist/lib/session/discover.d.ts +21 -1
- package/dist/lib/session/discover.js +124 -32
- package/dist/lib/session/insights.d.ts +19 -0
- package/dist/lib/session/insights.js +78 -10
- package/dist/lib/session/recovery.d.ts +17 -1
- package/dist/lib/session/recovery.js +111 -4
- package/dist/lib/session/remote-bundle.js +1 -1
- package/dist/lib/session/remote-list.d.ts +10 -9
- package/dist/lib/session/remote-list.js +25 -23
- package/dist/lib/session/resume-owner.d.ts +55 -0
- package/dist/lib/session/resume-owner.js +69 -0
- package/dist/lib/session/team-filter.d.ts +65 -0
- package/dist/lib/session/team-filter.js +98 -3
- package/dist/lib/session/types.d.ts +33 -0
- package/dist/lib/smart-launch.d.ts +31 -5
- package/dist/lib/smart-launch.js +43 -7
- package/dist/lib/ssh-exec.d.ts +27 -0
- package/dist/lib/ssh-exec.js +34 -1
- package/dist/lib/ssh-tunnel.d.ts +3 -2
- package/dist/lib/ssh-tunnel.js +25 -16
- package/dist/lib/staleness/detectors/skills.d.ts +2 -0
- package/dist/lib/staleness/detectors/skills.js +17 -2
- package/dist/lib/staleness/index.d.ts +10 -1
- package/dist/lib/staleness/index.js +28 -3
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/teams/agents.d.ts +1 -0
- package/dist/lib/teams/agents.js +38 -11
- package/dist/lib/teams/placement-probe.d.ts +1 -1
- package/dist/lib/teams/placement-probe.js +24 -24
- package/dist/lib/teams/remoteWorktree.d.ts +11 -7
- package/dist/lib/teams/remoteWorktree.js +29 -27
- package/dist/lib/tmux/session.d.ts +8 -0
- package/dist/lib/tmux/session.js +22 -0
- package/dist/lib/types.d.ts +2 -0
- package/dist/lib/versions.d.ts +1 -0
- package/dist/lib/versions.js +40 -22
- package/package.json +3 -1
|
@@ -33,16 +33,18 @@ import { stringWidth, truncateToWidth, padToWidth, terminalWidth } from '../lib/
|
|
|
33
33
|
import { inferSessionState } from '../lib/session/state.js';
|
|
34
34
|
import { discoverSessions, queryIndexedSessions, countSessionsInScope, resolveSessionById, isCompleteSessionId, looksLikeSessionId, searchContentIndex, getSessionRoots, scopeToManaged } from '../lib/session/discover.js';
|
|
35
35
|
import { findSessionsById, querySessions, getSessionById } from '../lib/session/db.js';
|
|
36
|
-
import { filterTeamSessions, safeTeamText } from '../lib/session/team-filter.js';
|
|
36
|
+
import { filterTeamSessions, safeTeamText, groupSessionsByTeam, NO_TEAM_GROUP_KEY, } from '../lib/session/team-filter.js';
|
|
37
37
|
import { parseSession } from '../lib/session/parse.js';
|
|
38
38
|
import { runRemoteSessions, buildForwardedArgs, ensureWholeIndex } from '../lib/session/remote.js';
|
|
39
39
|
import { formatRelativeTime, formatCompactAge, sessionAgeParts } from '../lib/session/relative-time.js';
|
|
40
40
|
import { renderConversationMarkdown, renderSummary, renderSummaryHeader, computeSummaryStats, renderJson, filterEvents, parseRoleList, linkPath, linkUrl, shortenModel } from '../lib/session/render.js';
|
|
41
41
|
import { linearIssueUrl } from '../lib/session/linear.js';
|
|
42
|
+
import { sessionOwnerDevice, RESUME_PINNED_ENV } from '../lib/session/resume-owner.js';
|
|
42
43
|
import { renderMarkdown } from '../lib/markdown.js';
|
|
43
44
|
import { AGENTS, colorAgent, resolveAgentName } from '../lib/agents.js';
|
|
44
45
|
import { getShimsDir } from '../lib/state.js';
|
|
45
46
|
import { fuzzyMatch, FUZZY_PRESETS } from '../lib/fuzzy.js';
|
|
47
|
+
import { itemPicker } from '../lib/picker.js';
|
|
46
48
|
import { resolveSessionAlias } from '../lib/session/actor-sidecar.js';
|
|
47
49
|
import { resolveVersionAliasLoose } from '../lib/versions.js';
|
|
48
50
|
import { getAgentsInvocation } from '../lib/daemon.js';
|
|
@@ -340,10 +342,9 @@ export function formatActiveRowDescription(s) {
|
|
|
340
342
|
if (todo)
|
|
341
343
|
parts.push(todo);
|
|
342
344
|
// Latest-turn snippet — avoid repeating label/topic when already used as identity.
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
else if (s.preview) {
|
|
345
|
+
// A cloud row with no preview yet says nothing here; its provider + task id
|
|
346
|
+
// ride the locator badge instead (RUSH-2336), not duplicated on this line.
|
|
347
|
+
if (s.preview) {
|
|
347
348
|
pushText(s.preview);
|
|
348
349
|
}
|
|
349
350
|
else if (!s.label && s.topic) {
|
|
@@ -538,18 +539,40 @@ export function isAwaitingUser(s) {
|
|
|
538
539
|
return s.status === 'input_required' || s.activity === 'waiting_input';
|
|
539
540
|
}
|
|
540
541
|
/**
|
|
541
|
-
* Whether a row belongs in an unqualified `--active` view
|
|
542
|
+
* Whether a row belongs in an unqualified `--active` view (RUSH-2336) — the
|
|
543
|
+
* ONE canonical selector every bare-active surface shares: the CLI's grouped
|
|
544
|
+
* table and `--json` (`renderActiveSessions`), the interactive browser's
|
|
545
|
+
* `--active` filter ({@link applyFilters} in `sessions-browser.ts`), `focus`'s
|
|
546
|
+
* attach gate (`isAttachableLiveSession`), and the menubar snapshot
|
|
547
|
+
* ({@link computeMenubarSnapshot}).
|
|
542
548
|
*
|
|
543
|
-
* The live registry deliberately retains terminally-dead
|
|
544
|
-
* `--closed` / `--crashed` and recovery to
|
|
545
|
-
* therefore is not, by itself, evidence
|
|
546
|
-
* Explicit lifecycle filters bypass this
|
|
547
|
-
* rows through {@link matchesLiveStatus}
|
|
549
|
+
* The live registry deliberately retains terminally-dead AND not-yet-started
|
|
550
|
+
* rows long enough for `--closed` / `--crashed` / `--queued` and recovery to
|
|
551
|
+
* find them. Presence in that registry therefore is not, by itself, evidence
|
|
552
|
+
* that a session is CURRENTLY active. Explicit lifecycle filters bypass this
|
|
553
|
+
* predicate and select those retained rows through {@link matchesLiveStatus}
|
|
554
|
+
* instead — this function only decides the unqualified default.
|
|
555
|
+
*
|
|
556
|
+
* - `queued` / `closed` / `crashed` are never active here: queued hasn't
|
|
557
|
+
* started yet, closed/crashed are unconditionally dead. All three stay
|
|
558
|
+
* reachable through their explicit filter.
|
|
559
|
+
* - A cloud row carries no local pid at all — it is active on the
|
|
560
|
+
* provider's own word (`cloudProvider` + `cloudTaskId`), never a
|
|
561
|
+
* fabricated pid.
|
|
562
|
+
* - Every other row is a real OS process (terminal/tmux/headless/team). It
|
|
563
|
+
* is active only when the row names the machine it runs on, carries a
|
|
564
|
+
* positive pid, AND has POSITIVELY verified that pid is alive
|
|
565
|
+
* (`pidAlive === true` — not merely "not known dead"). A live orphaned
|
|
566
|
+
* row, or a live-but-stuck abandoned row, still qualifies this way since
|
|
567
|
+
* both carry a genuinely alive pid; unknown liveness (an older peer's
|
|
568
|
+
* row, or a pid that could never be resolved) does not.
|
|
548
569
|
*/
|
|
549
570
|
export function isRunningLiveSession(s) {
|
|
550
|
-
if (s.status === 'closed' || s.status === 'crashed')
|
|
571
|
+
if (s.status === 'queued' || s.status === 'closed' || s.status === 'crashed')
|
|
551
572
|
return false;
|
|
552
|
-
|
|
573
|
+
if (s.context === 'cloud')
|
|
574
|
+
return Boolean(s.cloudProvider) && Boolean(s.cloudTaskId);
|
|
575
|
+
return Boolean(s.machine) && typeof s.pid === 'number' && s.pid > 0 && s.pidAlive === true;
|
|
553
576
|
}
|
|
554
577
|
/** Width of the live status column — `crashed` is the longest word it renders. */
|
|
555
578
|
const LIVE_STATUS_W = 8;
|
|
@@ -638,6 +661,11 @@ function signalBadges(s) {
|
|
|
638
661
|
* (a real `tmux attach -t <session:window>` target) over the raw `%pane` id. For
|
|
639
662
|
* a local Ghostty session we know the tab, show `tab N`. Local, unlocatable
|
|
640
663
|
* sessions add nothing (the common case).
|
|
664
|
+
*
|
|
665
|
+
* RUSH-2336: every row also carries its raw process/provider handle — a
|
|
666
|
+
* `machine:pid` for a real OS process (terminal/tmux/headless/team), or
|
|
667
|
+
* `provider · taskId` for a cloud row with no local pid — so the human view
|
|
668
|
+
* exposes the same locator `--json` now guarantees on every active row.
|
|
641
669
|
*/
|
|
642
670
|
function locatorBadge(s) {
|
|
643
671
|
const p = s.provenance;
|
|
@@ -660,6 +688,14 @@ function locatorBadge(s) {
|
|
|
660
688
|
}
|
|
661
689
|
if (s.ghosttyTab != null)
|
|
662
690
|
parts.push(chalk.green(`tab ${s.ghosttyTab}`));
|
|
691
|
+
if (s.context === 'cloud') {
|
|
692
|
+
const bits = [s.cloudProvider, s.cloudTaskId ? s.cloudTaskId.slice(0, 12) : undefined].filter(Boolean);
|
|
693
|
+
if (bits.length)
|
|
694
|
+
parts.push(chalk.dim(bits.join(' · ')));
|
|
695
|
+
}
|
|
696
|
+
else if (typeof s.pid === 'number' && s.pid > 0) {
|
|
697
|
+
parts.push(chalk.dim(`${s.machine ? `${s.machine}:` : ''}pid ${s.pid}`));
|
|
698
|
+
}
|
|
663
699
|
return parts.join(' ');
|
|
664
700
|
}
|
|
665
701
|
/**
|
|
@@ -1442,6 +1478,96 @@ export function hasNoBrowserDisqualifyingFlags(options, query) {
|
|
|
1442
1478
|
// legacy per-host stream, which prints each peer under its own banner.
|
|
1443
1479
|
(options.host?.length ?? 0) <= 1);
|
|
1444
1480
|
}
|
|
1481
|
+
/** Resolve a typed routine selector by exact name, substring, or one unambiguous typo. */
|
|
1482
|
+
export function resolveRoutineName(query, names) {
|
|
1483
|
+
const normalized = query.trim().toLowerCase();
|
|
1484
|
+
if (!normalized)
|
|
1485
|
+
return null;
|
|
1486
|
+
const exact = names.find((name) => name.toLowerCase() === normalized);
|
|
1487
|
+
if (exact)
|
|
1488
|
+
return exact;
|
|
1489
|
+
const containing = names.filter((name) => name.toLowerCase().includes(normalized));
|
|
1490
|
+
if (containing.length === 1)
|
|
1491
|
+
return containing[0];
|
|
1492
|
+
return fuzzyMatch(query, names, FUZZY_PRESETS.dynamic);
|
|
1493
|
+
}
|
|
1494
|
+
export function buildRoutineRunGroups(sessions) {
|
|
1495
|
+
const byRun = new Map();
|
|
1496
|
+
for (const session of sessions) {
|
|
1497
|
+
const runId = session.routineRunId ?? session.timestamp;
|
|
1498
|
+
(byRun.get(runId) ?? byRun.set(runId, []).get(runId)).push(session);
|
|
1499
|
+
}
|
|
1500
|
+
return [...byRun.entries()]
|
|
1501
|
+
.map(([runId, rows]) => ({
|
|
1502
|
+
runId,
|
|
1503
|
+
timestamp: rows.reduce((latest, row) => (row.lastActivity ?? row.timestamp) > latest ? (row.lastActivity ?? row.timestamp) : latest, rows[0].lastActivity ?? rows[0].timestamp),
|
|
1504
|
+
sessions: rows.sort((a, b) => (a.timestamp < b.timestamp ? 1 : -1)),
|
|
1505
|
+
}))
|
|
1506
|
+
.sort((a, b) => (a.timestamp < b.timestamp ? 1 : a.timestamp > b.timestamp ? -1 : a.runId.localeCompare(b.runId)));
|
|
1507
|
+
}
|
|
1508
|
+
export function buildRoutineChoices(sessions) {
|
|
1509
|
+
const byName = new Map();
|
|
1510
|
+
for (const session of sessions) {
|
|
1511
|
+
if (!session.routineName)
|
|
1512
|
+
continue;
|
|
1513
|
+
(byName.get(session.routineName) ?? byName.set(session.routineName, []).get(session.routineName)).push(session);
|
|
1514
|
+
}
|
|
1515
|
+
return [...byName.entries()]
|
|
1516
|
+
.map(([name, rows]) => {
|
|
1517
|
+
const runs = buildRoutineRunGroups(rows);
|
|
1518
|
+
return {
|
|
1519
|
+
name,
|
|
1520
|
+
lastRunAt: runs[0].timestamp,
|
|
1521
|
+
runCount: runs.length,
|
|
1522
|
+
latestRunSessionCount: runs[0].sessions.length,
|
|
1523
|
+
};
|
|
1524
|
+
})
|
|
1525
|
+
.sort((a, b) => (a.lastRunAt < b.lastRunAt ? 1 : a.lastRunAt > b.lastRunAt ? -1 : a.name.localeCompare(b.name)));
|
|
1526
|
+
}
|
|
1527
|
+
async function selectRoutineName(sessions) {
|
|
1528
|
+
const choices = buildRoutineChoices(sessions);
|
|
1529
|
+
const picked = await itemPicker({
|
|
1530
|
+
message: 'Select a routine:',
|
|
1531
|
+
items: choices,
|
|
1532
|
+
filter: (query) => {
|
|
1533
|
+
const normalized = query.trim().toLowerCase();
|
|
1534
|
+
if (!normalized)
|
|
1535
|
+
return choices;
|
|
1536
|
+
return choices.filter((choice) => choice.name.toLowerCase().includes(normalized));
|
|
1537
|
+
},
|
|
1538
|
+
labelFor: (choice) => {
|
|
1539
|
+
const runs = `${choice.runCount} run${choice.runCount === 1 ? '' : 's'}`;
|
|
1540
|
+
const sessions = `${choice.latestRunSessionCount} session${choice.latestRunSessionCount === 1 ? '' : 's'} in latest`;
|
|
1541
|
+
return `${choice.name} ${chalk.gray(`${runs} · ${sessions} · ${formatRelativeTime(choice.lastRunAt)}`)}`;
|
|
1542
|
+
},
|
|
1543
|
+
shortIdFor: (choice) => choice.name,
|
|
1544
|
+
emptyMessage: 'No routines match.',
|
|
1545
|
+
enterHint: 'show sessions',
|
|
1546
|
+
});
|
|
1547
|
+
return picked?.item.name ?? null;
|
|
1548
|
+
}
|
|
1549
|
+
export async function filterSessionsByRoutine(sessions, routine, interactive) {
|
|
1550
|
+
const routineNames = [...new Set(sessions.map((session) => session.routineName).filter((name) => !!name))].sort((a, b) => a.localeCompare(b));
|
|
1551
|
+
let selectedRoutine = null;
|
|
1552
|
+
if (typeof routine === 'string') {
|
|
1553
|
+
selectedRoutine = resolveRoutineName(routine, routineNames);
|
|
1554
|
+
if (!selectedRoutine) {
|
|
1555
|
+
console.error(chalk.red(`No routine matches "${routine}".`));
|
|
1556
|
+
if (routineNames.length > 0)
|
|
1557
|
+
console.error(chalk.gray(`Available routines: ${routineNames.join(', ')}`));
|
|
1558
|
+
process.exitCode = 1;
|
|
1559
|
+
return null;
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
else if (interactive) {
|
|
1563
|
+
selectedRoutine = await selectRoutineName(sessions);
|
|
1564
|
+
if (!selectedRoutine)
|
|
1565
|
+
return null;
|
|
1566
|
+
}
|
|
1567
|
+
return selectedRoutine
|
|
1568
|
+
? sessions.filter((session) => session.routineName === selectedRoutine)
|
|
1569
|
+
: sessions;
|
|
1570
|
+
}
|
|
1445
1571
|
/** The canonical `ag sessions …` command for a set of flags — the twin of the
|
|
1446
1572
|
* browser's `y` hotkey (see --print-cmd). Normalizes to the stable flag form. */
|
|
1447
1573
|
function canonicalSessionsCommand(query, options) {
|
|
@@ -1468,8 +1594,11 @@ function canonicalSessionsCommand(query, options) {
|
|
|
1468
1594
|
a.push('--teams');
|
|
1469
1595
|
if (options.inTeam)
|
|
1470
1596
|
a.push('--in-team', options.inTeam);
|
|
1471
|
-
if (options.routine)
|
|
1597
|
+
if (options.routine) {
|
|
1472
1598
|
a.push('--routine');
|
|
1599
|
+
if (typeof options.routine === 'string')
|
|
1600
|
+
a.push(options.routine);
|
|
1601
|
+
}
|
|
1473
1602
|
if (options.agent)
|
|
1474
1603
|
a.push('-a', options.agent);
|
|
1475
1604
|
for (const h of options.host ?? [])
|
|
@@ -1910,7 +2039,13 @@ limitSource) {
|
|
|
1910
2039
|
// filter the browser can't represent), or --no-interactive keep the existing
|
|
1911
2040
|
// printed/render paths (agents and scripts unaffected). An explicit --since seeds
|
|
1912
2041
|
// the browser's window so the flag is honored, not swallowed.
|
|
1913
|
-
|
|
2042
|
+
//
|
|
2043
|
+
// `--teams` diverts to the printed team-grouped report (printTeamsView, below):
|
|
2044
|
+
// grouping teammates under their team is a shape the flat fuzzy picker cannot
|
|
2045
|
+
// represent, so like --tree it is a printed listing, not an interactive pick.
|
|
2046
|
+
// --teams --flat/--tree keep their inline table rendering; a search query keeps
|
|
2047
|
+
// the picker so `<term> --teams` still searches.
|
|
2048
|
+
if (isBareBrowserListing(options, query) && !options.teams) {
|
|
1914
2049
|
const { runSessionBrowser, bareBrowserSeed } = await import('./sessions-browser.js');
|
|
1915
2050
|
await runSessionBrowser(bareBrowserSeed({
|
|
1916
2051
|
teams: options.teams,
|
|
@@ -1957,13 +2092,13 @@ limitSource) {
|
|
|
1957
2092
|
const wantsRender = !toolEvidenceMode && (mode === 'markdown' || hasAnyFilter(filterOpts));
|
|
1958
2093
|
// Artifact-list or artifact-read paths: widen scope and resolve session globally.
|
|
1959
2094
|
if ((options.artifacts || options.artifact !== undefined) && searchQuery) {
|
|
1960
|
-
await renderArtifactsGlobal(searchQuery, options.artifacts ?? false, options.artifact,
|
|
2095
|
+
await renderArtifactsGlobal(searchQuery, options.artifacts ?? false, options.artifact, artifactLookupScope(options.agent, options.project, options.routine));
|
|
1961
2096
|
return;
|
|
1962
2097
|
}
|
|
1963
2098
|
// When the user explicitly asks to render (via mode flag), resolve the
|
|
1964
2099
|
// query globally so sessions outside the default cwd/30d window are found.
|
|
1965
2100
|
if (wantsRender && searchQuery) {
|
|
1966
|
-
await renderOneSession(searchQuery, mode, { agent: options.agent, project: options.project, filter: filterOpts, redact: options.redact, local: options.local, hosts: options.host });
|
|
2101
|
+
await renderOneSession(searchQuery, mode, { agent: options.agent, project: options.project, routine: options.routine, filter: filterOpts, redact: options.redact, local: options.local, hosts: options.host });
|
|
1967
2102
|
return;
|
|
1968
2103
|
}
|
|
1969
2104
|
// Interactive picker loads a deep pool but shows only recent sessions
|
|
@@ -2146,6 +2281,14 @@ limitSource) {
|
|
|
2146
2281
|
const hiddenCount = options.teams || options.inTeam
|
|
2147
2282
|
? 0
|
|
2148
2283
|
: countSessionsInScope({ ...scope, onlyTeamOrigin: true });
|
|
2284
|
+
// A typed routine scope must apply before smart ID routing. Otherwise an ID
|
|
2285
|
+
// from another routine resolves and renders before the routine filter below.
|
|
2286
|
+
if (typeof options.routine === 'string') {
|
|
2287
|
+
const filteredByRoutine = await filterSessionsByRoutine(sessions, options.routine, false);
|
|
2288
|
+
if (!filteredByRoutine)
|
|
2289
|
+
return;
|
|
2290
|
+
sessions = filteredByRoutine;
|
|
2291
|
+
}
|
|
2149
2292
|
// Smart ID routing: a bare query that resolves to one session renders
|
|
2150
2293
|
// directly. If nothing matches in the scoped window and the query looks
|
|
2151
2294
|
// like a session ID, widen to global scope (incl. Claude /resume history).
|
|
@@ -2163,11 +2306,17 @@ limitSource) {
|
|
|
2163
2306
|
return;
|
|
2164
2307
|
}
|
|
2165
2308
|
if (idMatches.length === 0 && looksLikeSessionId(searchQuery)) {
|
|
2166
|
-
await renderOneSession(searchQuery, mode, { agent: options.agent, project: options.project, filter: filterOpts, redact: options.redact, local: options.local, hosts: options.host });
|
|
2309
|
+
await renderOneSession(searchQuery, mode, { agent: options.agent, project: options.project, routine: options.routine, filter: filterOpts, redact: options.redact, local: options.local, hosts: options.host });
|
|
2167
2310
|
return;
|
|
2168
2311
|
}
|
|
2169
2312
|
}
|
|
2170
2313
|
if (options.json) {
|
|
2314
|
+
if (options.routine === true) {
|
|
2315
|
+
const filteredByRoutine = await filterSessionsByRoutine(sessions, options.routine, false);
|
|
2316
|
+
if (!filteredByRoutine)
|
|
2317
|
+
return;
|
|
2318
|
+
sessions = filteredByRoutine;
|
|
2319
|
+
}
|
|
2171
2320
|
// An id-shaped query resolves by id ONLY — the same rule the render path
|
|
2172
2321
|
// uses (resolveSessionQuery). Without this a `--json <uuid>` (as issued by
|
|
2173
2322
|
// the fleet locate sweep, which runs `sessions <uuid> --json --local` on
|
|
@@ -2210,6 +2359,12 @@ limitSource) {
|
|
|
2210
2359
|
fanSpinner?.stop();
|
|
2211
2360
|
}
|
|
2212
2361
|
}
|
|
2362
|
+
if (options.routine) {
|
|
2363
|
+
const filteredByRoutine = await filterSessionsByRoutine(sessions, options.routine, isInteractive);
|
|
2364
|
+
if (!filteredByRoutine)
|
|
2365
|
+
return;
|
|
2366
|
+
sessions = filteredByRoutine;
|
|
2367
|
+
}
|
|
2213
2368
|
if (sessions.length === 0) {
|
|
2214
2369
|
if (pathFilter) {
|
|
2215
2370
|
console.log(chalk.gray(`No sessions found for ${pathFilter}.`));
|
|
@@ -2225,11 +2380,24 @@ limitSource) {
|
|
|
2225
2380
|
}
|
|
2226
2381
|
return;
|
|
2227
2382
|
}
|
|
2383
|
+
// `--teams` prints the team-grouped report: teammates under their team (with
|
|
2384
|
+
// spawner + spawn time), bare SDK spawns in their own bucket. --flat/--tree
|
|
2385
|
+
// keep their inline table rendering; a search query keeps the picker. Placed
|
|
2386
|
+
// before the project overview and picker so it wins the bare `--teams` case.
|
|
2387
|
+
if (options.teams && !options.flat && !options.tree && !searchQuery && !pathFilter) {
|
|
2388
|
+
const liveIndex = await maybeLiveIndex(options);
|
|
2389
|
+
printTeamsView(sessions, liveIndex, hiddenUnmanaged);
|
|
2390
|
+
return;
|
|
2391
|
+
}
|
|
2228
2392
|
// The grouped project overview is the bare interactive default: a scannable
|
|
2229
2393
|
// dashboard of the whole fleet grouped by project, newest-active first.
|
|
2230
2394
|
// Interact/resume via `agents sessions <project>` or `agents sessions resume`.
|
|
2231
2395
|
if (wantsOverview) {
|
|
2232
2396
|
const liveIndex = await maybeLiveIndex(options);
|
|
2397
|
+
if (options.routine) {
|
|
2398
|
+
printRoutineRunOverview(sessions, liveIndex, { hiddenCount, hiddenUnmanaged });
|
|
2399
|
+
return;
|
|
2400
|
+
}
|
|
2233
2401
|
// Per-project row cap is fixed (--limit carries a default of 50 and drives
|
|
2234
2402
|
// the fetch pool, not the display); `--all` expands every group instead.
|
|
2235
2403
|
printSessionOverview(sessions, hiddenCount, liveIndex, { perProjectCap: OVERVIEW_ROWS_PER_PROJECT, expand: !!options.all, hiddenUnmanaged });
|
|
@@ -2553,6 +2721,25 @@ function printSessionOverview(pool, hiddenCount, liveIndex, opts) {
|
|
|
2553
2721
|
if (opts.hiddenUnmanaged)
|
|
2554
2722
|
console.log(chalk.gray(formatUnmanagedHiddenFooter(opts.hiddenUnmanaged)));
|
|
2555
2723
|
}
|
|
2724
|
+
export function printRoutineRunOverview(sessions, liveIndex, opts = {}) {
|
|
2725
|
+
const groups = buildRoutineRunGroups(sessions);
|
|
2726
|
+
console.log(chalk.gray(`${sessions.length} session${sessions.length === 1 ? '' : 's'} · ${groups.length} routine run${groups.length === 1 ? '' : 's'} · newest run first\n`));
|
|
2727
|
+
let first = true;
|
|
2728
|
+
for (const group of groups) {
|
|
2729
|
+
if (!first)
|
|
2730
|
+
console.log();
|
|
2731
|
+
first = false;
|
|
2732
|
+
console.log(`${chalk.cyan('▸')} ${chalk.cyan.bold(group.runId)} ` +
|
|
2733
|
+
`${chalk.gray(`${group.sessions.length} session${group.sessions.length === 1 ? '' : 's'} · ${formatRelativeTime(group.timestamp)}`)}`);
|
|
2734
|
+
for (const session of group.sessions)
|
|
2735
|
+
console.log(treeSessionRow(session, liveIndex?.get(session.id)));
|
|
2736
|
+
}
|
|
2737
|
+
console.log(chalk.gray('\nGrouped by routine run id and timestamp.'));
|
|
2738
|
+
if (opts.hiddenCount)
|
|
2739
|
+
console.log(chalk.gray(formatTeamHiddenFooter(opts.hiddenCount)));
|
|
2740
|
+
if (opts.hiddenUnmanaged)
|
|
2741
|
+
console.log(chalk.gray(formatUnmanagedHiddenFooter(opts.hiddenUnmanaged)));
|
|
2742
|
+
}
|
|
2556
2743
|
function printSessionTable(sessions, hiddenCount = 0, tree = false, liveIndex) {
|
|
2557
2744
|
if (tree) {
|
|
2558
2745
|
// Group by directory; drop the id/version columns from view. The short id
|
|
@@ -2599,6 +2786,118 @@ function printSessionTable(sessions, hiddenCount = 0, tree = false, liveIndex) {
|
|
|
2599
2786
|
console.log(chalk.gray(formatTeamHiddenFooter(hiddenCount)));
|
|
2600
2787
|
}
|
|
2601
2788
|
}
|
|
2789
|
+
/** Width of the mode cell (plan/edit/auto/skip) in a team-member row. */
|
|
2790
|
+
const TEAM_MODE_W = 5;
|
|
2791
|
+
/** Longest teammate handle folded into a team-member row before it truncates. */
|
|
2792
|
+
const TEAM_HANDLE_W = 16;
|
|
2793
|
+
/**
|
|
2794
|
+
* One row under a team group: `shortId · agent · mode · handle · doing · time`.
|
|
2795
|
+
* Unlike {@link flatSessionRow}/{@link treeSessionRow} it drops the
|
|
2796
|
+
* `[team/handle]` topic prefix — the group header already names the team — and
|
|
2797
|
+
* promotes the teammate's mode and handle to their own cells, the richer
|
|
2798
|
+
* identity the `--teams` view exists to show (RUSH-1997).
|
|
2799
|
+
*/
|
|
2800
|
+
function teamMemberRow(session, live) {
|
|
2801
|
+
const agentColor = colorAgent(session.agent);
|
|
2802
|
+
const origin = session.teamOrigin;
|
|
2803
|
+
const age = sessionAgeParts(session.timestamp, session.lastActivity);
|
|
2804
|
+
const handle = safeTeamText(origin?.handle) ?? session.shortId;
|
|
2805
|
+
const mode = safeTeamText(origin?.mode) ?? '';
|
|
2806
|
+
const { glyph, preview } = liveGlyphAndPreview(live);
|
|
2807
|
+
// Match flatSessionRow: a live preview already folds ActiveSession.todos; a
|
|
2808
|
+
// resting row surfaces SessionMeta.todos when the scan attached it.
|
|
2809
|
+
const restingTodo = !live ? formatTodoCompact(session.todos) : '';
|
|
2810
|
+
const doing = [restingTodo, preview || session.topic || ''].filter(Boolean).join(' · ');
|
|
2811
|
+
const shownHandle = truncateToWidth(handle, TEAM_HANDLE_W);
|
|
2812
|
+
const head = doing ? `${shownHandle} · ${doing}` : shownHandle;
|
|
2813
|
+
const badges = signalBadges(metaSignals(session));
|
|
2814
|
+
const badgeW = badges ? stringWidth(badges) + 1 : 0;
|
|
2815
|
+
const { cell: statusCell, width: statusW } = liveStatusCell(live);
|
|
2816
|
+
const glyphW = glyph ? 2 : 0;
|
|
2817
|
+
const baseTopicW = terminalWidth() - (2 + 9 + 8 + (TEAM_MODE_W + 1)) - glyphW - statusW - badgeW - stringWidth(age.last) - 1;
|
|
2818
|
+
const when = timeCell(age, baseTopicW);
|
|
2819
|
+
const topicW = Math.max(12, baseTopicW - when.extraW);
|
|
2820
|
+
return (' ' +
|
|
2821
|
+
chalk.dim(padToWidth(session.shortId, 9)) +
|
|
2822
|
+
agentColor(padToWidth(truncateToWidth(session.agent, 7), 8)) +
|
|
2823
|
+
chalk.yellow(padToWidth(truncateToWidth(mode || '-', TEAM_MODE_W), TEAM_MODE_W + 1)) +
|
|
2824
|
+
(badges ? badges + ' ' : '') +
|
|
2825
|
+
(glyph ? glyph + ' ' : '') +
|
|
2826
|
+
statusCell +
|
|
2827
|
+
padToWidth(chalk.white(truncateToWidth(head, topicW)), topicW) +
|
|
2828
|
+
' ' +
|
|
2829
|
+
when.text);
|
|
2830
|
+
}
|
|
2831
|
+
/** The header line for one team group (or the residual no-team bucket). */
|
|
2832
|
+
function teamGroupHeader(g, glyph, labelById) {
|
|
2833
|
+
const count = chalk.gray(`(${g.sessions.length})`);
|
|
2834
|
+
if (g.kind === 'noTeam') {
|
|
2835
|
+
// These carry the isTeamOrigin flag but no teammate meta.json — a Task
|
|
2836
|
+
// sub-agent's transcript is never indexed, so in practice this bucket is
|
|
2837
|
+
// headless `agents run` spawns or teammates whose meta aged out. Named
|
|
2838
|
+
// honestly rather than claimed as real teammates.
|
|
2839
|
+
return (chalk.magenta('▸') + ' ' + chalk.magenta.bold(NO_TEAM_GROUP_KEY) + ' ' + count +
|
|
2840
|
+
' ' + chalk.gray('team-flagged spawns with no team record (`agents run`, or aged-out teammates)'));
|
|
2841
|
+
}
|
|
2842
|
+
const bits = [chalk.cyan('▸') + ' ' + chalk.cyan.bold(g.key), count];
|
|
2843
|
+
if (glyph)
|
|
2844
|
+
bits.push(glyph);
|
|
2845
|
+
if (g.spawnerSessionId) {
|
|
2846
|
+
const who = labelById.get(g.spawnerSessionId) ?? g.spawnerSessionId.slice(0, 8);
|
|
2847
|
+
bits.push(chalk.gray(`by ${who}`));
|
|
2848
|
+
}
|
|
2849
|
+
bits.push(chalk.gray(`spawned ${formatRelativeTime(g.firstSpawnTs)}`));
|
|
2850
|
+
return bits.join(' ');
|
|
2851
|
+
}
|
|
2852
|
+
/**
|
|
2853
|
+
* The `agents sessions --teams` view: team-origin sessions grouped by team, the
|
|
2854
|
+
* richer display RUSH-1997 asks for. Each named team names its spawner (the
|
|
2855
|
+
* orchestrator session that created it) and spawn time, with its teammates'
|
|
2856
|
+
* mode + handle under it; team-flagged spawns that carry no teammate record fall
|
|
2857
|
+
* into a trailing no-team bucket, so a real teammate and a bare spawn are never
|
|
2858
|
+
* conflated. A printed report like `--tree`, not an interactive pick.
|
|
2859
|
+
*/
|
|
2860
|
+
function printTeamsView(pool, liveIndex, hiddenUnmanaged = 0) {
|
|
2861
|
+
const groups = groupSessionsByTeam(pool);
|
|
2862
|
+
if (groups.length === 0) {
|
|
2863
|
+
console.log(chalk.gray('No team sessions found.'));
|
|
2864
|
+
console.log(chalk.gray('Team sessions are spawned by `agents teams` — see `agents teams status`.'));
|
|
2865
|
+
return;
|
|
2866
|
+
}
|
|
2867
|
+
// Resolve a spawner session id to its human label from the pool. The
|
|
2868
|
+
// orchestrator may or may not be in view; fall back to the short id, the same
|
|
2869
|
+
// degradation the --active teams rows use (active.ts resolveOrchestratorLabels).
|
|
2870
|
+
const labelById = new Map();
|
|
2871
|
+
for (const s of pool) {
|
|
2872
|
+
const label = s.label || s.topic;
|
|
2873
|
+
if (label)
|
|
2874
|
+
labelById.set(s.id, cleanPreview(label));
|
|
2875
|
+
}
|
|
2876
|
+
const total = groups.reduce((n, g) => n + g.sessions.length, 0);
|
|
2877
|
+
const teamCount = groups.filter((g) => g.kind === 'team').length;
|
|
2878
|
+
const noTeam = groups.find((g) => g.kind === 'noTeam');
|
|
2879
|
+
const parts = [
|
|
2880
|
+
`${total} team session${total === 1 ? '' : 's'}`,
|
|
2881
|
+
`${teamCount} team${teamCount === 1 ? '' : 's'}`,
|
|
2882
|
+
];
|
|
2883
|
+
if (noTeam)
|
|
2884
|
+
parts.push(`${noTeam.sessions.length} without a team record`);
|
|
2885
|
+
console.log(chalk.gray(parts.join(' · ') + '\n'));
|
|
2886
|
+
let first = true;
|
|
2887
|
+
for (const g of groups) {
|
|
2888
|
+
if (!first)
|
|
2889
|
+
console.log();
|
|
2890
|
+
first = false;
|
|
2891
|
+
const { glyph } = liveGlyphAndPreview(liveIndex?.get(g.sessions[0].id));
|
|
2892
|
+
console.log(teamGroupHeader(g, glyph, labelById));
|
|
2893
|
+
for (const s of g.sessions)
|
|
2894
|
+
console.log(teamMemberRow(s, liveIndex?.get(s.id)));
|
|
2895
|
+
}
|
|
2896
|
+
console.log();
|
|
2897
|
+
console.log(chalk.gray('newest-active team first · resume any row with `agents sessions resume <id>`'));
|
|
2898
|
+
if (hiddenUnmanaged > 0)
|
|
2899
|
+
console.log(chalk.gray(formatUnmanagedHiddenFooter(hiddenUnmanaged)));
|
|
2900
|
+
}
|
|
2602
2901
|
function buildFilterOptions(options) {
|
|
2603
2902
|
const opts = {};
|
|
2604
2903
|
if (options.include)
|
|
@@ -2973,15 +3272,6 @@ export async function pickSessionInteractive(sessions, message = 'Search session
|
|
|
2973
3272
|
throw err;
|
|
2974
3273
|
}
|
|
2975
3274
|
}
|
|
2976
|
-
/**
|
|
2977
|
-
* The machine a picked session lives on when its transcript is on that peer's
|
|
2978
|
-
* disk (folded in over the live fan-out), else undefined. Keys off `_remote`,
|
|
2979
|
-
* NOT `machine !== local`: a synced mirror is machine-tagged too, but its file
|
|
2980
|
-
* is a local mirror path, so it must be read/resumed locally like any other.
|
|
2981
|
-
*/
|
|
2982
|
-
function remoteMachineOf(session) {
|
|
2983
|
-
return session._remote ? session.machine : undefined;
|
|
2984
|
-
}
|
|
2985
3275
|
/** True when the peer wasn't a dialable device; prints one clear line so a
|
|
2986
3276
|
* remote pick never dead-ends silently. */
|
|
2987
3277
|
function warnNoPeerTarget(machine, session) {
|
|
@@ -3007,31 +3297,50 @@ export async function handlePickedSession(picked) {
|
|
|
3007
3297
|
console.log(chalk.gray(`Watch for it with: agents sessions --active${picked.session.machine ? ` --host ${picked.session.machine}` : ''}`));
|
|
3008
3298
|
return;
|
|
3009
3299
|
}
|
|
3010
|
-
//
|
|
3011
|
-
//
|
|
3012
|
-
//
|
|
3013
|
-
//
|
|
3014
|
-
|
|
3015
|
-
if (
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
warnNoPeerTarget(remote, picked.session);
|
|
3020
|
-
}
|
|
3021
|
-
else {
|
|
3022
|
-
console.log(chalk.gray(`Resuming ${picked.session.shortId} on ${remote} over SSH...`));
|
|
3023
|
-
const rc = await runOnPeer(['sessions', 'resume', picked.session.shortId], remote, { tty: true });
|
|
3300
|
+
// Reading and resuming are on DIFFERENT machines' terms, and conflating them
|
|
3301
|
+
// is what RUSH-2022 was. Reading follows the FILE: a synced mirror sits on
|
|
3302
|
+
// this disk, so only a live fan-out row (`_remote`) has to be read on the
|
|
3303
|
+
// peer. Resuming follows the HARNESS STATE, which is on the owning machine
|
|
3304
|
+
// whatever the transcript's location — a mirror included.
|
|
3305
|
+
if (picked.action === 'view') {
|
|
3306
|
+
const readFrom = picked.session._remote ? picked.session.machine : undefined;
|
|
3307
|
+
if (readFrom) {
|
|
3308
|
+
const rc = await runOnPeer(['sessions', picked.session.shortId, '--markdown'], readFrom);
|
|
3024
3309
|
if (rc === 'no-target')
|
|
3025
|
-
warnNoPeerTarget(
|
|
3310
|
+
warnNoPeerTarget(readFrom, picked.session);
|
|
3311
|
+
return;
|
|
3026
3312
|
}
|
|
3027
|
-
return;
|
|
3028
|
-
}
|
|
3029
|
-
if (picked.action === 'view') {
|
|
3030
3313
|
await renderSession(picked.session, 'summary', {});
|
|
3031
3314
|
return;
|
|
3032
3315
|
}
|
|
3316
|
+
if (await resumeOnOwnerIfRemote(picked.session))
|
|
3317
|
+
return;
|
|
3033
3318
|
await resumeSessionInPlace(picked.session);
|
|
3034
3319
|
}
|
|
3320
|
+
/**
|
|
3321
|
+
* Resume `session` on the machine that owns it, when that is not this one.
|
|
3322
|
+
* Returns true when it handled the resume (hopped, or reported an unreachable
|
|
3323
|
+
* owner), false when the session is local and the caller should take over here.
|
|
3324
|
+
*
|
|
3325
|
+
* The one hop for a foreground, one-session-at-a-time resume — shared by the
|
|
3326
|
+
* `agents sessions` picker and by `sessions resume`'s no-tab-backend path, which
|
|
3327
|
+
* would otherwise reach `resumeSessionInPlace` and be refused (RUSH-2022).
|
|
3328
|
+
*/
|
|
3329
|
+
export async function resumeOnOwnerIfRemote(session) {
|
|
3330
|
+
const owner = sessionOwnerDevice(session);
|
|
3331
|
+
if (!owner)
|
|
3332
|
+
return false;
|
|
3333
|
+
console.log(chalk.gray(`Resuming ${session.shortId} on ${owner} over SSH...`));
|
|
3334
|
+
// `agents resume <id>` is the strict single-session path — one pick, one
|
|
3335
|
+
// resume. (`sessions resume <shortId>` would re-open a picker over there.)
|
|
3336
|
+
const rc = await runOnPeer(['resume', session.id], owner, {
|
|
3337
|
+
tty: true,
|
|
3338
|
+
env: { [RESUME_PINNED_ENV]: '1' },
|
|
3339
|
+
});
|
|
3340
|
+
if (rc === 'no-target')
|
|
3341
|
+
warnNoPeerTarget(owner, session);
|
|
3342
|
+
return true;
|
|
3343
|
+
}
|
|
3035
3344
|
/**
|
|
3036
3345
|
* Resume a session in the current terminal — a foreground takeover of this
|
|
3037
3346
|
* process. Used by the single-select picker and by `sessions resume` when the
|
|
@@ -3040,6 +3349,20 @@ export async function handlePickedSession(picked) {
|
|
|
3040
3349
|
* version-pinned launcher is genuinely missing.
|
|
3041
3350
|
*/
|
|
3042
3351
|
export async function resumeSessionInPlace(session) {
|
|
3352
|
+
// This function is the LOCAL takeover, and every caller is responsible for
|
|
3353
|
+
// routing a peer-owned session before it gets here (the picker above,
|
|
3354
|
+
// `agents resume`, `sessions attach`). Reaching it with one anyway means a
|
|
3355
|
+
// caller skipped that step, so refuse rather than start the harness against
|
|
3356
|
+
// state this box does not have — the `fs.existsSync(session.cwd)` fallback
|
|
3357
|
+
// just below is exactly how that used to pass unnoticed, quietly swapping in
|
|
3358
|
+
// `process.cwd()` when the recorded directory was a peer's path (RUSH-2022).
|
|
3359
|
+
const owner = sessionOwnerDevice(session);
|
|
3360
|
+
if (owner) {
|
|
3361
|
+
console.error(chalk.red(`Session ${session.shortId} belongs to ${owner} — it cannot resume on this machine.`));
|
|
3362
|
+
console.error(chalk.gray(` Resume it there: agents resume ${session.id}`));
|
|
3363
|
+
process.exitCode = 1;
|
|
3364
|
+
return;
|
|
3365
|
+
}
|
|
3043
3366
|
const cwd = session.cwd && fs.existsSync(session.cwd)
|
|
3044
3367
|
? session.cwd
|
|
3045
3368
|
: process.cwd();
|
|
@@ -3459,7 +3782,7 @@ function scoreSessionQuery(session, terms) {
|
|
|
3459
3782
|
* the project you specified AND elsewhere, producing an ambiguity error
|
|
3460
3783
|
* even though the user already pointed at the correct scope.
|
|
3461
3784
|
*/
|
|
3462
|
-
function applyScopeFilters(sessions, scope) {
|
|
3785
|
+
export function applyScopeFilters(sessions, scope) {
|
|
3463
3786
|
let filtered = sessions;
|
|
3464
3787
|
if (scope.project) {
|
|
3465
3788
|
const projectQuery = scope.project.toLowerCase();
|
|
@@ -3482,8 +3805,21 @@ function applyScopeFilters(sessions, scope) {
|
|
|
3482
3805
|
return true;
|
|
3483
3806
|
});
|
|
3484
3807
|
}
|
|
3808
|
+
if (scope.routine) {
|
|
3809
|
+
filtered = filtered.filter((session) => session.origin === 'routine');
|
|
3810
|
+
if (typeof scope.routine === 'string') {
|
|
3811
|
+
const names = [...new Set(filtered.map((session) => session.routineName).filter((name) => !!name))];
|
|
3812
|
+
const selected = resolveRoutineName(scope.routine, names);
|
|
3813
|
+
filtered = selected
|
|
3814
|
+
? filtered.filter((session) => session.routineName === selected)
|
|
3815
|
+
: [];
|
|
3816
|
+
}
|
|
3817
|
+
}
|
|
3485
3818
|
return filtered;
|
|
3486
3819
|
}
|
|
3820
|
+
export function artifactLookupScope(agent, project, routine) {
|
|
3821
|
+
return { agent, project, routine };
|
|
3822
|
+
}
|
|
3487
3823
|
async function renderArtifactsGlobal(query, listAll, name, scope) {
|
|
3488
3824
|
const spinner = ora().start();
|
|
3489
3825
|
const tracker = createScanProgressTracker(FIND_VERBS, 'session', spinner);
|
|
@@ -3929,9 +4265,9 @@ export function registerSessionsCommands(program) {
|
|
|
3929
4265
|
.option('--opencode', 'Shorthand for --agent opencode')
|
|
3930
4266
|
.option('--all', 'Widen every non-status filter to "all": every directory (not just this project) and all time (no window cap). Status filters like --active still compose; -a/--device/--since still narrow their axis.')
|
|
3931
4267
|
.option('--unmanaged', "Also show sessions from your own ~/.<agent> installs (hidden once agents-cli manages that agent)")
|
|
3932
|
-
.option('--team, --teams', '
|
|
4268
|
+
.option('--team, --teams', 'Show team-spawned sessions (hidden by default), grouped by team — each team names its spawner and spawn time, teammates show their mode + handle, and team-flagged spawns with no teammate record sink into a (no team) bucket. --flat/--tree keep the plain inline table')
|
|
3933
4269
|
.option('--in-team <name>', "Only this team: the session that spawned it plus (with --teams) its teammates. Spans every directory and all time, since a team's worktrees and history sit outside the default window.")
|
|
3934
|
-
.option('--routine', 'Show
|
|
4270
|
+
.option('--routines, --routine [name]', 'Show routine-run sessions; omit the name on a TTY to pick one interactively (fuzzy name matching)')
|
|
3935
4271
|
.option('-p, --project <name>', 'Filter by project name (searches across all directories)')
|
|
3936
4272
|
.option('--skill <name>', 'Only sessions that invoked this skill (matches a bare name or a namespaced plugin skill\'s short name, e.g. --skill design finds rush:design)')
|
|
3937
4273
|
.option('--plugin <name>', 'Only sessions that used a skill/command owned by this plugin')
|
|
@@ -4014,12 +4350,18 @@ export function registerSessionsCommands(program) {
|
|
|
4014
4350
|
# Search across every directory, not just this project
|
|
4015
4351
|
agents sessions "topic" --all
|
|
4016
4352
|
|
|
4353
|
+
# Team-spawned sessions, grouped by team (spawner + spawn time per team,
|
|
4354
|
+
# teammate mode/handle per row; team-flagged spawns with no team record
|
|
4355
|
+
# in a trailing (no team) bucket)
|
|
4356
|
+
agents sessions --teams
|
|
4357
|
+
|
|
4017
4358
|
# Who spawned which team: an orchestrator row carries team:<name>, and a
|
|
4018
4359
|
# teammate row [<team>/<handle>]. --in-team narrows to one team's lineage.
|
|
4019
4360
|
agents sessions --in-team redesign --teams
|
|
4020
4361
|
|
|
4021
4362
|
# Show routine-run sessions and open one by routine run id
|
|
4022
4363
|
agents sessions --routine --all
|
|
4364
|
+
agents sessions --routine nightly-review --all
|
|
4023
4365
|
agents sessions 2026-07-21T10-30-00-000Z
|
|
4024
4366
|
|
|
4025
4367
|
# Export for analysis
|
|
@@ -4066,7 +4408,7 @@ export function registerSessionsCommands(program) {
|
|
|
4066
4408
|
- --first and --last are mutually exclusive.
|
|
4067
4409
|
- A filter flag (--include/--exclude/--first/--last) without --markdown/--json defaults to --markdown output.
|
|
4068
4410
|
- --cloud sources from Rush Cloud captured runs instead of local disk.
|
|
4069
|
-
- --routine shows
|
|
4411
|
+
- --routine [name] shows transcripts archived from routine runs. On a TTY, omit the name to pick a routine; a name accepts exact, substring, or unambiguous typo matches. --routines is an alias. Routine rows also resolve by run id.
|
|
4070
4412
|
- Without --teams, team-spawned sessions are hidden by default.
|
|
4071
4413
|
`,
|
|
4072
4414
|
});
|