@phnx-labs/agents-cli 1.22.31 → 1.22.32
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 +66 -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.d.ts +1 -1
- package/dist/commands/sessions.js +44 -10
- 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/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()) {
|
|
@@ -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. */
|
|
@@ -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';
|
|
@@ -1557,14 +1558,14 @@ export function buildRoutineRunGroups(sessions) {
|
|
|
1557
1558
|
}))
|
|
1558
1559
|
.sort((a, b) => (a.timestamp < b.timestamp ? 1 : a.timestamp > b.timestamp ? -1 : a.runId.localeCompare(b.runId)));
|
|
1559
1560
|
}
|
|
1560
|
-
export function buildRoutineChoices(sessions) {
|
|
1561
|
+
export function buildRoutineChoices(sessions, runOnlyNames = []) {
|
|
1561
1562
|
const byName = new Map();
|
|
1562
1563
|
for (const session of sessions) {
|
|
1563
1564
|
if (!session.routineName)
|
|
1564
1565
|
continue;
|
|
1565
1566
|
(byName.get(session.routineName) ?? byName.set(session.routineName, []).get(session.routineName)).push(session);
|
|
1566
1567
|
}
|
|
1567
|
-
|
|
1568
|
+
const choices = [...byName.entries()]
|
|
1568
1569
|
.map(([name, rows]) => {
|
|
1569
1570
|
const runs = buildRoutineRunGroups(rows);
|
|
1570
1571
|
return {
|
|
@@ -1573,11 +1574,28 @@ export function buildRoutineChoices(sessions) {
|
|
|
1573
1574
|
runCount: runs.length,
|
|
1574
1575
|
latestRunSessionCount: runs[0].sessions.length,
|
|
1575
1576
|
};
|
|
1576
|
-
})
|
|
1577
|
-
|
|
1577
|
+
});
|
|
1578
|
+
// Routines whose only attempts are pre-session (blocked/skipped/failed-before-
|
|
1579
|
+
// spawn) have run records but no transcript. Surface them so the picker is
|
|
1580
|
+
// built from definitions+runs, never only transcripts — otherwise a routine
|
|
1581
|
+
// that has never produced a session reads as "No routines match" (the plan).
|
|
1582
|
+
const seen = new Set(choices.map((c) => c.name));
|
|
1583
|
+
for (const name of runOnlyNames) {
|
|
1584
|
+
if (seen.has(name))
|
|
1585
|
+
continue;
|
|
1586
|
+
const runs = listRuns(name);
|
|
1587
|
+
const latest = runs[runs.length - 1];
|
|
1588
|
+
choices.push({
|
|
1589
|
+
name,
|
|
1590
|
+
lastRunAt: latest?.startedAt ?? '',
|
|
1591
|
+
runCount: runs.length,
|
|
1592
|
+
latestRunSessionCount: 0,
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
return choices.sort((a, b) => (a.lastRunAt < b.lastRunAt ? 1 : a.lastRunAt > b.lastRunAt ? -1 : a.name.localeCompare(b.name)));
|
|
1578
1596
|
}
|
|
1579
1597
|
async function selectRoutineName(sessions) {
|
|
1580
|
-
const choices = buildRoutineChoices(sessions);
|
|
1598
|
+
const choices = buildRoutineChoices(sessions, safeListJobsWithRuns());
|
|
1581
1599
|
const picked = await itemPicker({
|
|
1582
1600
|
message: 'Select a routine:',
|
|
1583
1601
|
items: choices,
|
|
@@ -1590,7 +1608,8 @@ async function selectRoutineName(sessions) {
|
|
|
1590
1608
|
labelFor: (choice) => {
|
|
1591
1609
|
const runs = `${choice.runCount} run${choice.runCount === 1 ? '' : 's'}`;
|
|
1592
1610
|
const sessions = `${choice.latestRunSessionCount} session${choice.latestRunSessionCount === 1 ? '' : 's'} in latest`;
|
|
1593
|
-
|
|
1611
|
+
const age = choice.lastRunAt ? ` · ${formatRelativeTime(choice.lastRunAt)}` : '';
|
|
1612
|
+
return `${choice.name} ${chalk.gray(`${runs} · ${sessions}${age}`)}`;
|
|
1594
1613
|
},
|
|
1595
1614
|
shortIdFor: (choice) => choice.name,
|
|
1596
1615
|
emptyMessage: 'No routines match.',
|
|
@@ -1598,8 +1617,22 @@ async function selectRoutineName(sessions) {
|
|
|
1598
1617
|
});
|
|
1599
1618
|
return picked?.item.name ?? null;
|
|
1600
1619
|
}
|
|
1620
|
+
/** `listJobsWithRuns` but never throws into the sessions reader (best-effort). */
|
|
1621
|
+
function safeListJobsWithRuns() {
|
|
1622
|
+
try {
|
|
1623
|
+
return [...new Set([...listJobs().map((job) => job.name), ...listJobsWithRuns()])];
|
|
1624
|
+
}
|
|
1625
|
+
catch {
|
|
1626
|
+
return [];
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1601
1629
|
export async function filterSessionsByRoutine(sessions, routine, interactive) {
|
|
1602
|
-
|
|
1630
|
+
// The name universe is transcripts UNION routines that have any run record —
|
|
1631
|
+
// so a routine whose attempts never produced a session still resolves.
|
|
1632
|
+
const routineNames = [...new Set([
|
|
1633
|
+
...sessions.map((session) => session.routineName).filter((name) => !!name),
|
|
1634
|
+
...safeListJobsWithRuns(),
|
|
1635
|
+
])].sort((a, b) => a.localeCompare(b));
|
|
1603
1636
|
let selectedRoutine = null;
|
|
1604
1637
|
if (typeof routine === 'string') {
|
|
1605
1638
|
selectedRoutine = resolveRoutineName(routine, routineNames);
|
|
@@ -1616,9 +1649,10 @@ export async function filterSessionsByRoutine(sessions, routine, interactive) {
|
|
|
1616
1649
|
if (!selectedRoutine)
|
|
1617
1650
|
return null;
|
|
1618
1651
|
}
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1652
|
+
if (!selectedRoutine)
|
|
1653
|
+
return sessions;
|
|
1654
|
+
const matched = sessions.filter((session) => session.routineName === selectedRoutine);
|
|
1655
|
+
return matched;
|
|
1622
1656
|
}
|
|
1623
1657
|
/** The canonical `ag sessions …` command for a set of flags — the twin of the
|
|
1624
1658
|
* browser's `y` hotkey (see --print-cmd). Normalizes to the stable flag form. */
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { formatAgentError, resolveAgentName } from '../lib/agents.js';
|
|
3
|
+
import { setHelpSections } from '../lib/help.js';
|
|
4
|
+
import { describeInstallation, listInstallations, resolveInstallation, selectUpdateStrategy, supportsPinnedUpdate, updateInstallation, } from '../lib/installations/index.js';
|
|
5
|
+
/**
|
|
6
|
+
* Split `<agent>[@<selector>]`. The selector names an INSTALLATION — its frozen
|
|
7
|
+
* label, or the release it currently carries — never a release to install; that
|
|
8
|
+
* is `--to`. Keeping them separate is what lets `agents update claude@2.0.65
|
|
9
|
+
* --to 2.0.71` read unambiguously.
|
|
10
|
+
*/
|
|
11
|
+
function parseTarget(raw) {
|
|
12
|
+
const at = raw.indexOf('@');
|
|
13
|
+
const name = at === -1 ? raw : raw.slice(0, at);
|
|
14
|
+
const selector = at === -1 ? undefined : raw.slice(at + 1).trim();
|
|
15
|
+
if (at !== -1 && !selector) {
|
|
16
|
+
throw new Error(`Missing installation in '${raw}'. Use <agent>@<installed-version>, or just <agent>.`);
|
|
17
|
+
}
|
|
18
|
+
const agent = resolveAgentName(name);
|
|
19
|
+
if (!agent)
|
|
20
|
+
throw new Error(formatAgentError(name));
|
|
21
|
+
return { agent, selector };
|
|
22
|
+
}
|
|
23
|
+
function serialize(installation) {
|
|
24
|
+
return {
|
|
25
|
+
id: installation.id,
|
|
26
|
+
agent: installation.agent,
|
|
27
|
+
label: installation.label,
|
|
28
|
+
releaseVersion: installation.releaseVersion,
|
|
29
|
+
createdAt: installation.createdAt,
|
|
30
|
+
updatedAt: installation.updatedAt,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function printOutcome(outcome, json) {
|
|
34
|
+
if (json) {
|
|
35
|
+
console.log(JSON.stringify({
|
|
36
|
+
installation: serialize(outcome.installation),
|
|
37
|
+
strategy: outcome.strategy,
|
|
38
|
+
fromRelease: outcome.fromRelease,
|
|
39
|
+
toRelease: outcome.toRelease,
|
|
40
|
+
unchanged: outcome.unchanged,
|
|
41
|
+
alsoUpdated: outcome.alsoUpdated.map(serialize),
|
|
42
|
+
}, null, 2));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const name = `${outcome.installation.agent}@${outcome.installation.label}`;
|
|
46
|
+
if (outcome.unchanged) {
|
|
47
|
+
console.log(chalk.gray(`${name} is already on release ${outcome.toRelease}.`));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
console.log(chalk.green(`Updated ${name}: release ${outcome.fromRelease} -> ${outcome.toRelease}`));
|
|
51
|
+
console.log(chalk.gray(`Its name is unchanged, so every default, project pin, and routine that names ${name} still resolves to it.`));
|
|
52
|
+
for (const other of outcome.alsoUpdated) {
|
|
53
|
+
console.log(chalk.gray(` ${other.agent}@${other.label} shares the same binary and now also reports release ${other.releaseVersion}.`));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function printInstallations(agent, json) {
|
|
57
|
+
const installations = listInstallations(agent);
|
|
58
|
+
if (json) {
|
|
59
|
+
console.log(JSON.stringify(installations.map(serialize), null, 2));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (!installations.length) {
|
|
63
|
+
console.log(chalk.gray(`No managed ${agent} installations. Install one with: agents add ${agent}@latest`));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
console.log(chalk.bold(`${agent} installations\n`));
|
|
67
|
+
for (const installation of installations) {
|
|
68
|
+
console.log(` ${chalk.cyan(installation.label)} release ${installation.releaseVersion} ${chalk.gray(installation.id)}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Surface a failure as one red line, not a stack trace. Everything this command
|
|
73
|
+
* can fail on — an unknown agent, an ambiguous selector, an unpinnable harness,
|
|
74
|
+
* a release that would not launch — is a message the user acts on, and a
|
|
75
|
+
* commander async action rejection otherwise reaches the user as a raw Node dump.
|
|
76
|
+
*/
|
|
77
|
+
function fail(err) {
|
|
78
|
+
console.error(chalk.red(err.message));
|
|
79
|
+
process.exitCode = 1;
|
|
80
|
+
}
|
|
81
|
+
export function registerUpdateCommand(program) {
|
|
82
|
+
const update = program
|
|
83
|
+
.command('update [target]')
|
|
84
|
+
.description('Move a frozen agent installation to a new release, keeping its name and every reference to it')
|
|
85
|
+
.option('--to <release>', 'Release to move to: latest (default), oldest, or an exact version')
|
|
86
|
+
.option('--account <label>', 'Disambiguate by signed-in account when several installations match')
|
|
87
|
+
.option('--json', 'Machine-readable result')
|
|
88
|
+
.action(async (target, options) => {
|
|
89
|
+
try {
|
|
90
|
+
if (!target) {
|
|
91
|
+
throw new Error('Which agent? Use: agents update <agent>[@<installed-version>]');
|
|
92
|
+
}
|
|
93
|
+
const { agent, selector } = parseTarget(target);
|
|
94
|
+
if (options.to && options.to !== 'latest' && !supportsPinnedUpdate(agent)) {
|
|
95
|
+
// Fail loud at the boundary rather than installing the current release
|
|
96
|
+
// and reporting it as the pin that was asked for.
|
|
97
|
+
throw new Error(`${agent} is a single self-updating binary with no pinnable releases — drop --to, or pass --to latest.`);
|
|
98
|
+
}
|
|
99
|
+
const installation = await resolveInstallation(agent, selector, { account: options.account });
|
|
100
|
+
const strategy = selectUpdateStrategy(agent);
|
|
101
|
+
if (!options.json && !strategy.transactional) {
|
|
102
|
+
console.log(chalk.yellow(`${agent} installs one vendor-managed binary, so this update cannot be staged or rolled back; `
|
|
103
|
+
+ `a failure leaves whatever its installer wrote.`));
|
|
104
|
+
}
|
|
105
|
+
if (!options.json) {
|
|
106
|
+
console.log(chalk.gray(`Updating ${agent}@${describeInstallation(installation)} via the ${strategy.id} strategy...`));
|
|
107
|
+
}
|
|
108
|
+
const outcome = await updateInstallation(installation, {
|
|
109
|
+
to: options.to,
|
|
110
|
+
onProgress: options.json ? undefined : (message) => console.log(chalk.gray(` ${message}`)),
|
|
111
|
+
});
|
|
112
|
+
printOutcome(outcome, !!options.json);
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
fail(err);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
update
|
|
119
|
+
.command('list <agent>')
|
|
120
|
+
.description('Show every frozen installation of an agent and the release each carries')
|
|
121
|
+
.option('--json', 'Machine-readable listing')
|
|
122
|
+
// `--json` is declared on both `update` and `update list`, and commander
|
|
123
|
+
// binds a flag the parent also declares to the PARENT's option store — so
|
|
124
|
+
// reading this subcommand's own opts alone silently drops it. Merge them.
|
|
125
|
+
.action((rawAgent, _options, command) => {
|
|
126
|
+
try {
|
|
127
|
+
const agent = resolveAgentName(rawAgent);
|
|
128
|
+
if (!agent)
|
|
129
|
+
throw new Error(formatAgentError(rawAgent));
|
|
130
|
+
printInstallations(agent, !!command.optsWithGlobals().json);
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
fail(err);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
setHelpSections(update, {
|
|
137
|
+
examples: `agents update list claude
|
|
138
|
+
agents update claude@2.0.65
|
|
139
|
+
agents update claude@2.0.65 --to 2.1.220
|
|
140
|
+
agents update claude --account work
|
|
141
|
+
agents update claude@2.0.65 --json`,
|
|
142
|
+
notes: 'An installation keeps its name for life; only the release inside it moves. That is why a default, a project pin, '
|
|
143
|
+
+ 'a routine version, or a profile that names claude@2.0.65 keeps working after you update it. '
|
|
144
|
+
+ 'The selector matches either the installation name or the release it currently carries — when two installations '
|
|
145
|
+
+ 'share a release, name one or pass --account <label> (see: agents accounts). '
|
|
146
|
+
+ 'The new release is fetched and launched before it replaces the working one, so a bad release leaves your agent running.',
|
|
147
|
+
});
|
|
148
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -94,7 +94,7 @@ if (IS_DEV_BUILD) {
|
|
|
94
94
|
// module on each invocation (which loaded the whole ~50-module tree before the
|
|
95
95
|
// first byte of output), the registry maps a command name to a thunk that
|
|
96
96
|
// imports only what that command needs. See src/lib/startup/command-registry.ts.
|
|
97
|
-
import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadMemory, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadExport, loadPackages, loadRoutines, loadDaemon, loadMonitors, loadProjects, loadRun, loadFork, loadDefaults, loadSet, loadModels, loadModes, loadPrune, loadTrash, loadRestore, loadDoctor, loadApply, loadStatus, loadSnapshot, loadProfiles, loadHarness, loadSecrets, loadLogin, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadFactory, loadUsage, loadCost, loadInsights, loadPerf, loadTrends, loadOutput, loadBudget, loadAlias, loadMine, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadWebhook, loadFunnel, loadHumans, loadAccounts, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadUninstall, loadBench, loadShare, loadSend, loadFeed, loadMailboxes, } from './lib/startup/command-registry.js';
|
|
97
|
+
import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadMemory, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadUpdate, loadImport, loadExport, loadPackages, loadRoutines, loadDaemon, loadMonitors, loadProjects, loadRun, loadFork, loadDefaults, loadSet, loadModels, loadModes, loadPrune, loadTrash, loadRestore, loadDoctor, loadApply, loadStatus, loadSnapshot, loadProfiles, loadHarness, loadSecrets, loadLogin, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadFactory, loadUsage, loadCost, loadInsights, loadPerf, loadTrends, loadOutput, loadBudget, loadAlias, loadMine, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadWebhook, loadFunnel, loadHumans, loadAccounts, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadUninstall, loadBench, loadShare, loadSend, loadFeed, loadMailboxes, } from './lib/startup/command-registry.js';
|
|
98
98
|
import { applyGlobalHelpConventions } from './lib/help.js';
|
|
99
99
|
import { renderWhatsNew } from './lib/whats-new.js';
|
|
100
100
|
import { getCliLaunch } from './lib/cli-entry.js';
|
|
@@ -292,6 +292,7 @@ Quick start:
|
|
|
292
292
|
Agent versions:
|
|
293
293
|
add <agent>[@version] Install an agent CLI (e.g. agents add grok or agents add codex)
|
|
294
294
|
import <agent> Adopt an existing global install (npm/homebrew) into agents-cli
|
|
295
|
+
update <agent>[@version] Move an installed agent to a new release, keeping its name (agents-cli itself is 'agents upgrade')
|
|
295
296
|
prune <agent>[@version] Uninstall a version
|
|
296
297
|
remove <agent>[@version] Alias for prune
|
|
297
298
|
use <agent>@<version> Set the default version
|
|
@@ -942,6 +943,7 @@ async function registerAllEagerCommands() {
|
|
|
942
943
|
await reg(loadWorkflows);
|
|
943
944
|
await reg(loadWorktree);
|
|
944
945
|
await reg(loadVersions);
|
|
946
|
+
await reg(loadUpdate);
|
|
945
947
|
await reg(loadImport);
|
|
946
948
|
await reg(loadExport);
|
|
947
949
|
await reg(loadPackages);
|
package/dist/lib/catchup.js
CHANGED
|
@@ -139,7 +139,10 @@ export async function runCatchup(opts = {}) {
|
|
|
139
139
|
continue;
|
|
140
140
|
}
|
|
141
141
|
try {
|
|
142
|
-
|
|
142
|
+
// No `scheduledFor` here on purpose: the missed slot is already claimed by
|
|
143
|
+
// `claimMissedFire` above (its atomic mkdir IS the catch-up single-fire), so
|
|
144
|
+
// the late run gets a fresh id rather than colliding with the missed record.
|
|
145
|
+
const meta = await executeJobDetached(config, undefined, { kind: 'catchup' });
|
|
143
146
|
outcomes.push({
|
|
144
147
|
name: entry.name,
|
|
145
148
|
expectedAt: entry.expectedAt,
|
package/dist/lib/daemon.d.ts
CHANGED
|
@@ -273,6 +273,23 @@ export interface DaemonStopResult {
|
|
|
273
273
|
surviving: string[];
|
|
274
274
|
detachedChildren: number[];
|
|
275
275
|
}
|
|
276
|
+
/**
|
|
277
|
+
* Live `__daemon-run` processes still registered in THIS state dir's instance
|
|
278
|
+
* registry, excluding `exclude`. State-dir-scoped by construction: the registry
|
|
279
|
+
* lives inside this daemon dir, so a daemon serving a DIFFERENT state dir (a test
|
|
280
|
+
* fixture with its own HOME, a separate install/home) registers elsewhere and is
|
|
281
|
+
* invisible here — it is never a stop/takeover target. POSIX-only (the registry
|
|
282
|
+
* and its `ps` liveness probe are); `[]` on Windows.
|
|
283
|
+
*
|
|
284
|
+
* Exported for `agents daemon status`/`doctor`/`services` (RUSH-2368): those
|
|
285
|
+
* commands previously flagged every `__daemon-run` on the box (a raw `ps` scan)
|
|
286
|
+
* as a "duplicate" of this daemon, which misreported test fixtures under their
|
|
287
|
+
* own HOME — and therefore their own state dir and registry — as strays to
|
|
288
|
+
* kill. This registry read is the same scope the reaper (`reapStrayDaemons`)
|
|
289
|
+
* and the stop postcondition (`stopDaemon`) already use, so the display and the
|
|
290
|
+
* reaper agree on what a duplicate is.
|
|
291
|
+
*/
|
|
292
|
+
export declare function findSurvivingStateDirDaemons(exclude: Set<number>): number[];
|
|
276
293
|
/**
|
|
277
294
|
* Stop the daemon and ASSERT its postcondition (SING-12, RUSH-2355), unloading it
|
|
278
295
|
* from launchd/systemd if applicable.
|