@phnx-labs/agents-cli 1.22.57 → 1.22.59
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 +294 -0
- package/README.md +29 -0
- package/dist/bootstrap.js +39 -1
- package/dist/commands/accounts.js +7 -3
- package/dist/commands/apply.js +10 -2
- package/dist/commands/fork.d.ts +23 -10
- package/dist/commands/fork.js +115 -58
- package/dist/commands/monitors.js +198 -23
- package/dist/commands/prune.js +5 -3
- package/dist/commands/routines.d.ts +8 -0
- package/dist/commands/routines.js +57 -3
- package/dist/commands/routines.test-fixture.js +5 -0
- package/dist/commands/send.d.ts +2 -1
- package/dist/commands/send.js +7 -5
- package/dist/commands/sessions-picker.d.ts +11 -0
- package/dist/commands/sessions-picker.js +16 -0
- package/dist/commands/sessions-stats.js +37 -5
- package/dist/commands/sessions.js +40 -5
- package/dist/commands/share.d.ts +14 -0
- package/dist/commands/share.js +43 -2
- package/dist/commands/ssh.js +12 -1
- package/dist/commands/status.js +1 -1
- package/dist/commands/sync.js +83 -7
- package/dist/commands/traces.js +7 -0
- package/dist/commands/versions.js +12 -4
- package/dist/commands/view.js +7 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +6 -1
- package/dist/lib/account-registry.d.ts +5 -1
- package/dist/lib/account-registry.js +47 -14
- package/dist/lib/accounting/capacity.d.ts +18 -7
- package/dist/lib/accounting/capacity.js +19 -8
- package/dist/lib/accounting/usage-sync.d.ts +29 -1
- package/dist/lib/accounting/usage-sync.js +76 -2
- package/dist/lib/accounting/usage.js +7 -1
- package/dist/lib/auth-mint.d.ts +11 -1
- package/dist/lib/auth-mint.js +21 -6
- package/dist/lib/auto-pull-worker.js +7 -2
- package/dist/lib/browser/ipc.d.ts +8 -0
- package/dist/lib/browser/ipc.js +87 -0
- package/dist/lib/browser/service.d.ts +19 -0
- package/dist/lib/browser/service.js +96 -11
- package/dist/lib/browser/sessions-list.js +10 -1
- package/dist/lib/cloud/rush.d.ts +7 -0
- package/dist/lib/cloud/rush.js +29 -1
- package/dist/lib/daemon/daemon.d.ts +22 -0
- package/dist/lib/daemon/daemon.js +39 -0
- package/dist/lib/daemon/runner.d.ts +3 -0
- package/dist/lib/daemon/runner.js +86 -45
- package/dist/lib/daemon/session-index-service.js +9 -1
- package/dist/lib/daemon/usage-sync-service.d.ts +3 -3
- package/dist/lib/daemon/usage-sync-service.js +14 -8
- package/dist/lib/daemon-services.js +1 -1
- package/dist/lib/daemon-ticks.d.ts +15 -0
- package/dist/lib/daemon-ticks.js +26 -0
- package/dist/lib/device-config.d.ts +5 -1
- package/dist/lib/device-config.js +2 -2
- package/dist/lib/devices/connect.d.ts +17 -8
- package/dist/lib/devices/connect.js +31 -14
- package/dist/lib/devices/health.js +5 -1
- package/dist/lib/devices/pool.d.ts +25 -2
- package/dist/lib/devices/pool.js +32 -2
- package/dist/lib/devices/stats-cache.d.ts +0 -6
- package/dist/lib/devices/stats-cache.js +2 -9
- package/dist/lib/doctor-diff.d.ts +14 -0
- package/dist/lib/doctor-diff.js +120 -9
- package/dist/lib/fleet/manifest.d.ts +17 -0
- package/dist/lib/fleet/manifest.js +26 -0
- package/dist/lib/git.d.ts +38 -0
- package/dist/lib/git.js +58 -0
- package/dist/lib/hooks/install.d.ts +27 -11
- package/dist/lib/hooks/install.js +42 -17
- package/dist/lib/hosts/ready.d.ts +8 -0
- package/dist/lib/hosts/ready.js +13 -2
- package/dist/lib/hosts/reconnect.d.ts +52 -203
- package/dist/lib/hosts/reconnect.js +64 -284
- package/dist/lib/installations/migrate.d.ts +6 -120
- package/dist/lib/installations/migrate.js +27 -259
- package/dist/lib/installations/shims.d.ts +13 -95
- package/dist/lib/installations/shims.js +22 -139
- package/dist/lib/installations/store.js +1 -1
- package/dist/lib/installations/versions.d.ts +43 -133
- package/dist/lib/installations/versions.js +94 -206
- package/dist/lib/monitors/config.d.ts +71 -3
- package/dist/lib/monitors/config.js +100 -12
- package/dist/lib/monitors/pid-watch.d.ts +35 -0
- package/dist/lib/monitors/pid-watch.js +45 -0
- package/dist/lib/monitors/remote.d.ts +18 -0
- package/dist/lib/monitors/remote.js +11 -0
- package/dist/lib/permissions.js +7 -2
- package/dist/lib/plugins/plugins.d.ts +17 -3
- package/dist/lib/plugins/plugins.js +84 -9
- package/dist/lib/plugins/skills.d.ts +8 -1
- package/dist/lib/plugins/skills.js +18 -2
- package/dist/lib/pty-server.d.ts +14 -0
- package/dist/lib/pty-server.js +49 -5
- package/dist/lib/refresh.d.ts +9 -0
- package/dist/lib/refresh.js +3 -1
- package/dist/lib/routine-readiness.d.ts +15 -1
- package/dist/lib/routine-readiness.js +41 -0
- package/dist/lib/sandbox.d.ts +4 -1
- package/dist/lib/sandbox.js +30 -1
- package/dist/lib/secrets/agent.d.ts +80 -225
- package/dist/lib/secrets/agent.js +139 -401
- package/dist/lib/secrets/bundles.d.ts +73 -222
- package/dist/lib/secrets/bundles.js +168 -467
- package/dist/lib/secrets/drivers/rush.js +5 -0
- package/dist/lib/secrets/reaper.d.ts +28 -70
- package/dist/lib/secrets/reaper.js +30 -85
- package/dist/lib/secrets/remote.d.ts +42 -129
- package/dist/lib/secrets/remote.js +55 -173
- package/dist/lib/self-heal/checks/install-staging.d.ts +4 -0
- package/dist/lib/self-heal/checks/install-staging.js +96 -0
- package/dist/lib/self-heal/registry.js +2 -0
- package/dist/lib/self-heal/types.d.ts +1 -1
- package/dist/lib/self-update.d.ts +65 -0
- package/dist/lib/self-update.js +138 -0
- package/dist/lib/session/active.d.ts +13 -1
- package/dist/lib/session/active.js +2 -0
- package/dist/lib/session/cloud.js +5 -0
- package/dist/lib/session/db.d.ts +51 -6
- package/dist/lib/session/db.js +266 -20
- package/dist/lib/session/fork.d.ts +45 -26
- package/dist/lib/session/fork.js +32 -95
- package/dist/lib/session/tool-calls.d.ts +43 -1
- package/dist/lib/session/tool-calls.js +74 -44
- package/dist/lib/session/tool-store.d.ts +33 -2
- package/dist/lib/session/tool-store.js +56 -3
- package/dist/lib/smart-launch.d.ts +6 -0
- package/dist/lib/smart-launch.js +5 -2
- package/dist/lib/staleness/writers/plugins.js +5 -2
- package/dist/lib/staleness/writers/sources.d.ts +5 -0
- package/dist/lib/staleness/writers/sources.js +2 -1
- package/dist/lib/staleness/writers/subagents.js +13 -3
- package/dist/lib/state.d.ts +7 -4
- package/dist/lib/state.js +7 -4
- package/dist/lib/subagents.js +8 -2
- package/dist/lib/sync-status.d.ts +22 -0
- package/dist/lib/sync-status.js +27 -0
- package/dist/lib/sync-umbrella.d.ts +9 -0
- package/dist/lib/sync-umbrella.js +21 -2
- package/dist/lib/teams/scheduler.d.ts +10 -0
- package/dist/lib/teams/scheduler.js +8 -0
- package/dist/lib/traces/insights.d.ts +47 -14
- package/dist/lib/traces/insights.js +92 -21
- package/dist/lib/traces/phenotype.d.ts +23 -3
- package/dist/lib/traces/phenotype.js +72 -24
- package/dist/lib/traces/sync.d.ts +128 -6
- package/dist/lib/traces/sync.js +294 -35
- package/dist/lib/traces/worker-template.js +154 -1
- package/dist/lib/view-types.d.ts +12 -0
- package/package.json +2 -2
package/dist/commands/fork.js
CHANGED
|
@@ -1,79 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents sessions fork <session>` — branch an existing conversation into a new,
|
|
3
|
+
* independent same-harness sibling, seeded with a recap so it picks up where the
|
|
4
|
+
* original left off. The original is untouched. Also exposed as the hidden
|
|
5
|
+
* top-level alias `agents fork` (back-compat).
|
|
6
|
+
*
|
|
7
|
+
* The source is resolved CROSS-FLEET (the same path `agents sessions preview`
|
|
8
|
+
* uses), so a session that lives on another device forks fine — the sibling is
|
|
9
|
+
* handed a plain-text recap as its opening input and never has to reach the
|
|
10
|
+
* source transcript. Because the seed is text, any REPL harness can be forked,
|
|
11
|
+
* not just Claude.
|
|
12
|
+
*
|
|
13
|
+
* Thin command layer; the pure recap text lives in `lib/session/fork.ts`.
|
|
14
|
+
*/
|
|
15
|
+
import { spawnSync } from 'child_process';
|
|
1
16
|
import chalk from 'chalk';
|
|
2
17
|
import { setHelpSections } from '../lib/help.js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
18
|
+
import { getCliLaunch } from '../lib/cli-entry.js';
|
|
19
|
+
import { buildForkRecap, forkLabelFor } from '../lib/session/fork.js';
|
|
20
|
+
function defaultDeps() {
|
|
21
|
+
return {
|
|
22
|
+
runPreview: (sub) => {
|
|
23
|
+
const p = getCliLaunch(['sessions', 'preview', ...sub]);
|
|
24
|
+
// stderr inherited so preview's own resolution errors reach the user verbatim.
|
|
25
|
+
const r = spawnSync(p.command, p.args, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'inherit'] });
|
|
26
|
+
return { status: r.status, stdout: r.stdout ?? '' };
|
|
27
|
+
},
|
|
28
|
+
launch: (sub) => {
|
|
29
|
+
const l = getCliLaunch(sub);
|
|
30
|
+
// In-place stdio so the sibling takes over this terminal.
|
|
31
|
+
const r = spawnSync(l.command, l.args, { stdio: 'inherit' });
|
|
32
|
+
return { status: r.status };
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
6
36
|
const FORK_HELP = {
|
|
7
37
|
examples: `
|
|
8
|
-
# Fork a session by (partial) id
|
|
38
|
+
# Fork a session by (partial) id — launches a same-harness sibling seeded with a recap
|
|
9
39
|
agents sessions fork 4f3a9c21
|
|
10
|
-
agents sessions resume <new-id>
|
|
11
40
|
|
|
12
|
-
#
|
|
41
|
+
# Name the fork's session label
|
|
13
42
|
agents sessions fork 4f3a9c21 --name "try redis instead"
|
|
43
|
+
|
|
44
|
+
# Place the sibling on a fleet worker instead of here
|
|
45
|
+
agents sessions fork 4f3a9c21 --device auto
|
|
46
|
+
|
|
47
|
+
# Open the sibling in a fresh terminal tab where you work
|
|
48
|
+
agents sessions fork 4f3a9c21 --terminal
|
|
14
49
|
`,
|
|
15
50
|
notes: `
|
|
16
|
-
- 'resume' continues the SAME conversation; 'fork'
|
|
17
|
-
|
|
18
|
-
-
|
|
19
|
-
|
|
20
|
-
|
|
51
|
+
- 'resume' continues the SAME conversation; 'fork' launches a NEW same-harness
|
|
52
|
+
session seeded with a recap of the source, so the two diverge.
|
|
53
|
+
- Works cross-device and cross-harness: the source is resolved across the fleet
|
|
54
|
+
and the sibling gets a plain-text recap, so it never reaches the source transcript.
|
|
55
|
+
- The recap carries the source id — the sibling can run '/continue <id>' for the
|
|
56
|
+
full history if it needs more than the recap.
|
|
57
|
+
- Resolve the source the same way as resume: an exact or prefix id fragment.
|
|
21
58
|
`,
|
|
22
59
|
};
|
|
23
60
|
/**
|
|
24
|
-
* Resolve the source
|
|
25
|
-
*
|
|
61
|
+
* Resolve the source cross-fleet, build a recap from its preview digest, and
|
|
62
|
+
* launch a same-harness sibling seeded with that recap. Shared by
|
|
63
|
+
* `agents sessions fork` and the `agents fork` alias.
|
|
26
64
|
*/
|
|
27
|
-
export async function runFork(sessionArg, options) {
|
|
28
|
-
// Resolve
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
// `agents sessions fork <id> && agents sessions resume <new>` doesn't proceed
|
|
38
|
-
// on a failed fork.
|
|
39
|
-
console.error(chalk.red(`No session matching "${sessionArg}".`));
|
|
40
|
-
console.error(chalk.gray('List candidates with: agents sessions'));
|
|
65
|
+
export async function runFork(sessionArg, options, deps = defaultDeps()) {
|
|
66
|
+
// Resolve + digest in one cross-fleet hop by shelling the existing preview
|
|
67
|
+
// verb: it resolves the id across the fleet (SSH fan-out + peer hop), computes
|
|
68
|
+
// the digest on the OWNING device, and prints it as JSON — so a remote source
|
|
69
|
+
// resolves fine and we never re-implement resolution or digesting here.
|
|
70
|
+
// --terminal opens a tab on THIS machine; --device dispatches over SSH. `agents
|
|
71
|
+
// run` refuses the combination, so reject it here with a fork-specific message
|
|
72
|
+
// before resolving anything, rather than after an overpromising progress line.
|
|
73
|
+
if (options.device && options.terminal !== undefined) {
|
|
74
|
+
console.error(chalk.red('Pick one placement: --terminal opens a tab here; --device places the sibling on another box. They cannot combine.'));
|
|
41
75
|
process.exitCode = 1;
|
|
42
76
|
return;
|
|
43
77
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
process.exitCode = 1;
|
|
78
|
+
const res = deps.runPreview([sessionArg, '--json']);
|
|
79
|
+
if (res.status !== 0) {
|
|
80
|
+
// preview already explained why on stderr; propagate its exit code.
|
|
81
|
+
process.exitCode = res.status ?? 1;
|
|
50
82
|
return;
|
|
51
83
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
console.error(chalk.
|
|
58
|
-
console.error(chalk.gray(` Branch it by hand — start a fresh ${source.agent} and seed it with the source's context:`));
|
|
59
|
-
console.error(chalk.gray(` agents run ${source.agent} --terminal # then, in the new session:`));
|
|
60
|
-
console.error(chalk.gray(` /continue ${source.shortId}`));
|
|
84
|
+
let data;
|
|
85
|
+
try {
|
|
86
|
+
data = JSON.parse(res.stdout);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
console.error(chalk.red(`Could not read the source session for "${sessionArg}".`));
|
|
61
90
|
process.exitCode = 1;
|
|
62
91
|
return;
|
|
63
92
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
catch (err) {
|
|
69
|
-
console.error(chalk.red(`Could not fork ${source.shortId}: ${err.message}`));
|
|
93
|
+
const source = data?.session;
|
|
94
|
+
if (!source?.id || !source?.agent) {
|
|
95
|
+
console.error(chalk.red(`Could not resolve a forkable source for "${sessionArg}".`));
|
|
70
96
|
process.exitCode = 1;
|
|
71
97
|
return;
|
|
72
98
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
99
|
+
const digest = data?.preview ?? undefined;
|
|
100
|
+
// Most sessions have no explicit --name label; fall back to the auto-derived
|
|
101
|
+
// topic the rest of the CLI shows, not the raw short id (forkLabelFor is the
|
|
102
|
+
// shared 3-tier resolver, and preview's --json now carries `topic`).
|
|
103
|
+
const label = forkLabelFor({ label: source.label, topic: source.topic, shortId: source.shortId });
|
|
104
|
+
const recap = buildForkRecap({
|
|
105
|
+
agent: source.agent,
|
|
106
|
+
label,
|
|
107
|
+
cwd: source.cwd,
|
|
108
|
+
ticketId: source.ticketId,
|
|
109
|
+
machine: source.machine,
|
|
110
|
+
shortId: source.shortId,
|
|
111
|
+
id: source.id,
|
|
112
|
+
lastAssistant: digest?.lastAssistant,
|
|
113
|
+
changes: digest?.changes,
|
|
114
|
+
});
|
|
115
|
+
// Launch a NEW same-harness session, load-balanced across accounts (balanced),
|
|
116
|
+
// seeded with the recap as its opening input. Runs here by default; --device
|
|
117
|
+
// places it on the fleet; --terminal opens it in a fresh tab where the user works.
|
|
118
|
+
const runArgs = ['run', source.agent, recap, '-i', '--strategy', 'balanced', '--name', options.name || `fork of ${label}`];
|
|
119
|
+
if (options.device)
|
|
120
|
+
runArgs.push('--device', options.device);
|
|
121
|
+
if (options.terminal !== undefined) {
|
|
122
|
+
runArgs.push('--terminal');
|
|
123
|
+
if (typeof options.terminal === 'string')
|
|
124
|
+
runArgs.push(options.terminal);
|
|
125
|
+
}
|
|
126
|
+
const where = options.device ? ` on ${options.device}` : options.terminal !== undefined ? ' in a new terminal' : '';
|
|
127
|
+
console.error(chalk.gray(`Forking ${source.shortId} → new ${source.agent} session${where}, seeded with a recap…`));
|
|
128
|
+
const child = deps.launch(runArgs);
|
|
129
|
+
process.exitCode = child.status ?? 0;
|
|
77
130
|
}
|
|
78
131
|
/**
|
|
79
132
|
* Register `agents sessions fork <session>` — the canonical surface (fork is a
|
|
@@ -82,10 +135,12 @@ export async function runFork(sessionArg, options) {
|
|
|
82
135
|
export function registerSessionsForkCommand(sessionsCmd) {
|
|
83
136
|
const cmd = sessionsCmd
|
|
84
137
|
.command('fork <session>')
|
|
85
|
-
.description('Branch a session into a new,
|
|
86
|
-
.option('--name <label>', '
|
|
138
|
+
.description('Branch a session into a new same-harness sibling, seeded with a recap so it continues the work. The original is untouched.')
|
|
139
|
+
.option('--name <label>', 'Session label for the fork (default: "fork of <original>")')
|
|
140
|
+
.option('--device <host>', 'Place the sibling on a fleet device (name or "auto"); defaults to here')
|
|
141
|
+
.option('--terminal [backend]', 'Open the sibling in a real terminal tab (iterm | ghostty | terminal | tmux | vscodium-agent) instead of in-place');
|
|
87
142
|
setHelpSections(cmd, FORK_HELP);
|
|
88
|
-
cmd.action(runFork);
|
|
143
|
+
cmd.action((session, options) => runFork(session, options));
|
|
89
144
|
}
|
|
90
145
|
/**
|
|
91
146
|
* Register the hidden top-level `agents fork` alias. Kept working for back-compat
|
|
@@ -94,7 +149,9 @@ export function registerSessionsForkCommand(sessionsCmd) {
|
|
|
94
149
|
export function registerForkCommand(program) {
|
|
95
150
|
const cmd = program
|
|
96
151
|
.command('fork <session>', { hidden: true })
|
|
97
|
-
.description('Alias for `agents sessions fork` — branch a session into a new
|
|
98
|
-
.option('--name <label>', '
|
|
99
|
-
|
|
152
|
+
.description('Alias for `agents sessions fork` — branch a session into a new same-harness sibling.')
|
|
153
|
+
.option('--name <label>', 'Session label for the fork (default: "fork of <original>")')
|
|
154
|
+
.option('--device <host>', 'Place the sibling on a fleet device (name or "auto"); defaults to here')
|
|
155
|
+
.option('--terminal [backend]', 'Open the sibling in a real terminal tab (iterm | ghostty | terminal | tmux | vscodium-agent) instead of in-place');
|
|
156
|
+
cmd.action((session, options) => runFork(session, options));
|
|
100
157
|
}
|
|
@@ -14,10 +14,10 @@ import * as yaml from 'yaml';
|
|
|
14
14
|
import { isDaemonRunning, signalDaemonReload, startDaemon, } from '../lib/daemon/daemon.js';
|
|
15
15
|
import { findDuplicateMonitor, monitorFingerprint } from '../lib/monitors/fingerprint.js';
|
|
16
16
|
import { gatherFleetMonitors, NO_MONITOR_FANOUT_ENV } from '../lib/monitors/remote.js';
|
|
17
|
-
import { listMonitors, readMonitor, writeMonitor, deleteMonitor, setMonitorEnabled, getMonitorPath, validateMonitor, monitorRunsOnThisDevice, parseInterval, } from '../lib/monitors/config.js';
|
|
17
|
+
import { listMonitors, readMonitor, writeMonitor, deleteMonitor, setMonitorEnabled, getMonitorPath, validateMonitor, monitorRunsOnThisDevice, requiresSingleOwner, monitorSharedInputOwner, parseInterval, } from '../lib/monitors/config.js';
|
|
18
18
|
import { formatRelativeTime } from '../lib/session/relative-time.js';
|
|
19
19
|
import { evaluateMonitorOnce, POLL_SOURCE_TYPES } from '../lib/monitors/engine.js';
|
|
20
|
-
import { listFires, readState, readLiveness, resolveFireOutcome } from '../lib/monitors/state.js';
|
|
20
|
+
import { listFires, readState, readLiveness, resolveFireOutcome, getMonitorHistoryDir } from '../lib/monitors/state.js';
|
|
21
21
|
import { listRuns, getLatestRun, getRunDir } from '../lib/scheduling/routines.js';
|
|
22
22
|
import { getMonitorsDir } from '../lib/state.js';
|
|
23
23
|
import { IS_WINDOWS } from '../lib/platform/index.js';
|
|
@@ -26,6 +26,8 @@ import { machineId, normalizeHost } from '../lib/machine-id.js';
|
|
|
26
26
|
import { loadDevices } from '../lib/devices/registry.js';
|
|
27
27
|
import { assertDaemonEnabled } from '../lib/device-config.js';
|
|
28
28
|
import { setHelpSections } from '../lib/help.js';
|
|
29
|
+
import { isPidAlive } from '../lib/session/active.js';
|
|
30
|
+
import { PID_WATCH_EXITED_TOKEN, pidLivenessCommand } from '../lib/monitors/pid-watch.js';
|
|
29
31
|
import { isInteractiveTerminal, requireInteractiveSelection } from './utils.js';
|
|
30
32
|
function stdoutJson(payload) {
|
|
31
33
|
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
@@ -94,8 +96,79 @@ function ownerLabel(monitor) {
|
|
|
94
96
|
return monitor.device;
|
|
95
97
|
if (monitor.devices && monitor.devices.length > 0)
|
|
96
98
|
return monitor.devices.join(',');
|
|
99
|
+
// An unpinned shared-input monitor (a system built-in by default) is NOT
|
|
100
|
+
// fleet-wide — it fires only on the single resolved owner (SING-9). Show that
|
|
101
|
+
// owner, not a misleading "all"; surface the unresolved case loudly.
|
|
102
|
+
if (requiresSingleOwner(monitor)) {
|
|
103
|
+
const owner = monitorSharedInputOwner();
|
|
104
|
+
return owner ? `${owner} (owner)` : 'unowned — set interactive.host';
|
|
105
|
+
}
|
|
97
106
|
return 'all';
|
|
98
107
|
}
|
|
108
|
+
/** The `(built-in)` tag for a system-layer monitor, mirroring routines' list. */
|
|
109
|
+
function builtinTag(monitor) {
|
|
110
|
+
return monitor.scope === 'system' ? chalk.gray(' (built-in)') : '';
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* A one-line liveness note for a monitor living on a PEER, reconstructed from the
|
|
114
|
+
* `--json` fields that box reported (it computes the colorized local label, which
|
|
115
|
+
* doesn't cross the wire). Deliberately terse — the owning box's `monitors view`
|
|
116
|
+
* has the full detail.
|
|
117
|
+
*/
|
|
118
|
+
function remoteLivenessNote(d) {
|
|
119
|
+
if (!d)
|
|
120
|
+
return chalk.gray('—');
|
|
121
|
+
if (d.enabled === false)
|
|
122
|
+
return chalk.gray('paused');
|
|
123
|
+
if (d.stalled)
|
|
124
|
+
return chalk.red('STALLED');
|
|
125
|
+
if (d.lastActionFailed)
|
|
126
|
+
return chalk.red('ACTION FAILED');
|
|
127
|
+
if (d.lastFiredAt)
|
|
128
|
+
return chalk.green(`fired ${formatRelativeTime(d.lastFiredAt)}`);
|
|
129
|
+
if (typeof d.checkCount === 'number' && d.checkCount > 0)
|
|
130
|
+
return chalk.gray(`checked ${d.checkCount}x`);
|
|
131
|
+
if (d.lastCheckedAt)
|
|
132
|
+
return chalk.gray(`checked ${formatRelativeTime(d.lastCheckedAt)}`);
|
|
133
|
+
return chalk.yellow('never polled');
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Print a stderr note when the fleet fan-out couldn't consult every box, so a
|
|
137
|
+
* partial listing never silently reads as "these are all the monitors there are".
|
|
138
|
+
*/
|
|
139
|
+
function fleetReachNote(fleet) {
|
|
140
|
+
if (fleet.discoveryFailed) {
|
|
141
|
+
stderrLine(chalk.yellow(' Note: could not reach the device registry — the fleet was not checked, only this device is shown.'));
|
|
142
|
+
}
|
|
143
|
+
else if (fleet.skipped.length > 0) {
|
|
144
|
+
stderrLine(chalk.yellow(` Note: could not reach ${fleet.skipped.length} device(s): ${fleet.skipped.join(', ')} — their monitors are not shown.`));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/** A remote monitor rendered into the same `--json` row shape as a local one. */
|
|
148
|
+
function remoteMonitorJsonRow(r) {
|
|
149
|
+
const d = r.display;
|
|
150
|
+
return {
|
|
151
|
+
machine: r.machine,
|
|
152
|
+
name: r.monitor.name,
|
|
153
|
+
enabled: d?.enabled ?? null,
|
|
154
|
+
source: r.monitor.source,
|
|
155
|
+
condition: r.monitor.condition,
|
|
156
|
+
action: r.monitor.action,
|
|
157
|
+
owner: d?.owner ?? null,
|
|
158
|
+
scope: d?.scope ?? null,
|
|
159
|
+
builtin: d?.scope === 'system',
|
|
160
|
+
runsHere: false,
|
|
161
|
+
lastSeenAt: null,
|
|
162
|
+
lastFiredAt: d?.lastFiredAt ?? null,
|
|
163
|
+
lastCheckedAt: d?.lastCheckedAt ?? null,
|
|
164
|
+
checkCount: d?.checkCount ?? 0,
|
|
165
|
+
lastError: null,
|
|
166
|
+
consecutiveErrors: 0,
|
|
167
|
+
stalled: d?.stalled ?? false,
|
|
168
|
+
lastActionStatus: null,
|
|
169
|
+
lastActionFailed: d?.lastActionFailed ?? false,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
99
172
|
/** A monitor's evaluation cadence in ms (falls back to the engine default). */
|
|
100
173
|
function monitorIntervalMs(monitor) {
|
|
101
174
|
if (monitor.source.interval)
|
|
@@ -139,6 +212,13 @@ function livenessLabel(monitor, state, liveness) {
|
|
|
139
212
|
if (liveness.lastError) {
|
|
140
213
|
return chalk.red(`checked ${liveness.checkCount}x · error: ${liveness.lastError.replace(/\s+/g, ' ').slice(0, 60)}`);
|
|
141
214
|
}
|
|
215
|
+
const latestFire = listFires(monitor.name).at(-1);
|
|
216
|
+
if (latestFire) {
|
|
217
|
+
const outcome = resolveFireOutcome(monitor.name, latestFire);
|
|
218
|
+
if (!outcome.ok) {
|
|
219
|
+
return chalk.red(`ACTION FAILED ${formatRelativeTime(latestFire.firedAt)}`) + chalk.gray(` · checked ${liveness.checkCount}x`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
142
222
|
if (state?.lastFiredAt) {
|
|
143
223
|
return chalk.green(`fired ${formatRelativeTime(state.lastFiredAt)}`) + chalk.gray(` · checked ${liveness.checkCount}x`);
|
|
144
224
|
}
|
|
@@ -223,11 +303,28 @@ async function validateDevice(name) {
|
|
|
223
303
|
}
|
|
224
304
|
return normalized;
|
|
225
305
|
}
|
|
226
|
-
/** Parse the source flags into a MonitorSource, exiting on missing/ambiguous input. */
|
|
227
|
-
function buildSource(options) {
|
|
306
|
+
/** Parse the source flags into a MonitorSource, exiting on missing/ambiguous input. `name` seeds the --watch-pid running-seen marker. */
|
|
307
|
+
function buildSource(options, name) {
|
|
228
308
|
const chosen = [];
|
|
229
309
|
if (options.watch)
|
|
230
310
|
chosen.push({ type: 'command', source: { type: 'command', command: options.watch } });
|
|
311
|
+
if (options.watchPid) {
|
|
312
|
+
const pid = Number(options.watchPid);
|
|
313
|
+
if (!Number.isInteger(pid) || pid < 1) {
|
|
314
|
+
stderrLine(chalk.red(`--watch-pid must be a positive integer pid, got '${options.watchPid}'`));
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
317
|
+
// Fail loud rather than silently arming a watcher on a corpse (PHNX-3023):
|
|
318
|
+
// a pid that is already gone at arm time can never transition to "exited",
|
|
319
|
+
// so the monitor would sit enabled and never fire.
|
|
320
|
+
if (!isPidAlive(pid) && !options.force) {
|
|
321
|
+
stderrLine(chalk.red(`Process ${pid} is not running — there is nothing to watch.`));
|
|
322
|
+
stderrLine(chalk.gray('Pass --force to arm it anyway (e.g. the pid is about to be spawned by a concurrent step).'));
|
|
323
|
+
process.exit(1);
|
|
324
|
+
}
|
|
325
|
+
const seenRunningMarkerPath = path.join(getMonitorHistoryDir(name), 'pid-watch-seen-running');
|
|
326
|
+
chosen.push({ type: 'command', source: { type: 'command', command: pidLivenessCommand(pid, seenRunningMarkerPath) } });
|
|
327
|
+
}
|
|
231
328
|
if (options.poll) {
|
|
232
329
|
chosen.push({ type: 'poll', source: { type: 'poll', command: options.poll[0], interval: options.poll[1] } });
|
|
233
330
|
}
|
|
@@ -267,7 +364,7 @@ function buildSource(options) {
|
|
|
267
364
|
chosen.push({ type: 'webhook', source: { type: 'webhook', webhook } });
|
|
268
365
|
}
|
|
269
366
|
if (chosen.length === 0) {
|
|
270
|
-
stderrLine(chalk.red('A source is required: --watch, --poll, --poll-http, --ws, --watch-file, --watch-device, or --on'));
|
|
367
|
+
stderrLine(chalk.red('A source is required: --watch, --watch-pid, --poll, --poll-http, --ws, --watch-file, --watch-device, or --on'));
|
|
271
368
|
process.exit(1);
|
|
272
369
|
}
|
|
273
370
|
if (chosen.length > 1) {
|
|
@@ -276,7 +373,12 @@ function buildSource(options) {
|
|
|
276
373
|
}
|
|
277
374
|
return chosen[0].source;
|
|
278
375
|
}
|
|
279
|
-
/**
|
|
376
|
+
/**
|
|
377
|
+
* Parse the condition flags into a MonitorCondition (default on-change).
|
|
378
|
+
* `--watch-pid` with no explicit mode defaults to firing on exit rather than
|
|
379
|
+
* on-change, since "running"/"exited" is a two-value observation where
|
|
380
|
+
* on-change would fire the instant the daemon takes its first poll.
|
|
381
|
+
*/
|
|
280
382
|
function buildCondition(options) {
|
|
281
383
|
const modes = [];
|
|
282
384
|
if (options.onChange)
|
|
@@ -289,6 +391,9 @@ function buildCondition(options) {
|
|
|
289
391
|
stderrLine(chalk.red('--on-change, --match, and --every are mutually exclusive'));
|
|
290
392
|
process.exit(1);
|
|
291
393
|
}
|
|
394
|
+
if (options.watchPid && modes.length === 0) {
|
|
395
|
+
return { mode: 'match', match: PID_WATCH_EXITED_TOKEN };
|
|
396
|
+
}
|
|
292
397
|
const mode = modes[0] ?? (options.match ? 'match' : 'on-change');
|
|
293
398
|
const condition = { mode };
|
|
294
399
|
if (options.match)
|
|
@@ -451,11 +556,17 @@ export function registerMonitorsCommands(program) {
|
|
|
451
556
|
# A fleet box going loaded → spin up an agent
|
|
452
557
|
agents monitors add box-loaded --watch-device yosemite-s0 --match loaded \\
|
|
453
558
|
--run claude --prompt 'yosemite-s0 is loaded: {event}. Investigate.'
|
|
559
|
+
|
|
560
|
+
# A backgrounded shell that will never exit on its own (a watch loop, gh pr
|
|
561
|
+
# checks --watch, a long sleep) — arm a REAL watcher instead of trusting the
|
|
562
|
+
# harness's own exit hook (which only fires when the process dies):
|
|
563
|
+
agents monitors add pr-checks-1234 --watch-pid 48213 \\
|
|
564
|
+
--run claude --prompt 'PID 48213 exited: {event}. Resume and check the result.'
|
|
454
565
|
`,
|
|
455
566
|
notes: `
|
|
456
567
|
A monitor is a routine whose trigger is a watched SOURCE instead of a clock.
|
|
457
568
|
It has three parts:
|
|
458
|
-
- SOURCE (--watch, --poll, --poll-http, --ws, --watch-file, --watch-device, --on)
|
|
569
|
+
- SOURCE (--watch, --watch-pid, --poll, --poll-http, --ws, --watch-file, --watch-device, --on)
|
|
459
570
|
- CONDITION (--on-change [default], --match <re>, --every; --dedupe-key)
|
|
460
571
|
- ACTION (--run <agent> --prompt, --routine, --notify, --webhook-out)
|
|
461
572
|
--postcondition <cmd> on --run/--routine asserts the effect
|
|
@@ -470,6 +581,11 @@ export function registerMonitorsCommands(program) {
|
|
|
470
581
|
v1 evaluates poll sources (command, poll, poll-http, file, device). Push
|
|
471
582
|
sources (ws, webhook) are accepted but delivered through a receiver wired in
|
|
472
583
|
a follow-up.
|
|
584
|
+
|
|
585
|
+
--watch-pid <pid> refuses to arm (fails loud) when the pid is already dead —
|
|
586
|
+
a "will re-invoke me" watcher pointed at a corpse never fires. It defaults
|
|
587
|
+
the condition to fire on exit, unlike a raw --watch which defaults to
|
|
588
|
+
on-change.
|
|
473
589
|
`,
|
|
474
590
|
});
|
|
475
591
|
// ─── add ────────────────────────────────────────────────────────────────────
|
|
@@ -478,6 +594,7 @@ export function registerMonitorsCommands(program) {
|
|
|
478
594
|
.description('Create a monitor from inline flags or a YAML file. Auto-starts the daemon unless daemon.enabled is false.')
|
|
479
595
|
// SOURCE
|
|
480
596
|
.option('--watch <cmd>', 'Run a shell command; its stdout is the observation')
|
|
597
|
+
.option('--watch-pid <pid>', 'Watch a backgrounded process for exit — a reliable, daemon-polled alternative to a harness exit hook. Fails loud if the pid is already gone. Defaults to firing on exit')
|
|
481
598
|
.option('--poll <cmd...>', 'Re-run a command every interval: --poll "<cmd>" <interval> (e.g. 30s)')
|
|
482
599
|
.option('--poll-http <url...>', 'GET a URL every interval: --poll-http <url> <interval> (e.g. 15m)')
|
|
483
600
|
.option('--on <source:event>', 'Webhook trigger source: github:pull_request or linear:Issue')
|
|
@@ -515,7 +632,7 @@ export function registerMonitorsCommands(program) {
|
|
|
515
632
|
.option('--force', 'Overwrite a same-named monitor, or add one that duplicates an existing watcher')
|
|
516
633
|
.action(async (nameOrPath, options) => {
|
|
517
634
|
// File mode: a single arg pointing at an existing .yml with no source flags.
|
|
518
|
-
const hasSourceFlag = Boolean(options.watch || options.poll || options.pollHttp || options.on || options.ws || options.watchFile || options.watchDevice);
|
|
635
|
+
const hasSourceFlag = Boolean(options.watch || options.watchPid || options.poll || options.pollHttp || options.on || options.ws || options.watchFile || options.watchDevice);
|
|
519
636
|
if (!hasSourceFlag && nameOrPath && /\.ya?ml$/.test(nameOrPath) && fs.existsSync(path.resolve(nameOrPath))) {
|
|
520
637
|
const resolved = path.resolve(nameOrPath);
|
|
521
638
|
let parsed;
|
|
@@ -548,7 +665,7 @@ export function registerMonitorsCommands(program) {
|
|
|
548
665
|
stderrLine(chalk.gray('Usage: agents monitors add <name> --poll "<cmd>" 30s --match fail --run claude --prompt "..."'));
|
|
549
666
|
process.exit(1);
|
|
550
667
|
}
|
|
551
|
-
const source = buildSource(options);
|
|
668
|
+
const source = buildSource(options, nameOrPath);
|
|
552
669
|
// A --watch-device source name must resolve to a registered fleet member —
|
|
553
670
|
// validate it (fail fast with the registered list) so a typo/removed device
|
|
554
671
|
// can't silently watch the local machine (same gate as --device/--devices).
|
|
@@ -621,15 +738,37 @@ export function registerMonitorsCommands(program) {
|
|
|
621
738
|
// ─── list ────────────────────────────────────────────────────────────────────
|
|
622
739
|
monitorsCmd
|
|
623
740
|
.command('list')
|
|
624
|
-
.description('See all monitors: source, condition, action, owner, and
|
|
741
|
+
.description('See all monitors across the fleet: source, condition, action, owner, last fire, and the box each lives on.')
|
|
625
742
|
.option('--json', 'Emit machine-readable JSON')
|
|
626
|
-
.
|
|
743
|
+
.option('--local', 'This device only — skip the fleet fan-out')
|
|
744
|
+
.action(async (options) => {
|
|
745
|
+
const self = machineId();
|
|
627
746
|
const monitors = listMonitors();
|
|
747
|
+
// A peer answering the fan-out (NO_MONITOR_FANOUT_ENV set) reports its own
|
|
748
|
+
// box only, so the parent's gather is a flat union and never recurses. The
|
|
749
|
+
// duplicate guard reads exactly this bare local array, so its shape must not
|
|
750
|
+
// change when the env is set. `--local` is the same opt-out for a human.
|
|
751
|
+
const peerMode = !!process.env[NO_MONITOR_FANOUT_ENV];
|
|
752
|
+
const localOnly = peerMode || options.local === true;
|
|
753
|
+
let fleet = null;
|
|
754
|
+
if (!localOnly) {
|
|
755
|
+
// Same cross-machine fan-out `sessions --active` and the add-time guard
|
|
756
|
+
// use — visibility, not git-sync (PHNX-2506 item 3). Never throws; an
|
|
757
|
+
// unreachable fleet degrades to an empty remote set plus skipped names.
|
|
758
|
+
fleet = await gatherFleetMonitors();
|
|
759
|
+
}
|
|
760
|
+
// Peers may echo this box's own monitors back (a synced mirror, or the box
|
|
761
|
+
// resolving its own name); drop anything on `self` so local rows aren't
|
|
762
|
+
// double-listed.
|
|
763
|
+
const remote = (fleet?.monitors ?? []).filter((r) => normalizeHost(r.machine) !== self);
|
|
628
764
|
if (options.json) {
|
|
629
|
-
const
|
|
765
|
+
const localRows = monitors.map((m) => {
|
|
630
766
|
const state = readState(m.name);
|
|
631
767
|
const liveness = readLiveness(m.name);
|
|
768
|
+
const latestFire = listFires(m.name).at(-1);
|
|
769
|
+
const latestOutcome = latestFire ? resolveFireOutcome(m.name, latestFire) : null;
|
|
632
770
|
return {
|
|
771
|
+
machine: self,
|
|
633
772
|
name: m.name,
|
|
634
773
|
enabled: m.enabled,
|
|
635
774
|
source: m.source,
|
|
@@ -640,6 +779,11 @@ export function registerMonitorsCommands(program) {
|
|
|
640
779
|
// exactly the case it exists for. `source` already ships whole.
|
|
641
780
|
action: m.action,
|
|
642
781
|
owner: ownerLabel(m),
|
|
782
|
+
// `scope`/`builtin` (PHNX-2506 item 2): where the monitor came from,
|
|
783
|
+
// so "is this a shipped built-in?" is answerable without knowing the
|
|
784
|
+
// system mirror exists. Peers read `scope` off this field.
|
|
785
|
+
scope: m.scope ?? 'user',
|
|
786
|
+
builtin: m.scope === 'system',
|
|
643
787
|
runsHere: monitorRunsOnThisDevice(m),
|
|
644
788
|
lastSeenAt: state?.lastSeenAt ?? null,
|
|
645
789
|
lastFiredAt: state?.lastFiredAt ?? null,
|
|
@@ -650,26 +794,52 @@ export function registerMonitorsCommands(program) {
|
|
|
650
794
|
lastError: liveness?.lastError ?? null,
|
|
651
795
|
consecutiveErrors: liveness?.consecutiveErrors ?? 0,
|
|
652
796
|
stalled: isStalled(m, liveness),
|
|
797
|
+
lastActionStatus: latestOutcome?.runStatus ?? null,
|
|
798
|
+
lastActionFailed: latestOutcome ? !latestOutcome.ok : false,
|
|
653
799
|
};
|
|
654
800
|
});
|
|
655
|
-
stdoutJson(
|
|
801
|
+
stdoutJson([...localRows, ...remote.map(remoteMonitorJsonRow)]);
|
|
656
802
|
return;
|
|
657
803
|
}
|
|
658
|
-
if (monitors.length === 0) {
|
|
804
|
+
if (monitors.length === 0 && remote.length === 0) {
|
|
659
805
|
console.log(chalk.gray('No monitors configured'));
|
|
660
806
|
console.log(chalk.gray(' Add one: agents monitors add <name> --poll "<cmd>" 30s --match fail --run claude --prompt "..."'));
|
|
807
|
+
if (fleet && (fleet.discoveryFailed || fleet.skipped.length > 0))
|
|
808
|
+
fleetReachNote(fleet);
|
|
661
809
|
return;
|
|
662
810
|
}
|
|
663
811
|
console.log(chalk.bold('Monitors\n'));
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
const
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
812
|
+
// This box first — it has full liveness detail.
|
|
813
|
+
if (monitors.length > 0) {
|
|
814
|
+
if (remote.length > 0)
|
|
815
|
+
console.log(chalk.gray(` ${self} (this device)`));
|
|
816
|
+
for (const m of monitors) {
|
|
817
|
+
const state = readState(m.name);
|
|
818
|
+
const liveness = readLiveness(m.name);
|
|
819
|
+
const enabled = m.enabled ? chalk.green('on') : chalk.gray('off');
|
|
820
|
+
const here = monitorRunsOnThisDevice(m);
|
|
821
|
+
const owner = here ? ownerLabel(m) : chalk.gray(ownerLabel(m));
|
|
822
|
+
console.log(` ${chalk.cyan(m.name.padEnd(22))} ${enabled.padEnd(3)} ${sourceLabel(m.source)}${builtinTag(m)}`);
|
|
823
|
+
console.log(` ${' '.repeat(22)} ${chalk.gray(`[${m.condition.mode}]`)} → ${actionLabel(m.action)} ${chalk.gray(`owner: ${owner}`)} ${livenessLabel(m, state, liveness)}`);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
// Then each peer's monitors, grouped by the box they live on.
|
|
827
|
+
const byMachine = new Map();
|
|
828
|
+
for (const r of remote) {
|
|
829
|
+
const key = r.machine;
|
|
830
|
+
(byMachine.get(key) ?? byMachine.set(key, []).get(key)).push(r);
|
|
831
|
+
}
|
|
832
|
+
for (const machine of [...byMachine.keys()].sort()) {
|
|
833
|
+
console.log(chalk.gray(`\n ${machine}`));
|
|
834
|
+
for (const r of byMachine.get(machine)) {
|
|
835
|
+
const m = r.monitor;
|
|
836
|
+
const enabled = r.display?.enabled === false ? chalk.gray('off') : chalk.green('on');
|
|
837
|
+
console.log(` ${chalk.cyan(m.name.padEnd(22))} ${enabled.padEnd(3)} ${sourceLabel(m.source)}${builtinTag({ scope: r.display?.scope })}`);
|
|
838
|
+
console.log(` ${' '.repeat(22)} ${chalk.gray(`[${m.condition.mode}]`)} → ${actionLabel(m.action)} ${chalk.gray(`owner: ${r.display?.owner ?? machine}`)} ${remoteLivenessNote(r.display)}`);
|
|
839
|
+
}
|
|
672
840
|
}
|
|
841
|
+
if (fleet && (fleet.discoveryFailed || fleet.skipped.length > 0))
|
|
842
|
+
fleetReachNote(fleet);
|
|
673
843
|
console.log();
|
|
674
844
|
});
|
|
675
845
|
// ─── view ──────────────────────────────────────────────────────────────────
|
|
@@ -809,7 +979,12 @@ export function registerMonitorsCommands(program) {
|
|
|
809
979
|
monitorPath = safeJoin(dir, `${name}.yml`);
|
|
810
980
|
const builtIn = readMonitor(name);
|
|
811
981
|
if (builtIn) {
|
|
812
|
-
|
|
982
|
+
// Route through writeMonitor, NOT a raw yaml.stringify: readMonitor
|
|
983
|
+
// stamps the derived `scope` (and a built-in's `enabled: true`), and
|
|
984
|
+
// only writeMonitor strips those from the on-disk schema. A raw dump
|
|
985
|
+
// would persist `scope: system` into the user copy — dead, contradictory
|
|
986
|
+
// state that violates the "scope never persists" contract.
|
|
987
|
+
writeMonitor(builtIn);
|
|
813
988
|
console.log(chalk.gray(`Editing a copy of built-in monitor '${name}' in your user dir: ${monitorPath}`));
|
|
814
989
|
}
|
|
815
990
|
else {
|
package/dist/commands/prune.js
CHANGED
|
@@ -66,9 +66,11 @@ function collectOrphans(types, all) {
|
|
|
66
66
|
}
|
|
67
67
|
if (types.includes('hooks')) {
|
|
68
68
|
for (const { agent, version } of scopePairs(iterHooksCapableVersions(), all)) {
|
|
69
|
-
// Orphan hooks = scripts present in the version home
|
|
70
|
-
//
|
|
71
|
-
//
|
|
69
|
+
// Orphan hooks = scripts present in the version home but absent from
|
|
70
|
+
// every configured source (user + system + extras) — genuinely dead
|
|
71
|
+
// files, not merely unregistered ones (sync copies helper/test scripts
|
|
72
|
+
// that no manifest entry declares). Same definition the doctor overview
|
|
73
|
+
// reports (PHNX-2693).
|
|
72
74
|
const orphans = listUnmanagedHooksInVersionHome(agent, version);
|
|
73
75
|
if (orphans.length > 0) {
|
|
74
76
|
groups.push({ type: 'hooks', agent, version, orphans });
|
|
@@ -46,5 +46,13 @@ export declare function groupRoutineJobsByDevice(jobs: JobConfig[], registry: De
|
|
|
46
46
|
*/
|
|
47
47
|
export declare function groupRoutineJobsByProject(jobs: JobConfig[], knownProjectNames: Set<string>): RoutineListGroup[];
|
|
48
48
|
export declare function buildRunsJson(runs: RunMeta[]): Record<string, unknown>[];
|
|
49
|
+
/**
|
|
50
|
+
* The short, human reason a run did not simply complete — for inline display in
|
|
51
|
+
* the list/detail so "why did it fail" needs no dig into the run dir. Prefers the
|
|
52
|
+
* concrete `errorMessage` (which carries `auth_failed: …`, OAuth-revoked, timeouts),
|
|
53
|
+
* then the readiness block for a `blocked` run, then the mapped skip reason. Returns
|
|
54
|
+
* null for a healthy (`completed`/`running`) run, which needs no annotation.
|
|
55
|
+
*/
|
|
56
|
+
export declare function runFailureReason(run: RunMeta): string | null;
|
|
49
57
|
/** Register the `agents routines` command tree. */
|
|
50
58
|
export declare function registerRoutinesCommands(program: Command): void;
|