@phnx-labs/agents-cli 1.22.14 → 1.22.16
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 +53 -0
- package/README.md +7 -5
- package/dist/bin/agents +0 -0
- package/dist/commands/cloud.js +4 -6
- package/dist/commands/exec.js +96 -11
- package/dist/commands/humans.d.ts +10 -0
- package/dist/commands/humans.js +91 -0
- package/dist/commands/packages.js +90 -22
- package/dist/commands/projects.d.ts +2 -5
- package/dist/commands/projects.js +92 -65
- package/dist/commands/resume.d.ts +17 -0
- package/dist/commands/resume.js +80 -0
- package/dist/commands/routines.js +53 -71
- package/dist/commands/sessions.d.ts +13 -4
- package/dist/commands/sessions.js +37 -19
- package/dist/commands/setup-watchdog.d.ts +3 -0
- package/dist/commands/setup-watchdog.js +54 -0
- package/dist/commands/setup.js +8 -1
- package/dist/commands/ssh.d.ts +11 -0
- package/dist/commands/ssh.js +18 -4
- package/dist/commands/watchdog.d.ts +3 -4
- package/dist/commands/watchdog.js +40 -28
- package/dist/index.js +3 -2
- package/dist/lib/auto-dispatch.d.ts +8 -10
- package/dist/lib/auto-dispatch.js +20 -42
- package/dist/lib/channels/send.d.ts +4 -1
- package/dist/lib/channels/send.js +15 -7
- package/dist/lib/devices/doctor-findings.d.ts +14 -0
- package/dist/lib/devices/doctor-findings.js +92 -51
- package/dist/lib/exec.d.ts +6 -2
- package/dist/lib/exec.js +22 -4
- package/dist/lib/hooks.d.ts +4 -2
- package/dist/lib/hooks.js +173 -23
- package/dist/lib/humans.d.ts +25 -0
- package/dist/lib/humans.js +65 -0
- package/dist/lib/memory.js +2 -1
- 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 +1 -1
- package/dist/lib/migrate.js +116 -7
- package/dist/lib/notify.js +6 -2
- package/dist/lib/permissions.js +12 -10
- package/dist/lib/project-import.d.ts +7 -46
- package/dist/lib/project-import.js +9 -108
- package/dist/lib/projects.d.ts +20 -3
- package/dist/lib/projects.js +36 -18
- package/dist/lib/registry.d.ts +1 -1
- package/dist/lib/registry.js +11 -0
- package/dist/lib/routine-activation.d.ts +14 -0
- package/dist/lib/routine-activation.js +78 -0
- package/dist/lib/routines-project.d.ts +1 -1
- package/dist/lib/routines-project.js +3 -9
- package/dist/lib/routines.d.ts +3 -1
- package/dist/lib/routines.js +32 -19
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/bundles.d.ts +11 -0
- package/dist/lib/secrets/bundles.js +122 -5
- package/dist/lib/session/actor-sidecar.d.ts +5 -2
- package/dist/lib/session/actor-sidecar.js +4 -2
- package/dist/lib/session/db.d.ts +2 -1
- package/dist/lib/session/db.js +19 -3
- package/dist/lib/session/types.d.ts +4 -0
- package/dist/lib/staleness/writers/sources.d.ts +6 -1
- package/dist/lib/staleness/writers/sources.js +93 -7
- package/dist/lib/startup/command-registry.d.ts +2 -0
- package/dist/lib/startup/command-registry.js +5 -0
- package/dist/lib/state.d.ts +2 -0
- package/dist/lib/state.js +23 -10
- package/dist/lib/types.d.ts +64 -1
- package/dist/lib/versions.js +48 -14
- package/dist/lib/watchdog/rotate.d.ts +1 -1
- package/dist/lib/watchdog/rotate.js +1 -1
- package/package.json +1 -1
- package/dist/lib/watchdog/routine.d.ts +0 -44
- package/dist/lib/watchdog/routine.js +0 -69
|
@@ -814,8 +814,8 @@ export function serializeSessionsJson(sessions) {
|
|
|
814
814
|
return JSON.stringify(serializable, null, 2) + '\n';
|
|
815
815
|
}
|
|
816
816
|
/** The intentionally small metadata contract emitted by `sessions --resolve`.
|
|
817
|
-
*
|
|
818
|
-
*
|
|
817
|
+
* It includes only launch identity needed to route/resume. Transcript paths,
|
|
818
|
+
* extracted plans, costs, and content stay on the machine that owns them. */
|
|
819
819
|
export function serializeResolvedSessionsJson(sessions) {
|
|
820
820
|
const safe = sessions.map((session) => ({
|
|
821
821
|
id: session.id,
|
|
@@ -826,6 +826,7 @@ export function serializeResolvedSessionsJson(sessions) {
|
|
|
826
826
|
lastActivity: session.lastActivity,
|
|
827
827
|
project: session.project,
|
|
828
828
|
version: session.version,
|
|
829
|
+
mode: session.mode,
|
|
829
830
|
label: session.label,
|
|
830
831
|
topic: session.topic,
|
|
831
832
|
machine: session.machine,
|
|
@@ -3447,6 +3448,7 @@ function modeFlag(mode) {
|
|
|
3447
3448
|
return '--json';
|
|
3448
3449
|
return undefined; // summary — the peer's default render
|
|
3449
3450
|
}
|
|
3451
|
+
const FULL_SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
3450
3452
|
/** Resolve a fleet sweep through the same canonical full-id / prefix resolver as
|
|
3451
3453
|
* local lookups, then group copies by logical session id. Synced mirrors of one
|
|
3452
3454
|
* session therefore stay one candidate even when several machines report them;
|
|
@@ -3501,19 +3503,46 @@ export function toolSearchForwardedArgs(argv, hosts) {
|
|
|
3501
3503
|
args.push('--local');
|
|
3502
3504
|
return args;
|
|
3503
3505
|
}
|
|
3504
|
-
/** Resolution
|
|
3505
|
-
*
|
|
3506
|
+
/** Resolution fails closed for prefixes/keywords when a peer did not answer.
|
|
3507
|
+
* A full UUID is globally unique, so one exact hit is sufficient even when an
|
|
3508
|
+
* unrelated registered device is offline. */
|
|
3506
3509
|
export function metadataResolveOutcome(localMatches, remote, selector) {
|
|
3507
|
-
if (remote.unreachable.length > 0) {
|
|
3508
|
-
return { kind: 'partial', failedPeers: remote.unreachable };
|
|
3509
|
-
}
|
|
3510
3510
|
const candidates = fleetCandidatesByQuery([...localMatches, ...remote.sessions], selector);
|
|
3511
|
+
if (FULL_SESSION_ID_RE.test(selector) && candidates.length === 1 && candidates[0].id.toLowerCase() === selector.toLowerCase()) {
|
|
3512
|
+
return { kind: 'resolved', session: candidates[0].hits[0].session };
|
|
3513
|
+
}
|
|
3514
|
+
if (remote.unreachable.length > 0)
|
|
3515
|
+
return { kind: 'partial', failedPeers: remote.unreachable };
|
|
3511
3516
|
if (candidates.length === 0)
|
|
3512
3517
|
return { kind: 'not-found' };
|
|
3513
3518
|
if (candidates.length > 1)
|
|
3514
3519
|
return { kind: 'ambiguous', candidates };
|
|
3515
3520
|
return { kind: 'resolved', session: candidates[0].hits[0].session };
|
|
3516
3521
|
}
|
|
3522
|
+
/** Reusable local-first resolver for `agents run --resume` and `agents resume`.
|
|
3523
|
+
* Full UUIDs hit the local SQLite index without any SSH fan-out. */
|
|
3524
|
+
export async function resolveSessionMetadataValue(selector, scope = {}, deps = { gatherRemoteList }) {
|
|
3525
|
+
const localMachine = machineId();
|
|
3526
|
+
const includeLocal = !scope.hosts?.length || shouldIncludeLocal(scope.hosts, localMachine);
|
|
3527
|
+
const indexed = includeLocal ? applyScopeFilters(querySessions(), scope) : [];
|
|
3528
|
+
const localMatches = resolveIndexedMetadataRows(indexed, selector)
|
|
3529
|
+
.map(session => ({ ...session, machine: session.machine || localMachine }));
|
|
3530
|
+
if (FULL_SESSION_ID_RE.test(selector)) {
|
|
3531
|
+
const localOutcome = metadataResolveOutcome(localMatches, { sessions: [], unreachable: [] }, selector);
|
|
3532
|
+
if (localOutcome.kind === 'resolved')
|
|
3533
|
+
return localOutcome;
|
|
3534
|
+
}
|
|
3535
|
+
if (scope.local === true)
|
|
3536
|
+
return metadataResolveOutcome(localMatches, { sessions: [], unreachable: [] }, selector);
|
|
3537
|
+
try {
|
|
3538
|
+
const forwarded = metadataResolveForwardedArgs(selector, scope);
|
|
3539
|
+
const remote = await deps.gatherRemoteList(forwarded, scope.hosts);
|
|
3540
|
+
return metadataResolveOutcome(localMatches, remote, selector);
|
|
3541
|
+
}
|
|
3542
|
+
catch (error) {
|
|
3543
|
+
return metadataResolveOutcome(localMatches, { sessions: [], unreachable: [error?.message ?? 'fleet fan-out'] }, selector);
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3517
3546
|
/** Resolve one selector to indexed metadata across the fleet without reading or
|
|
3518
3547
|
* rendering transcript events. A peer answering the parent sweep returns every
|
|
3519
3548
|
* local candidate; the parent performs the one logical-session uniqueness gate. */
|
|
@@ -3529,18 +3558,7 @@ async function resolveSessionMetadata(selector, scope, deps = { gatherRemoteList
|
|
|
3529
3558
|
process.stdout.write(serializeResolvedSessionsJson(localMatches));
|
|
3530
3559
|
return;
|
|
3531
3560
|
}
|
|
3532
|
-
|
|
3533
|
-
if (scope.local !== true) {
|
|
3534
|
-
try {
|
|
3535
|
-
const forwarded = metadataResolveForwardedArgs(selector, scope);
|
|
3536
|
-
const result = await deps.gatherRemoteList(forwarded, scope.hosts);
|
|
3537
|
-
remoteResult = { sessions: result.sessions, unreachable: result.unreachable };
|
|
3538
|
-
}
|
|
3539
|
-
catch (error) {
|
|
3540
|
-
remoteResult = { sessions: [], unreachable: [error?.message ?? 'fleet fan-out'] };
|
|
3541
|
-
}
|
|
3542
|
-
}
|
|
3543
|
-
const outcome = metadataResolveOutcome(localMatches, remoteResult, selector);
|
|
3561
|
+
const outcome = await resolveSessionMetadataValue(selector, scope, deps);
|
|
3544
3562
|
if (outcome.kind === 'partial') {
|
|
3545
3563
|
console.error(chalk.red(`Partial session resolution: ${outcome.failedPeers.join(', ')} did not answer.`));
|
|
3546
3564
|
console.error(chalk.gray('No unique/no-match decision was made. Upgrade or reconnect every peer, then retry.'));
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { getCliLaunch } from '../lib/cli-entry.js';
|
|
4
|
+
import { loadDevices } from '../lib/devices/registry.js';
|
|
5
|
+
import { machineId, normalizeHost } from '../lib/machine-id.js';
|
|
6
|
+
import { devicesWithRoutineEnabled } from '../lib/routine-activation.js';
|
|
7
|
+
import { readJob, setJobEnabled } from '../lib/routines.js';
|
|
8
|
+
import { isPromptCancelled, requireInteractiveSelection } from './utils.js';
|
|
9
|
+
const ROUTINE = 'watchdog';
|
|
10
|
+
export async function runWatchdogSetupWizard() {
|
|
11
|
+
if (!readJob(ROUTINE)) {
|
|
12
|
+
throw new Error("Built-in routine 'watchdog' is missing. Run: agents repo pull system");
|
|
13
|
+
}
|
|
14
|
+
const registry = await loadDevices();
|
|
15
|
+
const devices = [...new Set([...Object.keys(registry), machineId()].map(normalizeHost))].sort();
|
|
16
|
+
const enabled = new Set(devicesWithRoutineEnabled(ROUTINE).map(normalizeHost));
|
|
17
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
18
|
+
requireInteractiveSelection('watchdog devices', ['agents watchdog on', 'agents watchdog on --host <device>']);
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const { checkbox } = await import('@inquirer/prompts');
|
|
22
|
+
const selected = await checkbox({
|
|
23
|
+
message: 'Devices where the watchdog should run:',
|
|
24
|
+
choices: devices.map((device) => ({ name: device, value: device, checked: enabled.has(device) })),
|
|
25
|
+
});
|
|
26
|
+
const selectedSet = new Set(selected);
|
|
27
|
+
for (const device of devices) {
|
|
28
|
+
const on = selectedSet.has(device);
|
|
29
|
+
if (device === normalizeHost(machineId())) {
|
|
30
|
+
setJobEnabled(ROUTINE, on);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const launch = getCliLaunch(['routines', on ? 'resume' : 'pause', ROUTINE, '--host', device]);
|
|
34
|
+
const result = spawnSync(launch.command, launch.args, { stdio: 'inherit', env: process.env });
|
|
35
|
+
if ((result.status ?? 1) !== 0)
|
|
36
|
+
throw new Error(`Could not configure watchdog on ${device}`);
|
|
37
|
+
}
|
|
38
|
+
console.log(chalk.green(selected.length === 0
|
|
39
|
+
? 'Watchdog disabled on every registered device'
|
|
40
|
+
: `Watchdog enabled on: ${selected.join(', ')}`));
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (isPromptCancelled(error)) {
|
|
44
|
+
console.log(chalk.gray('Cancelled'));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function registerSetupWatchdogCommand(setup) {
|
|
51
|
+
setup.command('watchdog')
|
|
52
|
+
.description('Choose the devices where the built-in watchdog routine runs.')
|
|
53
|
+
.action(runWatchdogSetupWizard);
|
|
54
|
+
}
|
package/dist/commands/setup.js
CHANGED
|
@@ -24,6 +24,7 @@ import { registerSetupShareCommand, runShareWizard } from './setup-share.js';
|
|
|
24
24
|
import { registerSetupMineCommand } from './setup-mine.js';
|
|
25
25
|
import { registerSetupSecretsCommand } from './setup-secrets.js';
|
|
26
26
|
import { registerSetupFleetCommand } from './setup-fleet.js';
|
|
27
|
+
import { registerSetupWatchdogCommand, runWatchdogSetupWizard } from './setup-watchdog.js';
|
|
27
28
|
import { runPreferencesStep } from './setup-preferences.js';
|
|
28
29
|
const HOME = os.homedir();
|
|
29
30
|
/**
|
|
@@ -247,6 +248,7 @@ async function runSetupHub() {
|
|
|
247
248
|
{ name: 'share — publish shareable links (Cloudflare R2 + Worker)', value: 'share' },
|
|
248
249
|
{ name: 'secrets — choose storage defaults and import existing credentials', value: 'secrets' },
|
|
249
250
|
{ name: 'fleet — discover Tailscale devices and configure SSH access', value: 'fleet' },
|
|
251
|
+
{ name: 'watchdog — resume stalled agent sessions on selected devices', value: 'watchdog' },
|
|
250
252
|
],
|
|
251
253
|
});
|
|
252
254
|
for (const pick of picks) {
|
|
@@ -261,6 +263,8 @@ async function runSetupHub() {
|
|
|
261
263
|
await import('./setup-secrets.js').then((m) => m.runSecretsSetupWizard());
|
|
262
264
|
else if (pick === 'fleet')
|
|
263
265
|
await import('./setup-fleet.js').then((m) => m.runFleetSetupWizard());
|
|
266
|
+
else if (pick === 'watchdog')
|
|
267
|
+
await runWatchdogSetupWizard();
|
|
264
268
|
}
|
|
265
269
|
}
|
|
266
270
|
catch (err) {
|
|
@@ -283,6 +287,7 @@ export function registerSetupCommand(program) {
|
|
|
283
287
|
registerSetupMineCommand(setupCmd);
|
|
284
288
|
registerSetupSecretsCommand(setupCmd);
|
|
285
289
|
registerSetupFleetCommand(setupCmd);
|
|
290
|
+
registerSetupWatchdogCommand(setupCmd);
|
|
286
291
|
setHelpSections(setupCmd, {
|
|
287
292
|
examples: `
|
|
288
293
|
# First-time setup (clones the system repo into ~/.agents/.system/)
|
|
@@ -297,12 +302,13 @@ export function registerSetupCommand(program) {
|
|
|
297
302
|
agents setup share
|
|
298
303
|
agents setup secrets
|
|
299
304
|
agents setup fleet
|
|
305
|
+
agents setup watchdog
|
|
300
306
|
`,
|
|
301
307
|
notes: `
|
|
302
308
|
What it does:
|
|
303
309
|
1. Clones the system repo into ~/.agents/.system/
|
|
304
310
|
2. Imports any unmanaged agent installations it finds
|
|
305
|
-
3. On a TTY, offers to set up optional capabilities (browser/computer/share/secrets/fleet)
|
|
311
|
+
3. On a TTY, offers to set up optional capabilities (browser/computer/share/secrets/fleet/watchdog)
|
|
306
312
|
4. On a TTY, asks preferences: which machine you sit at (interactive host)
|
|
307
313
|
and which browser agents drive here — both skippable, both the same
|
|
308
314
|
keys 'agents devices set-interactive' / 'browser profiles set-default' write
|
|
@@ -313,6 +319,7 @@ export function registerSetupCommand(program) {
|
|
|
313
319
|
agents setup share # provision or join a Cloudflare share endpoint
|
|
314
320
|
agents setup secrets # choose secrets backend/policy defaults + import
|
|
315
321
|
agents setup fleet # discover Tailscale devices + configure SSH access
|
|
322
|
+
agents setup watchdog # choose which devices run the watchdog routine
|
|
316
323
|
|
|
317
324
|
To install CLIs from agents.yaml and sync resources into version homes:
|
|
318
325
|
agents sync --local -y
|
package/dist/commands/ssh.d.ts
CHANGED
|
@@ -20,6 +20,17 @@ import { type CrabboxBox } from '../lib/crabbox/cli.js';
|
|
|
20
20
|
* the row formatting is deterministic in tests.
|
|
21
21
|
*/
|
|
22
22
|
export declare function renderLeasedBoxesSection(boxes: CrabboxBox[], nowSecs: number): string[];
|
|
23
|
+
/**
|
|
24
|
+
* Whether `agents devices list` shows the "Leased boxes" section (RUSH-2190).
|
|
25
|
+
* Opt-in via --all only: loading it scans the keychain for bundle credentials
|
|
26
|
+
* and can raise a Touch ID sheet after the table has printed, so the default
|
|
27
|
+
* list must never reach for it. --no-stats stays the hard "instant, no provider
|
|
28
|
+
* calls" opt-out even when --all is passed.
|
|
29
|
+
*/
|
|
30
|
+
export declare function showLeasedBoxesSection(opts: {
|
|
31
|
+
all?: boolean;
|
|
32
|
+
stats?: boolean;
|
|
33
|
+
}): boolean;
|
|
23
34
|
/**
|
|
24
35
|
* Race `fanOut` against an overall wall-clock deadline (RUSH-2041).
|
|
25
36
|
*
|
package/dist/commands/ssh.js
CHANGED
|
@@ -191,6 +191,16 @@ function loadLeasedBoxesSection() {
|
|
|
191
191
|
return []; // crabbox not installed / no provider creds — omit the section
|
|
192
192
|
}
|
|
193
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* Whether `agents devices list` shows the "Leased boxes" section (RUSH-2190).
|
|
196
|
+
* Opt-in via --all only: loading it scans the keychain for bundle credentials
|
|
197
|
+
* and can raise a Touch ID sheet after the table has printed, so the default
|
|
198
|
+
* list must never reach for it. --no-stats stays the hard "instant, no provider
|
|
199
|
+
* calls" opt-out even when --all is passed.
|
|
200
|
+
*/
|
|
201
|
+
export function showLeasedBoxesSection(opts) {
|
|
202
|
+
return opts.all === true && opts.stats !== false;
|
|
203
|
+
}
|
|
194
204
|
/**
|
|
195
205
|
* `agents ssh <slug>` targeting a leased crabbox box. crabbox provisions a
|
|
196
206
|
* per-lease identity key, so we ssh via crabbox's OWN emitted invocation
|
|
@@ -1152,10 +1162,13 @@ Typical workflow:
|
|
|
1152
1162
|
console.log(chalk.gray(` updated ${formatCheckedAge(freshness.oldestFetchedAt)} — pass --refresh (--live) for a live probe`));
|
|
1153
1163
|
}
|
|
1154
1164
|
// Ephemeral crabbox leases live alongside the registered fleet but are never
|
|
1155
|
-
// written into the registry — surface them as their own live section
|
|
1156
|
-
//
|
|
1157
|
-
//
|
|
1158
|
-
|
|
1165
|
+
// written into the registry — surface them as their own live section, but only
|
|
1166
|
+
// behind --all (RUSH-2190). Reading them routes through crabboxEnv, whose
|
|
1167
|
+
// bundle auto-detect scans the keychain and can raise a Touch ID sheet AFTER
|
|
1168
|
+
// the table has printed — unacceptable for the default list, which hooks and
|
|
1169
|
+
// other non-interactive callers rely on. The predicate is exported so the
|
|
1170
|
+
// gate itself is unit-tested; keep it the ONLY condition guarding this call.
|
|
1171
|
+
if (showLeasedBoxesSection(opts)) {
|
|
1159
1172
|
for (const line of loadLeasedBoxesSection())
|
|
1160
1173
|
console.log(line);
|
|
1161
1174
|
}
|
|
@@ -1170,6 +1183,7 @@ Typical workflow:
|
|
|
1170
1183
|
.option('--refresh', 'force a live probe of every device, bypassing the cache')
|
|
1171
1184
|
.option('--live', 'alias of --refresh (shorter to type)')
|
|
1172
1185
|
.option('-f, --full', 'full mode: add per-device core count and free/total memory')
|
|
1186
|
+
.option('--all', 'also show ephemeral leased boxes (live crabbox call; may need bundle secrets)')
|
|
1173
1187
|
.action(runList);
|
|
1174
1188
|
devicesCmd
|
|
1175
1189
|
.command('status')
|
|
@@ -10,13 +10,12 @@
|
|
|
10
10
|
* agents watchdog --nudge one tick, actually injects (explicit opt-in)
|
|
11
11
|
* agents watchdog --watch manual poll loop (dry unless --nudge)
|
|
12
12
|
* agents watchdog --json machine-readable tick output (for the menu-bar)
|
|
13
|
-
* agents watchdog
|
|
13
|
+
* agents watchdog on|off turn the always-on watchdog routine on/off
|
|
14
14
|
* agents watchdog policy <id> <p> per-session policy: off | keep | handsoff
|
|
15
15
|
*
|
|
16
16
|
* The always-on watchdog is a daemon-fired ROUTINE, not a private sentinel + a
|
|
17
|
-
* hand-rolled loop
|
|
18
|
-
*
|
|
19
|
-
* `disable` pauses it. See ../lib/watchdog/routine.ts.
|
|
17
|
+
* hand-rolled loop. Its immutable definition comes from the system DotAgents
|
|
18
|
+
* repo; on/off only changes this device's routine membership.
|
|
20
19
|
*/
|
|
21
20
|
import type { Command } from 'commander';
|
|
22
21
|
/** Register the `agents watchdog` command tree. */
|
|
@@ -10,26 +10,26 @@
|
|
|
10
10
|
* agents watchdog --nudge one tick, actually injects (explicit opt-in)
|
|
11
11
|
* agents watchdog --watch manual poll loop (dry unless --nudge)
|
|
12
12
|
* agents watchdog --json machine-readable tick output (for the menu-bar)
|
|
13
|
-
* agents watchdog
|
|
13
|
+
* agents watchdog on|off turn the always-on watchdog routine on/off
|
|
14
14
|
* agents watchdog policy <id> <p> per-session policy: off | keep | handsoff
|
|
15
15
|
*
|
|
16
16
|
* The always-on watchdog is a daemon-fired ROUTINE, not a private sentinel + a
|
|
17
|
-
* hand-rolled loop
|
|
18
|
-
*
|
|
19
|
-
* `disable` pauses it. See ../lib/watchdog/routine.ts.
|
|
17
|
+
* hand-rolled loop. Its immutable definition comes from the system DotAgents
|
|
18
|
+
* repo; on/off only changes this device's routine membership.
|
|
20
19
|
*/
|
|
21
20
|
import chalk from 'chalk';
|
|
22
21
|
import * as path from 'path';
|
|
23
22
|
import { setHelpSections } from '../lib/help.js';
|
|
24
23
|
import { parseDuration } from '../lib/hooks/cache.js';
|
|
25
24
|
import { getRuntimeStateDir } from '../lib/state.js';
|
|
26
|
-
import { setJobEnabled } from '../lib/routines.js';
|
|
25
|
+
import { readJob, setJobEnabled } from '../lib/routines.js';
|
|
27
26
|
import { getActiveSessions } from '../lib/session/active.js';
|
|
28
27
|
import { mailboxIdForActiveSession } from '../lib/mailbox-target.js';
|
|
29
28
|
import { gcMailbox } from '../lib/mailbox-gc.js';
|
|
30
|
-
import {
|
|
29
|
+
import { devicesWithRoutineEnabled, routineEnabledOnThisDevice } from '../lib/routine-activation.js';
|
|
31
30
|
import { runWatchdogTick, writePolicySentinel, DEFAULT_THRESHOLDS, } from '../lib/watchdog/runner.js';
|
|
32
31
|
import { isWatchdogRotateEnabled, listRotateStates, setWatchdogRotateEnabled } from '../lib/watchdog/rotate.js';
|
|
32
|
+
const WATCHDOG_ROUTINE_NAME = 'watchdog';
|
|
33
33
|
/** Default state dir the runner and these subcommands share. */
|
|
34
34
|
function stateDir() {
|
|
35
35
|
return path.join(getRuntimeStateDir(), 'watchdog');
|
|
@@ -120,7 +120,7 @@ export function registerWatchdogCommand(program) {
|
|
|
120
120
|
.command('watchdog')
|
|
121
121
|
.description('Auto-nudge stalled agent terminals: detect stalls, resolve the exact split, inject "Continue." — no menu-bar needed.')
|
|
122
122
|
.option('--nudge', 'Actually inject (default is a dry run that only reports what it would do)')
|
|
123
|
-
.option('--watch', 'Manual poll loop: run a tick every --interval (dry unless --nudge; the always-on path is `watchdog
|
|
123
|
+
.option('--watch', 'Manual poll loop: run a tick every --interval (dry unless --nudge; the always-on path is `watchdog on`)')
|
|
124
124
|
.option('--interval <dur>', 'Poll interval in --watch mode (e.g. 30s, 1m)', '30s')
|
|
125
125
|
.option('--stall <dur>', 'Idle time before a session counts as stalled', humanMs(DEFAULT_THRESHOLDS.stallMs))
|
|
126
126
|
.option('--cooldown <dur>', 'Minimum time between nudges to the same session', humanMs(DEFAULT_THRESHOLDS.cooldownMs))
|
|
@@ -166,7 +166,7 @@ export function registerWatchdogCommand(program) {
|
|
|
166
166
|
const intervalMs = durationMsOr(opts.interval, 30_000);
|
|
167
167
|
if (!computeWillInject() && !opts.json) {
|
|
168
168
|
console.log(chalk.yellow(`watchdog --watch is DETECT-ONLY. Pass --nudge to inject, ` +
|
|
169
|
-
`or run 'agents watchdog
|
|
169
|
+
`or run 'agents watchdog on' for the always-on daemon routine.`));
|
|
170
170
|
}
|
|
171
171
|
// eslint-disable-next-line no-constant-condition
|
|
172
172
|
while (true) {
|
|
@@ -197,7 +197,7 @@ export function registerWatchdogCommand(program) {
|
|
|
197
197
|
agents watchdog --json
|
|
198
198
|
|
|
199
199
|
# Turn on the ALWAYS-ON watchdog (a daemon-fired routine)
|
|
200
|
-
agents watchdog
|
|
200
|
+
agents watchdog on
|
|
201
201
|
|
|
202
202
|
# Show the routine, rotate config, and any in-flight in-place rotates
|
|
203
203
|
agents watchdog status
|
|
@@ -242,36 +242,45 @@ export function registerWatchdogCommand(program) {
|
|
|
242
242
|
'watchdog.rotate: off' to ~/.agents/agents.yaml; nudging stays on).
|
|
243
243
|
State machine: ~/.agents/.cache/state/watchdog/rotate/<sessionId>.json.
|
|
244
244
|
|
|
245
|
-
Always-on: 'agents watchdog
|
|
246
|
-
|
|
247
|
-
|
|
245
|
+
Always-on: 'agents watchdog on' enables the system-defined 'watchdog' routine
|
|
246
|
+
on this device and reloads the daemon; 'off' disables it here. Inspect it with
|
|
247
|
+
'agents routines list'. Defaults
|
|
248
248
|
OFF. Per-session policy: off (ignore), keep (default), handsoff (detect + flag).
|
|
249
249
|
|
|
250
250
|
State (tray-readable): ${path.join('~/.agents/.cache/state/watchdog', '{nudges,flags,last-tick}.json')}
|
|
251
251
|
`,
|
|
252
252
|
});
|
|
253
253
|
// --- always-on enable/disable/status (backed by the daemon routine) --------
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
254
|
+
const turnOn = async () => {
|
|
255
|
+
const routine = readJob(WATCHDOG_ROUTINE_NAME);
|
|
256
|
+
if (!routine)
|
|
257
|
+
throw new Error("Built-in routine 'watchdog' is missing. Run: agents repo pull system");
|
|
258
|
+
setJobEnabled(WATCHDOG_ROUTINE_NAME, true);
|
|
258
259
|
await reloadDaemonForRoutine(true);
|
|
259
|
-
console.log(chalk.green(`watchdog:
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
if (!watchdogRoutineExists()) {
|
|
265
|
-
console.log(chalk.dim('watchdog: already off (no routine)'));
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
260
|
+
console.log(chalk.green(`watchdog: ON on this device (${routine.schedule ?? 'event-triggered'})`));
|
|
261
|
+
};
|
|
262
|
+
const turnOff = async () => {
|
|
263
|
+
if (!readJob(WATCHDOG_ROUTINE_NAME))
|
|
264
|
+
throw new Error("Built-in routine 'watchdog' is missing. Run: agents repo pull system");
|
|
268
265
|
setJobEnabled(WATCHDOG_ROUTINE_NAME, false);
|
|
269
266
|
await reloadDaemonForRoutine(false);
|
|
270
|
-
console.log(chalk.yellow('watchdog:
|
|
267
|
+
console.log(chalk.yellow('watchdog: OFF on this device'));
|
|
268
|
+
};
|
|
269
|
+
cmd.command('on')
|
|
270
|
+
.description('Enable the built-in watchdog routine on this device.')
|
|
271
|
+
.action(async () => {
|
|
272
|
+
await turnOn();
|
|
273
|
+
});
|
|
274
|
+
cmd.command('off')
|
|
275
|
+
.description('Disable the built-in watchdog routine on this device.')
|
|
276
|
+
.action(async () => {
|
|
277
|
+
await turnOff();
|
|
271
278
|
});
|
|
279
|
+
cmd.command('enable', { hidden: true }).action(turnOn);
|
|
280
|
+
cmd.command('disable', { hidden: true }).action(turnOff);
|
|
272
281
|
cmd.command('rotate <state>')
|
|
273
282
|
.description('Turn in-place rotate of rate-limited sessions on|off (watchdog.rotate in agents.yaml). ' +
|
|
274
|
-
'Rotate-only: nudging stays on — unlike `watchdog
|
|
283
|
+
'Rotate-only: nudging stays on — unlike `watchdog off`, which disables the whole watchdog on this device.')
|
|
275
284
|
.action((state) => {
|
|
276
285
|
const s = state.toLowerCase();
|
|
277
286
|
if (s !== 'on' && s !== 'off') {
|
|
@@ -292,13 +301,15 @@ export function registerWatchdogCommand(program) {
|
|
|
292
301
|
// parent, not this subcommand. optsWithGlobals() merges both levels, so we
|
|
293
302
|
// read it correctly regardless of which command commander bound it to.
|
|
294
303
|
const json = command.optsWithGlobals().json === true;
|
|
295
|
-
const on =
|
|
304
|
+
const on = routineEnabledOnThisDevice(WATCHDOG_ROUTINE_NAME) === true;
|
|
305
|
+
const enabledDevices = devicesWithRoutineEnabled(WATCHDOG_ROUTINE_NAME);
|
|
296
306
|
const rotate = isWatchdogRotateEnabled() ? 'on' : 'off';
|
|
297
307
|
const rotates = listRotateStates(stateDir());
|
|
298
308
|
const inflight = rotates.filter((r) => r.phase !== 'done' && r.phase !== 'failed');
|
|
299
309
|
if (json) {
|
|
300
310
|
console.log(JSON.stringify({
|
|
301
311
|
enabled: on,
|
|
312
|
+
enabledDevices,
|
|
302
313
|
routine: WATCHDOG_ROUTINE_NAME,
|
|
303
314
|
stateDir: stateDir(),
|
|
304
315
|
rotate,
|
|
@@ -314,6 +325,7 @@ export function registerWatchdogCommand(program) {
|
|
|
314
325
|
return;
|
|
315
326
|
}
|
|
316
327
|
console.log(`always-on watchdog: ${on ? chalk.green('ON') : chalk.dim('off')} (routine '${WATCHDOG_ROUTINE_NAME}')`);
|
|
328
|
+
console.log(`enabled devices: ${enabledDevices.length > 0 ? enabledDevices.join(', ') : chalk.dim('none')}`);
|
|
317
329
|
console.log(`rotate: ${rotate === 'on' ? chalk.green('on') : chalk.yellow('off')} (watchdog.rotate in agents.yaml) · ${inflight.length} in-flight`);
|
|
318
330
|
for (const r of inflight) {
|
|
319
331
|
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}`) : ''}`);
|
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, loadMonitors, loadProjects, loadRun, loadFork, loadDefaults, loadSet, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadApply, loadStatus, loadSnapshot, loadProfiles, loadHarness, loadSecrets, loadLogin, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadFactory, loadUsage, loadCost, loadPerf, loadTrends, loadOutput, loadBudget, loadAlias, loadMine, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadWebhook, loadFunnel, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadUninstall, 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, loadImport, loadExport, loadPackages, loadRoutines, loadMonitors, loadProjects, loadRun, loadFork, loadDefaults, loadSet, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadApply, loadStatus, loadSnapshot, loadProfiles, loadHarness, loadSecrets, loadLogin, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadFactory, loadUsage, loadCost, loadPerf, loadTrends, loadOutput, loadBudget, loadAlias, loadMine, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadWebhook, loadFunnel, loadHumans, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadUninstall, 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';
|
|
@@ -980,6 +980,7 @@ async function registerAllEagerCommands() {
|
|
|
980
980
|
await reg(loadAudit);
|
|
981
981
|
await reg(loadWebhook);
|
|
982
982
|
await reg(loadFunnel);
|
|
983
|
+
await reg(loadHumans);
|
|
983
984
|
registerHqTombstoneCommand(program);
|
|
984
985
|
await reg(loadFeed);
|
|
985
986
|
await reg(loadMailboxes);
|
|
@@ -1177,7 +1178,7 @@ if (process.env.AGENTS_SKIP_MIGRATION !== '1') {
|
|
|
1177
1178
|
// Bumping the suffix re-runs migrations for every user; binary releases that
|
|
1178
1179
|
// don't change the schema must NOT re-run (they would destroy user content
|
|
1179
1180
|
// when migration steps overlap with user-authored paths). See issue #20.
|
|
1180
|
-
const sentinelValue = '
|
|
1181
|
+
const sentinelValue = 'v15';
|
|
1181
1182
|
let needRun = true;
|
|
1182
1183
|
try {
|
|
1183
1184
|
if (fs.existsSync(sentinel) && fs.readFileSync(sentinel, 'utf-8').trim() === sentinelValue) {
|
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
*
|
|
9
9
|
* The dispatch goes through the same provider layer as `agents cloud run`, so Rush
|
|
10
10
|
* (Prix) is just one provider among rush/codex/factory — NOT a hidden requirement.
|
|
11
|
-
* A project
|
|
11
|
+
* A project pins its provider via `dispatch.provider` in its
|
|
12
|
+
* ~/.agents/projects/<name>.yaml definition.
|
|
12
13
|
*
|
|
13
|
-
* OPT-IN: a project auto-dispatches ONLY when `
|
|
14
|
-
* Nothing
|
|
14
|
+
* OPT-IN: a project auto-dispatches ONLY when `dispatch.enabled: true` AND
|
|
15
|
+
* `dispatch.maxAgents > 0` AND it has a `linear.projectId`. Nothing is on by default.
|
|
15
16
|
*/
|
|
16
|
-
/** The subset of a
|
|
17
|
+
/** The subset of a project definition this module needs. */
|
|
17
18
|
export interface AutoDispatchProject {
|
|
18
19
|
id: string;
|
|
19
20
|
name: string;
|
|
@@ -44,13 +45,10 @@ export interface PlannedDispatch {
|
|
|
44
45
|
delegateName: string;
|
|
45
46
|
}
|
|
46
47
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* tested end-to-end against a real fixture file instead of a mock.
|
|
48
|
+
* Read auto-dispatch candidates from canonical project YAML definitions
|
|
49
|
+
* (~/.agents/projects/<name>.yaml). Only projects with `dispatch.enabled: true`
|
|
50
|
+
* are returned; the caller's `isEligible` gate then checks the cap and Linear id.
|
|
51
51
|
*/
|
|
52
|
-
export declare function factoryProjectsPath(): string;
|
|
53
|
-
/** Read the project registry rows this module cares about. */
|
|
54
52
|
export declare function readAutoDispatchProjects(): AutoDispatchProject[];
|
|
55
53
|
/** A project is eligible only when it has explicitly opted in with a positive cap. */
|
|
56
54
|
export declare function isEligible(p: AutoDispatchProject): boolean;
|
|
@@ -8,53 +8,31 @@
|
|
|
8
8
|
*
|
|
9
9
|
* The dispatch goes through the same provider layer as `agents cloud run`, so Rush
|
|
10
10
|
* (Prix) is just one provider among rush/codex/factory — NOT a hidden requirement.
|
|
11
|
-
* A project
|
|
11
|
+
* A project pins its provider via `dispatch.provider` in its
|
|
12
|
+
* ~/.agents/projects/<name>.yaml definition.
|
|
12
13
|
*
|
|
13
|
-
* OPT-IN: a project auto-dispatches ONLY when `
|
|
14
|
-
* Nothing
|
|
14
|
+
* OPT-IN: a project auto-dispatches ONLY when `dispatch.enabled: true` AND
|
|
15
|
+
* `dispatch.maxAgents > 0` AND it has a `linear.projectId`. Nothing is on by default.
|
|
15
16
|
*/
|
|
16
|
-
import
|
|
17
|
-
import * as path from 'path';
|
|
18
|
-
import { homedir } from 'os';
|
|
17
|
+
import { listProjectDefs } from './projects.js';
|
|
19
18
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* tested end-to-end against a real fixture file instead of a mock.
|
|
19
|
+
* Read auto-dispatch candidates from canonical project YAML definitions
|
|
20
|
+
* (~/.agents/projects/<name>.yaml). Only projects with `dispatch.enabled: true`
|
|
21
|
+
* are returned; the caller's `isEligible` gate then checks the cap and Linear id.
|
|
24
22
|
*/
|
|
25
|
-
export function factoryProjectsPath() {
|
|
26
|
-
return process.env.AGENTS_FACTORY_PROJECTS_PATH ?? path.join(homedir(), '.agents', 'factory', 'projects.json');
|
|
27
|
-
}
|
|
28
|
-
/** Read the project registry rows this module cares about. */
|
|
29
23
|
export function readAutoDispatchProjects() {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
continue;
|
|
43
|
-
const o = r;
|
|
44
|
-
if (typeof o.id !== 'string' || typeof o.name !== 'string')
|
|
45
|
-
continue;
|
|
46
|
-
out.push({
|
|
47
|
-
id: o.id,
|
|
48
|
-
name: o.name,
|
|
49
|
-
linearProjectId: typeof o.linearProjectId === 'string' ? o.linearProjectId : undefined,
|
|
50
|
-
repoSlug: typeof o.repoSlug === 'string' ? o.repoSlug : undefined,
|
|
51
|
-
autoDispatch: o.autoDispatch === true,
|
|
52
|
-
maxAgents: typeof o.maxAgents === 'number' ? o.maxAgents : undefined,
|
|
53
|
-
provider: typeof o.provider === 'string' ? o.provider : undefined,
|
|
54
|
-
host: typeof o.host === 'string' ? o.host : undefined,
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
return out;
|
|
24
|
+
return listProjectDefs()
|
|
25
|
+
.filter((d) => d.dispatch?.enabled === true)
|
|
26
|
+
.map((d) => ({
|
|
27
|
+
id: d.name,
|
|
28
|
+
name: d.name,
|
|
29
|
+
linearProjectId: d.linear?.projectId,
|
|
30
|
+
repoSlug: d.repo ?? d.repos?.[0]?.slug,
|
|
31
|
+
autoDispatch: d.dispatch?.enabled ?? false,
|
|
32
|
+
maxAgents: d.dispatch?.maxAgents,
|
|
33
|
+
provider: d.dispatch?.provider,
|
|
34
|
+
host: d.dispatch?.host,
|
|
35
|
+
}));
|
|
58
36
|
}
|
|
59
37
|
/** A project is eligible only when it has explicitly opted in with a positive cap. */
|
|
60
38
|
export function isEligible(p) {
|
|
@@ -59,7 +59,10 @@ export type ResolveSendResult = {
|
|
|
59
59
|
export declare function isOwnerAlias(to: string | undefined): boolean;
|
|
60
60
|
/** Compose body + optional URL lines (skip urls already present in the body). */
|
|
61
61
|
export declare function composeSendText(text: string, urls?: string[]): string;
|
|
62
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* Read the owner destination; humans.yaml is the primary source, agents.yaml
|
|
64
|
+
* notify.owner is the fallback. Returns null when neither is set.
|
|
65
|
+
*/
|
|
63
66
|
export declare function readOwnerDest(meta: Meta): {
|
|
64
67
|
channel: string;
|
|
65
68
|
to: string;
|