@phnx-labs/agents-cli 1.22.31 → 1.22.33
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 +72 -0
- package/README.md +8 -2
- package/dist/bin/agents +0 -0
- package/dist/commands/daemon.js +52 -12
- package/dist/commands/doctor.d.ts +19 -0
- package/dist/commands/doctor.js +119 -17
- package/dist/commands/routines.js +164 -36
- package/dist/commands/sessions-browser.js +2 -2
- package/dist/commands/sessions.d.ts +1 -1
- package/dist/commands/sessions.js +66 -22
- package/dist/commands/update.d.ts +2 -0
- package/dist/commands/update.js +148 -0
- package/dist/index.js +3 -1
- package/dist/lib/catchup.js +4 -1
- package/dist/lib/daemon.d.ts +17 -0
- package/dist/lib/daemon.js +69 -3
- package/dist/lib/devices/doctor-findings.d.ts +7 -2
- package/dist/lib/devices/doctor-findings.js +53 -2
- package/dist/lib/devices/doctor-overview-cache.d.ts +7 -0
- package/dist/lib/devices/doctor-overview-cache.js +15 -0
- package/dist/lib/devices/fleet-divergence.d.ts +11 -0
- package/dist/lib/devices/fleet-divergence.js +6 -0
- package/dist/lib/devices/fleet-inventory.js +16 -2
- package/dist/lib/drift.d.ts +6 -1
- package/dist/lib/drift.js +9 -0
- package/dist/lib/hooks/cache.js +20 -1
- package/dist/lib/hooks.d.ts +91 -1
- package/dist/lib/hooks.js +289 -3
- package/dist/lib/hosts/passthrough.js +3 -0
- package/dist/lib/installations/index.d.ts +14 -0
- package/dist/lib/installations/index.js +14 -0
- package/dist/lib/installations/resolve.d.ts +43 -0
- package/dist/lib/installations/resolve.js +93 -0
- package/dist/lib/installations/store.d.ts +56 -0
- package/dist/lib/installations/store.js +196 -0
- package/dist/lib/installations/strategies.d.ts +73 -0
- package/dist/lib/installations/strategies.js +293 -0
- package/dist/lib/installations/types.d.ts +78 -0
- package/dist/lib/installations/types.js +8 -0
- package/dist/lib/installations/update.d.ts +40 -0
- package/dist/lib/installations/update.js +131 -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/migrate.d.ts +27 -0
- package/dist/lib/migrate.js +112 -2
- package/dist/lib/routine-context.d.ts +144 -0
- package/dist/lib/routine-context.js +268 -0
- package/dist/lib/routine-readiness.d.ts +47 -0
- package/dist/lib/routine-readiness.js +239 -0
- package/dist/lib/routines.d.ts +97 -1
- package/dist/lib/routines.js +107 -1
- package/dist/lib/runner.d.ts +18 -4
- package/dist/lib/runner.js +291 -98
- package/dist/lib/scheduler.d.ts +7 -1
- package/dist/lib/scheduler.js +5 -2
- 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/self-heal/checks/hook-runtime.d.ts +2 -0
- package/dist/lib/self-heal/checks/hook-runtime.js +16 -0
- package/dist/lib/self-heal/registry.js +5 -2
- package/dist/lib/self-heal/types.d.ts +1 -1
- package/dist/lib/session/state.js +4 -1
- package/dist/lib/session/team-filter.d.ts +11 -0
- package/dist/lib/session/team-filter.js +10 -0
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/versions.d.ts +24 -0
- package/dist/lib/versions.js +49 -16
- package/package.json +2 -2
|
@@ -16,6 +16,7 @@ import { resolveAgentName, isAgentHardDeprecated, hardDeprecationError, ROUTINE_
|
|
|
16
16
|
import { humanizeCron, humanizeNextRun, formatRepoLink, REPO_DISPLAY_MAX } from '../lib/routines-format.js';
|
|
17
17
|
import { listJobs as listAllJobs, deleteJob, readJob, validateJob, writeJob, setJobEnabled, listRuns, routineStats, getLatestRun, getRunDir, getJobPath, parseAtTime, hasCompletedOneShotRun, isOneShotLikeSchedule, isOneShotRoutine, isPastOneShotRoutine, jobRunsOnThisDevice, checkJobDeviceEligibility, normalizeTriggerEvent, parseHostStrategy, resolveHostStrategy, HOST_STRATEGIES, computeProjectGroup, computeProjectGroupKind, projectGroupKey, projectGroupTitle, projectGroupOrder, normalizeProjects, } from '../lib/routines.js';
|
|
18
18
|
import { listProjectDefs, isSafeProjectName } from '../lib/projects.js';
|
|
19
|
+
import { evaluateActivationReadinessLive } from '../lib/routine-readiness.js';
|
|
19
20
|
import { discoverProjectRoutinesAt, enableProjectRoutines, disableProjectRoutines, syncProjectRoutines, syncAllProjectRoutines, listEnabledProjectRoots, resolveProjectRoot, displayProjectPath, listProjectRoutineFiles, } from '../lib/routines-project.js';
|
|
20
21
|
import { fireWebhookJobs, matchJobsToWebhook } from '../lib/triggers/webhook.js';
|
|
21
22
|
import { getRoutinesDir } from '../lib/state.js';
|
|
@@ -724,6 +725,8 @@ export function registerRoutinesCommands(program) {
|
|
|
724
725
|
.option('--resume <sessionId>', 'At fire time, resume this existing session id (via `agents run <agent> --resume`) instead of starting fresh — the actual session reopens with full context and the prompt becomes its next turn. Powers self-scheduled wake-ups (e.g. /hibernate). Requires --agent claude or codex; runs un-sandboxed (the session store lives in the real home, not the job overlay).')
|
|
725
726
|
.option('--project <name>', 'Associate with a named project (repeatable; use --all-projects for all)', collectProject, [])
|
|
726
727
|
.option('--all-projects', 'Associate this routine with all defined projects (sets projects: ["*"])')
|
|
728
|
+
.option('--project-anchor <name>', 'Singular EXECUTION anchor: the named project whose base directory the run lands in (distinct from --project, which is grouping metadata). Rootless (Linear-imported) projects anchor a relative --cwd at the target home.')
|
|
729
|
+
.option('--cwd <path>', 'Portable execution directory. Relative values resolve under --project-anchor when usable, otherwise under the execution target $HOME. Supersedes --run-cwd/remoteCwd.')
|
|
727
730
|
.option('--json', 'Emit machine-readable JSON with the created routine id and status')
|
|
728
731
|
.action(async (nameOrPath, options) => {
|
|
729
732
|
// Check if inline mode (has flags) or file mode
|
|
@@ -852,6 +855,8 @@ export function registerRoutinesCommands(program) {
|
|
|
852
855
|
...(options.runOn ? { host: options.runOn } : {}),
|
|
853
856
|
...(hostStrategy ? { hostStrategy } : {}),
|
|
854
857
|
...(options.runCwd ? { remoteCwd: options.runCwd } : {}),
|
|
858
|
+
...(options.projectAnchor ? { project: options.projectAnchor } : {}),
|
|
859
|
+
...(options.cwd ? { cwd: options.cwd } : {}),
|
|
855
860
|
...(runOnce ? { runOnce: true } : {}),
|
|
856
861
|
...(options.catchup === false ? { catchup: false } : {}),
|
|
857
862
|
...(options.endAt ? { endAt: options.endAt } : {}),
|
|
@@ -867,7 +872,23 @@ export function registerRoutinesCommands(program) {
|
|
|
867
872
|
process.exit(1);
|
|
868
873
|
}
|
|
869
874
|
writeJob(config);
|
|
870
|
-
|
|
875
|
+
// Readiness gate: a routine only activates when its execution context
|
|
876
|
+
// resolves and the harness is available. A proven blocker saves the
|
|
877
|
+
// definition PAUSED with a stable code + repair, so a broken routine can
|
|
878
|
+
// never fire (and storm) — the plan's save-paused contract.
|
|
879
|
+
const deviceMatch = !devices || devices.map(normalizeHost).includes(normalizeHost(machineId()));
|
|
880
|
+
const readiness = await evaluateActivationReadinessLive(config);
|
|
881
|
+
const activate = config.enabled && deviceMatch && readiness.ready;
|
|
882
|
+
setJobEnabled(config.name, activate);
|
|
883
|
+
if (config.enabled && deviceMatch && !readiness.ready) {
|
|
884
|
+
const r = readiness.readiness;
|
|
885
|
+
if (!options.json) {
|
|
886
|
+
console.log(chalk.yellow(`Saved paused — not ready to activate: ${r.code}`));
|
|
887
|
+
console.log(chalk.gray(` ${r.message}`));
|
|
888
|
+
if (r.repair)
|
|
889
|
+
console.log(chalk.gray(` repair: ${r.repair}`));
|
|
890
|
+
}
|
|
891
|
+
}
|
|
871
892
|
if (options.json) {
|
|
872
893
|
writeJson({
|
|
873
894
|
ok: true,
|
|
@@ -875,8 +896,11 @@ export function registerRoutinesCommands(program) {
|
|
|
875
896
|
job: config,
|
|
876
897
|
jobId: config.name,
|
|
877
898
|
name: config.name,
|
|
878
|
-
status: 'added',
|
|
899
|
+
status: activate ? 'added' : 'added_paused',
|
|
879
900
|
enabled: config.enabled,
|
|
901
|
+
activated: activate,
|
|
902
|
+
ready: readiness.ready,
|
|
903
|
+
...(readiness.readiness ? { readiness: readiness.readiness } : {}),
|
|
880
904
|
schedule: config.schedule ?? null,
|
|
881
905
|
trigger: config.trigger ?? null,
|
|
882
906
|
});
|
|
@@ -933,7 +957,17 @@ export function registerRoutinesCommands(program) {
|
|
|
933
957
|
console.error(chalk.yellow(`Schedule "${config.schedule}" pins minute, hour, day, and month; treating it as one-shot. Prefer --at for one-time routines.`));
|
|
934
958
|
}
|
|
935
959
|
writeJob(config);
|
|
936
|
-
|
|
960
|
+
const deviceMatch = !config.devices || config.devices.map(normalizeHost).includes(normalizeHost(machineId()));
|
|
961
|
+
const readiness = await evaluateActivationReadinessLive(config);
|
|
962
|
+
const activate = config.enabled && deviceMatch && readiness.ready;
|
|
963
|
+
setJobEnabled(config.name, activate);
|
|
964
|
+
if (config.enabled && deviceMatch && !readiness.ready && !options.json) {
|
|
965
|
+
const r = readiness.readiness;
|
|
966
|
+
console.log(chalk.yellow(`Saved paused — not ready to activate: ${r.code}`));
|
|
967
|
+
console.log(chalk.gray(` ${r.message}`));
|
|
968
|
+
if (r.repair)
|
|
969
|
+
console.log(chalk.gray(` repair: ${r.repair}`));
|
|
970
|
+
}
|
|
937
971
|
if (options.json) {
|
|
938
972
|
writeJson({
|
|
939
973
|
ok: true,
|
|
@@ -941,8 +975,11 @@ export function registerRoutinesCommands(program) {
|
|
|
941
975
|
job: config,
|
|
942
976
|
jobId: config.name,
|
|
943
977
|
name: config.name,
|
|
944
|
-
status: 'added',
|
|
978
|
+
status: activate ? 'added' : 'added_paused',
|
|
945
979
|
enabled: config.enabled,
|
|
980
|
+
activated: activate,
|
|
981
|
+
ready: readiness.ready,
|
|
982
|
+
...(readiness.readiness ? { readiness: readiness.readiness } : {}),
|
|
946
983
|
schedule: config.schedule ?? null,
|
|
947
984
|
trigger: config.trigger ?? null,
|
|
948
985
|
});
|
|
@@ -1029,7 +1066,8 @@ export function registerRoutinesCommands(program) {
|
|
|
1029
1066
|
});
|
|
1030
1067
|
routinesCmd
|
|
1031
1068
|
.command('edit [name]')
|
|
1032
|
-
.description('
|
|
1069
|
+
.description('Edit a prefilled routine transactionally; invalid YAML never replaces the live definition.')
|
|
1070
|
+
.option('--yaml', 'Open the raw YAML in $EDITOR (the current edit surface)')
|
|
1033
1071
|
.option('--state-to <name>', 'Update the Linear current-state filter before opening the editor')
|
|
1034
1072
|
.option('--state-from <name>', 'Update the Linear previous-state filter before opening the editor')
|
|
1035
1073
|
.action(async (name, options) => {
|
|
@@ -1048,53 +1086,55 @@ export function registerRoutinesCommands(program) {
|
|
|
1048
1086
|
existing.trigger.stateTo = options.stateTo || undefined;
|
|
1049
1087
|
if (options.stateFrom !== undefined)
|
|
1050
1088
|
existing.trigger.stateFrom = options.stateFrom || undefined;
|
|
1051
|
-
writeJob(existing);
|
|
1052
1089
|
}
|
|
1053
1090
|
const jobPath = getJobPath(name);
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1091
|
+
const cronDir = getRoutinesDir();
|
|
1092
|
+
fs.mkdirSync(cronDir, { recursive: true });
|
|
1093
|
+
const targetPath = jobPath || safeJoin(cronDir, `${name}.yml`);
|
|
1094
|
+
const editPath = safeJoin(cronDir, `.${name}.edit-${process.pid}.yml`);
|
|
1095
|
+
const initial = existing
|
|
1096
|
+
? yaml.stringify(existing)
|
|
1097
|
+
: yaml.stringify({
|
|
1060
1098
|
name,
|
|
1061
1099
|
schedule: '0 9 * * *',
|
|
1062
1100
|
agent: 'claude',
|
|
1063
1101
|
prompt: 'Your prompt here',
|
|
1064
1102
|
});
|
|
1065
|
-
|
|
1066
|
-
console.log(chalk.gray(`Created new job file: ${newPath}`));
|
|
1067
|
-
}
|
|
1068
|
-
const targetPath = jobPath || path.join(getRoutinesDir(), `${name}.yml`);
|
|
1103
|
+
fs.writeFileSync(editPath, initial, { encoding: 'utf-8', mode: 0o600 });
|
|
1069
1104
|
const editor = process.env.EDITOR || process.env.VISUAL || (IS_WINDOWS ? 'notepad' : 'vi');
|
|
1070
1105
|
const editorParts = editor.split(/\s+/).filter(Boolean);
|
|
1071
1106
|
const editorBin = editorParts[0];
|
|
1072
|
-
const editorArgs = [...editorParts.slice(1),
|
|
1107
|
+
const editorArgs = [...editorParts.slice(1), editPath];
|
|
1073
1108
|
const { spawn: spawnSync } = await import('child_process');
|
|
1074
1109
|
const child = spawnSync(editorBin, editorArgs, {
|
|
1075
1110
|
stdio: 'inherit',
|
|
1076
1111
|
});
|
|
1077
|
-
child.on('close', (code) => {
|
|
1078
|
-
if (code
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1112
|
+
child.on('close', async (code) => {
|
|
1113
|
+
if (code !== 0) {
|
|
1114
|
+
fs.rmSync(editPath, { force: true });
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
try {
|
|
1118
|
+
const raw = fs.readFileSync(editPath, 'utf-8');
|
|
1119
|
+
const job = yaml.parse(raw);
|
|
1120
|
+
const errors = validateJob(job);
|
|
1121
|
+
if (errors.length > 0)
|
|
1122
|
+
throw new Error(errors.join('\n'));
|
|
1123
|
+
const readiness = await evaluateActivationReadinessLive(job);
|
|
1124
|
+
fs.renameSync(editPath, targetPath);
|
|
1125
|
+
if (!readiness.ready)
|
|
1126
|
+
setJobEnabled(job.name, false);
|
|
1127
|
+
console.log(chalk.green(`\nJob '${name}' saved${readiness.ready ? '' : ' paused (not ready)'}`));
|
|
1128
|
+
if (isDaemonRunning()) {
|
|
1129
|
+
signalDaemonReload();
|
|
1130
|
+
console.log(chalk.gray('Daemon reloaded'));
|
|
1096
1131
|
}
|
|
1097
1132
|
}
|
|
1133
|
+
catch (err) {
|
|
1134
|
+
fs.rmSync(editPath, { force: true });
|
|
1135
|
+
console.error(chalk.red(`\nRoutine not saved: ${err.message}`));
|
|
1136
|
+
process.exitCode = 1;
|
|
1137
|
+
}
|
|
1098
1138
|
});
|
|
1099
1139
|
});
|
|
1100
1140
|
routinesCmd
|
|
@@ -1498,6 +1538,79 @@ export function registerRoutinesCommands(program) {
|
|
|
1498
1538
|
console.log(chalk.gray(`Run: ${runId}\n`));
|
|
1499
1539
|
console.log(fs.readFileSync(reportPath, 'utf-8'));
|
|
1500
1540
|
});
|
|
1541
|
+
routinesCmd
|
|
1542
|
+
.command('doctor [name]')
|
|
1543
|
+
.description('Check a routine\'s execution-context and harness readiness. Bare or --all checks every routine; --fix applies safe activation repairs (activate a now-ready paused routine; pause a broken active one).')
|
|
1544
|
+
.option('--all', 'Check every routine (the default when no name is given)')
|
|
1545
|
+
.option('--fix', 'Apply safe, deterministic activation repairs')
|
|
1546
|
+
.option('--json', 'Machine-readable output')
|
|
1547
|
+
.action(async (name, options) => {
|
|
1548
|
+
const all = options.all || !name;
|
|
1549
|
+
let targets;
|
|
1550
|
+
if (all) {
|
|
1551
|
+
targets = listAllJobs();
|
|
1552
|
+
}
|
|
1553
|
+
else {
|
|
1554
|
+
const job = readJob(name);
|
|
1555
|
+
if (!job) {
|
|
1556
|
+
if (options.json) {
|
|
1557
|
+
writeJson({ ok: false, error: `routine '${name}' not found` });
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
console.error(chalk.red(`Routine '${name}' not found`));
|
|
1561
|
+
process.exit(1);
|
|
1562
|
+
}
|
|
1563
|
+
targets = [job];
|
|
1564
|
+
}
|
|
1565
|
+
const thisDevice = normalizeHost(machineId());
|
|
1566
|
+
const results = await Promise.all(targets.map(async (job) => {
|
|
1567
|
+
const deviceMatch = !job.devices || job.devices.map(normalizeHost).includes(thisDevice);
|
|
1568
|
+
const readiness = await evaluateActivationReadinessLive(job);
|
|
1569
|
+
// `job.enabled` reflects THIS device's activation state (applyDeviceActivation).
|
|
1570
|
+
let action;
|
|
1571
|
+
if (options.fix && deviceMatch) {
|
|
1572
|
+
if (readiness.ready && !job.enabled) {
|
|
1573
|
+
setJobEnabled(job.name, true);
|
|
1574
|
+
action = 'activated';
|
|
1575
|
+
}
|
|
1576
|
+
else if (!readiness.ready && job.enabled) {
|
|
1577
|
+
setJobEnabled(job.name, false);
|
|
1578
|
+
action = 'paused';
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
return {
|
|
1582
|
+
name: job.name,
|
|
1583
|
+
ready: readiness.ready,
|
|
1584
|
+
active: job.enabled,
|
|
1585
|
+
deviceScoped: deviceMatch,
|
|
1586
|
+
...(readiness.readiness ? { readiness: readiness.readiness } : {}),
|
|
1587
|
+
...(action ? { action } : {}),
|
|
1588
|
+
};
|
|
1589
|
+
}));
|
|
1590
|
+
if (options.fix && results.some((r) => r.action) && isDaemonRunning()) {
|
|
1591
|
+
signalDaemonReload();
|
|
1592
|
+
}
|
|
1593
|
+
if (options.json) {
|
|
1594
|
+
writeJson({ ok: true, results });
|
|
1595
|
+
return;
|
|
1596
|
+
}
|
|
1597
|
+
const blocked = results.filter((r) => !r.ready);
|
|
1598
|
+
for (const r of results) {
|
|
1599
|
+
if (r.ready) {
|
|
1600
|
+
console.log(`${chalk.green('✓')} ${r.name}${r.action === 'activated' ? chalk.gray(' (activated)') : ''}`);
|
|
1601
|
+
}
|
|
1602
|
+
else {
|
|
1603
|
+
const rd = r.readiness;
|
|
1604
|
+
console.log(`${chalk.red('✗')} ${r.name} — ${chalk.yellow(rd.code)}${r.action === 'paused' ? chalk.gray(' (paused)') : ''}`);
|
|
1605
|
+
console.log(chalk.gray(` ${rd.message}`));
|
|
1606
|
+
if (rd.repair)
|
|
1607
|
+
console.log(chalk.gray(` repair: ${rd.repair}`));
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
if (blocked.length > 0 && !options.fix) {
|
|
1611
|
+
console.log(chalk.gray(`\n${blocked.length} routine${blocked.length === 1 ? '' : 's'} blocked — re-run with --fix to pause them, or apply each repair above.`));
|
|
1612
|
+
}
|
|
1613
|
+
});
|
|
1501
1614
|
routinesCmd
|
|
1502
1615
|
.command('resume [name]')
|
|
1503
1616
|
.description('Re-enable a paused routine so the daemon schedules it again')
|
|
@@ -1509,6 +1622,21 @@ export function registerRoutinesCommands(program) {
|
|
|
1509
1622
|
return;
|
|
1510
1623
|
}
|
|
1511
1624
|
try {
|
|
1625
|
+
// Resume re-runs readiness — it can never bypass a proven blocker (the
|
|
1626
|
+
// plan: "resume cannot bypass readiness"). A blocked routine stays paused.
|
|
1627
|
+
const job = readJob(name);
|
|
1628
|
+
if (job) {
|
|
1629
|
+
const readiness = await evaluateActivationReadinessLive(job);
|
|
1630
|
+
if (!readiness.ready) {
|
|
1631
|
+
const r = readiness.readiness;
|
|
1632
|
+
console.log(chalk.red(`Cannot resume '${name}' — not ready: ${r.code}`));
|
|
1633
|
+
console.log(chalk.gray(` ${r.message}`));
|
|
1634
|
+
if (r.repair)
|
|
1635
|
+
console.log(chalk.gray(` repair: ${r.repair}`));
|
|
1636
|
+
console.log(chalk.gray(` fix it, then: agents routines doctor ${name} --fix`));
|
|
1637
|
+
process.exit(1);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1512
1640
|
setJobEnabled(name, true);
|
|
1513
1641
|
console.log(chalk.green(`Job '${name}' resumed`));
|
|
1514
1642
|
if (isDaemonRunning()) {
|
|
@@ -18,7 +18,7 @@ import { discoverSessions } from '../lib/session/discover.js';
|
|
|
18
18
|
import { gatherRemoteList } from '../lib/session/remote-list.js';
|
|
19
19
|
import { resolveVersionAliasLoose } from '../lib/versions.js';
|
|
20
20
|
import { AGENTS } from '../lib/agents.js';
|
|
21
|
-
import { enrichTeamOrigins, safeTeamText } from '../lib/session/team-filter.js';
|
|
21
|
+
import { enrichTeamOrigins, safeTeamText, shouldShowTeamSessions } from '../lib/session/team-filter.js';
|
|
22
22
|
import { listBookmarks, toggleBookmark } from '../lib/session/bookmarks.js';
|
|
23
23
|
import { machineId, normalizeHost } from '../lib/session/sync/config.js';
|
|
24
24
|
import { buildPreview } from './sessions-picker.js';
|
|
@@ -280,7 +280,7 @@ async function fetchRawPool(f, self, local, hosts, fixedFilters = false) {
|
|
|
280
280
|
origin: f.routine ? 'routine' : undefined,
|
|
281
281
|
skill: f.skill,
|
|
282
282
|
plugin: f.plugin,
|
|
283
|
-
excludeTeamOrigin: !f
|
|
283
|
+
excludeTeamOrigin: !shouldShowTeamSessions(f),
|
|
284
284
|
// A team filter reaches back past the usual browse window, so the pool it
|
|
285
285
|
// draws from has to as well — otherwise the newest 500 rows decide which
|
|
286
286
|
// teams exist.
|
|
@@ -434,7 +434,7 @@ export interface RoutineChoice {
|
|
|
434
434
|
latestRunSessionCount: number;
|
|
435
435
|
}
|
|
436
436
|
export declare function buildRoutineRunGroups(sessions: SessionMeta[]): RoutineRunGroup[];
|
|
437
|
-
export declare function buildRoutineChoices(sessions: SessionMeta[]): RoutineChoice[];
|
|
437
|
+
export declare function buildRoutineChoices(sessions: SessionMeta[], runOnlyNames?: string[]): RoutineChoice[];
|
|
438
438
|
export declare function filterSessionsByRoutine(sessions: SessionMeta[], routine: boolean | string, interactive: boolean): Promise<SessionMeta[] | null>;
|
|
439
439
|
/** Resolve a session by id/query globally and print its compact preview (no pager).
|
|
440
440
|
* Backs `--preview` — the fast path for the "peek before resume" hot loop. */
|
|
@@ -33,7 +33,7 @@ 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, groupSessionsByTeam, NO_TEAM_GROUP_KEY, } from '../lib/session/team-filter.js';
|
|
36
|
+
import { filterTeamSessions, shouldShowTeamSessions, 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';
|
|
@@ -43,6 +43,7 @@ import { sessionOwnerDevice, RESUME_PINNED_ENV } from '../lib/session/resume-own
|
|
|
43
43
|
import { renderMarkdown } from '../lib/markdown.js';
|
|
44
44
|
import { AGENTS, colorAgent, resolveAgentName } from '../lib/agents.js';
|
|
45
45
|
import { getShimsDir } from '../lib/state.js';
|
|
46
|
+
import { listJobs, listJobsWithRuns, listRuns } from '../lib/routines.js';
|
|
46
47
|
import { fuzzyMatch, FUZZY_PRESETS } from '../lib/fuzzy.js';
|
|
47
48
|
import { itemPicker } from '../lib/picker.js';
|
|
48
49
|
import { resolveSessionAlias } from '../lib/session/actor-sidecar.js';
|
|
@@ -1519,6 +1520,11 @@ export function hasNoBrowserDisqualifyingFlags(options, query) {
|
|
|
1519
1520
|
!options.skill &&
|
|
1520
1521
|
!options.plugin &&
|
|
1521
1522
|
!options.sort &&
|
|
1523
|
+
// --routine without a name owns a two-stage picker (routine -> run ->
|
|
1524
|
+
// session), and a named routine owns the run-grouped overview. The generic
|
|
1525
|
+
// browser can only flatten routine sessions into its ordinary row list, so
|
|
1526
|
+
// routing this flag there silently bypasses both routine-specific views.
|
|
1527
|
+
!options.routine &&
|
|
1522
1528
|
!options.artifacts &&
|
|
1523
1529
|
options.artifact === undefined &&
|
|
1524
1530
|
// --cloud lists a provider's tasks, not the transcript index, and
|
|
@@ -1557,14 +1563,14 @@ export function buildRoutineRunGroups(sessions) {
|
|
|
1557
1563
|
}))
|
|
1558
1564
|
.sort((a, b) => (a.timestamp < b.timestamp ? 1 : a.timestamp > b.timestamp ? -1 : a.runId.localeCompare(b.runId)));
|
|
1559
1565
|
}
|
|
1560
|
-
export function buildRoutineChoices(sessions) {
|
|
1566
|
+
export function buildRoutineChoices(sessions, runOnlyNames = []) {
|
|
1561
1567
|
const byName = new Map();
|
|
1562
1568
|
for (const session of sessions) {
|
|
1563
1569
|
if (!session.routineName)
|
|
1564
1570
|
continue;
|
|
1565
1571
|
(byName.get(session.routineName) ?? byName.set(session.routineName, []).get(session.routineName)).push(session);
|
|
1566
1572
|
}
|
|
1567
|
-
|
|
1573
|
+
const choices = [...byName.entries()]
|
|
1568
1574
|
.map(([name, rows]) => {
|
|
1569
1575
|
const runs = buildRoutineRunGroups(rows);
|
|
1570
1576
|
return {
|
|
@@ -1573,11 +1579,28 @@ export function buildRoutineChoices(sessions) {
|
|
|
1573
1579
|
runCount: runs.length,
|
|
1574
1580
|
latestRunSessionCount: runs[0].sessions.length,
|
|
1575
1581
|
};
|
|
1576
|
-
})
|
|
1577
|
-
|
|
1582
|
+
});
|
|
1583
|
+
// Routines whose only attempts are pre-session (blocked/skipped/failed-before-
|
|
1584
|
+
// spawn) have run records but no transcript. Surface them so the picker is
|
|
1585
|
+
// built from definitions+runs, never only transcripts — otherwise a routine
|
|
1586
|
+
// that has never produced a session reads as "No routines match" (the plan).
|
|
1587
|
+
const seen = new Set(choices.map((c) => c.name));
|
|
1588
|
+
for (const name of runOnlyNames) {
|
|
1589
|
+
if (seen.has(name))
|
|
1590
|
+
continue;
|
|
1591
|
+
const runs = listRuns(name);
|
|
1592
|
+
const latest = runs[runs.length - 1];
|
|
1593
|
+
choices.push({
|
|
1594
|
+
name,
|
|
1595
|
+
lastRunAt: latest?.startedAt ?? '',
|
|
1596
|
+
runCount: runs.length,
|
|
1597
|
+
latestRunSessionCount: 0,
|
|
1598
|
+
});
|
|
1599
|
+
}
|
|
1600
|
+
return choices.sort((a, b) => (a.lastRunAt < b.lastRunAt ? 1 : a.lastRunAt > b.lastRunAt ? -1 : a.name.localeCompare(b.name)));
|
|
1578
1601
|
}
|
|
1579
1602
|
async function selectRoutineName(sessions) {
|
|
1580
|
-
const choices = buildRoutineChoices(sessions);
|
|
1603
|
+
const choices = buildRoutineChoices(sessions, safeListJobsWithRuns());
|
|
1581
1604
|
const picked = await itemPicker({
|
|
1582
1605
|
message: 'Select a routine:',
|
|
1583
1606
|
items: choices,
|
|
@@ -1590,7 +1613,8 @@ async function selectRoutineName(sessions) {
|
|
|
1590
1613
|
labelFor: (choice) => {
|
|
1591
1614
|
const runs = `${choice.runCount} run${choice.runCount === 1 ? '' : 's'}`;
|
|
1592
1615
|
const sessions = `${choice.latestRunSessionCount} session${choice.latestRunSessionCount === 1 ? '' : 's'} in latest`;
|
|
1593
|
-
|
|
1616
|
+
const age = choice.lastRunAt ? ` · ${formatRelativeTime(choice.lastRunAt)}` : '';
|
|
1617
|
+
return `${choice.name} ${chalk.gray(`${runs} · ${sessions}${age}`)}`;
|
|
1594
1618
|
},
|
|
1595
1619
|
shortIdFor: (choice) => choice.name,
|
|
1596
1620
|
emptyMessage: 'No routines match.',
|
|
@@ -1598,8 +1622,22 @@ async function selectRoutineName(sessions) {
|
|
|
1598
1622
|
});
|
|
1599
1623
|
return picked?.item.name ?? null;
|
|
1600
1624
|
}
|
|
1625
|
+
/** `listJobsWithRuns` but never throws into the sessions reader (best-effort). */
|
|
1626
|
+
function safeListJobsWithRuns() {
|
|
1627
|
+
try {
|
|
1628
|
+
return [...new Set([...listJobs().map((job) => job.name), ...listJobsWithRuns()])];
|
|
1629
|
+
}
|
|
1630
|
+
catch {
|
|
1631
|
+
return [];
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1601
1634
|
export async function filterSessionsByRoutine(sessions, routine, interactive) {
|
|
1602
|
-
|
|
1635
|
+
// The name universe is transcripts UNION routines that have any run record —
|
|
1636
|
+
// so a routine whose attempts never produced a session still resolves.
|
|
1637
|
+
const routineNames = [...new Set([
|
|
1638
|
+
...sessions.map((session) => session.routineName).filter((name) => !!name),
|
|
1639
|
+
...safeListJobsWithRuns(),
|
|
1640
|
+
])].sort((a, b) => a.localeCompare(b));
|
|
1603
1641
|
let selectedRoutine = null;
|
|
1604
1642
|
if (typeof routine === 'string') {
|
|
1605
1643
|
selectedRoutine = resolveRoutineName(routine, routineNames);
|
|
@@ -1616,9 +1654,10 @@ export async function filterSessionsByRoutine(sessions, routine, interactive) {
|
|
|
1616
1654
|
if (!selectedRoutine)
|
|
1617
1655
|
return null;
|
|
1618
1656
|
}
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1657
|
+
if (!selectedRoutine)
|
|
1658
|
+
return sessions;
|
|
1659
|
+
const matched = sessions.filter((session) => session.routineName === selectedRoutine);
|
|
1660
|
+
return matched;
|
|
1622
1661
|
}
|
|
1623
1662
|
/** The canonical `ag sessions …` command for a set of flags — the twin of the
|
|
1624
1663
|
* browser's `y` hotkey (see --print-cmd). Normalizes to the stable flag form. */
|
|
@@ -2259,6 +2298,11 @@ limitSource) {
|
|
|
2259
2298
|
// silently returns nothing for any team older than that — so the flag widens its
|
|
2260
2299
|
// own scope, the way --all does, unless the caller set an explicit --limit.
|
|
2261
2300
|
const wantsWholeTeam = !!options.inTeam;
|
|
2301
|
+
// A routine is not tied to the shell's cwd: its archived sessions retain the
|
|
2302
|
+
// working directory in which the scheduled agent actually ran. Scope the
|
|
2303
|
+
// routine catalog across directories so invoking it from ~/.agents/.system,
|
|
2304
|
+
// a project repo, or $HOME yields the same routine history.
|
|
2305
|
+
const wantsWholeRoutine = !!options.routine;
|
|
2262
2306
|
// `--limit` has a commander default, so an untouched flag still arrives as a
|
|
2263
2307
|
// string and truthiness can't tell it from a typed one. Commander records where
|
|
2264
2308
|
// each value came from, which is the only signal that distinguishes an explicit
|
|
@@ -2267,7 +2311,7 @@ limitSource) {
|
|
|
2267
2311
|
const userSetLimit = limitSource === 'cli' || limitSource === 'env';
|
|
2268
2312
|
const limit = wantsOverview
|
|
2269
2313
|
? OVERVIEW_POOL_LIMIT
|
|
2270
|
-
: parseInt(userSetLimit ? options.limit : wantsWholeTeam ? String(WHOLE_TEAM_POOL_LIMIT) : DEFAULT_LIMIT, 10);
|
|
2314
|
+
: parseInt(userSetLimit ? options.limit : wantsWholeTeam || wantsWholeRoutine ? String(WHOLE_TEAM_POOL_LIMIT) : DEFAULT_LIMIT, 10);
|
|
2271
2315
|
if (toolEvidenceMode && (!Number.isSafeInteger(limit) || limit < 1 || limit > TOOL_QUERY_MAX_RESULT_SESSIONS)) {
|
|
2272
2316
|
console.error(chalk.red(`Tool search --limit must be from 1 to ${TOOL_QUERY_MAX_RESULT_SESSIONS}.`));
|
|
2273
2317
|
process.exitCode = 1;
|
|
@@ -2277,7 +2321,7 @@ limitSource) {
|
|
|
2277
2321
|
// --since still narrows. Non-overview keeps the prior interactive-30d default.
|
|
2278
2322
|
const since = wantsOverview
|
|
2279
2323
|
? options.since
|
|
2280
|
-
: (options.since ?? (isInteractive && !options.all && !wantsWholeTeam ? '30d' : undefined));
|
|
2324
|
+
: (options.since ?? (isInteractive && !options.all && !wantsWholeTeam && !wantsWholeRoutine ? '30d' : undefined));
|
|
2281
2325
|
const toolSpansDevices = toolEvidenceMode
|
|
2282
2326
|
&& (options.fleet || (options.host?.length ?? 0) > 0);
|
|
2283
2327
|
const toolSortError = toolSearchFleetSortError(options.sort, toolSpansDevices);
|
|
@@ -2301,12 +2345,12 @@ limitSource) {
|
|
|
2301
2345
|
// --in-team spans directories by construction: a team's teammates run in
|
|
2302
2346
|
// their own worktrees, so scoping to the current cwd hides most of the
|
|
2303
2347
|
// lineage the flag exists to show.
|
|
2304
|
-
all: pathFilter ? undefined : options.all || wantsWholeTeam || toolSpansDevices,
|
|
2348
|
+
all: pathFilter ? undefined : options.all || wantsWholeTeam || wantsWholeRoutine || toolSpansDevices,
|
|
2305
2349
|
cwd: process.cwd(),
|
|
2306
2350
|
// Default overview scopes to the current repo SUBTREE (prefix match), so a
|
|
2307
2351
|
// monorepo shows its sub-projects grouped instead of collapsing to the one
|
|
2308
2352
|
// exact-cwd project. `--all` clears the prefix and spans the whole index.
|
|
2309
|
-
cwdPrefix: pathFilter ?? (wantsOverview && !options.all && !wantsWholeTeam && !toolSpansDevices ? process.cwd() : undefined),
|
|
2353
|
+
cwdPrefix: pathFilter ?? (wantsOverview && !options.all && !wantsWholeTeam && !wantsWholeRoutine && !toolSpansDevices ? process.cwd() : undefined),
|
|
2310
2354
|
project: options.project,
|
|
2311
2355
|
since,
|
|
2312
2356
|
until: options.until,
|
|
@@ -2339,7 +2383,7 @@ limitSource) {
|
|
|
2339
2383
|
const readOptions = {
|
|
2340
2384
|
...scope,
|
|
2341
2385
|
limit,
|
|
2342
|
-
excludeTeamOrigin: !options
|
|
2386
|
+
excludeTeamOrigin: !shouldShowTeamSessions(options),
|
|
2343
2387
|
onProgress: tracker.onProgress,
|
|
2344
2388
|
includeUnmanaged: options.unmanaged,
|
|
2345
2389
|
onHiddenUnmanaged: (n) => { hiddenUnmanaged = n; },
|
|
@@ -2356,7 +2400,7 @@ limitSource) {
|
|
|
2356
2400
|
// meta.json in ~/.agents/teams/agents whose is_team_origin flag was
|
|
2357
2401
|
// never set (legacy rows). Keep the in-memory pass so those are still
|
|
2358
2402
|
// enriched/hidden.
|
|
2359
|
-
const { visible: visibleSessions } = filterTeamSessions(sessions,
|
|
2403
|
+
const { visible: visibleSessions } = filterTeamSessions(sessions, shouldShowTeamSessions(options));
|
|
2360
2404
|
sessions = visibleSessions;
|
|
2361
2405
|
// --in-team spans both ends of the lineage, so it can't be one SQL predicate:
|
|
2362
2406
|
// the orchestrator matches on the scan-derived `spawnedTeam` column, while a
|
|
@@ -2424,7 +2468,7 @@ limitSource) {
|
|
|
2424
2468
|
}
|
|
2425
2469
|
// Under --in-team the visible list is one team, so the whole-index team-origin
|
|
2426
2470
|
// count would be a non-sequitur next to it.
|
|
2427
|
-
const hiddenCount = options
|
|
2471
|
+
const hiddenCount = shouldShowTeamSessions(options) || options.inTeam
|
|
2428
2472
|
? 0
|
|
2429
2473
|
: countSessionsInScope({ ...scope, onlyTeamOrigin: true });
|
|
2430
2474
|
// A typed routine scope must apply before smart ID routing. Otherwise an ID
|
|
@@ -4573,9 +4617,9 @@ export function registerSessionsCommands(program) {
|
|
|
4573
4617
|
# teammate row [<team>/<handle>]. --in-team narrows to one team's lineage.
|
|
4574
4618
|
agents sessions --in-team redesign --teams
|
|
4575
4619
|
|
|
4576
|
-
#
|
|
4577
|
-
agents sessions --routine
|
|
4578
|
-
agents sessions --routine nightly-review
|
|
4620
|
+
# Pick a routine across every directory, then open one of its run sessions
|
|
4621
|
+
agents sessions --routine
|
|
4622
|
+
agents sessions --routine nightly-review
|
|
4579
4623
|
agents sessions 2026-07-21T10-30-00-000Z
|
|
4580
4624
|
|
|
4581
4625
|
# Export for analysis
|
|
@@ -4623,7 +4667,7 @@ export function registerSessionsCommands(program) {
|
|
|
4623
4667
|
- --first and --last are mutually exclusive.
|
|
4624
4668
|
- A filter flag (--include/--exclude/--first/--last) without --markdown/--json defaults to --markdown output.
|
|
4625
4669
|
- --cloud sources from Rush Cloud captured runs instead of local disk.
|
|
4626
|
-
- --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.
|
|
4670
|
+
- --routine [name] spans every directory and 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.
|
|
4627
4671
|
- Without --teams, team-spawned sessions are hidden by default.
|
|
4628
4672
|
`,
|
|
4629
4673
|
});
|