@phnx-labs/agents-cli 1.22.53 → 1.22.55
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 +208 -0
- package/README.md +59 -9
- package/dist/bootstrap.js +55 -154
- package/dist/cli/command-registry.d.ts +5 -0
- package/dist/cli/command-registry.js +8 -1
- package/dist/commands/accounts.js +219 -173
- package/dist/commands/apply.js +6 -3
- package/dist/commands/auth-mint.d.ts +8 -0
- package/dist/commands/auth-mint.js +96 -0
- package/dist/commands/auth.js +5 -1
- package/dist/commands/browser.js +1 -1
- package/dist/commands/cost.js +8 -2
- package/dist/commands/daemon.js +2 -2
- package/dist/commands/doctor.js +6 -1
- package/dist/commands/exec.js +10 -8
- package/dist/commands/focus.d.ts +1 -0
- package/dist/commands/focus.js +2 -2
- package/dist/commands/go.d.ts +5 -4
- package/dist/commands/go.js +7 -7
- package/dist/commands/insights.js +9 -0
- package/dist/commands/monitors.js +85 -30
- package/dist/commands/output.js +8 -2
- package/dist/commands/repo.js +18 -0
- package/dist/commands/routines.js +31 -2
- package/dist/commands/secrets.js +33 -14
- package/dist/commands/sessions.d.ts +20 -12
- package/dist/commands/sessions.js +64 -20
- package/dist/commands/setup-accounts.d.ts +8 -0
- package/dist/commands/setup-accounts.js +47 -0
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.js +11 -2
- package/dist/commands/share.d.ts +79 -3
- package/dist/commands/share.js +347 -18
- package/dist/commands/ssh.d.ts +7 -0
- package/dist/commands/ssh.js +18 -2
- package/dist/commands/status.js +14 -0
- package/dist/commands/view.d.ts +11 -1
- package/dist/commands/view.js +35 -7
- package/dist/lib/account-registry.js +15 -3
- package/dist/lib/accounting/rotate.d.ts +20 -6
- package/dist/lib/accounting/rotate.js +38 -7
- package/dist/lib/accounting/usage.d.ts +68 -1
- package/dist/lib/accounting/usage.js +116 -10
- package/dist/lib/agent-spec/agents.d.ts +5 -2
- package/dist/lib/agent-spec/agents.js +25 -7
- package/dist/lib/analytics/mix-commands.js +12 -6
- package/dist/lib/auth-mint.d.ts +150 -0
- package/dist/lib/auth-mint.js +434 -0
- package/dist/lib/browser/cdp.d.ts +1 -1
- package/dist/lib/browser/cdp.js +1 -1
- package/dist/lib/browser/ffmpeg.d.ts +12 -0
- package/dist/lib/browser/ffmpeg.js +184 -0
- package/dist/lib/browser/remote-control.d.ts +9 -7
- package/dist/lib/browser/remote-control.js +9 -7
- package/dist/lib/browser/service.js +119 -25
- package/dist/lib/claude-account-token.d.ts +10 -0
- package/dist/lib/claude-account-token.js +14 -4
- package/dist/lib/config-drift.d.ts +37 -0
- package/dist/lib/config-drift.js +72 -0
- package/dist/lib/daemon/auth-sync-service.d.ts +19 -0
- package/dist/lib/daemon/auth-sync-service.js +34 -0
- package/dist/lib/daemon/browser-task-reap-service.d.ts +14 -0
- package/dist/lib/daemon/browser-task-reap-service.js +26 -0
- package/dist/lib/daemon/daemon.js +87 -176
- package/dist/lib/daemon/heartbeat-service.d.ts +13 -0
- package/dist/lib/daemon/heartbeat-service.js +26 -0
- package/dist/lib/daemon/monitor-engine-service.d.ts +9 -5
- package/dist/lib/daemon/monitor-engine-service.js +15 -7
- package/dist/lib/daemon/runner.d.ts +15 -0
- package/dist/lib/daemon/runner.js +23 -0
- package/dist/lib/daemon/secrets-broker-service.d.ts +5 -4
- package/dist/lib/daemon/secrets-broker-service.js +17 -32
- package/dist/lib/daemon/service.d.ts +2 -2
- package/dist/lib/daemon/service.js +1 -1
- package/dist/lib/daemon/session-state-service.d.ts +21 -0
- package/dist/lib/daemon/session-state-service.js +34 -0
- package/dist/lib/daemon/supervisor.d.ts +17 -7
- package/dist/lib/daemon/supervisor.js +87 -14
- package/dist/lib/daemon/tmux-reap-service.d.ts +11 -0
- package/dist/lib/daemon/tmux-reap-service.js +28 -0
- package/dist/lib/daemon/webhook-receiver-service.d.ts +9 -0
- package/dist/lib/daemon/webhook-receiver-service.js +17 -0
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +25 -0
- package/dist/lib/device-config.d.ts +3 -3
- package/dist/lib/device-config.js +5 -5
- package/dist/lib/devices/connect.d.ts +26 -0
- package/dist/lib/devices/connect.js +48 -1
- package/dist/lib/devices/doctor-findings.d.ts +5 -1
- package/dist/lib/devices/doctor-findings.js +19 -1
- package/dist/lib/devices/harness-inventory.js +5 -2
- package/dist/lib/exec.d.ts +28 -0
- package/dist/lib/exec.js +73 -7
- package/dist/lib/feed/feed.d.ts +1 -1
- package/dist/lib/feed/feed.js +23 -1
- package/dist/lib/feed-broadcast.js +1 -1
- package/dist/lib/fleet/apply.d.ts +11 -0
- package/dist/lib/fleet/apply.js +23 -3
- package/dist/lib/fleet/auth-sync.js +5 -3
- package/dist/lib/help.d.ts +9 -0
- package/dist/lib/help.js +29 -1
- package/dist/lib/hosts/passthrough.d.ts +1 -10
- package/dist/lib/hosts/passthrough.js +1 -13
- package/dist/lib/installations/versions.js +9 -1
- package/dist/lib/linux-userns.d.ts +58 -0
- package/dist/lib/linux-userns.js +116 -0
- package/dist/lib/memory.d.ts +26 -0
- package/dist/lib/memory.js +80 -1
- package/dist/lib/monitors/config.d.ts +11 -0
- package/dist/lib/monitors/config.js +8 -0
- package/dist/lib/monitors/engine.d.ts +5 -1
- package/dist/lib/monitors/engine.js +13 -4
- package/dist/lib/monitors/state.d.ts +37 -1
- package/dist/lib/monitors/state.js +79 -4
- package/dist/lib/permissions-registry.d.ts +2 -0
- package/dist/lib/permissions-registry.js +116 -14
- package/dist/lib/permissions.d.ts +5 -3
- package/dist/lib/permissions.js +25 -27
- package/dist/lib/profiles.d.ts +8 -7
- package/dist/lib/profiles.js +12 -0
- package/dist/lib/project-key.d.ts +9 -0
- package/dist/lib/project-key.js +11 -0
- package/dist/lib/scheduling/routines.d.ts +47 -0
- package/dist/lib/scheduling/routines.js +70 -1
- package/dist/lib/secrets/bundles.d.ts +35 -0
- package/dist/lib/secrets/bundles.js +78 -1
- package/dist/lib/secrets/push.d.ts +3 -8
- package/dist/lib/secrets/push.js +18 -14
- package/dist/lib/secrets/remote.d.ts +9 -18
- package/dist/lib/secrets/remote.js +11 -26
- package/dist/lib/secrets/reserved-sync.d.ts +65 -0
- package/dist/lib/secrets/reserved-sync.js +129 -0
- package/dist/lib/self-heal/checks/hook-manifest.d.ts +2 -0
- package/dist/lib/self-heal/checks/hook-manifest.js +56 -0
- package/dist/lib/self-heal/registry.js +4 -0
- package/dist/lib/self-heal/types.d.ts +1 -1
- package/dist/lib/session/active.js +1 -4
- package/dist/lib/session/db.d.ts +41 -5
- package/dist/lib/session/db.js +132 -30
- package/dist/lib/session/discover.d.ts +32 -4
- package/dist/lib/session/discover.js +119 -25
- package/dist/lib/session/insights.d.ts +14 -0
- package/dist/lib/session/insights.js +25 -2
- package/dist/lib/session/linear.js +1 -1
- package/dist/lib/session/shell-programs.d.ts +17 -0
- package/dist/lib/session/shell-programs.js +21 -0
- package/dist/lib/session/state.js +2 -1
- package/dist/lib/session/stream-render.js +2 -1
- package/dist/lib/session/tool-calls.js +2 -5
- package/dist/lib/session/trajectory-html.js +2 -1
- package/dist/lib/session/trajectory.js +3 -12
- package/dist/lib/session/types.d.ts +8 -0
- package/dist/lib/share/capture.js +11 -2
- package/dist/lib/share/publish.d.ts +56 -5
- package/dist/lib/share/publish.js +126 -18
- package/dist/lib/share/worker-template.d.ts +3 -12
- package/dist/lib/share/worker-template.js +860 -59
- package/dist/lib/startup/root-command.js +2 -1
- package/dist/lib/state.d.ts +16 -0
- package/dist/lib/state.js +178 -46
- package/dist/lib/sync-status.d.ts +4 -0
- package/dist/lib/sync-status.js +3 -0
- package/dist/lib/traces/classify.js +24 -19
- package/dist/lib/usage-refresh.js +2 -1
- package/dist/lib/view-types.d.ts +7 -0
- package/package.json +9 -2
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { setHelpSections } from '../lib/help.js';
|
|
3
|
+
import { readStdinSync, runOrDie } from '../lib/format.js';
|
|
4
|
+
import { isInteractiveTerminal } from './utils.js';
|
|
5
|
+
import { getMintFlow, listMintableHarnesses, mintAndSeed, } from '../lib/auth-mint.js';
|
|
6
|
+
function collect(value, previous) {
|
|
7
|
+
return [...previous, value];
|
|
8
|
+
}
|
|
9
|
+
function printResult(result, json) {
|
|
10
|
+
if (json) {
|
|
11
|
+
console.log(JSON.stringify({
|
|
12
|
+
harness: result.harness,
|
|
13
|
+
account: result.account,
|
|
14
|
+
email: result.email,
|
|
15
|
+
authBundleKey: result.authBundleKey,
|
|
16
|
+
rotated: result.rotated,
|
|
17
|
+
fleet: result.fleet,
|
|
18
|
+
}, null, 2));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const verb = result.rotated ? 'Rotated' : 'Minted';
|
|
22
|
+
console.log(chalk.green(`${verb} ${result.harness} setup-token for ${result.email} into account '${result.account}' and reserved auth bundle key ${result.authBundleKey}.`));
|
|
23
|
+
console.log(chalk.gray('The token is never printed. Native OAuth was not copied.'));
|
|
24
|
+
console.log(chalk.gray(`Run: agents run ${result.harness} --account ${result.account}`));
|
|
25
|
+
if (result.fleet.length) {
|
|
26
|
+
for (const row of result.fleet) {
|
|
27
|
+
console.log(chalk.green(` synced ${row.device} (${row.message})`));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function registerMintCommand(parent, testHooks) {
|
|
32
|
+
const mint = parent
|
|
33
|
+
.command('mint <harness>')
|
|
34
|
+
.description('Mint a long-lived setup-token and seed it as a named account')
|
|
35
|
+
.option('--account <name-or-email>', 'Named account to create/rotate, or the account email')
|
|
36
|
+
.option('--email <email>', 'Email that keys the reserved auth bundle when --account is a name')
|
|
37
|
+
.option('--token-stdin', 'Seed an already-minted token from stdin (skip the PTY drive)')
|
|
38
|
+
.option('--code <code>', 'Authorization code to paste into the setup-token prompt')
|
|
39
|
+
.option('--no-open', 'Print the authorize URL instead of opening a browser')
|
|
40
|
+
.option('--fleet', 'Sync the minted account and reserved auth bundle to every other registered device')
|
|
41
|
+
.option('--device <name>', 'Sync to this device (repeatable)', collect, [])
|
|
42
|
+
.option('--json', 'Machine-readable result (never includes the token)')
|
|
43
|
+
.action((harness, o, command) => {
|
|
44
|
+
const json = !!o.json || !!command.optsWithGlobals().json;
|
|
45
|
+
return runOrDie(async () => {
|
|
46
|
+
getMintFlow(harness);
|
|
47
|
+
let token;
|
|
48
|
+
if (o.tokenStdin) {
|
|
49
|
+
token = readStdinSync();
|
|
50
|
+
if (!token)
|
|
51
|
+
throw new Error('No value received on stdin. Pass the setup-token on stdin with --token-stdin.');
|
|
52
|
+
}
|
|
53
|
+
else if (!isInteractiveTerminal() && !o.code) {
|
|
54
|
+
throw new Error(`Minting ${harness} interactively needs a TTY (or --token-stdin / --code). Supported harnesses: ${listMintableHarnesses().join(', ')}.`);
|
|
55
|
+
}
|
|
56
|
+
const result = await mintAndSeed({
|
|
57
|
+
harness,
|
|
58
|
+
account: o.account,
|
|
59
|
+
email: o.email,
|
|
60
|
+
token,
|
|
61
|
+
code: o.code,
|
|
62
|
+
open: o.open,
|
|
63
|
+
fleet: o.fleet,
|
|
64
|
+
devices: o.device,
|
|
65
|
+
json,
|
|
66
|
+
hooks: {
|
|
67
|
+
...testHooks,
|
|
68
|
+
...(o.tokenStdin || o.code || !isInteractiveTerminal()
|
|
69
|
+
? {}
|
|
70
|
+
: {
|
|
71
|
+
readCode: async () => {
|
|
72
|
+
const { input } = await import('@inquirer/prompts');
|
|
73
|
+
return input({ message: 'Paste the authorization code from the browser' });
|
|
74
|
+
},
|
|
75
|
+
}),
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
printResult(result, json);
|
|
79
|
+
}, { json });
|
|
80
|
+
});
|
|
81
|
+
setHelpSections(mint, {
|
|
82
|
+
examples: `agents accounts mint claude
|
|
83
|
+
agents accounts mint claude --account ada@example.com
|
|
84
|
+
agents accounts mint claude --account work --email ada@example.com
|
|
85
|
+
agents accounts mint claude --token-stdin < token.txt
|
|
86
|
+
agents accounts mint claude --code AUTHCODE --json
|
|
87
|
+
agents accounts mint claude --fleet
|
|
88
|
+
agents auth mint claude --account ada@example.com --json`,
|
|
89
|
+
notes: `Claude is the only harness with an interactive long-lived setup-token (\`claude setup-token\`, ~1 year, shareable across machines). The command drives that flow in a PTY, opens the authorize URL, captures a well-formed sk-ant-oat01- token (refusing the #1767 TTY-banner capture), and seeds:
|
|
90
|
+
1. a named provider account (policy never) for \`agents run --account\` / \`agents accounts sync\`
|
|
91
|
+
2. the reserved FILE-BASED auth bundle keyed per email, which usage/probe reads
|
|
92
|
+
Native rotating OAuth is never copied. Other harnesses fail loud: device-code login is \`agents fleet login\`; API keys are \`agents accounts add\`.
|
|
93
|
+
Already have a token? \`--token-stdin\` skips the browser dance. \`agents auth mint\` is the same command as \`agents accounts mint\`.`,
|
|
94
|
+
});
|
|
95
|
+
return mint;
|
|
96
|
+
}
|
package/dist/commands/auth.js
CHANGED
|
@@ -2,6 +2,7 @@ import chalk from 'chalk';
|
|
|
2
2
|
import { PhoenixApiError, clearSession, createSpace, createSpaceInvite, fetchWhoAmI, listSpaceMembers, listSpaces, pollDeviceToken, readSession, removeSpaceMember, resolveMemberFromList, resolveSpaceFromList, slugify, startDeviceAuthorization, updateSpaceMemberRole, writeSession, } from '../lib/identity/index.js';
|
|
3
3
|
import { setHelpSections } from '../lib/help.js';
|
|
4
4
|
import { runOrDie } from '../lib/format.js';
|
|
5
|
+
import { registerMintCommand } from './auth-mint.js';
|
|
5
6
|
/**
|
|
6
7
|
* `agents auth` — sign in to Phoenix ID, the account layer behind team spaces.
|
|
7
8
|
* Signing in is optional: every local feature works with no account. Everything
|
|
@@ -95,13 +96,15 @@ export function registerAuthCommand(program) {
|
|
|
95
96
|
setHelpSections(auth, {
|
|
96
97
|
examples: `agents auth login # device-code sign-in via your browser
|
|
97
98
|
agents auth whoami # who this machine is signed in as
|
|
99
|
+
agents auth mint claude # mint a Claude setup-token into a named account
|
|
98
100
|
agents auth space create "Design Team" # start a space
|
|
99
101
|
agents auth space invite ada@example.com # add a teammate
|
|
100
102
|
agents auth logout # clear this machine only`,
|
|
101
103
|
notes: `Signing in is optional — every local feature works with no account; team spaces are what it unlocks.
|
|
102
104
|
Sign-in is Google-only and opens a Phoenix-branded page; the CLI never sees a password.
|
|
103
105
|
The session lives in this machine's agents state dir, so logging out here signs out nothing else.
|
|
104
|
-
Point at a different backend with PHOENIX_ID_BASE (defaults to the production service)
|
|
106
|
+
Point at a different backend with PHOENIX_ID_BASE (defaults to the production service).
|
|
107
|
+
\`agents auth mint\` is the harness setup-token mint (same command as \`agents accounts mint\`); it does not sign you into Phoenix ID.`,
|
|
105
108
|
});
|
|
106
109
|
auth
|
|
107
110
|
.command('login')
|
|
@@ -115,6 +118,7 @@ Point at a different backend with PHOENIX_ID_BASE (defaults to the production se
|
|
|
115
118
|
const json = !!o.json || !!command.optsWithGlobals().json;
|
|
116
119
|
return runOrDie(() => whoami(json), { json });
|
|
117
120
|
});
|
|
121
|
+
registerMintCommand(auth);
|
|
118
122
|
auth
|
|
119
123
|
.command('logout')
|
|
120
124
|
.description("Clear this machine's session (no other device is affected)")
|
package/dist/commands/browser.js
CHANGED
|
@@ -1308,7 +1308,7 @@ function registerTaskCommands(browser) {
|
|
|
1308
1308
|
});
|
|
1309
1309
|
browser
|
|
1310
1310
|
.command('remote-control [state]')
|
|
1311
|
-
.description("Allow or deny other fleet machines driving THIS machine's browser over `browser --device
|
|
1311
|
+
.description("Allow or deny other fleet machines driving THIS machine's browser over `browser --device` or `agents ssh …` (`agents browser` / `ag browser` / standalone `browser`). " +
|
|
1312
1312
|
'`on`/`off` to set (device-local, never synced); no argument prints the current value. Default off.')
|
|
1313
1313
|
.option('--json', 'Output as JSON')
|
|
1314
1314
|
.action((state, opts) => {
|
package/dist/commands/cost.js
CHANGED
|
@@ -21,8 +21,14 @@ Examples:
|
|
|
21
21
|
|
|
22
22
|
Cost is computed offline from a versioned per-model price table (${PRICING_VERSION}).
|
|
23
23
|
`)
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
// Read opts via optsWithGlobals(): `--json`/`--since`/`--by` collide by name
|
|
25
|
+
// with the `insights` parent's own options, so commander binds them to the
|
|
26
|
+
// parent at parse time and the leaf's plain opts() never sees them. Merging
|
|
27
|
+
// ancestor opts is what the sibling `insights mix` recipes already do
|
|
28
|
+
// (mix-commands.ts) — without it every flag on this command is silently
|
|
29
|
+
// dropped (e.g. `agents insights cost --json` printed the human table).
|
|
30
|
+
.action(async (_options, command) => {
|
|
31
|
+
await costAction(command.optsWithGlobals());
|
|
26
32
|
});
|
|
27
33
|
}
|
|
28
34
|
/** Map the --by flag to a rollup group, rejecting unknown values. */
|
package/dist/commands/daemon.js
CHANGED
|
@@ -886,7 +886,7 @@ export function registerDaemonCommand(program) {
|
|
|
886
886
|
always. Use 'agents routines stats' for per-routine failure detail.
|
|
887
887
|
|
|
888
888
|
'disable' is a persisted kill switch: it stops routines/add,
|
|
889
|
-
routines/start, routines/catchup, and webhook triggers from auto-starting
|
|
889
|
+
routines/start, routines/catchup, monitors/add, and webhook triggers from auto-starting
|
|
890
890
|
the daemon (daemon.enabled: false in ~/.agents/devices/<host>/agents.yaml).
|
|
891
891
|
'agents daemon start' still starts it explicitly, same as
|
|
892
892
|
'systemctl start' on a disabled unit.
|
|
@@ -974,7 +974,7 @@ export function registerDaemonCommand(program) {
|
|
|
974
974
|
.description('Clear the daemon.enabled kill switch. Does not start the daemon by itself.')
|
|
975
975
|
.action(() => {
|
|
976
976
|
setConfigValue('daemon.enabled', true);
|
|
977
|
-
console.log(chalk.green('daemon.enabled: true') + chalk.gray(' — auto-start surfaces (routines add/start/catchup, webhooks) may bring the daemon up again'));
|
|
977
|
+
console.log(chalk.green('daemon.enabled: true') + chalk.gray(' — auto-start surfaces (routines add/start/catchup, monitors add, webhooks) may bring the daemon up again'));
|
|
978
978
|
});
|
|
979
979
|
cmd.command('disable')
|
|
980
980
|
.description('Persist daemon.enabled: false — nothing auto-starts the daemon until re-enabled. Does not stop a running daemon.')
|
package/dist/commands/doctor.js
CHANGED
|
@@ -24,6 +24,7 @@ import { resolveAgentTargets, AgentSpecError } from '../lib/agent-spec/index.js'
|
|
|
24
24
|
import { loadManifest, isStale } from '../lib/staleness/index.js';
|
|
25
25
|
import { diffVersionResources, DOCTOR_ALL_KINDS, } from '../lib/doctor-diff.js';
|
|
26
26
|
import { checkVersionHookWiring, inspectDuplicateVersionHooks, registerHooksToSettings, repairManagedHookRuntimeArtifacts } from '../lib/hooks/install.js';
|
|
27
|
+
import { inspectReservedAuthBundle } from '../lib/secrets/bundles.js';
|
|
27
28
|
import { isVersionIsolated } from '../lib/installations/versions.js';
|
|
28
29
|
import { computeDrift, checkSyncStatus, countOrphans, computeSourceBehind } from '../lib/drift.js';
|
|
29
30
|
import { readAuthHealthCache, summarizeHostAuth } from '../lib/auth-health.js';
|
|
@@ -118,13 +119,15 @@ function printWrappedLine(prefix, text) {
|
|
|
118
119
|
* file and line. Run `agents doctor` on that box for the specifics — the
|
|
119
120
|
* message says so.
|
|
120
121
|
*/
|
|
121
|
-
const REMOTE_FORWARDED_KINDS = ['rc-secret-export', 'env-secret-export', 'ssh-key-enrollment'];
|
|
122
|
+
const REMOTE_FORWARDED_KINDS = ['rc-secret-export', 'env-secret-export', 'auth-bundle-wrong-backend', 'ssh-key-enrollment'];
|
|
122
123
|
/** Canonical, locally-authored text for a forwarded kind. Never the remote's. */
|
|
123
124
|
const REMOTE_SECRET_MESSAGE = {
|
|
124
125
|
'rc-secret-export': 'a credential-shaped export was found in this box\'s shell rc files'
|
|
125
126
|
+ ' — run `agents doctor` there for the file and line',
|
|
126
127
|
'env-secret-export': 'AGENTS_SECRETS_PASSPHRASE is set in this box\'s process environment'
|
|
127
128
|
+ ' — run `agents doctor` there for detail',
|
|
129
|
+
'auth-bundle-wrong-backend': "reserved secrets bundle 'auth' exists but is not file-backed"
|
|
130
|
+
+ ' — run `agents doctor` there to recreate it',
|
|
128
131
|
'ssh-key-enrollment': 'Windows OpenSSH key enrollment is invalid'
|
|
129
132
|
+ ' — run `agents doctor` on this box for the effective path or ACL failure',
|
|
130
133
|
};
|
|
@@ -442,6 +445,7 @@ async function runDevicesDoctor(opts) {
|
|
|
442
445
|
hostClis: toHostCliInput(listCliStatus(cwd)),
|
|
443
446
|
rcSecrets: scanUserRcFiles(),
|
|
444
447
|
masterPassphraseInEnv: masterPassphraseInEnv(),
|
|
448
|
+
authBundleWrongBackend: !inspectReservedAuthBundle().ok,
|
|
445
449
|
execPolicy: process.platform === 'win32'
|
|
446
450
|
? { platform: process.platform, policy: getEffectiveExecutionPolicy() }
|
|
447
451
|
: undefined,
|
|
@@ -1680,6 +1684,7 @@ export function registerDoctorCommand(program) {
|
|
|
1680
1684
|
hostClis: toHostCliInput(hostClis),
|
|
1681
1685
|
rcSecrets: scanUserRcFiles(),
|
|
1682
1686
|
masterPassphraseInEnv: masterPassphraseInEnv(),
|
|
1687
|
+
authBundleWrongBackend: !inspectReservedAuthBundle().ok,
|
|
1683
1688
|
// getEffectiveExecutionPolicy spawns powershell — a doomed process on
|
|
1684
1689
|
// POSIX, where the advisory never applies. Probe only on Windows.
|
|
1685
1690
|
execPolicy: process.platform === 'win32'
|
package/dist/commands/exec.js
CHANGED
|
@@ -654,13 +654,13 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
654
654
|
Interactive plan (omit the prompt) works everywhere it is listed.
|
|
655
655
|
|
|
656
656
|
Run strategy (set via --strategy or run.<agent>.strategy in agents.yaml):
|
|
657
|
-
pinned use the workspace/global pinned version
|
|
657
|
+
pinned use the workspace/global pinned version; if that version is logged out on this device, pick a signed-in sibling instead of dying (an explicit @version pin is unchanged)
|
|
658
658
|
available use pinned if it can run right now; otherwise switch to another signed-in version
|
|
659
659
|
balanced distribute load across healthy accounts by remaining capacity (default)
|
|
660
660
|
A version/account is skipped when it is rate-limited right now — any usage window (incl. the 5-hour session window) at 100%, matching the 'agents view' badge.
|
|
661
661
|
--balanced is shorthand for --strategy balanced. Ignored when @version is pinned, when a profile is used, or with --fallback.
|
|
662
|
-
Zero healthy accounts under balanced/available exits nonzero naming each
|
|
663
|
-
excluded account and the earliest window reset — use --strategy pinned to force.
|
|
662
|
+
Zero healthy accounts under balanced/available (or a logged-out pinned default with no signed-in sibling) exits nonzero naming each
|
|
663
|
+
excluded account and the earliest window reset — use --strategy pinned to force a rate-limited default; a logged-out default is never forced.
|
|
664
664
|
|
|
665
665
|
'auto' harness (agents run auto): picks the host (14d usage affinity,
|
|
666
666
|
unless --device is given), the harness (installed CLIs weighted by
|
|
@@ -2368,13 +2368,15 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2368
2368
|
process.exit(1);
|
|
2369
2369
|
}
|
|
2370
2370
|
const strategy = options.balanced ? 'balanced' : explicitStrategy ?? configuredStrategy;
|
|
2371
|
-
// Strategy
|
|
2372
|
-
// profiles already
|
|
2373
|
-
//
|
|
2374
|
-
//
|
|
2371
|
+
// Strategy applies to bare (unpinned) invocations. Explicit @version and
|
|
2372
|
+
// profiles already name the target. A --fallback chain does NOT pin the
|
|
2373
|
+
// primary: it only names where to cascade on a rate limit, so the bare
|
|
2374
|
+
// primary still resolves through the strategy — otherwise every
|
|
2375
2375
|
// `agents run claude --fallback codex` run lands on the pinned default
|
|
2376
2376
|
// account and account rotation silently stops (the gh-monitor heal bug).
|
|
2377
|
-
|
|
2377
|
+
// `pinned` still resolves so a logged-out default can yield to a signed-in
|
|
2378
|
+
// sibling on this device (PHNX-2685); an explicit @version pin is unchanged.
|
|
2379
|
+
if (!accountPickerRequested && !configuredAccount && (!version || strategy !== 'pinned' || options.balanced || explicitStrategy)) {
|
|
2378
2380
|
if (version) {
|
|
2379
2381
|
process.stderr.write(chalk.yellow(`[agents] strategy ${strategy} ignored: version ${version} is pinned\n`));
|
|
2380
2382
|
}
|
package/dist/commands/focus.d.ts
CHANGED
|
@@ -110,6 +110,7 @@ export declare function dedupeSessionsByLogicalId(rows: SessionMeta[], self?: st
|
|
|
110
110
|
* peer-owned session without `--device`, matching `resume`/`preview`.
|
|
111
111
|
*/
|
|
112
112
|
export declare function focusTargetForResolved(poolMatch: SessionMeta | undefined, resolved: SessionMeta): SessionMeta;
|
|
113
|
+
export declare function focusResolvedSession(meta: SessionMeta, liveById: Map<string, ActiveSession>, self: string, fallback: UnreachableFallback, attachOnly: boolean, reconnectReattach?: boolean): Promise<void>;
|
|
113
114
|
/** Focus one row selected by the shared session browser through the same
|
|
114
115
|
* attach/recover decision as `agents sessions focus <id>`. */
|
|
115
116
|
export declare function focusSelectedSession(meta: SessionMeta, active: ActiveSession | undefined, self: string): Promise<void>;
|
package/dist/commands/focus.js
CHANGED
|
@@ -494,10 +494,10 @@ async function pickFocusCandidates(sessions, liveById, initialSearch) {
|
|
|
494
494
|
export function focusTargetForResolved(poolMatch, resolved) {
|
|
495
495
|
return poolMatch ?? resolved;
|
|
496
496
|
}
|
|
497
|
-
async function focusResolvedSession(meta, liveById, self, fallback, attachOnly, reconnectReattach = false) {
|
|
497
|
+
export async function focusResolvedSession(meta, liveById, self, fallback, attachOnly, reconnectReattach = false) {
|
|
498
498
|
const active = liveById.get(meta.id);
|
|
499
499
|
if (active && isAttachableLiveSession(active)) {
|
|
500
|
-
await jumpTo(active, self, fallback);
|
|
500
|
+
await jumpTo(active, self, fallback, meta.id);
|
|
501
501
|
return;
|
|
502
502
|
}
|
|
503
503
|
if (attachOnly) {
|
package/dist/commands/go.d.ts
CHANGED
|
@@ -104,9 +104,10 @@ export declare function describeWhere(s: ActiveSession, self: string): Where;
|
|
|
104
104
|
/**
|
|
105
105
|
* What to do when a session can't be *attached* (no tmux/Ghostty rail). `go`
|
|
106
106
|
* refuses; `focus` opens a new tab and resumes. `remote` is the peer name when
|
|
107
|
-
* the session lives on another machine, else undefined.
|
|
107
|
+
* the session lives on another machine, else undefined. `fallbackId` is the
|
|
108
|
+
* indexed session id when the live row has not registered one yet (PHNX-3356).
|
|
108
109
|
*/
|
|
109
|
-
export type UnreachableFallback = (s: ActiveSession, remote: string | undefined) => void | Promise<void>;
|
|
110
|
+
export type UnreachableFallback = (s: ActiveSession, remote: string | undefined, fallbackId?: string) => void | Promise<void>;
|
|
110
111
|
export type AttachRailLiveness = {
|
|
111
112
|
state: 'alive';
|
|
112
113
|
} | {
|
|
@@ -121,5 +122,5 @@ export type AttachRailLiveness = {
|
|
|
121
122
|
export declare function probeAttachRail(s: ActiveSession, self: string): Promise<AttachRailLiveness>;
|
|
122
123
|
/** Strict attach-only fallback: no pane means no attach. Never open a shell or
|
|
123
124
|
* start recovery, because both would violate the caller's no-fork intent. */
|
|
124
|
-
export declare function refuseFallback(s: ActiveSession, remote: string | undefined): Promise<void>;
|
|
125
|
-
export declare function jumpTo(s: ActiveSession, self: string, fallback?: UnreachableFallback): Promise<void>;
|
|
125
|
+
export declare function refuseFallback(s: ActiveSession, remote: string | undefined, fallbackId?: string): Promise<void>;
|
|
126
|
+
export declare function jumpTo(s: ActiveSession, self: string, fallback?: UnreachableFallback, fallbackId?: string): Promise<void>;
|
package/dist/commands/go.js
CHANGED
|
@@ -312,17 +312,17 @@ export async function probeAttachRail(s, self) {
|
|
|
312
312
|
}
|
|
313
313
|
/** Strict attach-only fallback: no pane means no attach. Never open a shell or
|
|
314
314
|
* start recovery, because both would violate the caller's no-fork intent. */
|
|
315
|
-
export async function refuseFallback(s, remote) {
|
|
315
|
+
export async function refuseFallback(s, remote, fallbackId) {
|
|
316
316
|
if (remote) {
|
|
317
317
|
console.log(chalk.yellow(`Can't attach ${shortId(s)} on ${remote} — it has no living tmux pane.`));
|
|
318
318
|
process.exitCode = 1;
|
|
319
319
|
return;
|
|
320
320
|
}
|
|
321
321
|
console.log(chalk.yellow(`Can't jump to ${shortId(s)} — it's in ${s.host ?? 'an unknown terminal'} with no attach rail (not tmux/Ghostty).`) +
|
|
322
|
-
chalk.gray(`\n${addressabilityRecoveryHint(s)}`));
|
|
322
|
+
chalk.gray(`\n${addressabilityRecoveryHint(s, fallbackId)}`));
|
|
323
323
|
process.exitCode = 1;
|
|
324
324
|
}
|
|
325
|
-
export async function jumpTo(s, self, fallback = refuseFallback) {
|
|
325
|
+
export async function jumpTo(s, self, fallback = refuseFallback, fallbackId) {
|
|
326
326
|
const remote = sessionProcessHost(s, self);
|
|
327
327
|
const mux = s.provenance?.mux;
|
|
328
328
|
// Path C: remote tmux — ssh in and attach, resolving the pane's session on the remote.
|
|
@@ -330,7 +330,7 @@ export async function jumpTo(s, self, fallback = refuseFallback) {
|
|
|
330
330
|
if (mux?.kind === 'tmux' && mux.pane) {
|
|
331
331
|
const liveness = await probeAttachRail(s, self);
|
|
332
332
|
if (liveness.state !== 'alive') {
|
|
333
|
-
await fallback(s, remote);
|
|
333
|
+
await fallback(s, remote, fallbackId);
|
|
334
334
|
return;
|
|
335
335
|
}
|
|
336
336
|
assertValidSshTarget(remote);
|
|
@@ -349,14 +349,14 @@ export async function jumpTo(s, self, fallback = refuseFallback) {
|
|
|
349
349
|
process.exit(code);
|
|
350
350
|
}
|
|
351
351
|
// Remote, not in tmux → hand off to the fallback (go: shell; focus: resume in a tab).
|
|
352
|
-
await fallback(s, remote);
|
|
352
|
+
await fallback(s, remote, fallbackId);
|
|
353
353
|
return;
|
|
354
354
|
}
|
|
355
355
|
// Path B: local tmux — attach (or switch-client if we're already inside tmux).
|
|
356
356
|
if (mux?.kind === 'tmux' && mux.pane) {
|
|
357
357
|
const liveness = await probeAttachRail(s, self);
|
|
358
358
|
if (liveness.state !== 'alive') {
|
|
359
|
-
await fallback(s, undefined);
|
|
359
|
+
await fallback(s, undefined, fallbackId);
|
|
360
360
|
return;
|
|
361
361
|
}
|
|
362
362
|
const socket = mux.socket ?? getDefaultSocketPath();
|
|
@@ -403,7 +403,7 @@ export async function jumpTo(s, self, fallback = refuseFallback) {
|
|
|
403
403
|
return;
|
|
404
404
|
}
|
|
405
405
|
// Path D: no attach rail (headless / plain terminal) → hand off to the fallback.
|
|
406
|
-
await fallback(s, undefined);
|
|
406
|
+
await fallback(s, undefined, fallbackId);
|
|
407
407
|
}
|
|
408
408
|
/** Resolve a local tmux pane id to its session name + window index. */
|
|
409
409
|
async function resolveLocalPane(socket, pane) {
|
|
@@ -246,6 +246,9 @@ function renderReport(groups, dim, meta, actions, harnesses) {
|
|
|
246
246
|
for (const g of groups)
|
|
247
247
|
mergeFacets(all, g.facets);
|
|
248
248
|
renderCounts('Top tools', topEntries(all.toolCounts, 8), out);
|
|
249
|
+
// The `Bash`/`exec` bar above says a shell ran; this says WHICH binary — git, gh,
|
|
250
|
+
// agents, find, ssh→git — so the tool mix is actionable rather than one lump.
|
|
251
|
+
renderCounts('Shell commands', topEntries(all.bashCommands, 12), out);
|
|
249
252
|
renderCounts('Languages', topEntries(all.languages, 6), out);
|
|
250
253
|
renderCounts('Models', topEntries(all.models, 6), out);
|
|
251
254
|
// Friction — the section that earns the command.
|
|
@@ -281,6 +284,9 @@ function renderReport(groups, dim, meta, actions, harnesses) {
|
|
|
281
284
|
out.push(` ${chalk.gray('·')} ${padToWidth(e.name, 16)} ${chalk.gray(String(e.count))}`);
|
|
282
285
|
}
|
|
283
286
|
renderCounts('Friction / thrash', topEntries(all.frictionSignals, 10), out);
|
|
287
|
+
// Which shell binary was failing when a `failed tool loop: Bash` fired — makes the
|
|
288
|
+
// loop attributable (`git reconcile`, `find`, `gh`) instead of a bare tool name.
|
|
289
|
+
renderCounts('Shell command failures', topEntries(all.bashCommandFailures, 8), out);
|
|
284
290
|
renderCounts('Dissatisfaction / corrections', topEntries(all.correctionSignals, 10), out);
|
|
285
291
|
renderCounts('Automatable repeats', topEntries(all.automationSignals, 10), out);
|
|
286
292
|
renderCounts('Harness split', harnesses, out);
|
|
@@ -297,6 +303,9 @@ function renderReport(groups, dim, meta, actions, harnesses) {
|
|
|
297
303
|
out.push(` ${padToWidth(action.priority, 7)} ${padToWidth(action.category, 11)} ` +
|
|
298
304
|
`${String(action.evidenceCount).padStart(8)} ${padToWidth(action.sampleSessionIds.join(', '), 25)} ${action.action}`);
|
|
299
305
|
}
|
|
306
|
+
// This report is aggregate by contract (SES-IF-4c); a sample id above drills into
|
|
307
|
+
// one session's timeline via the per-session surface, not a single-session mode here.
|
|
308
|
+
out.push(chalk.gray(' drill into a sample session: agents sessions trace <id>'));
|
|
300
309
|
}
|
|
301
310
|
// Output
|
|
302
311
|
out.push('');
|
|
@@ -24,6 +24,7 @@ import { IS_WINDOWS } from '../lib/platform/index.js';
|
|
|
24
24
|
import { safeJoin } from '../lib/paths.js';
|
|
25
25
|
import { machineId, normalizeHost } from '../lib/machine-id.js';
|
|
26
26
|
import { loadDevices } from '../lib/devices/registry.js';
|
|
27
|
+
import { assertDaemonEnabled } from '../lib/device-config.js';
|
|
27
28
|
import { setHelpSections } from '../lib/help.js';
|
|
28
29
|
import { isInteractiveTerminal, requireInteractiveSelection } from './utils.js';
|
|
29
30
|
function stdoutJson(payload) {
|
|
@@ -52,6 +53,26 @@ function sourceLabel(source) {
|
|
|
52
53
|
return source.type;
|
|
53
54
|
}
|
|
54
55
|
}
|
|
56
|
+
/** Warn when a run/routine action has no postcondition — completed-exit-0 still records as ok (PHNX-2842). */
|
|
57
|
+
function warnMissingPostcondition(action) {
|
|
58
|
+
if ((action.type === 'run' || action.type === 'routine') && !action.postcondition) {
|
|
59
|
+
stderrLine(chalk.yellow(` Note: ${action.type} has no --postcondition — a completed agent that did nothing will still record as ok. Add --postcondition '<cmd>' that exits 0 when the intended effect happened.`));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Reconciled one-line outcome for a fire (ok / no effect / failed). */
|
|
63
|
+
function fireOutcomeDisplay(name, f) {
|
|
64
|
+
const rec = resolveFireOutcome(name, f);
|
|
65
|
+
if (rec.ok)
|
|
66
|
+
return { label: chalk.green('ok'), note: '' };
|
|
67
|
+
if (rec.effect === 'none') {
|
|
68
|
+
const note = rec.error ? chalk.gray(` ${rec.error}`) : '';
|
|
69
|
+
return { label: chalk.yellow('no effect'), note };
|
|
70
|
+
}
|
|
71
|
+
const corrected = f.ok !== false && rec.runStatus
|
|
72
|
+
? chalk.yellow(` (run ${rec.runStatus})`)
|
|
73
|
+
: '';
|
|
74
|
+
return { label: chalk.red('failed') + corrected, note: '' };
|
|
75
|
+
}
|
|
55
76
|
/** A one-line human label for a monitor's action. */
|
|
56
77
|
function actionLabel(action) {
|
|
57
78
|
switch (action.type) {
|
|
@@ -123,12 +144,26 @@ function livenessLabel(monitor, state, liveness) {
|
|
|
123
144
|
}
|
|
124
145
|
return chalk.gray(`checked ${liveness.checkCount}x · last ${checkedAgo} · no match yet`);
|
|
125
146
|
}
|
|
126
|
-
/**
|
|
147
|
+
/**
|
|
148
|
+
* Start or reload the background daemon so a newly-added monitor is watched.
|
|
149
|
+
* Returns false when auto-start was refused (`daemon.enabled=false`); the add
|
|
150
|
+
* itself already succeeded — the monitor is config and stays valid fleet-wide.
|
|
151
|
+
* The refusal names the setting and the fix. Mirrors `ensureSchedulerRunning`
|
|
152
|
+
* in commands/routines.ts (PHNX-2637).
|
|
153
|
+
*/
|
|
127
154
|
function ensureDaemonRunning() {
|
|
155
|
+
try {
|
|
156
|
+
assertDaemonEnabled();
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
// Loud stated skip, on stderr so --json stdout stays clean.
|
|
160
|
+
stderrLine(chalk.yellow(err.message));
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
128
163
|
if (isDaemonRunning()) {
|
|
129
164
|
signalDaemonReload();
|
|
130
165
|
stderrLine(chalk.gray('Daemon reloaded'));
|
|
131
|
-
return;
|
|
166
|
+
return true;
|
|
132
167
|
}
|
|
133
168
|
const result = startDaemon();
|
|
134
169
|
if (result.pid) {
|
|
@@ -138,6 +173,7 @@ function ensureDaemonRunning() {
|
|
|
138
173
|
else {
|
|
139
174
|
stderrLine(chalk.yellow('Could not start the daemon. Start it manually with: agents routines start'));
|
|
140
175
|
}
|
|
176
|
+
return true;
|
|
141
177
|
}
|
|
142
178
|
/**
|
|
143
179
|
* Assert the postcondition of `add`: the engine actually picked the monitor up.
|
|
@@ -272,10 +308,16 @@ function buildAction(options) {
|
|
|
272
308
|
action.effort = options.effort;
|
|
273
309
|
if (options.actionTimeout)
|
|
274
310
|
action.timeout = options.actionTimeout;
|
|
311
|
+
if (options.postcondition)
|
|
312
|
+
action.postcondition = options.postcondition;
|
|
313
|
+
chosen.push(action);
|
|
314
|
+
}
|
|
315
|
+
if (options.routine) {
|
|
316
|
+
const action = { type: 'routine', routine: options.routine };
|
|
317
|
+
if (options.postcondition)
|
|
318
|
+
action.postcondition = options.postcondition;
|
|
275
319
|
chosen.push(action);
|
|
276
320
|
}
|
|
277
|
-
if (options.routine)
|
|
278
|
-
chosen.push({ type: 'routine', routine: options.routine });
|
|
279
321
|
if (options.notify !== undefined) {
|
|
280
322
|
// --notify may be a bare flag (notify the owner) or carry a channel that
|
|
281
323
|
// overrides notify.owner.channel. Left unset, the send resolves the owner
|
|
@@ -293,6 +335,10 @@ function buildAction(options) {
|
|
|
293
335
|
stderrLine(chalk.red(`Exactly one action is allowed; got ${chosen.map((c) => c.type).join(', ')}`));
|
|
294
336
|
process.exit(1);
|
|
295
337
|
}
|
|
338
|
+
if (options.postcondition && chosen[0].type !== 'run' && chosen[0].type !== 'routine') {
|
|
339
|
+
stderrLine(chalk.red('--postcondition only applies to --run or --routine'));
|
|
340
|
+
process.exit(1);
|
|
341
|
+
}
|
|
296
342
|
return chosen[0];
|
|
297
343
|
}
|
|
298
344
|
/** Interactive monitor picker. Returns the selected name or null on cancel/empty. */
|
|
@@ -379,7 +425,7 @@ async function guardAgainstDuplicateMonitor(config, force) {
|
|
|
379
425
|
export function registerMonitorsCommands(program) {
|
|
380
426
|
const monitorsCmd = program
|
|
381
427
|
.command('monitors')
|
|
382
|
-
.description('Durable event-triggered watchers: watch a source, detect a change, fire an action. The daemon auto-starts on first add.');
|
|
428
|
+
.description('Durable event-triggered watchers: watch a source, detect a change, fire an action. The daemon auto-starts on first add unless daemon.enabled is false.');
|
|
383
429
|
setHelpSections(monitorsCmd, {
|
|
384
430
|
examples: `
|
|
385
431
|
# CI went red → triage it (poll a command, diff, fire an agent)
|
|
@@ -388,6 +434,12 @@ export function registerMonitorsCommands(program) {
|
|
|
388
434
|
--run claude --prompt 'CI failed on #1119: {event}. Diagnose and fix.' \\
|
|
389
435
|
--device yosemite-s0
|
|
390
436
|
|
|
437
|
+
# Merge-on-green: the fire is ok only if the PR actually merged
|
|
438
|
+
agents monitors add merge-1682 \\
|
|
439
|
+
--poll 'gh pr view 1682 --json state --jq .state' 2m --match OPEN \\
|
|
440
|
+
--run claude --prompt 'Rebase-merge #1682: {event}' \\
|
|
441
|
+
--postcondition 'gh pr view 1682 --json state --jq .state | grep -qx MERGED'
|
|
442
|
+
|
|
391
443
|
# SSL cert issued → notify (poll an HTTPS endpoint every 8h)
|
|
392
444
|
agents monitors add cert-issued \\
|
|
393
445
|
--poll-http 'https://secure.ssl.com/team/.../co-ec1l5dgjofa' 8h \\
|
|
@@ -406,10 +458,14 @@ export function registerMonitorsCommands(program) {
|
|
|
406
458
|
- SOURCE (--watch, --poll, --poll-http, --ws, --watch-file, --watch-device, --on)
|
|
407
459
|
- CONDITION (--on-change [default], --match <re>, --every; --dedupe-key)
|
|
408
460
|
- ACTION (--run <agent> --prompt, --routine, --notify, --webhook-out)
|
|
461
|
+
--postcondition <cmd> on --run/--routine asserts the effect
|
|
462
|
+
happened (exit 0) after the agent settles; otherwise the
|
|
463
|
+
fire records as "no effect", not ok.
|
|
409
464
|
|
|
410
465
|
The fired event is injected into a run/routine prompt as {event}.
|
|
411
466
|
Pin the single OWNER device with --device (exactly-once). The daemon (shared
|
|
412
|
-
with routines) auto-starts on first add
|
|
467
|
+
with routines) auto-starts on first add unless daemon.enabled is false on this
|
|
468
|
+
device; manage it with 'agents daemon start|stop' (or 'agents routines start|stop').
|
|
413
469
|
|
|
414
470
|
v1 evaluates poll sources (command, poll, poll-http, file, device). Push
|
|
415
471
|
sources (ws, webhook) are accepted but delivered through a receiver wired in
|
|
@@ -419,7 +475,7 @@ export function registerMonitorsCommands(program) {
|
|
|
419
475
|
// ─── add ────────────────────────────────────────────────────────────────────
|
|
420
476
|
monitorsCmd
|
|
421
477
|
.command('add [nameOrPath]')
|
|
422
|
-
.description('Create a monitor from inline flags or a YAML file. Auto-starts the daemon.')
|
|
478
|
+
.description('Create a monitor from inline flags or a YAML file. Auto-starts the daemon unless daemon.enabled is false.')
|
|
423
479
|
// SOURCE
|
|
424
480
|
.option('--watch <cmd>', 'Run a shell command; its stdout is the observation')
|
|
425
481
|
.option('--poll <cmd...>', 'Re-run a command every interval: --poll "<cmd>" <interval> (e.g. 30s)')
|
|
@@ -445,6 +501,7 @@ export function registerMonitorsCommands(program) {
|
|
|
445
501
|
.option('--mode <mode>', 'Execution mode for --run: plan, edit, auto, or skip')
|
|
446
502
|
.option('--effort <effort>', 'Reasoning effort for --run: low | medium | high | xhigh | max | auto')
|
|
447
503
|
.option('--action-timeout <t>', 'Kill the --run action if it runs longer than this (e.g. 10m)')
|
|
504
|
+
.option('--postcondition <cmd>', 'Shell command that must exit 0 after a --run/--routine action settles; otherwise the fire records as no-effect, not ok. {event} is replaced with the fired event summary')
|
|
448
505
|
.option('--routine <name>', 'Fire an existing routine on change')
|
|
449
506
|
.option('--notify [channel]', 'Notify the owner (notify.owner); [channel] overrides the owner channel')
|
|
450
507
|
.option('--webhook-out <url>', 'POST the event to this URL')
|
|
@@ -481,8 +538,9 @@ export function registerMonitorsCommands(program) {
|
|
|
481
538
|
await guardAgainstDuplicateMonitor(config, options.force === true);
|
|
482
539
|
writeMonitor(config);
|
|
483
540
|
console.log(chalk.green(`Monitor '${name}' added`));
|
|
484
|
-
|
|
485
|
-
|
|
541
|
+
warnMissingPostcondition(config.action);
|
|
542
|
+
if (ensureDaemonRunning())
|
|
543
|
+
await assertEnginePickup(config);
|
|
486
544
|
return;
|
|
487
545
|
}
|
|
488
546
|
if (!nameOrPath) {
|
|
@@ -556,8 +614,9 @@ export function registerMonitorsCommands(program) {
|
|
|
556
614
|
writeMonitor(config);
|
|
557
615
|
console.log(chalk.green(`Monitor '${nameOrPath}' added`));
|
|
558
616
|
console.log(chalk.gray(` ${sourceLabel(source)} → [${condition.mode}] → ${actionLabel(action)} · owner: ${ownerLabel(config)}`));
|
|
559
|
-
|
|
560
|
-
|
|
617
|
+
warnMissingPostcondition(action);
|
|
618
|
+
if (ensureDaemonRunning())
|
|
619
|
+
await assertEnginePickup(config);
|
|
561
620
|
});
|
|
562
621
|
// ─── list ────────────────────────────────────────────────────────────────────
|
|
563
622
|
monitorsCmd
|
|
@@ -641,7 +700,7 @@ export function registerMonitorsCommands(program) {
|
|
|
641
700
|
state,
|
|
642
701
|
liveness,
|
|
643
702
|
stalled: isStalled(monitor, liveness),
|
|
644
|
-
recentFires,
|
|
703
|
+
recentFires: recentFires.map((f) => ({ ...f, reconciled: resolveFireOutcome(name, f) })),
|
|
645
704
|
});
|
|
646
705
|
return;
|
|
647
706
|
}
|
|
@@ -671,9 +730,11 @@ export function registerMonitorsCommands(program) {
|
|
|
671
730
|
if (recentFires.length > 0) {
|
|
672
731
|
console.log(chalk.bold('\nRecent fires'));
|
|
673
732
|
for (const f of recentFires) {
|
|
674
|
-
// Reconciled against the run's real status
|
|
675
|
-
const {
|
|
676
|
-
console.log(` ${chalk.gray(f.firedAt)} ${f.action ?? '?'} ${
|
|
733
|
+
// Reconciled against the run's real status + postcondition (RUSH-2690, PHNX-2842).
|
|
734
|
+
const { label, note } = fireOutcomeDisplay(name, f);
|
|
735
|
+
console.log(` ${chalk.gray(f.firedAt)} ${f.action ?? '?'} ${label}`);
|
|
736
|
+
if (note)
|
|
737
|
+
console.log(note);
|
|
677
738
|
}
|
|
678
739
|
}
|
|
679
740
|
});
|
|
@@ -847,23 +908,17 @@ export function registerMonitorsCommands(program) {
|
|
|
847
908
|
}
|
|
848
909
|
console.log(chalk.bold(`Fire history: ${name}\n`));
|
|
849
910
|
for (const f of fires.slice(-20)) {
|
|
850
|
-
// Reconcile against the run's REAL, current status (RUSH-2690)
|
|
851
|
-
// than trusting the frozen
|
|
852
|
-
//
|
|
853
|
-
//
|
|
854
|
-
//
|
|
855
|
-
|
|
856
|
-
const { ok, runStatus } = resolveFireOutcome(name, f);
|
|
857
|
-
const outcome = ok ? chalk.green('ok') : chalk.red('failed');
|
|
858
|
-
// Surface the divergence explicitly when the frozen write disagreed
|
|
859
|
-
// with the reconciled read — the exact "fire says ok, run says
|
|
860
|
-
// otherwise" symptom RUSH-2690 reported.
|
|
861
|
-
const corrected = f.ok !== false && !ok && runStatus
|
|
862
|
-
? chalk.yellow(` (run ${runStatus})`)
|
|
863
|
-
: '';
|
|
911
|
+
// Reconcile against the run's REAL, current status (RUSH-2690) and a
|
|
912
|
+
// declared postcondition (PHNX-2842) rather than trusting the frozen
|
|
913
|
+
// `ok` written at fire time — `dispatchAction` only sees a synchronous
|
|
914
|
+
// 'running' snapshot before the run has actually settled, and a
|
|
915
|
+
// `completed` agent that did nothing used to read `ok` here forever.
|
|
916
|
+
const { label, note } = fireOutcomeDisplay(name, f);
|
|
864
917
|
const runRef = f.runId ? chalk.gray(` run ${f.runId}`) : '';
|
|
865
|
-
console.log(` ${f.firedAt} ${(f.action ?? '?').padEnd(12)} ${
|
|
918
|
+
console.log(` ${f.firedAt} ${(f.action ?? '?').padEnd(12)} ${label}${runRef}`);
|
|
866
919
|
console.log(chalk.gray(` ${f.summary.slice(0, 100)}`));
|
|
920
|
+
if (note)
|
|
921
|
+
console.log(note);
|
|
867
922
|
}
|
|
868
923
|
});
|
|
869
924
|
// ─── pause / resume ───────────────────────────────────────────────────────────
|