@phnx-labs/agents-cli 1.22.33 → 1.22.34
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 +49 -0
- package/README.md +2 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/accounts.js +3 -3
- package/dist/commands/browser-sessions-picker.d.ts +16 -0
- package/dist/commands/browser-sessions-picker.js +179 -0
- package/dist/commands/browser.js +9 -4
- package/dist/commands/hosts.js +1 -5
- package/dist/commands/inspect.js +174 -41
- package/dist/commands/message.d.ts +6 -1
- package/dist/commands/message.js +60 -3
- package/dist/commands/sessions.d.ts +54 -4
- package/dist/commands/sessions.js +252 -46
- package/dist/commands/ssh.js +17 -5
- package/dist/commands/teams.d.ts +28 -0
- package/dist/commands/teams.js +148 -13
- package/dist/commands/upgrade.d.ts +7 -0
- package/dist/commands/upgrade.js +10 -0
- package/dist/commands/watchdog.d.ts +2 -0
- package/dist/commands/watchdog.js +112 -27
- package/dist/index.js +51 -59
- package/dist/lib/agents.js +6 -0
- package/dist/lib/browser/sessions-list.d.ts +81 -0
- package/dist/lib/browser/sessions-list.js +179 -4
- package/dist/lib/daemon.js +45 -5
- package/dist/lib/devices/connect.d.ts +33 -0
- package/dist/lib/devices/connect.js +61 -3
- package/dist/lib/devices/doctor-findings.d.ts +4 -2
- package/dist/lib/devices/doctor-findings.js +4 -2
- package/dist/lib/exec.js +63 -6
- package/dist/lib/help.d.ts +3 -2
- package/dist/lib/help.js +4 -0
- package/dist/lib/hosts/dispatch.d.ts +2 -32
- package/dist/lib/hosts/dispatch.js +6 -61
- package/dist/lib/hosts/tasks.d.ts +7 -0
- package/dist/lib/hosts/tasks.js +9 -0
- package/dist/lib/mailbox-target.d.ts +27 -0
- package/dist/lib/mailbox-target.js +21 -0
- package/dist/lib/mcp.d.ts +10 -0
- package/dist/lib/mcp.js +21 -2
- 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 +11 -0
- package/dist/lib/migrate.js +40 -0
- package/dist/lib/project-root.d.ts +47 -0
- package/dist/lib/project-root.js +68 -0
- 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 +9 -2
- package/dist/lib/secrets/agent.js +52 -7
- package/dist/lib/secrets/reaper.d.ts +24 -3
- package/dist/lib/secrets/reaper.js +55 -6
- package/dist/lib/session/discover.js +12 -0
- package/dist/lib/session/render.d.ts +2 -0
- package/dist/lib/session/render.js +1 -1
- package/dist/lib/shims.js +9 -2
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +8 -3
- package/dist/lib/startup/root-command.d.ts +3 -0
- package/dist/lib/startup/root-command.js +10 -0
- package/dist/lib/teams/agents.d.ts +136 -6
- package/dist/lib/teams/agents.js +324 -58
- package/dist/lib/teams/worktree.d.ts +39 -2
- package/dist/lib/teams/worktree.js +60 -4
- package/dist/lib/types.d.ts +11 -0
- package/dist/lib/versions.js +2 -2
- package/dist/lib/watchdog/history.d.ts +20 -0
- package/dist/lib/watchdog/history.js +46 -0
- package/dist/lib/watchdog/log.d.ts +16 -1
- package/dist/lib/watchdog/log.js +82 -2
- package/dist/lib/watchdog/runner.d.ts +12 -0
- package/dist/lib/watchdog/runner.js +20 -0
- package/package.json +1 -1
package/dist/commands/teams.js
CHANGED
|
@@ -15,7 +15,7 @@ import { runPrWatch, DEFAULT_MAX_WAVES, } from '../lib/teams/pr-watch.js';
|
|
|
15
15
|
import { handleSpawn, handleStatus, handleStop, toTaskStatusSummary, } from '../lib/teams/api.js';
|
|
16
16
|
import { createTeam, ensureTeam, getTeam, loadTeams, removeTeam, teamExists, } from '../lib/teams/registry.js';
|
|
17
17
|
import { setHelpSections } from '../lib/help.js';
|
|
18
|
-
import { createWorktree, isGitRepo, hasUncommittedChanges, removeWorktree, } from '../lib/teams/worktree.js';
|
|
18
|
+
import { createWorktree, isGitRepo, hasUncommittedChanges, removeWorktree, worktreeCheckoutExists, worktreeExists, } from '../lib/teams/worktree.js';
|
|
19
19
|
import { resolveHost } from '../lib/hosts/registry.js';
|
|
20
20
|
import { isDeviceAuto, resolveDeviceAuto } from '../lib/smart-launch.js';
|
|
21
21
|
import { sshTargetFor } from '../lib/hosts/types.js';
|
|
@@ -251,6 +251,54 @@ function factoryWorkerPreamble(team, name, taskType, after) {
|
|
|
251
251
|
function mkManager() {
|
|
252
252
|
return new AgentManager();
|
|
253
253
|
}
|
|
254
|
+
/**
|
|
255
|
+
* Tell the user a worktree from a failed `teams add` is still on disk, and give
|
|
256
|
+
* them the exact commands to remove it. Reached only when we could not remove it
|
|
257
|
+
* ourselves — a leftover `agents/<name>` branch is what makes the retry fail with
|
|
258
|
+
* `fatal: a branch ... already exists`, so it must never be silent (RUSH-2356).
|
|
259
|
+
*/
|
|
260
|
+
/**
|
|
261
|
+
* Tear down a worktree a failed operation left behind — but ONLY if it is a
|
|
262
|
+
* genuine orphan.
|
|
263
|
+
*
|
|
264
|
+
* Extracted because this guard was hand-duplicated at two call sites (`teams
|
|
265
|
+
* add` and the pr-watch fixer) and the copy at the fixer path was written
|
|
266
|
+
* WITHOUT it, which is RUSH-2356 reoccurring at the one site that skipped the
|
|
267
|
+
* check. One implementation, one place to get it right.
|
|
268
|
+
*
|
|
269
|
+
* The two errors here are not symmetric, so the guard deliberately fails
|
|
270
|
+
* CLOSED: a spurious "claimed" strands a branch a human can delete, while a
|
|
271
|
+
* spurious "unclaimed" runs `git worktree remove --force` over a live
|
|
272
|
+
* teammate's checkout and destroys uncommitted work. So an unreadable record
|
|
273
|
+
* means "leave it alone and tell the user", never "assume it is free".
|
|
274
|
+
*
|
|
275
|
+
* A staged teammate is exactly the case that makes this necessary: `spawn()`
|
|
276
|
+
* persists its meta.json and only THEN runs the retention pass, which refreshes
|
|
277
|
+
* every sibling and can throw on a distributed one — so a failure can land here
|
|
278
|
+
* with a live, durably-recorded, merely-pending `--after` teammate already
|
|
279
|
+
* owning this worktree.
|
|
280
|
+
*/
|
|
281
|
+
export async function tearDownOrphanWorktree(mgr, baseCwd, name) {
|
|
282
|
+
try {
|
|
283
|
+
if (await mgr.isWorktreeClaimed(name))
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
warnOrphanWorktree(baseCwd, name, 'the teammate record could not be read');
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
try {
|
|
291
|
+
await removeWorktree(baseCwd, name);
|
|
292
|
+
}
|
|
293
|
+
catch (cleanupErr) {
|
|
294
|
+
warnOrphanWorktree(baseCwd, name, cleanupErr.message);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function warnOrphanWorktree(baseCwd, name, reason) {
|
|
298
|
+
process.stderr.write(chalk.yellow(`\nWarning: the worktree '${name}' from this failed add is still on disk — ${reason}.\n` +
|
|
299
|
+
` Remove it manually: git -C ${baseCwd} worktree remove --force .agents/worktrees/${name} ` +
|
|
300
|
+
`&& git -C ${baseCwd} branch -D agents/${name}\n`));
|
|
301
|
+
}
|
|
254
302
|
/**
|
|
255
303
|
* Register the generic cloud dispatcher — staged cloud teammates get
|
|
256
304
|
* dispatched when their --after deps resolve, using repo/branch stored on
|
|
@@ -476,7 +524,27 @@ async function reactWithTeammate(mgr, team, action, prompt) {
|
|
|
476
524
|
cwd = baseCwd;
|
|
477
525
|
}
|
|
478
526
|
}
|
|
479
|
-
|
|
527
|
+
let result;
|
|
528
|
+
try {
|
|
529
|
+
result = await handleSpawn(mgr, team, 'claude', prompt, cwd, 'edit', 'medium', null, cwd, null, name, after, null, null, taskType, null, null, null, null, worktreeName, worktreePath);
|
|
530
|
+
}
|
|
531
|
+
catch (err) {
|
|
532
|
+
// The spawn failed after we created this fixer's worktree — tear it down so
|
|
533
|
+
// the branch `agents/<name>` doesn't block the next wave's retry with
|
|
534
|
+
// `fatal: a branch ... already exists` (RUSH-2356). Best-effort; the
|
|
535
|
+
// original failure is what propagates.
|
|
536
|
+
//
|
|
537
|
+
// Guarded exactly like the `teams add` teardown, and for the same reason:
|
|
538
|
+
// handleSpawn -> manager.spawn() persists a staged teammate's meta.json and
|
|
539
|
+
// only THEN runs the retention pass, which refreshes every sibling and can
|
|
540
|
+
// throw on a distributed one. A fixer teammate is staged with `--after` when
|
|
541
|
+
// it follows a source teammate, so a throw there lands here with a LIVE,
|
|
542
|
+
// durably-recorded, merely-pending teammate already owning this worktree —
|
|
543
|
+
// and removing it would destroy real work. Only tear down a genuine orphan.
|
|
544
|
+
if (worktreeName)
|
|
545
|
+
await tearDownOrphanWorktree(mgr, baseCwd, worktreeName);
|
|
546
|
+
throw err;
|
|
547
|
+
}
|
|
480
548
|
return result.name ?? shortId(result.agent_id);
|
|
481
549
|
}
|
|
482
550
|
// Pick the display handle for a teammate: explicit teammate name, Claude
|
|
@@ -1489,6 +1557,31 @@ export function registerTeamsCommands(program) {
|
|
|
1489
1557
|
const sharedWorktree = teamMeta?.use_worktree ?? null;
|
|
1490
1558
|
let worktreeName = null;
|
|
1491
1559
|
let worktreePath = null;
|
|
1560
|
+
const mgr = mkManager();
|
|
1561
|
+
// Validate name uniqueness + --after deps BEFORE creating a worktree
|
|
1562
|
+
// (RUSH-2356): a rejected add must not leave an orphan `agents/<name>`
|
|
1563
|
+
// branch that then breaks the retry with `fatal: a branch ... already
|
|
1564
|
+
// exists`. spawn() calls the same method, which memoizes this result for
|
|
1565
|
+
// exactly one consumer — so validation still lives in one place without
|
|
1566
|
+
// paying for a second sibling status refresh on the way there.
|
|
1567
|
+
try {
|
|
1568
|
+
await mgr.validateAddPreconditions(team, opts.name ?? null, after);
|
|
1569
|
+
}
|
|
1570
|
+
catch (err) {
|
|
1571
|
+
dieFriction('teams', 'add-precondition-failed', err.message);
|
|
1572
|
+
}
|
|
1573
|
+
// Track a worktree WE create in this add so a later failure (dep race,
|
|
1574
|
+
// launch error, cloud dispatch) can tear it down instead of stranding the
|
|
1575
|
+
// branch. Only a worktree THIS add created is ever torn down, so a
|
|
1576
|
+
// legitimately-pending teammate's worktree is never touched.
|
|
1577
|
+
let createdWorktree = null;
|
|
1578
|
+
const tearDownCreatedWorktree = async () => {
|
|
1579
|
+
if (!createdWorktree)
|
|
1580
|
+
return;
|
|
1581
|
+
const { baseCwd, name } = createdWorktree;
|
|
1582
|
+
createdWorktree = null;
|
|
1583
|
+
await tearDownOrphanWorktree(mgr, baseCwd, name);
|
|
1584
|
+
};
|
|
1492
1585
|
if (hostName) {
|
|
1493
1586
|
// Distributed teammate: the checkout lives on the host, so we NEVER touch
|
|
1494
1587
|
// the local filesystem here. A shared local worktree makes no sense for a
|
|
@@ -1539,12 +1632,43 @@ export function registerTeamsCommands(program) {
|
|
|
1539
1632
|
if (!(await isGitRepo(baseCwd))) {
|
|
1540
1633
|
dieFriction('teams', 'worktree-needs-repo', `Worktrees require a git repository. ${baseCwd} is not inside a git repo.`);
|
|
1541
1634
|
}
|
|
1635
|
+
// Was anything under this name already here BEFORE we tried? That is
|
|
1636
|
+
// the only sound basis for cleaning up a failed create: the common
|
|
1637
|
+
// failure is `fatal: a branch named 'agents/<name>' already exists`,
|
|
1638
|
+
// and removing a checkout we did not make can destroy uncommitted work
|
|
1639
|
+
// (`teams stop` deliberately KEEPS a dirty worktree, and that teammate's
|
|
1640
|
+
// record is terminal by then, so no record-based check protects it).
|
|
1641
|
+
const preexisting = await worktreeExists(baseCwd, opts.worktree).catch(() => true);
|
|
1542
1642
|
try {
|
|
1543
1643
|
worktreeName = opts.worktree;
|
|
1544
1644
|
worktreePath = await createWorktree(baseCwd, worktreeName);
|
|
1645
|
+
createdWorktree = { baseCwd, name: worktreeName };
|
|
1545
1646
|
}
|
|
1546
1647
|
catch (err) {
|
|
1547
|
-
|
|
1648
|
+
// `git worktree add -b` can also fail PART WAY — branch ref created,
|
|
1649
|
+
// checkout not — stranding `agents/<name>` in exactly the way that
|
|
1650
|
+
// breaks the retry. Clean that up only when nothing was here before
|
|
1651
|
+
// AND there is still no checkout now: all that can be left is a
|
|
1652
|
+
// dangling branch ref, so this can never delete anyone's files. The
|
|
1653
|
+
// second half matters because a concurrent add can create a real
|
|
1654
|
+
// worktree under this name during our `git fetch`, long after the
|
|
1655
|
+
// pre-flight probe answered.
|
|
1656
|
+
const checkoutNow = await worktreeCheckoutExists(baseCwd, opts.worktree).catch(() => true);
|
|
1657
|
+
if (!preexisting && !checkoutNow) {
|
|
1658
|
+
try {
|
|
1659
|
+
await removeWorktree(baseCwd, opts.worktree);
|
|
1660
|
+
}
|
|
1661
|
+
catch {
|
|
1662
|
+
// Nothing to remove (the add died before git wrote anything), or
|
|
1663
|
+
// we can't. Either way the real error below is what matters.
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
const detail = err.message;
|
|
1667
|
+
const hint = /already exists/.test(detail)
|
|
1668
|
+
? `\n Another teammate already owns the worktree '${opts.worktree}'. Pick a different --worktree name,` +
|
|
1669
|
+
` or free this one: agents teams status ${team}`
|
|
1670
|
+
: '';
|
|
1671
|
+
dieFriction('teams', 'worktree-create-failed', `Failed to create worktree '${opts.worktree}': ${detail}${hint}`);
|
|
1548
1672
|
}
|
|
1549
1673
|
}
|
|
1550
1674
|
else if (opts.worktree) {
|
|
@@ -1554,7 +1678,6 @@ export function registerTeamsCommands(program) {
|
|
|
1554
1678
|
// host (repoPath / the remote worktree). Local teammates default to the
|
|
1555
1679
|
// worktree path, then --cwd, then the current directory.
|
|
1556
1680
|
const cwd = hostName ? null : (worktreePath ?? opts.cwd ?? process.cwd());
|
|
1557
|
-
const mgr = mkManager();
|
|
1558
1681
|
// Factory teammates: prepend the worker-skill preamble to every task
|
|
1559
1682
|
// prompt so implementers/testers/reviewers know about the Ledger, the
|
|
1560
1683
|
// dynamic DAG, and the pattern for filing new tasks mid-flight. No
|
|
@@ -1586,20 +1709,28 @@ export function registerTeamsCommands(program) {
|
|
|
1586
1709
|
if (cloudProviderId && !isStaged) {
|
|
1587
1710
|
// Ready to run now: dispatch to the cloud provider before registering
|
|
1588
1711
|
// the teammate so we have the remote session id up front.
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
branch: opts.branch,
|
|
1595
|
-
model: opts.model,
|
|
1596
|
-
env: shareRuntimeEnv(),
|
|
1597
|
-
};
|
|
1712
|
+
//
|
|
1713
|
+
// resolveProvider() and shareRuntimeEnv() are INSIDE the try on purpose:
|
|
1714
|
+
// both read config (the cloud registry; `agents.yaml`'s share block) and
|
|
1715
|
+
// both throw on a malformed one, and by this point the worktree already
|
|
1716
|
+
// exists — leaving them outside stranded the branch with no catch at all.
|
|
1598
1717
|
try {
|
|
1718
|
+
const prov = resolveProvider(cloudProviderId);
|
|
1719
|
+
const dispatchOpts = {
|
|
1720
|
+
prompt: effectiveTask,
|
|
1721
|
+
agent,
|
|
1722
|
+
repo: opts.repo,
|
|
1723
|
+
branch: opts.branch,
|
|
1724
|
+
model: opts.model,
|
|
1725
|
+
env: shareRuntimeEnv(),
|
|
1726
|
+
};
|
|
1599
1727
|
const cloudTask = await prov.dispatch(dispatchOpts);
|
|
1600
1728
|
cloudSessionId = cloudTask.id;
|
|
1601
1729
|
}
|
|
1602
1730
|
catch (err) {
|
|
1731
|
+
// Same orphan class as a failed spawn: this add already created its
|
|
1732
|
+
// worktree, so a dispatch failure must not strand the branch either.
|
|
1733
|
+
await tearDownCreatedWorktree();
|
|
1603
1734
|
dieFriction('teams', 'cloud-dispatch-failed', `Cloud dispatch failed: ${err.message}`);
|
|
1604
1735
|
}
|
|
1605
1736
|
}
|
|
@@ -1654,6 +1785,10 @@ export function registerTeamsCommands(program) {
|
|
|
1654
1785
|
}
|
|
1655
1786
|
}
|
|
1656
1787
|
catch (err) {
|
|
1788
|
+
// The add failed after we created its worktree — tear the worktree +
|
|
1789
|
+
// branch down so a retry isn't blocked by `fatal: a branch ... already
|
|
1790
|
+
// exists` (RUSH-2356). Best-effort: report the original failure either way.
|
|
1791
|
+
await tearDownCreatedWorktree();
|
|
1657
1792
|
dieFriction('teams', 'add-failed', `Could not add ${fullName(agent, version)} to ${team}: ${err.message}`);
|
|
1658
1793
|
}
|
|
1659
1794
|
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
export interface UpgradeOptions {
|
|
3
|
+
yes?: boolean;
|
|
4
|
+
}
|
|
5
|
+
export type UpgradeAction = (version: string | undefined, options: UpgradeOptions) => Promise<void>;
|
|
6
|
+
/** Register the public self-upgrade surface; the entry point supplies its runtime action. */
|
|
7
|
+
export declare function registerUpgradeCommand(program: Command, action?: UpgradeAction): Command;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Register the public self-upgrade surface; the entry point supplies its runtime action. */
|
|
2
|
+
export function registerUpgradeCommand(program, action) {
|
|
3
|
+
const command = program.command('upgrade')
|
|
4
|
+
.description('Upgrade agents-cli to the latest version (or a specific [version])')
|
|
5
|
+
.argument('[version]', 'Target version or dist-tag to install (default: latest)')
|
|
6
|
+
.option('-y, --yes', 'Install without an interactive confirmation prompt');
|
|
7
|
+
if (action)
|
|
8
|
+
command.action(action);
|
|
9
|
+
return command;
|
|
10
|
+
}
|
|
@@ -17,5 +17,7 @@
|
|
|
17
17
|
* persisted state; it never runs a tick.
|
|
18
18
|
*/
|
|
19
19
|
import type { Command } from 'commander';
|
|
20
|
+
import { type WatchdogTickResult } from '../lib/watchdog/runner.js';
|
|
21
|
+
export declare function formatWatchdogTickLines(result: WatchdogTickResult, willInject: boolean, verbose?: boolean): string[];
|
|
20
22
|
/** Register the `agents watchdog` command tree. */
|
|
21
23
|
export declare function registerWatchdogCommand(program: Command): void;
|
|
@@ -25,6 +25,8 @@ import { getConfigValue, setConfigValue } from '../lib/device-config.js';
|
|
|
25
25
|
import { writePolicySentinel, DEFAULT_THRESHOLDS, } from '../lib/watchdog/runner.js';
|
|
26
26
|
import { isWatchdogRotateEnabled, listRotateStates, setWatchdogRotateEnabled } from '../lib/watchdog/rotate.js';
|
|
27
27
|
import { loadWatchdogSessions, runWatchdogPass } from '../lib/watchdog/service.js';
|
|
28
|
+
import { readWatchdogEvents, WATCHDOG_LOG_PATH } from '../lib/watchdog/log.js';
|
|
29
|
+
import { selectWatchdogHistory } from '../lib/watchdog/history.js';
|
|
28
30
|
/** Default state dir the runner and these subcommands share. */
|
|
29
31
|
function stateDir() {
|
|
30
32
|
return path.join(getRuntimeStateDir(), 'watchdog');
|
|
@@ -57,39 +59,75 @@ function humanMs(ms) {
|
|
|
57
59
|
return `${Math.round(ms / 60_000)}m`;
|
|
58
60
|
return `${Math.round(ms / 1000)}s`;
|
|
59
61
|
}
|
|
60
|
-
function colorForOutcome(o) {
|
|
61
|
-
if (o.injected)
|
|
62
|
-
return chalk.green;
|
|
63
|
-
if (o.decision === 'rotate')
|
|
64
|
-
return chalk.magenta;
|
|
65
|
-
if (o.addressable === false)
|
|
66
|
-
return chalk.yellow;
|
|
67
|
-
if (o.stall === 'stalled' && o.decision === 'nudge')
|
|
68
|
-
return chalk.cyan;
|
|
69
|
-
return chalk.dim;
|
|
70
|
-
}
|
|
71
62
|
/** Render one tick's outcomes as a human status block. */
|
|
72
|
-
function
|
|
63
|
+
function elapsedLabel(atMs, eventMs) {
|
|
64
|
+
const seconds = Math.max(0, Math.round((atMs - eventMs) / 1000));
|
|
65
|
+
if (seconds < 60)
|
|
66
|
+
return `${seconds} second${seconds === 1 ? '' : 's'} ago`;
|
|
67
|
+
const minutes = Math.round(seconds / 60);
|
|
68
|
+
if (minutes < 60)
|
|
69
|
+
return `${minutes} minute${minutes === 1 ? '' : 's'} ago`;
|
|
70
|
+
const hours = Math.round(minutes / 60);
|
|
71
|
+
if (hours < 48)
|
|
72
|
+
return `${hours} hour${hours === 1 ? '' : 's'} ago`;
|
|
73
|
+
const days = Math.round(hours / 24);
|
|
74
|
+
return `${days} day${days === 1 ? '' : 's'} ago`;
|
|
75
|
+
}
|
|
76
|
+
function isAttentionOutcome(outcome) {
|
|
77
|
+
return !outcome.sessionId
|
|
78
|
+
|| outcome.lastActivityMs === undefined
|
|
79
|
+
|| outcome.stall === 'stalled'
|
|
80
|
+
|| outcome.decision !== 'skip'
|
|
81
|
+
|| outcome.injected === true
|
|
82
|
+
|| outcome.addressable === false
|
|
83
|
+
|| outcome.rotatePhase === 'failed';
|
|
84
|
+
}
|
|
85
|
+
export function formatWatchdogTickLines(result, willInject, verbose = false) {
|
|
73
86
|
const { counts } = result;
|
|
74
|
-
const mode = willInject ?
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
87
|
+
const mode = willInject ? 'nudge' : 'dry';
|
|
88
|
+
const lines = [
|
|
89
|
+
`watchdog ${mode} · checked ${new Date(result.atMs).toLocaleString()} · ` +
|
|
90
|
+
`${counts.total} live · ${counts.stalled} stalled · ` +
|
|
91
|
+
`${counts.nudged} nudged · ${counts.unaddressable} un-addressable` +
|
|
92
|
+
(counts.rotating > 0 ? ` · ${counts.rotating} rotating` : ''),
|
|
93
|
+
];
|
|
94
|
+
const visible = verbose ? result.outcomes : result.outcomes.filter(isAttentionOutcome);
|
|
95
|
+
for (const o of visible) {
|
|
81
96
|
const tag = o.injected ? 'NUDGED'
|
|
82
97
|
: o.decision === 'rotate' ? (o.rotatePhase === 'failed' ? 'ROTATE-FAIL' : 'ROTATE')
|
|
83
98
|
: o.addressable === false ? 'FLAGGED'
|
|
84
99
|
: o.decision === 'nudge' ? 'WOULD-NUDGE'
|
|
85
100
|
: 'skip';
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
101
|
+
const id = o.sessionId?.slice(0, 8) ?? 'no-session-id';
|
|
102
|
+
const title = o.label || o.name || o.topic;
|
|
103
|
+
lines.push(` ${tag.padEnd(11)} ${id}${title ? ` · ${title}` : ''}`);
|
|
104
|
+
const metadata = [
|
|
105
|
+
o.kind,
|
|
106
|
+
o.host,
|
|
107
|
+
o.machine ?? 'local',
|
|
108
|
+
o.project ?? (o.cwd ? path.basename(o.cwd) : undefined),
|
|
109
|
+
o.activity ?? o.status,
|
|
110
|
+
o.origin === 'routine' ? `routine ${o.routineName ?? 'unknown'}` : undefined,
|
|
111
|
+
o.owner ? `owner ${o.owner}` : undefined,
|
|
112
|
+
o.startedAtMs ? `started ${elapsedLabel(result.atMs, o.startedAtMs)}` : undefined,
|
|
113
|
+
o.lastActivityMs ? `activity ${elapsedLabel(result.atMs, o.lastActivityMs)}` : undefined,
|
|
114
|
+
o.rail ? `rail ${o.rail}` : undefined,
|
|
115
|
+
].filter((value) => Boolean(value));
|
|
116
|
+
lines.push(` ${metadata.join(' · ')}`);
|
|
117
|
+
if (o.cwd)
|
|
118
|
+
lines.push(` cwd ${o.cwd}`);
|
|
119
|
+
if (o.preview)
|
|
120
|
+
lines.push(` latest ${o.preview.replace(/\s+/g, ' ').slice(0, 140)}`);
|
|
121
|
+
lines.push(` reason ${o.reason}`);
|
|
92
122
|
}
|
|
123
|
+
const omitted = result.outcomes.length - visible.length;
|
|
124
|
+
if (omitted > 0)
|
|
125
|
+
lines.push(` ${omitted} healthy/non-actionable session${omitted === 1 ? '' : 's'} omitted · use --verbose or --json to inspect all`);
|
|
126
|
+
return lines;
|
|
127
|
+
}
|
|
128
|
+
function printTick(result, willInject, verbose) {
|
|
129
|
+
for (const line of formatWatchdogTickLines(result, willInject, verbose))
|
|
130
|
+
console.log(line);
|
|
93
131
|
}
|
|
94
132
|
async function sleep(ms) {
|
|
95
133
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -109,6 +147,7 @@ export function registerWatchdogCommand(program) {
|
|
|
109
147
|
.option('--smart', 'Use the LLM decider (agents run) instead of the deterministic path (non-reproducible)')
|
|
110
148
|
.option('--smart-agent <agent>', 'Agent the --smart decider runs as', 'claude')
|
|
111
149
|
.option('--allow-ghostty-focus', 'Permit the coarse, focus-stealing Ghostty path (off by default)')
|
|
150
|
+
.option('--verbose', 'Show healthy and non-actionable session inspections too')
|
|
112
151
|
.option('--json', 'Emit the tick result as JSON (for the menu-bar / scripts)')
|
|
113
152
|
.action(async (opts) => {
|
|
114
153
|
const thresholds = {
|
|
@@ -139,7 +178,7 @@ export function registerWatchdogCommand(program) {
|
|
|
139
178
|
if (opts.json)
|
|
140
179
|
console.log(JSON.stringify(result, null, 2));
|
|
141
180
|
else
|
|
142
|
-
printTick(result, willInject);
|
|
181
|
+
printTick(result, willInject, opts.verbose === true);
|
|
143
182
|
return;
|
|
144
183
|
}
|
|
145
184
|
// Manual poll loop for ad-hoc use; the daemon owns the automatic cadence.
|
|
@@ -157,7 +196,7 @@ export function registerWatchdogCommand(program) {
|
|
|
157
196
|
if (opts.json)
|
|
158
197
|
console.log(JSON.stringify(result));
|
|
159
198
|
else
|
|
160
|
-
printTick(result, willInject);
|
|
199
|
+
printTick(result, willInject, opts.verbose === true);
|
|
161
200
|
await sleep(intervalMs);
|
|
162
201
|
}
|
|
163
202
|
});
|
|
@@ -181,6 +220,12 @@ export function registerWatchdogCommand(program) {
|
|
|
181
220
|
# Show device enablement, rotate config, and in-flight rotates
|
|
182
221
|
agents watchdog status
|
|
183
222
|
|
|
223
|
+
# Show what Watchdog decided and acted on during the last day
|
|
224
|
+
agents watchdog history --since 24h
|
|
225
|
+
|
|
226
|
+
# Follow one session's Watchdog decisions
|
|
227
|
+
agents watchdog history <sessionId>
|
|
228
|
+
|
|
184
229
|
# Leave one session detected-but-untouched
|
|
185
230
|
agents watchdog policy <sessionId> handsoff
|
|
186
231
|
|
|
@@ -302,6 +347,46 @@ export function registerWatchdogCommand(program) {
|
|
|
302
347
|
console.log(` ${chalk.magenta(r.phase.padEnd(12))} ${chalk.bold(r.sessionId.slice(0, 8))} → ${r.newSessionId.slice(0, 8)}${r.error ? chalk.red(` ${r.error}`) : ''}`);
|
|
303
348
|
}
|
|
304
349
|
console.log(`state dir: ${chalk.dim(stateDir())}`);
|
|
350
|
+
console.log(`history: ${chalk.dim('agents watchdog history')}`);
|
|
351
|
+
});
|
|
352
|
+
cmd.command('history [sessionId]')
|
|
353
|
+
.description('Show persisted Watchdog decisions and actions, newest first.')
|
|
354
|
+
.option('--limit <count>', 'Maximum events to show', '50')
|
|
355
|
+
.option('--since <duration>', 'Only events within a duration such as 2h or 7d')
|
|
356
|
+
.option('--all', 'Include heartbeat tick events')
|
|
357
|
+
.option('--json', 'Emit safe structured history (transcript content is excluded)')
|
|
358
|
+
.action((sessionId, opts, command) => {
|
|
359
|
+
const globals = command.optsWithGlobals();
|
|
360
|
+
const limit = Number.parseInt(opts.limit, 10);
|
|
361
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
362
|
+
throw new Error('--limit must be a positive integer');
|
|
363
|
+
}
|
|
364
|
+
const sinceSeconds = opts.since === undefined ? undefined : parseDuration(opts.since);
|
|
365
|
+
if (sinceSeconds !== undefined && (sinceSeconds === null || sinceSeconds <= 0)) {
|
|
366
|
+
throw new Error('--since must be a positive duration such as 2h or 7d');
|
|
367
|
+
}
|
|
368
|
+
const sinceMs = sinceSeconds == null ? undefined : sinceSeconds * 1000;
|
|
369
|
+
const entries = selectWatchdogHistory(readWatchdogEvents(), {
|
|
370
|
+
limit,
|
|
371
|
+
sinceMs,
|
|
372
|
+
sessionId,
|
|
373
|
+
includeTicks: opts.all === true,
|
|
374
|
+
});
|
|
375
|
+
if (globals.json === true) {
|
|
376
|
+
console.log(JSON.stringify({ logPath: WATCHDOG_LOG_PATH, events: entries }, null, 2));
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
if (entries.length === 0) {
|
|
380
|
+
console.log(chalk.dim(`No Watchdog events found in ${WATCHDOG_LOG_PATH}`));
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
for (const entry of entries) {
|
|
384
|
+
const when = new Date(entry.ts).toLocaleString();
|
|
385
|
+
const session = entry.sessionId ? entry.sessionId.slice(0, 8) : '-';
|
|
386
|
+
const reason = entry.reason ? ` · ${entry.reason}` : '';
|
|
387
|
+
console.log(`${chalk.dim(when)} ${entry.kind.padEnd(8)} ${chalk.bold(session)} ${entry.agent ?? '-'} ${entry.message}${reason}`);
|
|
388
|
+
}
|
|
389
|
+
console.log(chalk.dim(`${entries.length} event${entries.length === 1 ? '' : 's'} · ${WATCHDOG_LOG_PATH}`));
|
|
305
390
|
});
|
|
306
391
|
// --- per-session policy ----------------------------------------------------
|
|
307
392
|
cmd.command('policy <sessionId> <policy>')
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import * as fs from 'fs';
|
|
|
11
11
|
import * as path from 'path';
|
|
12
12
|
import { fileURLToPath } from 'url';
|
|
13
13
|
import { detectDevBuild } from './lib/startup/dev-build.js';
|
|
14
|
+
import { configureRootCommand } from './lib/startup/root-command.js';
|
|
14
15
|
// `ora`, `@inquirer/prompts`, `./commands/utils.js`, and the agents/versions/shims
|
|
15
16
|
// modules are imported dynamically at their use sites: they are needed only on
|
|
16
17
|
// interactive / update / shim-repair paths, never for fast commands like
|
|
@@ -71,6 +72,7 @@ if (process.argv[2] === SYNC_GET_CMD ||
|
|
|
71
72
|
process.exit(code);
|
|
72
73
|
}
|
|
73
74
|
import { NPM_PACKAGE_NAME, deriveGlobalPrefix, detectPackageManager, installPackageIntoPrefix, installPackageWithBun, verifyInstalledVersion, refreshAliasShims, downloadVerifiedTarball, } from './lib/self-update.js';
|
|
75
|
+
import { registerUpgradeCommand } from './commands/upgrade.js';
|
|
74
76
|
// Detect dev/working-tree builds and default the noisy startup steps off.
|
|
75
77
|
// Three cases trip this:
|
|
76
78
|
// 1. Dev install (scripts/install.sh) — package.json version stamped 0.0.0-dev.<sha>
|
|
@@ -140,14 +142,7 @@ if (process.argv[2] === '__daemon-tick') {
|
|
|
140
142
|
// CLI presents its own name/help/errors as the brand. Unbranded (AGENTS_BRAND
|
|
141
143
|
// unset) resolves to 'agents' and everything below is byte-identical to before.
|
|
142
144
|
const BRAND = resolveBrandName();
|
|
143
|
-
const program = new Command();
|
|
144
|
-
program
|
|
145
|
-
.name(BRAND)
|
|
146
|
-
.description('Environment manager for AI agents')
|
|
147
|
-
.version(VERSION)
|
|
148
|
-
.option('--verbose', 'Show startup self-heal details on stderr')
|
|
149
|
-
.helpOption('-h, --help', 'Show help')
|
|
150
|
-
.addHelpCommand(false);
|
|
145
|
+
const program = configureRootCommand(new Command(), BRAND, VERSION);
|
|
151
146
|
// ─── Audit backbone ────────────────────────────────────────────────────────────
|
|
152
147
|
// One choke point logs every `agents <module> <cmd>` invocation to the structured
|
|
153
148
|
// event log — so team create/disband, agent run, secrets access, and everything
|
|
@@ -805,59 +800,56 @@ function registerInternalCommand(p) {
|
|
|
805
800
|
process.exit(0);
|
|
806
801
|
});
|
|
807
802
|
}
|
|
808
|
-
/**
|
|
809
|
-
function
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
const
|
|
817
|
-
const
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
803
|
+
/** Runtime action for the shared `agents upgrade [version]` command definition. */
|
|
804
|
+
async function runUpgrade(version, options) {
|
|
805
|
+
const { default: ora } = await import('ora');
|
|
806
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
807
|
+
const { isInteractiveTerminal, isPromptCancelled } = await import('./commands/utils.js');
|
|
808
|
+
const target = version ?? 'latest';
|
|
809
|
+
let spinner = ora(version ? `Resolving ${NPM_PACKAGE_NAME}@${target}...` : 'Checking for updates...').start();
|
|
810
|
+
try {
|
|
811
|
+
const metadata = await fetchNpmPackageMetadata(target);
|
|
812
|
+
const resolvedVersion = metadata.version;
|
|
813
|
+
if (resolvedVersion === VERSION) {
|
|
814
|
+
spinner.succeed(`Already on ${VERSION}`);
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
// For `latest` (no explicit version) skip when already ahead. When a
|
|
818
|
+
// version is named explicitly, honor it even if it's a downgrade.
|
|
819
|
+
if (!version && compareVersions(resolvedVersion, VERSION) <= 0) {
|
|
820
|
+
spinner.succeed(`Already ahead of latest (${VERSION} >= ${resolvedVersion})`);
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
const direction = compareVersions(resolvedVersion, VERSION) < 0 ? 'Downgrade' : 'Upgrade';
|
|
824
|
+
spinner.succeed(`Resolved ${NPM_PACKAGE_NAME}@${resolvedVersion}`);
|
|
825
|
+
printResolvedPackage(metadata);
|
|
826
|
+
if (isInteractiveTerminal() && !options.yes) {
|
|
827
|
+
const approved = await confirm({
|
|
828
|
+
message: `Install ${NPM_PACKAGE_NAME}@${resolvedVersion}?`,
|
|
829
|
+
default: false,
|
|
830
|
+
});
|
|
831
|
+
if (!approved) {
|
|
832
|
+
console.log(chalk.gray('Upgrade cancelled'));
|
|
831
833
|
return;
|
|
832
834
|
}
|
|
833
|
-
const direction = compareVersions(resolvedVersion, VERSION) < 0 ? 'Downgrade' : 'Upgrade';
|
|
834
|
-
spinner.succeed(`Resolved ${NPM_PACKAGE_NAME}@${resolvedVersion}`);
|
|
835
|
-
printResolvedPackage(metadata);
|
|
836
|
-
if (isInteractiveTerminal() && !options.yes) {
|
|
837
|
-
const approved = await confirm({
|
|
838
|
-
message: `Install ${NPM_PACKAGE_NAME}@${resolvedVersion}?`,
|
|
839
|
-
default: false,
|
|
840
|
-
});
|
|
841
|
-
if (!approved) {
|
|
842
|
-
console.log(chalk.gray('Upgrade cancelled'));
|
|
843
|
-
return;
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
spinner = ora(`${direction === 'Downgrade' ? 'Downgrading' : 'Upgrading'} ${VERSION} -> ${resolvedVersion}...`).start();
|
|
847
|
-
await installResolvedPackage(metadata);
|
|
848
|
-
spinner.succeed(`${direction}d to ${resolvedVersion}`);
|
|
849
|
-
// Only show the changelog for a genuine upgrade range.
|
|
850
|
-
if (compareVersions(resolvedVersion, VERSION) > 0) {
|
|
851
|
-
await showWhatsNew(VERSION, resolvedVersion);
|
|
852
|
-
}
|
|
853
835
|
}
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
836
|
+
spinner = ora(`${direction === 'Downgrade' ? 'Downgrading' : 'Upgrading'} ${VERSION} -> ${resolvedVersion}...`).start();
|
|
837
|
+
await installResolvedPackage(metadata);
|
|
838
|
+
spinner.succeed(`${direction}d to ${resolvedVersion}`);
|
|
839
|
+
// Only show the changelog for a genuine upgrade range.
|
|
840
|
+
if (compareVersions(resolvedVersion, VERSION) > 0) {
|
|
841
|
+
await showWhatsNew(VERSION, resolvedVersion);
|
|
859
842
|
}
|
|
860
|
-
}
|
|
843
|
+
}
|
|
844
|
+
catch (err) {
|
|
845
|
+
if (isPromptCancelled(err))
|
|
846
|
+
return;
|
|
847
|
+
spinner.fail(`Upgrade failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
848
|
+
console.log(chalk.gray(`Run manually: agents upgrade ${version ? version + ' ' : ''}--yes`));
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
function registerUpgradeRuntimeCommand(p) {
|
|
852
|
+
registerUpgradeCommand(p, runUpgrade);
|
|
861
853
|
}
|
|
862
854
|
// --- Lazy registration orchestration -----------------------------------------
|
|
863
855
|
/** Import a command module via its loader and register it on the program. */
|
|
@@ -905,7 +897,7 @@ async function registerEagerForRequest(name) {
|
|
|
905
897
|
registerInternalCommand(program);
|
|
906
898
|
return true;
|
|
907
899
|
case 'upgrade':
|
|
908
|
-
|
|
900
|
+
registerUpgradeRuntimeCommand(program);
|
|
909
901
|
return true;
|
|
910
902
|
}
|
|
911
903
|
const loaders = COMMAND_LOADERS[name];
|
|
@@ -1008,7 +1000,7 @@ async function registerAllEagerCommands() {
|
|
|
1008
1000
|
registerJobsCronAliasCommand(program, 'jobs');
|
|
1009
1001
|
registerJobsCronAliasCommand(program, 'cron');
|
|
1010
1002
|
registerInternalCommand(program);
|
|
1011
|
-
|
|
1003
|
+
registerUpgradeRuntimeCommand(program);
|
|
1012
1004
|
await reg(loadPull);
|
|
1013
1005
|
await reg(loadPush);
|
|
1014
1006
|
await reg(loadRepo);
|
package/dist/lib/agents.js
CHANGED
|
@@ -1351,6 +1351,12 @@ const CREDENTIAL_FILE_SEGMENTS = {
|
|
|
1351
1351
|
// Muse Code stores OAuth / API credentials at ~/.config/muse/auth.json
|
|
1352
1352
|
// (or META_API_KEY in the environment, which is not a file).
|
|
1353
1353
|
muse: [['.config', 'muse', 'auth.json']],
|
|
1354
|
+
// Cursor's OAuth token — the login gate — is at $XDG_CONFIG_HOME/cursor/auth.json
|
|
1355
|
+
// (~/.config/cursor/auth.json by default). cli-config.json holds only account
|
|
1356
|
+
// metadata; the token file is what a real launch authenticates from, so a
|
|
1357
|
+
// version home with no token of its own is genuinely logged out once runs pin
|
|
1358
|
+
// XDG_CONFIG_HOME per home (see buildExecEnv).
|
|
1359
|
+
cursor: [['.config', 'cursor', 'auth.json']],
|
|
1354
1360
|
};
|
|
1355
1361
|
/** Whether an agent's credential file exists under a given home. */
|
|
1356
1362
|
function credentialFileExistsUnder(agentId, home) {
|