@phnx-labs/agents-cli 1.22.52 → 1.22.54
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 +336 -0
- package/README.md +42 -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 +220 -174
- 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 +26 -17
- package/dist/commands/fleet-capture.js +7 -0
- package/dist/commands/focus.d.ts +1 -0
- package/dist/commands/focus.js +4 -2
- package/dist/commands/go.d.ts +5 -4
- package/dist/commands/go.js +8 -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/secrets.js +33 -14
- package/dist/commands/sessions-inject.js +8 -3
- package/dist/commands/sessions-picker.js +2 -1
- package/dist/commands/sessions.d.ts +20 -12
- package/dist/commands/sessions.js +94 -40
- 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 +52 -3
- package/dist/commands/share.js +262 -18
- package/dist/commands/ssh.d.ts +7 -0
- package/dist/commands/ssh.js +53 -14
- package/dist/commands/status.js +14 -0
- package/dist/commands/sync.js +44 -0
- package/dist/commands/view.d.ts +3 -1
- package/dist/commands/view.js +5 -4
- package/dist/lib/account-registry.d.ts +15 -5
- package/dist/lib/account-registry.js +165 -53
- package/dist/lib/accounting/rotate.d.ts +20 -6
- package/dist/lib/accounting/rotate.js +38 -7
- package/dist/lib/accounting/usage.d.ts +37 -1
- package/dist/lib/accounting/usage.js +71 -6
- 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/answer-router.js +2 -1
- package/dist/lib/auth-mint.d.ts +150 -0
- package/dist/lib/auth-mint.js +434 -0
- package/dist/lib/browser/profiles.d.ts +18 -0
- package/dist/lib/browser/profiles.js +26 -1
- package/dist/lib/browser/registry.d.ts +44 -14
- package/dist/lib/browser/registry.js +141 -45
- package/dist/lib/browser/remote-control.d.ts +9 -7
- package/dist/lib/browser/remote-control.js +9 -7
- 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/daemon.js +30 -4
- package/dist/lib/daemon/runner.js +10 -2
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +5 -0
- package/dist/lib/device-config.d.ts +3 -3
- package/dist/lib/device-config.js +8 -7
- package/dist/lib/devices/config-migration.js +147 -1
- package/dist/lib/devices/connect.d.ts +26 -0
- package/dist/lib/devices/connect.js +48 -1
- package/dist/lib/devices/device-docs.d.ts +35 -0
- package/dist/lib/devices/device-docs.js +163 -0
- package/dist/lib/devices/discovery-policy.d.ts +14 -2
- package/dist/lib/devices/discovery-policy.js +31 -21
- package/dist/lib/devices/doctor-findings.d.ts +5 -1
- package/dist/lib/devices/doctor-findings.js +19 -1
- package/dist/lib/devices/registry.d.ts +11 -5
- package/dist/lib/devices/registry.js +46 -18
- package/dist/lib/exec.d.ts +88 -30
- package/dist/lib/exec.js +138 -34
- package/dist/lib/feed/feed.d.ts +10 -2
- package/dist/lib/feed/feed.js +35 -2
- 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/dispatch.d.ts +4 -3
- package/dist/lib/hosts/dispatch.js +12 -8
- package/dist/lib/hosts/passthrough.d.ts +1 -10
- package/dist/lib/hosts/passthrough.js +1 -13
- package/dist/lib/hosts/providers/local.d.ts +9 -3
- package/dist/lib/hosts/providers/local.js +23 -12
- package/dist/lib/hosts/reconnect.d.ts +7 -4
- package/dist/lib/hosts/reconnect.js +29 -25
- package/dist/lib/hosts/registry.js +4 -1
- package/dist/lib/hosts/remote-os.js +3 -1
- 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.js +8 -1
- 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/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.d.ts +10 -1
- package/dist/lib/session/active.js +8 -5
- package/dist/lib/session/actor-sidecar.d.ts +7 -0
- package/dist/lib/session/actor-sidecar.js +2 -0
- package/dist/lib/session/db.d.ts +39 -4
- package/dist/lib/session/db.js +168 -31
- package/dist/lib/session/discover.d.ts +32 -4
- package/dist/lib/session/discover.js +126 -37
- 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/live-metadata.js +1 -0
- package/dist/lib/session/pid-registry.d.ts +7 -0
- package/dist/lib/session/prompt.d.ts +15 -0
- package/dist/lib/session/prompt.js +21 -0
- 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 +25 -0
- package/dist/lib/session/types.js +10 -0
- package/dist/lib/share/publish.d.ts +53 -5
- package/dist/lib/share/publish.js +99 -17
- package/dist/lib/share/worker-template.js +594 -64
- package/dist/lib/startup/root-command.js +2 -1
- package/dist/lib/state.d.ts +24 -0
- package/dist/lib/state.js +318 -54
- package/dist/lib/sync-status.d.ts +4 -0
- package/dist/lib/sync-status.js +3 -0
- package/dist/lib/terminal/resolve.d.ts +7 -0
- package/dist/lib/terminal/resolve.js +41 -2
- package/dist/lib/traces/classify.js +24 -19
- package/dist/lib/traces/insights.d.ts +67 -0
- package/dist/lib/traces/insights.js +178 -0
- package/dist/lib/traces/phenotype.d.ts +67 -0
- package/dist/lib/traces/phenotype.js +437 -0
- package/dist/lib/traces/segments.d.ts +133 -0
- package/dist/lib/traces/segments.js +301 -0
- package/dist/lib/traces/sync.d.ts +33 -0
- package/dist/lib/traces/sync.js +11 -2
- package/dist/lib/types.d.ts +47 -1
- package/dist/lib/usage-refresh.js +2 -1
- package/dist/lib/view-types.d.ts +2 -0
- package/dist/lib/watchdog/runner.js +18 -4
- package/package.json +2 -1
|
@@ -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
|
|
@@ -1569,15 +1569,19 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
1569
1569
|
});
|
|
1570
1570
|
}
|
|
1571
1571
|
}
|
|
1572
|
-
// A network drop kills the local ssh client (exit 255)
|
|
1573
|
-
// agent
|
|
1574
|
-
// (
|
|
1575
|
-
//
|
|
1576
|
-
//
|
|
1577
|
-
//
|
|
1578
|
-
//
|
|
1579
|
-
//
|
|
1580
|
-
//
|
|
1572
|
+
// A network drop kills the local ssh client (exit 255). What the
|
|
1573
|
+
// remote agent does next depends on the peer's tmux.enabled
|
|
1574
|
+
// (PHNX-3316): wrapped, it survives in its detached pane and the
|
|
1575
|
+
// reconnect rejoins it; bare (the default), the drop SIGHUPs it and
|
|
1576
|
+
// the reconnect resumes the session in place from disk. Either way,
|
|
1577
|
+
// with a known session id (Claude's forced id, a resumed run, or a
|
|
1578
|
+
// non-Claude id we just resolved from the remote hook record) we
|
|
1579
|
+
// reconnect automatically instead of exiting — the user never has
|
|
1580
|
+
// to notice the drop and `agents sessions focus` by hand.
|
|
1581
|
+
// `raw` runs opted out of all of this, so there is nothing to
|
|
1582
|
+
// reconnect to. For `run auto` prefer the join-resolved id (the
|
|
1583
|
+
// harness the remote ACTUALLY picked) over the explicit
|
|
1584
|
+
// --session-id only claude adopts.
|
|
1581
1585
|
const { pickReconnectTarget, reconnectInteractiveSession, afterInteractiveRemoteExit, SSH_CONN_FAILURE } = await import('../lib/hosts/reconnect.js');
|
|
1582
1586
|
const reconnectTarget = pickReconnectTarget({
|
|
1583
1587
|
agent: runAgent,
|
|
@@ -1814,6 +1818,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
1814
1818
|
let accountConfigVersion;
|
|
1815
1819
|
let profileProvider;
|
|
1816
1820
|
let fromProfile = false;
|
|
1821
|
+
let profileName;
|
|
1817
1822
|
let profileFallbackModel;
|
|
1818
1823
|
let workflowModel;
|
|
1819
1824
|
// WORKFLOW.md capability scoping, translated to Claude headless flags below.
|
|
@@ -1897,6 +1902,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
1897
1902
|
profileProvider = readProfile(rawAgent).provider;
|
|
1898
1903
|
profileFallbackModel = resolved.fallbackModel;
|
|
1899
1904
|
fromProfile = true;
|
|
1905
|
+
profileName = resolved.profileName;
|
|
1900
1906
|
process.stderr.write(chalk.gray(`Resolved custom harness '${resolved.profileName}' -> ${agent}${version ? `@${version}` : ''}\n`));
|
|
1901
1907
|
if (resolved.tierNote) {
|
|
1902
1908
|
process.stderr.write(chalk.gray(`[agents] ${resolved.tierNote}\n`));
|
|
@@ -2362,13 +2368,15 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2362
2368
|
process.exit(1);
|
|
2363
2369
|
}
|
|
2364
2370
|
const strategy = options.balanced ? 'balanced' : explicitStrategy ?? configuredStrategy;
|
|
2365
|
-
// Strategy
|
|
2366
|
-
// profiles already
|
|
2367
|
-
//
|
|
2368
|
-
//
|
|
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
|
|
2369
2375
|
// `agents run claude --fallback codex` run lands on the pinned default
|
|
2370
2376
|
// account and account rotation silently stops (the gh-monitor heal bug).
|
|
2371
|
-
|
|
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)) {
|
|
2372
2380
|
if (version) {
|
|
2373
2381
|
process.stderr.write(chalk.yellow(`[agents] strategy ${strategy} ignored: version ${version} is pinned\n`));
|
|
2374
2382
|
}
|
|
@@ -2745,6 +2753,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2745
2753
|
}
|
|
2746
2754
|
const execOptions = {
|
|
2747
2755
|
agent,
|
|
2756
|
+
harnessName: profileName,
|
|
2748
2757
|
version,
|
|
2749
2758
|
configVersion: accountConfigVersion,
|
|
2750
2759
|
prompt,
|
|
@@ -71,6 +71,13 @@ async function runCapture(opts) {
|
|
|
71
71
|
secretsBundles: listBundles().map((b) => b.name),
|
|
72
72
|
routines: listJobs().map((j) => j.name),
|
|
73
73
|
};
|
|
74
|
+
// Discovery decisions and dismissals are device-scoped now (PHNX-3315): they
|
|
75
|
+
// live in each box's device doc and sync via the repo, so `capture` must NOT
|
|
76
|
+
// hoist the cross-box union back into the shared `agents.yaml` `fleet:` block
|
|
77
|
+
// (that would recreate the N-boxes-rewrite-one-map conflict Task A removed and
|
|
78
|
+
// re-arm the fold-then-delete migration on every capture). `meta.fleet` carries
|
|
79
|
+
// only the roster/defaults; any lingering central-legacy discovery/ignored is
|
|
80
|
+
// carried forward verbatim and drained by the migration.
|
|
74
81
|
const next = captureFleet(meta.fleet, inputs);
|
|
75
82
|
if (opts.dryRun) {
|
|
76
83
|
console.log(chalk.gray('# agents.yaml fleet: block (dry run — not written)'));
|
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
|
@@ -31,6 +31,7 @@ import { multiItemPicker } from '../lib/picker.js';
|
|
|
31
31
|
import { sessionRecoveryRunArgs } from '../lib/session/recovery.js';
|
|
32
32
|
import { shellQuote, assertValidSshTarget } from '../lib/ssh-exec.js';
|
|
33
33
|
import { openSurfaces, currentContext, availableBackends, detectCurrentBackend, } from '../lib/terminal/index.js';
|
|
34
|
+
import { addressabilityRecoveryHint } from '../lib/terminal/resolve.js';
|
|
34
35
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
35
36
|
import { setHelpSections } from '../lib/help.js';
|
|
36
37
|
const INHERITED_FOCUS_OPTIONS = [
|
|
@@ -493,10 +494,10 @@ async function pickFocusCandidates(sessions, liveById, initialSearch) {
|
|
|
493
494
|
export function focusTargetForResolved(poolMatch, resolved) {
|
|
494
495
|
return poolMatch ?? resolved;
|
|
495
496
|
}
|
|
496
|
-
async function focusResolvedSession(meta, liveById, self, fallback, attachOnly, reconnectReattach = false) {
|
|
497
|
+
export async function focusResolvedSession(meta, liveById, self, fallback, attachOnly, reconnectReattach = false) {
|
|
497
498
|
const active = liveById.get(meta.id);
|
|
498
499
|
if (active && isAttachableLiveSession(active)) {
|
|
499
|
-
await jumpTo(active, self, fallback);
|
|
500
|
+
await jumpTo(active, self, fallback, meta.id);
|
|
500
501
|
return;
|
|
501
502
|
}
|
|
502
503
|
if (attachOnly) {
|
|
@@ -534,6 +535,7 @@ export async function focusSelectedSession(meta, active, self) {
|
|
|
534
535
|
return;
|
|
535
536
|
}
|
|
536
537
|
console.log(chalk.yellow('This live session has no session id or living attach rail to focus.'));
|
|
538
|
+
console.log(chalk.gray(addressabilityRecoveryHint(active, meta.id)));
|
|
537
539
|
process.exitCode = 1;
|
|
538
540
|
return;
|
|
539
541
|
}
|
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
|
@@ -34,6 +34,7 @@ import { getDefaultSocketPath } from '../lib/tmux/paths.js';
|
|
|
34
34
|
import { sshExec, sshStream, assertValidSshTarget, shellQuote, SSH_CONN_FAILURE_CODE } from '../lib/ssh-exec.js';
|
|
35
35
|
import { connectionEndedNotice } from '../lib/hosts/reconnect.js';
|
|
36
36
|
import { enumerateGhosttyTabs, assignGhosttyTabs } from '../lib/session/ghostty-tabs.js';
|
|
37
|
+
import { addressabilityRecoveryHint } from '../lib/terminal/resolve.js';
|
|
37
38
|
const execFileAsync = promisify(execFile);
|
|
38
39
|
export function registerGoCommand(program) {
|
|
39
40
|
program
|
|
@@ -311,17 +312,17 @@ export async function probeAttachRail(s, self) {
|
|
|
311
312
|
}
|
|
312
313
|
/** Strict attach-only fallback: no pane means no attach. Never open a shell or
|
|
313
314
|
* start recovery, because both would violate the caller's no-fork intent. */
|
|
314
|
-
export async function refuseFallback(s, remote) {
|
|
315
|
+
export async function refuseFallback(s, remote, fallbackId) {
|
|
315
316
|
if (remote) {
|
|
316
317
|
console.log(chalk.yellow(`Can't attach ${shortId(s)} on ${remote} — it has no living tmux pane.`));
|
|
317
318
|
process.exitCode = 1;
|
|
318
319
|
return;
|
|
319
320
|
}
|
|
320
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).`) +
|
|
321
|
-
chalk.gray(`\
|
|
322
|
+
chalk.gray(`\n${addressabilityRecoveryHint(s, fallbackId)}`));
|
|
322
323
|
process.exitCode = 1;
|
|
323
324
|
}
|
|
324
|
-
export async function jumpTo(s, self, fallback = refuseFallback) {
|
|
325
|
+
export async function jumpTo(s, self, fallback = refuseFallback, fallbackId) {
|
|
325
326
|
const remote = sessionProcessHost(s, self);
|
|
326
327
|
const mux = s.provenance?.mux;
|
|
327
328
|
// Path C: remote tmux — ssh in and attach, resolving the pane's session on the remote.
|
|
@@ -329,7 +330,7 @@ export async function jumpTo(s, self, fallback = refuseFallback) {
|
|
|
329
330
|
if (mux?.kind === 'tmux' && mux.pane) {
|
|
330
331
|
const liveness = await probeAttachRail(s, self);
|
|
331
332
|
if (liveness.state !== 'alive') {
|
|
332
|
-
await fallback(s, remote);
|
|
333
|
+
await fallback(s, remote, fallbackId);
|
|
333
334
|
return;
|
|
334
335
|
}
|
|
335
336
|
assertValidSshTarget(remote);
|
|
@@ -348,14 +349,14 @@ export async function jumpTo(s, self, fallback = refuseFallback) {
|
|
|
348
349
|
process.exit(code);
|
|
349
350
|
}
|
|
350
351
|
// Remote, not in tmux → hand off to the fallback (go: shell; focus: resume in a tab).
|
|
351
|
-
await fallback(s, remote);
|
|
352
|
+
await fallback(s, remote, fallbackId);
|
|
352
353
|
return;
|
|
353
354
|
}
|
|
354
355
|
// Path B: local tmux — attach (or switch-client if we're already inside tmux).
|
|
355
356
|
if (mux?.kind === 'tmux' && mux.pane) {
|
|
356
357
|
const liveness = await probeAttachRail(s, self);
|
|
357
358
|
if (liveness.state !== 'alive') {
|
|
358
|
-
await fallback(s, undefined);
|
|
359
|
+
await fallback(s, undefined, fallbackId);
|
|
359
360
|
return;
|
|
360
361
|
}
|
|
361
362
|
const socket = mux.socket ?? getDefaultSocketPath();
|
|
@@ -402,7 +403,7 @@ export async function jumpTo(s, self, fallback = refuseFallback) {
|
|
|
402
403
|
return;
|
|
403
404
|
}
|
|
404
405
|
// Path D: no attach rail (headless / plain terminal) → hand off to the fallback.
|
|
405
|
-
await fallback(s, undefined);
|
|
406
|
+
await fallback(s, undefined, fallbackId);
|
|
406
407
|
}
|
|
407
408
|
/** Resolve a local tmux pane id to its session name + window index. */
|
|
408
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('');
|