@phnx-labs/agents-cli 1.22.23 → 1.22.25
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 +359 -0
- package/README.md +19 -8
- package/dist/bin/agents +0 -0
- package/dist/commands/attach.d.ts +2 -0
- package/dist/commands/attach.js +32 -7
- package/dist/commands/cloud.js +9 -5
- package/dist/commands/defaults.js +2 -0
- package/dist/commands/doctor.d.ts +24 -0
- package/dist/commands/doctor.js +105 -9
- package/dist/commands/exec.js +99 -44
- package/dist/commands/feed.d.ts +23 -0
- package/dist/commands/feed.js +65 -2
- package/dist/commands/focus.d.ts +34 -3
- package/dist/commands/focus.js +301 -57
- package/dist/commands/go.d.ts +14 -1
- package/dist/commands/go.js +49 -5
- package/dist/commands/insights.js +6 -2
- package/dist/commands/inspect.js +39 -5
- package/dist/commands/menubar.js +14 -1
- package/dist/commands/models.js +1 -0
- package/dist/commands/modes.d.ts +12 -0
- package/dist/commands/modes.js +147 -0
- package/dist/commands/routines.d.ts +3 -0
- package/dist/commands/routines.js +70 -47
- package/dist/commands/run-cloud.js +1 -1
- package/dist/commands/secrets-sync.js +11 -13
- package/dist/commands/secrets.d.ts +2 -0
- package/dist/commands/secrets.js +54 -17
- package/dist/commands/sessions-browser.d.ts +36 -1
- package/dist/commands/sessions-browser.js +165 -19
- package/dist/commands/sessions-resume.d.ts +2 -0
- package/dist/commands/sessions-resume.js +28 -12
- package/dist/commands/sessions.d.ts +23 -1
- package/dist/commands/sessions.js +58 -61
- package/dist/commands/setup-secrets.js +1 -1
- package/dist/commands/setup-watchdog.js +5 -10
- package/dist/commands/setup.js +1 -1
- package/dist/commands/sync.js +246 -42
- package/dist/commands/teams.js +3 -2
- package/dist/commands/view.js +2 -0
- package/dist/commands/watchdog.d.ts +3 -4
- package/dist/commands/watchdog.js +26 -66
- package/dist/index.js +38 -2
- package/dist/lib/agent-modes.d.ts +49 -0
- package/dist/lib/agent-modes.js +70 -0
- package/dist/lib/agents.js +126 -21
- package/dist/lib/cloud/cursor.d.ts +79 -0
- package/dist/lib/cloud/cursor.js +228 -0
- package/dist/lib/cloud/registry.js +2 -0
- package/dist/lib/cloud/types.d.ts +7 -2
- package/dist/lib/cloud/types.js +14 -0
- package/dist/lib/crabbox/cli.d.ts +2 -2
- package/dist/lib/crabbox/config.d.ts +7 -8
- package/dist/lib/crabbox/config.js +14 -14
- package/dist/lib/crabbox/lease.d.ts +11 -4
- package/dist/lib/crabbox/lease.js +40 -8
- package/dist/lib/crabbox/setup-copy.d.ts +5 -0
- package/dist/lib/crabbox/setup-copy.js +17 -1
- package/dist/lib/daemon.js +27 -1
- package/dist/lib/device-config.js +7 -0
- package/dist/lib/devices/doctor-findings.d.ts +7 -1
- package/dist/lib/devices/doctor-findings.js +40 -1
- package/dist/lib/doctor-diff.d.ts +3 -0
- package/dist/lib/doctor-diff.js +15 -13
- package/dist/lib/event-stream.d.ts +3 -1
- package/dist/lib/event-stream.js +14 -1
- package/dist/lib/events.d.ts +9 -0
- package/dist/lib/events.js +58 -0
- package/dist/lib/exec.d.ts +15 -3
- package/dist/lib/exec.js +112 -19
- package/dist/lib/feed-outcome.d.ts +3 -0
- package/dist/lib/feed-outcome.js +18 -10
- package/dist/lib/feed.d.ts +4 -0
- package/dist/lib/hooks/cache.js +36 -3
- package/dist/lib/hooks.d.ts +19 -7
- package/dist/lib/hooks.js +88 -38
- package/dist/lib/hosts/passthrough.d.ts +21 -0
- package/dist/lib/hosts/passthrough.js +39 -12
- package/dist/lib/hosts/session-index.d.ts +4 -0
- package/dist/lib/hosts/session-index.js +7 -0
- package/dist/lib/manifest.d.ts +12 -2
- package/dist/lib/manifest.js +60 -5
- package/dist/lib/mcp.js +5 -1
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +5 -1
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +15 -2
- package/dist/lib/menubar/install-menubar.d.ts +52 -2
- package/dist/lib/menubar/install-menubar.js +128 -6
- package/dist/lib/menubar/snapshot.d.ts +15 -0
- package/dist/lib/menubar/snapshot.js +40 -0
- package/dist/lib/plugins.js +13 -1
- package/dist/lib/refresh.d.ts +5 -0
- package/dist/lib/refresh.js +37 -33
- package/dist/lib/resource-inventory.d.ts +79 -0
- package/dist/lib/resource-inventory.js +122 -0
- package/dist/lib/resources/mcp.js +3 -0
- package/dist/lib/resources.js +8 -5
- package/dist/lib/routine-process-cleanup.d.ts +9 -0
- package/dist/lib/routine-process-cleanup.js +73 -0
- package/dist/lib/run-defaults.d.ts +2 -0
- package/dist/lib/run-defaults.js +23 -2
- package/dist/lib/runner.js +27 -22
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
- package/dist/lib/secrets/bundles.js +158 -85
- package/dist/lib/secrets/filestore.d.ts +7 -3
- package/dist/lib/secrets/filestore.js +25 -8
- package/dist/lib/secrets/index.js +34 -8
- package/dist/lib/secrets/rc-hygiene.d.ts +14 -0
- package/dist/lib/secrets/rc-hygiene.js +14 -1
- package/dist/lib/secrets/reaper.d.ts +10 -0
- package/dist/lib/secrets/reaper.js +50 -15
- package/dist/lib/secrets/sync-passphrase.d.ts +27 -0
- package/dist/lib/secrets/sync-passphrase.js +78 -0
- package/dist/lib/session/active.d.ts +4 -0
- package/dist/lib/session/db.js +8 -2
- package/dist/lib/session/recovery.d.ts +37 -0
- package/dist/lib/session/recovery.js +95 -0
- package/dist/lib/session/remote-list.d.ts +2 -0
- package/dist/lib/session/remote-list.js +1 -0
- package/dist/lib/session/session-cache.d.ts +4 -4
- package/dist/lib/session/session-cache.js +4 -4
- package/dist/lib/shims.d.ts +2 -2
- package/dist/lib/shims.js +45 -6
- package/dist/lib/signin-badge.js +2 -0
- package/dist/lib/startup/command-registry.d.ts +16 -0
- package/dist/lib/startup/command-registry.js +44 -0
- package/dist/lib/sync-umbrella.d.ts +5 -0
- package/dist/lib/sync-umbrella.js +5 -4
- package/dist/lib/teams/agents.js +1 -1
- package/dist/lib/teams/parsers.d.ts +1 -1
- package/dist/lib/tmux/session.d.ts +4 -2
- package/dist/lib/tmux/session.js +5 -5
- package/dist/lib/types.d.ts +3 -1
- package/dist/lib/versions.js +16 -1
- package/dist/lib/watchdog/service.d.ts +17 -0
- package/dist/lib/watchdog/service.js +39 -0
- package/package.json +1 -1
|
@@ -22,11 +22,35 @@
|
|
|
22
22
|
* apply pending sync.
|
|
23
23
|
*/
|
|
24
24
|
import type { Command } from 'commander';
|
|
25
|
+
import { type DoctorFinding } from '../lib/devices/doctor-findings.js';
|
|
25
26
|
import { type VersionResourceReport } from '../lib/doctor-diff.js';
|
|
26
27
|
import { type DuplicateVersionHook } from '../lib/hooks.js';
|
|
27
28
|
import { type SyncStatusRow, type OrphanRow } from '../lib/drift.js';
|
|
28
29
|
import { type FetchStatusMarker } from '../lib/auto-pull.js';
|
|
29
30
|
export declare function wrapLine(prefix: string, text: string, width?: number): string[];
|
|
31
|
+
export declare function asRemoteSecretFindings(raw: unknown, device: string): DoctorFinding[];
|
|
32
|
+
/**
|
|
33
|
+
* Fetch a device's harness inventory by running its top-level `agents doctor
|
|
34
|
+
* --json` (which carries the `fleet` field, RUSH-2027). Separate from
|
|
35
|
+
* {@link probeFleetTarget} — that forwards `teams doctor --json` for per-agent
|
|
36
|
+
* readiness, a different payload. Returns the parsed {@link FleetInventory} or
|
|
37
|
+
* null (unreachable, non-zero exit, unparseable, or an older CLI with no
|
|
38
|
+
* `fleet` field); a null is skipped by the comparator, never a false gap.
|
|
39
|
+
*/
|
|
40
|
+
/**
|
|
41
|
+
* How long one remote `doctor --json` probe may take.
|
|
42
|
+
*
|
|
43
|
+
* 180s, matching `ChildProcess.doctorTimeout`, which was set to 180 for this
|
|
44
|
+
* same command for this same reason. The previous 30s sat BELOW the command's
|
|
45
|
+
* real cost — 56s measured on `yosemite-m0`, 136s on an idle box — so every slow
|
|
46
|
+
* device silently contributed nothing at all: no inventory, no sign-in, no
|
|
47
|
+
* divergence, no secret findings. A ceiling under the true cost does not make a
|
|
48
|
+
* probe cheap, it makes it always fail.
|
|
49
|
+
*
|
|
50
|
+
* This does not multiply by device count: `fanOutDevices` is `Promise.all`, so
|
|
51
|
+
* whole-fleet wall time is bounded near the slowest box, not 180s x N.
|
|
52
|
+
*/
|
|
53
|
+
export declare const FLEET_INVENTORY_TIMEOUT_MS = 180000;
|
|
30
54
|
export type IssueSeverity = 'critical' | 'warning' | 'info';
|
|
31
55
|
/**
|
|
32
56
|
* One triaged health finding. `severity`/`category`/`subject`/`impact`/`fix` are
|
package/dist/commands/doctor.js
CHANGED
|
@@ -8,7 +8,7 @@ import { enterDoctorOverviewGate, writeDoctorOverviewCache } from '../lib/device
|
|
|
8
8
|
import { fleetDialTarget } from '../lib/devices/connect.js';
|
|
9
9
|
import { compareFleetInventories } from '../lib/devices/fleet-divergence.js';
|
|
10
10
|
import { collectLocalFleetInventory } from '../lib/devices/fleet-inventory.js';
|
|
11
|
-
import { buildLocalFindings, fleetDivergenceToFindings, signInToFindings, renderFindings, } from '../lib/devices/doctor-findings.js';
|
|
11
|
+
import { buildLocalFindings, fleetDivergenceToFindings, signInToFindings, renderFindings, remediationFor, FINDING_SEVERITY, } from '../lib/devices/doctor-findings.js';
|
|
12
12
|
import { getCliVersion } from '../lib/version.js';
|
|
13
13
|
import { resolveHost } from '../lib/hosts/registry.js';
|
|
14
14
|
import { sshExecAsync } from '../lib/ssh-exec.js';
|
|
@@ -31,7 +31,7 @@ import { listCliStatus, listCliStatusAsync } from '../lib/cli-resources.js';
|
|
|
31
31
|
import { setHelpSections } from '../lib/help.js';
|
|
32
32
|
import { heal, healChangedAnything } from '../lib/heal.js';
|
|
33
33
|
import { getEffectiveExecutionPolicy } from '../lib/platform/winpath.js';
|
|
34
|
-
import { scanUserRcFiles } from '../lib/secrets/rc-hygiene.js';
|
|
34
|
+
import { scanUserRcFiles, masterPassphraseInEnv } from '../lib/secrets/rc-hygiene.js';
|
|
35
35
|
import { terminalWidth, truncateToWidth, stringWidth, padToWidth } from '../lib/session/width.js';
|
|
36
36
|
import { readRepoBehindMarkers } from '../lib/auto-pull.js';
|
|
37
37
|
import * as fs from 'fs';
|
|
@@ -80,6 +80,70 @@ function printWrappedLine(prefix, text) {
|
|
|
80
80
|
for (const line of wrapLine(prefix, text))
|
|
81
81
|
console.log(chalk.gray(line));
|
|
82
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Narrow a remote `findings` array to the secret-hygiene rows, or `[]`.
|
|
85
|
+
*
|
|
86
|
+
* Deliberately NOT "forward every remote finding". The aggregator already
|
|
87
|
+
* rebuilds a remote's sign-in rows from its inventory and its divergence rows
|
|
88
|
+
* from the comparator, so forwarding wholesale would double them; and pulling a
|
|
89
|
+
* remote's orphan/drift rows into a fleet readout is a much larger UX change
|
|
90
|
+
* than this fix. The two secret kinds are the ones that are BOTH unrecomputable
|
|
91
|
+
* centrally and security-relevant, which is exactly why they were being lost.
|
|
92
|
+
*
|
|
93
|
+
* **The remote contributes exactly one thing: the KIND.** Severity, message and
|
|
94
|
+
* remediation are all generated HERE. That is not defensiveness for its own
|
|
95
|
+
* sake — the remote runs its own agents-cli, whose version and integrity we do
|
|
96
|
+
* not control, and each forwarded string is load-bearing in a different way:
|
|
97
|
+
*
|
|
98
|
+
* - `remediation` is a command a human copies and runs. Accepting it from the
|
|
99
|
+
* wire is an injection channel, full stop.
|
|
100
|
+
* - `message` is the one place a secret VALUE could re-enter a readout that
|
|
101
|
+
* otherwise guarantees it never prints one. A local builder cannot leak what
|
|
102
|
+
* it never receives.
|
|
103
|
+
* - `severity` decides the CRITICAL section, so a remote could otherwise
|
|
104
|
+
* promote its own warning and bury real criticals under it.
|
|
105
|
+
* - `device` is overwritten with the name we dialled, so no box can pin a
|
|
106
|
+
* finding on another.
|
|
107
|
+
*
|
|
108
|
+
* The cost is detail: the fleet row says which box and which kind, not which
|
|
109
|
+
* file and line. Run `agents doctor` on that box for the specifics — the
|
|
110
|
+
* message says so.
|
|
111
|
+
*/
|
|
112
|
+
const REMOTE_FORWARDED_KINDS = ['rc-secret-export', 'env-secret-export'];
|
|
113
|
+
/** Canonical, locally-authored text for a forwarded kind. Never the remote's. */
|
|
114
|
+
const REMOTE_SECRET_MESSAGE = {
|
|
115
|
+
'rc-secret-export': 'a credential-shaped export was found in this box\'s shell rc files'
|
|
116
|
+
+ ' — run `agents doctor` there for the file and line',
|
|
117
|
+
'env-secret-export': 'AGENTS_SECRETS_PASSPHRASE is set in this box\'s process environment'
|
|
118
|
+
+ ' — run `agents doctor` there for detail',
|
|
119
|
+
};
|
|
120
|
+
export function asRemoteSecretFindings(raw, device) {
|
|
121
|
+
if (!Array.isArray(raw))
|
|
122
|
+
return [];
|
|
123
|
+
const seen = new Set();
|
|
124
|
+
const out = [];
|
|
125
|
+
for (const item of raw) {
|
|
126
|
+
if (!item || typeof item !== 'object' || Array.isArray(item))
|
|
127
|
+
continue;
|
|
128
|
+
const kind = item.kind;
|
|
129
|
+
if (typeof kind !== 'string')
|
|
130
|
+
continue;
|
|
131
|
+
if (!REMOTE_FORWARDED_KINDS.includes(kind))
|
|
132
|
+
continue;
|
|
133
|
+
if (seen.has(kind))
|
|
134
|
+
continue; // one row per kind per box
|
|
135
|
+
seen.add(kind);
|
|
136
|
+
const k = kind;
|
|
137
|
+
const base = {
|
|
138
|
+
severity: FINDING_SEVERITY[k],
|
|
139
|
+
kind: k,
|
|
140
|
+
device,
|
|
141
|
+
message: REMOTE_SECRET_MESSAGE[k],
|
|
142
|
+
};
|
|
143
|
+
out.push({ ...base, remediation: remediationFor({ ...base, remediation: '' }) });
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
83
147
|
async function resolveFleetTargets(opts) {
|
|
84
148
|
const singleName = opts.host || opts.device;
|
|
85
149
|
if (singleName) {
|
|
@@ -155,15 +219,32 @@ async function probeFleetTarget(target) {
|
|
|
155
219
|
* null (unreachable, non-zero exit, unparseable, or an older CLI with no
|
|
156
220
|
* `fleet` field); a null is skipped by the comparator, never a false gap.
|
|
157
221
|
*/
|
|
222
|
+
/**
|
|
223
|
+
* How long one remote `doctor --json` probe may take.
|
|
224
|
+
*
|
|
225
|
+
* 180s, matching `ChildProcess.doctorTimeout`, which was set to 180 for this
|
|
226
|
+
* same command for this same reason. The previous 30s sat BELOW the command's
|
|
227
|
+
* real cost — 56s measured on `yosemite-m0`, 136s on an idle box — so every slow
|
|
228
|
+
* device silently contributed nothing at all: no inventory, no sign-in, no
|
|
229
|
+
* divergence, no secret findings. A ceiling under the true cost does not make a
|
|
230
|
+
* probe cheap, it makes it always fail.
|
|
231
|
+
*
|
|
232
|
+
* This does not multiply by device count: `fanOutDevices` is `Promise.all`, so
|
|
233
|
+
* whole-fleet wall time is bounded near the slowest box, not 180s x N.
|
|
234
|
+
*/
|
|
235
|
+
export const FLEET_INVENTORY_TIMEOUT_MS = 180_000;
|
|
158
236
|
async function probeFleetInventory(target) {
|
|
159
237
|
const isWin = /^win/i.test((target.os ?? '').trim());
|
|
160
238
|
const remoteCmd = buildRemoteAgentsInvocation(['doctor', '--json'], undefined, isWin ? 'windows' : undefined, isWin ? undefined : { PATH: '$HOME/.agents/.cache/shims:$HOME/.local/bin:$PATH' });
|
|
161
|
-
const res = await sshExecAsync(target.sshTarget, remoteCmd, { timeoutMs:
|
|
239
|
+
const res = await sshExecAsync(target.sshTarget, remoteCmd, { timeoutMs: FLEET_INVENTORY_TIMEOUT_MS, multiplex: true });
|
|
162
240
|
if (res.code !== 0)
|
|
163
241
|
return null;
|
|
164
242
|
try {
|
|
165
243
|
const parsed = JSON.parse(res.stdout);
|
|
166
|
-
return
|
|
244
|
+
return {
|
|
245
|
+
inventory: asFleetInventory(parsed.fleet),
|
|
246
|
+
secretFindings: asRemoteSecretFindings(parsed.findings, target.name),
|
|
247
|
+
};
|
|
167
248
|
}
|
|
168
249
|
catch {
|
|
169
250
|
return null;
|
|
@@ -252,19 +333,22 @@ async function runDevicesDoctor(opts) {
|
|
|
252
333
|
fanOutDevices(targets, probeFleetTarget),
|
|
253
334
|
fanOutDevices(targets, probeFleetInventory),
|
|
254
335
|
]);
|
|
255
|
-
const
|
|
336
|
+
const payloadByName = new Map();
|
|
256
337
|
for (const r of inventoryResults)
|
|
257
|
-
|
|
338
|
+
payloadByName.set(r.name, r.status === 'ok' ? (r.value ?? null) : null);
|
|
258
339
|
results.push(...remoteResults.map((r) => {
|
|
259
|
-
const
|
|
340
|
+
const payload = payloadByName.get(r.name) ?? null;
|
|
341
|
+
const inventory = payload?.inventory ?? undefined;
|
|
342
|
+
const secretFindings = payload?.secretFindings;
|
|
260
343
|
if (r.status === 'ok' && r.value)
|
|
261
|
-
return { ...r.value, inventory
|
|
344
|
+
return { ...r.value, inventory, secretFindings };
|
|
262
345
|
return {
|
|
263
346
|
name: r.name,
|
|
264
347
|
online: false,
|
|
265
348
|
error: r.error ?? String(r.reason ?? 'skipped'),
|
|
266
349
|
agents: {},
|
|
267
|
-
inventory
|
|
350
|
+
inventory,
|
|
351
|
+
secretFindings,
|
|
268
352
|
};
|
|
269
353
|
}));
|
|
270
354
|
// Cross-device divergence: compare every device's inventory against the local
|
|
@@ -314,6 +398,7 @@ async function runDevicesDoctor(opts) {
|
|
|
314
398
|
duplicateHooks: inspectDuplicateVersionHooks(cwd),
|
|
315
399
|
hostClis: toHostCliInput(listCliStatus(cwd)),
|
|
316
400
|
rcSecrets: scanUserRcFiles(),
|
|
401
|
+
masterPassphraseInEnv: masterPassphraseInEnv(),
|
|
317
402
|
execPolicy: process.platform === 'win32'
|
|
318
403
|
? { platform: process.platform, policy: getEffectiveExecutionPolicy() }
|
|
319
404
|
: undefined,
|
|
@@ -335,6 +420,11 @@ async function runDevicesDoctor(opts) {
|
|
|
335
420
|
});
|
|
336
421
|
continue;
|
|
337
422
|
}
|
|
423
|
+
// Secret hygiene the aggregator cannot see for itself — the remote's shell
|
|
424
|
+
// rc files and its process environment. Without this the fleet readout
|
|
425
|
+
// reports a leaking box as clean (RUSH-1968).
|
|
426
|
+
if (r.secretFindings?.length)
|
|
427
|
+
findings.push(...r.secretFindings);
|
|
338
428
|
if (r.inventory?.signIn) {
|
|
339
429
|
findings.push(...signInToFindings(r.name, r.inventory.signIn));
|
|
340
430
|
accounts[r.name] = r.inventory.signIn;
|
|
@@ -848,6 +938,11 @@ function renderTargetText(report, options) {
|
|
|
848
938
|
// sees what was checked. options.requestedKinds drives this.
|
|
849
939
|
if (options.requestedKinds && !options.requestedKinds.has(kind))
|
|
850
940
|
continue;
|
|
941
|
+
if (kind === 'hooks' && report.hookInventory) {
|
|
942
|
+
const wired = report.hookInventory.wiringSupported ? String(report.hookInventory.wired.length) : 'unknown';
|
|
943
|
+
const unmanaged = report.hookInventory.unmanaged.length > 0 ? ` · unmanaged ${report.hookInventory.unmanaged.length}` : '';
|
|
944
|
+
console.log(chalk.gray(` inventory: capable ${report.hookInventory.capable ? 'yes' : 'no'} · on-disk ${report.hookInventory.onDisk.length} · wired ${wired}${unmanaged}`));
|
|
945
|
+
}
|
|
851
946
|
renderKindSection(kind, rows, report.layers, options);
|
|
852
947
|
// A hook file can reconcile "ok" above yet be absent from settings.json — a
|
|
853
948
|
// present-but-dead hook. Surface that right under the hooks section.
|
|
@@ -1384,6 +1479,7 @@ export function registerDoctorCommand(program) {
|
|
|
1384
1479
|
duplicateHooks,
|
|
1385
1480
|
hostClis: toHostCliInput(hostClis),
|
|
1386
1481
|
rcSecrets: scanUserRcFiles(),
|
|
1482
|
+
masterPassphraseInEnv: masterPassphraseInEnv(),
|
|
1387
1483
|
// getEffectiveExecutionPolicy spawns powershell — a doomed process on
|
|
1388
1484
|
// POSIX, where the advisory never applies. Probe only on Windows.
|
|
1389
1485
|
execPolicy: process.platform === 'win32'
|
package/dist/commands/exec.js
CHANGED
|
@@ -532,7 +532,7 @@ export function registerRunCommand(program) {
|
|
|
532
532
|
.option('--headless', 'Force headless mode. Auto-enabled when a prompt is provided; pass explicitly to stay headless with no prompt (reads the prompt from stdin).', false)
|
|
533
533
|
.option('--no-auth-check', 'Skip the pre-launch "looks logged out" warning on an interactive run (advisory; never blocks anyway). Also silenced by AGENTS_NO_AUTH_CHECK=1.')
|
|
534
534
|
.option('-i, --interactive', 'Force interactive mode even when a prompt is provided. Mutually exclusive with --headless.')
|
|
535
|
-
.option('--resume [id]', '
|
|
535
|
+
.option('--resume [id]', 'Recover a previous conversation on its origin device. The exact healthy origin uses native resume; otherwise a healthy version of the same harness replays via /continue. Pair with a prompt to continue headlessly.')
|
|
536
536
|
.option('--session-id <id>', 'Force a NEW conversation to use this exact session UUID (Claude only). This CREATES a session — to resume an existing one, use --resume.')
|
|
537
537
|
.option('--name <slug>', 'Name the run — seeds the session label so it shows up as `<name>` in `agents sessions` and resolves by it (and `agents hosts logs <name>` for --host runs) instead of an opaque id. An agent-generated title later refines the label; your name shows until then. Optional.')
|
|
538
538
|
.option('--notify', 'Post a desktop notification when a headless run finishes. Fired by this process on exit, so it survives whatever launched the run (the menu bar dispatching it, a terminal you closed).')
|
|
@@ -568,8 +568,8 @@ export function registerRunCommand(program) {
|
|
|
568
568
|
.option('--bare', 'With --lease, skip copying your local ~/.agents setup (skills/hooks/commands/MCP) onto the box.')
|
|
569
569
|
.option('--tailscale', 'Lease the box onto your tailnet (reachable only over Tailscale) rather than a public IP.')
|
|
570
570
|
.option('--no-tailscale', 'Force a public-IP lease even when a reuse context would default to Tailscale.')
|
|
571
|
-
.option('--cloud', 'Vendor cloud placement: dispatch to the agent\'s native cloud (claude→rush, codex→codex, droid→factory, antigravity→antigravity) and stream the result. Same dispatch as `agents cloud run --agent <agent>`; tracked by `agents cloud list/status/logs`. Same as --where cloud. Mutually exclusive with --host/--lease and local-run flags.')
|
|
572
|
-
.option('--provider <id>', 'With --cloud: override the agent\'s native cloud provider (rush | codex | factory | antigravity | host).')
|
|
571
|
+
.option('--cloud', 'Vendor cloud placement: dispatch to the agent\'s native cloud (claude→rush, codex→codex, cursor→cursor, droid→factory, antigravity→antigravity) and stream the result. Same dispatch as `agents cloud run --agent <agent>`; tracked by `agents cloud list/status/logs`. Same as --where cloud. Mutually exclusive with --host/--lease and local-run flags.')
|
|
572
|
+
.option('--provider <id>', 'With --cloud: override the agent\'s native cloud provider (rush | codex | cursor | factory | antigravity | host).')
|
|
573
573
|
.option('--repo <owner/repo>', 'With --cloud: GitHub repository. Repeatable for multi-repo dispatch (Rush Cloud only).', (val, prev) => [...prev, val], [])
|
|
574
574
|
.option('--branch <name>', 'With --cloud: target git branch.')
|
|
575
575
|
.option('--cloud-env <id>', 'With --cloud: Codex Cloud environment ID (run\'s --env is the KEY=VAL passthrough, so the cloud env id gets its own flag).');
|
|
@@ -647,16 +647,18 @@ export function registerRunCommand(program) {
|
|
|
647
647
|
agents run claude "fix the bug" -- --custom-flag
|
|
648
648
|
`,
|
|
649
649
|
notes: `
|
|
650
|
-
Modes (not every agent supports every mode —
|
|
650
|
+
Modes (not every agent supports every mode — run \`agents modes <agent>\`):
|
|
651
651
|
plan read-only investigation; no writes, no shell side-effects
|
|
652
652
|
edit may edit files; prompts for shell / risky operations
|
|
653
653
|
auto smart classifier auto-approves safe ops, prompts for risky (claude, copilot)
|
|
654
654
|
skip bypass every permission prompt (dangerously-skip-permissions)
|
|
655
655
|
Legacy 'full' is silently rewritten to 'skip'.
|
|
656
|
+
List per-harness support + native flags: agents modes · agents modes claude
|
|
657
|
+
Models (cheap|default|best|ultra): agents models <agent[@version]>
|
|
656
658
|
|
|
657
659
|
Headless plan support (a prompt makes the run headless):
|
|
658
|
-
plan works headless on claude, codex, droid, opencode.
|
|
659
|
-
kimi, grok,
|
|
660
|
+
plan works headless on claude, codex, cursor, droid, opencode.
|
|
661
|
+
kimi, grok, antigravity have no headless plan mode — a headless
|
|
660
662
|
--mode plan auto-downgrades to --mode auto (with a stderr warning).
|
|
661
663
|
Interactive plan (omit the prompt) works everywhere it is listed.
|
|
662
664
|
|
|
@@ -689,7 +691,7 @@ export function registerRunCommand(program) {
|
|
|
689
691
|
given. --cloud is mutually exclusive with --host/--lease and with
|
|
690
692
|
local-run flags (--loop, --resume, --secrets, --terminal, …).
|
|
691
693
|
|
|
692
|
-
Resume: --resume <id> resolves full IDs locally first, then fleet-wide, and
|
|
694
|
+
Resume: --resume <id> resolves full IDs locally first, then fleet-wide, and recovers on the source device with its cwd/mode. The exact healthy origin version uses native resume; otherwise a healthy version of the same harness replays via /continue. agents resume <id> infers the harness too.
|
|
693
695
|
|
|
694
696
|
Passthrough: everything after -- is forwarded verbatim to the underlying agent CLI.
|
|
695
697
|
agents run kimi -- --plan --some-native-flag value
|
|
@@ -835,6 +837,7 @@ export function registerRunCommand(program) {
|
|
|
835
837
|
}
|
|
836
838
|
let autoHarnessRequested = normalizedAgentSpec === RUN_AUTO_KEYWORD;
|
|
837
839
|
let resolvedResumeSource;
|
|
840
|
+
let resolvedRecoveryTarget;
|
|
838
841
|
// Concrete resume ids resolve BEFORE placement. Full UUIDs take the local
|
|
839
842
|
// SQLite fast path; only a local miss fans out to the fleet. This lets a
|
|
840
843
|
// command entered on zion discover that the owning version-home is on a
|
|
@@ -890,32 +893,40 @@ export function registerRunCommand(program) {
|
|
|
890
893
|
process.stderr.write(chalk.yellow(`[agents] session ${resolvedResumeSource.shortId} predates stored launch modes; using --mode ${options.mode}\n`));
|
|
891
894
|
}
|
|
892
895
|
const { machineId } = await import('../lib/machine-id.js');
|
|
896
|
+
const { sessionRecoveryDestinationMatches, sessionRecoveryPeer, } = await import('../lib/session/recovery.js');
|
|
893
897
|
const sourceMachine = resolvedResumeSource.machine;
|
|
898
|
+
const sourcePeer = sessionRecoveryPeer(resolvedResumeSource);
|
|
894
899
|
const explicitPlacement = hostTargetGiven(options).length > 0;
|
|
895
|
-
if (
|
|
896
|
-
options.host =
|
|
900
|
+
if (sourcePeer && !explicitPlacement) {
|
|
901
|
+
options.host = sourcePeer;
|
|
897
902
|
}
|
|
898
|
-
else if (
|
|
899
|
-
console.error(chalk.red(`
|
|
900
|
-
`
|
|
903
|
+
else if (sourcePeer && explicitPlacement && !hostTargetGiven(options).some((host) => sessionRecoveryDestinationMatches(resolvedResumeSource, host))) {
|
|
904
|
+
console.error(chalk.red(`Session ${resolvedResumeSource.shortId} must recover on ${sourcePeer}, where its indexed transcript and version history are owned; ` +
|
|
905
|
+
`the requested device was ${hostTargetGiven(options).join(', ')}.`));
|
|
901
906
|
process.exit(1);
|
|
902
907
|
}
|
|
903
|
-
//
|
|
904
|
-
//
|
|
905
|
-
//
|
|
906
|
-
//
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
const {
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
process.stderr.write(chalk.gray(`[agents] auto resume → native ${normalizedAgentSpec} on ${sourceMachine ?? machineId()}\n`));
|
|
917
|
-
}
|
|
908
|
+
// Recovery is resolved on the device that owns the transcript. A remote
|
|
909
|
+
// dispatch is pinned to the source HARNESS (not `run auto`'s cross-harness
|
|
910
|
+
// picker); the peer repeats this block with the injected SessionMeta and
|
|
911
|
+
// chooses its own healthy version. Locally, resolve it now.
|
|
912
|
+
const sourceAgent = resolvedResumeSource.agent;
|
|
913
|
+
if (!sourcePeer) {
|
|
914
|
+
try {
|
|
915
|
+
const { resolveSessionRecovery } = await import('../lib/session/recovery.js');
|
|
916
|
+
resolvedRecoveryTarget = await resolveSessionRecovery(resolvedResumeSource);
|
|
917
|
+
}
|
|
918
|
+
catch (err) {
|
|
919
|
+
console.error(chalk.red(err.message));
|
|
920
|
+
process.exit(1);
|
|
918
921
|
}
|
|
922
|
+
normalizedAgentSpec = `${resolvedRecoveryTarget.agent}@${resolvedRecoveryTarget.version}`;
|
|
923
|
+
autoHarnessRequested = false;
|
|
924
|
+
if (!options.quiet)
|
|
925
|
+
process.stderr.write(chalk.gray(`[agents] session recovery → ${resolvedRecoveryTarget.mode} ${normalizedAgentSpec} on ${sourceMachine ?? machineId()} · ${resolvedRecoveryTarget.reason}\n`));
|
|
926
|
+
}
|
|
927
|
+
else if (autoHarnessRequested) {
|
|
928
|
+
normalizedAgentSpec = sourceAgent;
|
|
929
|
+
autoHarnessRequested = false;
|
|
919
930
|
}
|
|
920
931
|
}
|
|
921
932
|
if (autoHarnessRequested) {
|
|
@@ -1008,19 +1019,21 @@ export function registerRunCommand(program) {
|
|
|
1008
1019
|
const leaseSecretsBundle = process.env.AGENTS_LEASE_SECRETS_BUNDLE;
|
|
1009
1020
|
const nowSecs = Math.floor(Date.now() / 1000);
|
|
1010
1021
|
let reuseSlug = options.box;
|
|
1011
|
-
//
|
|
1012
|
-
//
|
|
1013
|
-
//
|
|
1014
|
-
// pool-reuse match interchangeable with a fresh warmup.
|
|
1022
|
+
// Lease runs share one default warm pool across repositories. The generic
|
|
1023
|
+
// `.crabbox.yaml profile:` remains available to repo sandbox/CI scripts;
|
|
1024
|
+
// `leaseProfile:` is the explicit opt-in for a dedicated lease hot box.
|
|
1015
1025
|
const repoRoot = gitToplevel(leaseCwd);
|
|
1016
|
-
const {
|
|
1017
|
-
const poolProfile = repoRoot ?
|
|
1026
|
+
const { readCrabboxLeaseProfile } = await import('../lib/crabbox/config.js');
|
|
1027
|
+
const poolProfile = repoRoot ? readCrabboxLeaseProfile(repoRoot) : 'default';
|
|
1018
1028
|
if (options.lease && !reuseSlug && !options.fresh) {
|
|
1019
|
-
const { crabboxList } = await import('../lib/crabbox/cli.js');
|
|
1020
|
-
const {
|
|
1029
|
+
const { crabboxList, poolReusableBoxes } = await import('../lib/crabbox/cli.js');
|
|
1030
|
+
const { formatBoxRow } = await import('./lease.js');
|
|
1021
1031
|
let warm = [];
|
|
1022
1032
|
try {
|
|
1023
|
-
warm =
|
|
1033
|
+
warm = poolReusableBoxes(crabboxList({ secretsBundle: leaseSecretsBundle }), {
|
|
1034
|
+
profile: poolProfile,
|
|
1035
|
+
nowSecs,
|
|
1036
|
+
});
|
|
1024
1037
|
}
|
|
1025
1038
|
catch {
|
|
1026
1039
|
warm = []; // crabbox unavailable / no creds → the pool check in leaseAndRun decides
|
|
@@ -1091,7 +1104,7 @@ export function registerRunCommand(program) {
|
|
|
1091
1104
|
}
|
|
1092
1105
|
}
|
|
1093
1106
|
const { detectSignedInRuntimes, resolveClaudeCredentialsBlob, inferLeaseRuntime, profileNeedsBaseRuntimeCredentials } = await import('../lib/crabbox/runtimes.js');
|
|
1094
|
-
const { leaseAndRun } = await import('../lib/crabbox/lease.js');
|
|
1107
|
+
const { leaseAndRun, leaseWorkspaceId } = await import('../lib/crabbox/lease.js');
|
|
1095
1108
|
const { boxAddress } = await import('./lease.js');
|
|
1096
1109
|
const { getConfiguredRunStrategy, resolveRunVersion } = await import('../lib/rotate.js');
|
|
1097
1110
|
const { profileExists, readProfile, resolveProfileEnv } = await import('../lib/profiles.js');
|
|
@@ -1186,7 +1199,7 @@ export function registerRunCommand(program) {
|
|
|
1186
1199
|
? 'the box is kept after the run'
|
|
1187
1200
|
: options.fresh
|
|
1188
1201
|
? 'the box is destroyed after the run'
|
|
1189
|
-
: '
|
|
1202
|
+
: 'the shared-pool box is kept after the run';
|
|
1190
1203
|
console.error(chalk.gray(`${boxLifecycle} · shipping ${whatShips}${credentialRuntimes.length > 0 && leaseEmail ? ` (${leaseEmail})` : ''}; ${boxAfterRun}.`));
|
|
1191
1204
|
// Read the Claude OAuth token from the local Keychain (silent) so it can be
|
|
1192
1205
|
// written to ~/.claude/.credentials.json on the box — otherwise Claude boots
|
|
@@ -1268,6 +1281,7 @@ export function registerRunCommand(program) {
|
|
|
1268
1281
|
reuseBox: reuseSlug,
|
|
1269
1282
|
fresh: options.fresh,
|
|
1270
1283
|
profile: poolProfile,
|
|
1284
|
+
workspaceId: leaseWorkspaceId(repoRoot ?? leaseCwd),
|
|
1271
1285
|
copySetup,
|
|
1272
1286
|
netMode,
|
|
1273
1287
|
onData: (chunk) => router.push(chunk),
|
|
@@ -2217,10 +2231,44 @@ export function registerRunCommand(program) {
|
|
|
2217
2231
|
session = picked.session;
|
|
2218
2232
|
forceInteractive = true; // bare resume always lands in the agent's TUI
|
|
2219
2233
|
}
|
|
2220
|
-
//
|
|
2221
|
-
//
|
|
2222
|
-
//
|
|
2223
|
-
|
|
2234
|
+
// Bare `run <harness> --resume` learns the chosen SessionMeta only after
|
|
2235
|
+
// the host-placement phase above. If the picker chose a synced session
|
|
2236
|
+
// from another device, route the recovery command now; resolving local
|
|
2237
|
+
// candidates would otherwise native-resume through this device's
|
|
2238
|
+
// unrelated isolated home.
|
|
2239
|
+
if (!resolvedResumeSource) {
|
|
2240
|
+
const { sessionRecoveryPeer, sessionRecoveryRunArgs, } = await import('../lib/session/recovery.js');
|
|
2241
|
+
const peer = sessionRecoveryPeer(session);
|
|
2242
|
+
if (peer) {
|
|
2243
|
+
const { runOnPeer } = await import('../lib/session/remote-list.js');
|
|
2244
|
+
const routed = await runOnPeer(sessionRecoveryRunArgs(session), peer, { tty: true });
|
|
2245
|
+
if (routed === 'no-target') {
|
|
2246
|
+
console.error(chalk.red(`Cannot recover session ${session.shortId}: origin device ${peer} is not a registered reachable peer.`));
|
|
2247
|
+
process.exitCode = 1;
|
|
2248
|
+
}
|
|
2249
|
+
return;
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
// Bare interactive --resume selects the SessionMeta only down here, so
|
|
2253
|
+
// it has not gone through the early concrete-id resolver. Resolve it now;
|
|
2254
|
+
// concrete ids reuse the exact same target chosen above.
|
|
2255
|
+
if (!resolvedRecoveryTarget) {
|
|
2256
|
+
try {
|
|
2257
|
+
const { resolveSessionRecovery } = await import('../lib/session/recovery.js');
|
|
2258
|
+
resolvedRecoveryTarget = await resolveSessionRecovery(session);
|
|
2259
|
+
}
|
|
2260
|
+
catch (err) {
|
|
2261
|
+
console.error(chalk.red(err.message));
|
|
2262
|
+
process.exit(1);
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
if (resolvedRecoveryTarget.agent !== agent) {
|
|
2266
|
+
console.error(chalk.red(`Session ${session.shortId} belongs to ${resolvedRecoveryTarget.agent}, not ${agent}. ` +
|
|
2267
|
+
`Use: agents run auto --resume ${session.id}`));
|
|
2268
|
+
process.exit(1);
|
|
2269
|
+
}
|
|
2270
|
+
version = resolvedRecoveryTarget.version;
|
|
2271
|
+
const canResumeNatively = resolvedRecoveryTarget.mode === 'native';
|
|
2224
2272
|
if (canResumeNatively) {
|
|
2225
2273
|
version = session.version;
|
|
2226
2274
|
resumeNative = true;
|
|
@@ -2330,6 +2378,12 @@ export function registerRunCommand(program) {
|
|
|
2330
2378
|
console.error(chalk.red(`agents: ${agent}@${launchTarget} is not runnable and could not be repaired. Try: ${hint}`));
|
|
2331
2379
|
process.exit(1);
|
|
2332
2380
|
}
|
|
2381
|
+
if (resolvedRecoveryTarget && healed !== launchTarget) {
|
|
2382
|
+
console.error(chalk.red(`agents: session recovery target ${agent}@${launchTarget} became unavailable on ` +
|
|
2383
|
+
`${resolvedResumeSource?.machine ?? 'this device'}; refusing to resume through another version home. ` +
|
|
2384
|
+
`Retry the command so recovery can select a healthy ${agent} version.`));
|
|
2385
|
+
process.exit(1);
|
|
2386
|
+
}
|
|
2333
2387
|
// Always adopt the healed version explicitly. In the version-undefined
|
|
2334
2388
|
// path a fallback re-pins the GLOBAL default, but `resolveVersion`
|
|
2335
2389
|
// prefers a PROJECT pin — so leaving `version` undefined would let the
|
|
@@ -2404,7 +2458,7 @@ export function registerRunCommand(program) {
|
|
|
2404
2458
|
process.exit(1);
|
|
2405
2459
|
}
|
|
2406
2460
|
// Default CLI mode is the generic 'plan'. Agents without a read-only
|
|
2407
|
-
// mode (antigravity,
|
|
2461
|
+
// mode (antigravity, kiro, …) degrade via resolveMode to their
|
|
2408
2462
|
// safest native mode (modes[0], typically edit). That covers both the
|
|
2409
2463
|
// implicit default and an explicit `--mode plan`, so multi-agent
|
|
2410
2464
|
// scripts can pass a uniform plan flag without per-agent branching.
|
|
@@ -2447,7 +2501,8 @@ export function registerRunCommand(program) {
|
|
|
2447
2501
|
console.error(chalk.gray(`Pass --mode plan explicitly if you really want a read-only run.`));
|
|
2448
2502
|
process.exit(1);
|
|
2449
2503
|
}
|
|
2450
|
-
const
|
|
2504
|
+
const effortSource = runCmd.getOptionValueSource('effort');
|
|
2505
|
+
const effort = (effortSource === 'default' && runDefaults.effort ? runDefaults.effort : options.effort);
|
|
2451
2506
|
if (!['low', 'medium', 'high', 'xhigh', 'max', 'auto'].includes(effort)) {
|
|
2452
2507
|
console.error(chalk.red(`Invalid effort: ${effort}. Use 'low', 'medium', 'high', 'xhigh', 'max', or 'auto'`));
|
|
2453
2508
|
process.exit(1);
|
package/dist/commands/feed.d.ts
CHANGED
|
@@ -42,6 +42,8 @@ export declare function prepareLocalFeedBlocks(localBlocks: OpenBlock[], opts: {
|
|
|
42
42
|
dispatch: OpenBlock[];
|
|
43
43
|
filter: ReturnType<typeof filterBlocksForFeed>;
|
|
44
44
|
};
|
|
45
|
+
/** Specific host app + routine provenance for one feed row. */
|
|
46
|
+
export declare function formatFeedRuntime(block: Pick<OpenBlock, 'runtime' | 'origin' | 'routineName'>): string;
|
|
45
47
|
/** Human summary line for one outcome rollup. */
|
|
46
48
|
export declare function formatOutcomeHeader(group: OutcomeGroup): string;
|
|
47
49
|
/** Map active sessions into the lightweight hints outcome enrichment needs. */
|
|
@@ -59,7 +61,28 @@ export declare function sessionHintsFromActive(sessions: Array<{
|
|
|
59
61
|
worktree?: {
|
|
60
62
|
slug?: string;
|
|
61
63
|
};
|
|
64
|
+
host?: string;
|
|
65
|
+
origin?: 'cli' | 'routine';
|
|
66
|
+
routineName?: string;
|
|
62
67
|
}>): SessionOutcomeHint[];
|
|
68
|
+
/**
|
|
69
|
+
* True when a SQLite open/query failed because another writer holds the lock.
|
|
70
|
+
* better-sqlite3 surfaces this as message "database is locked" and/or code
|
|
71
|
+
* SQLITE_BUSY (RUSH-2006).
|
|
72
|
+
*/
|
|
73
|
+
export declare function isSqliteBusyError(err: unknown): boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Load session metas for feed outcome enrichment. The sessions index can be
|
|
76
|
+
* locked by a concurrent scanner/daemon — enrichment is best-effort, so a lock
|
|
77
|
+
* degrades to an empty list instead of crashing `agents feed` (RUSH-2006).
|
|
78
|
+
*
|
|
79
|
+
* `load` is injected so tests can throw a real lock-shaped error without
|
|
80
|
+
* mocking the session module.
|
|
81
|
+
*/
|
|
82
|
+
export declare function loadSessionMetasForFeedEnrichment<T>(load: () => Promise<T[]>): Promise<{
|
|
83
|
+
metas: T[];
|
|
84
|
+
skippedLock: boolean;
|
|
85
|
+
}>;
|
|
63
86
|
export declare function registerFeedCommand(program: Command): void;
|
|
64
87
|
/** Feed view selector (RUSH-2015): decisions, progress, or both. */
|
|
65
88
|
export type FeedFilter = 'needs' | 'updates' | 'all';
|
package/dist/commands/feed.js
CHANGED
|
@@ -16,6 +16,7 @@ import { notifyUrgentBlock } from '../lib/notify.js';
|
|
|
16
16
|
import { gcMailbox } from '../lib/mailbox-gc.js';
|
|
17
17
|
import { isValidMailboxId } from '../lib/mailbox.js';
|
|
18
18
|
import { getActiveSessions } from '../lib/session/active.js';
|
|
19
|
+
import { backfillActiveRowsFromMeta } from './sessions.js';
|
|
19
20
|
import { mailboxIdForActiveSession } from '../lib/mailbox-target.js';
|
|
20
21
|
import { GLYPH, masthead } from '../lib/comms-render.js';
|
|
21
22
|
import { discoverSessions } from '../lib/session/discover.js';
|
|
@@ -154,7 +155,7 @@ export function prepareLocalFeedBlocks(localBlocks, opts) {
|
|
|
154
155
|
}
|
|
155
156
|
function renderBlock(b, localHost, indent = '') {
|
|
156
157
|
const host = b.host !== localHost ? chalk.yellow(` [${b.host}]`) : '';
|
|
157
|
-
const runtime = chalk.gray(b
|
|
158
|
+
const runtime = chalk.gray(formatFeedRuntime(b));
|
|
158
159
|
const age = chalk.gray(relTime(b.ts));
|
|
159
160
|
const cls = b.blockClass ? chalk.gray(`(${b.blockClass})`) : '';
|
|
160
161
|
const consequence = b.consequence && b.consequence !== 'normal' ? chalk.red(`[${b.consequence}]`) : '';
|
|
@@ -218,6 +219,21 @@ function renderBlock(b, localHost, indent = '') {
|
|
|
218
219
|
}
|
|
219
220
|
console.log();
|
|
220
221
|
}
|
|
222
|
+
/** Specific host app + routine provenance for one feed row. */
|
|
223
|
+
export function formatFeedRuntime(block) {
|
|
224
|
+
const host = {
|
|
225
|
+
ghostty: 'Ghostty',
|
|
226
|
+
iterm: 'iTerm',
|
|
227
|
+
terminal: 'terminal',
|
|
228
|
+
kitty: 'Kitty',
|
|
229
|
+
codium: 'VSCodium',
|
|
230
|
+
code: 'VS Code',
|
|
231
|
+
cursor: 'Cursor',
|
|
232
|
+
}[block.runtime.toLowerCase()] ?? block.runtime;
|
|
233
|
+
if (block.origin !== 'routine')
|
|
234
|
+
return host;
|
|
235
|
+
return `${host} · routine${block.routineName ? `:${block.routineName}` : ''}`;
|
|
236
|
+
}
|
|
221
237
|
/** Human summary line for one outcome rollup. */
|
|
222
238
|
export function formatOutcomeHeader(group) {
|
|
223
239
|
const { agents, open, answered, parked } = group.counts;
|
|
@@ -251,8 +267,44 @@ export function sessionHintsFromActive(sessions) {
|
|
|
251
267
|
prUrl: s.pr?.url,
|
|
252
268
|
worktreeSlug: s.worktree?.slug,
|
|
253
269
|
project: s.cwd ? projectKeyFromCwd(s.cwd) : undefined,
|
|
270
|
+
host: s.host,
|
|
271
|
+
origin: s.origin,
|
|
272
|
+
routineName: s.routineName,
|
|
254
273
|
}));
|
|
255
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* True when a SQLite open/query failed because another writer holds the lock.
|
|
277
|
+
* better-sqlite3 surfaces this as message "database is locked" and/or code
|
|
278
|
+
* SQLITE_BUSY (RUSH-2006).
|
|
279
|
+
*/
|
|
280
|
+
export function isSqliteBusyError(err) {
|
|
281
|
+
if (err == null)
|
|
282
|
+
return false;
|
|
283
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
284
|
+
const code = typeof err === 'object' && err !== null && 'code' in err
|
|
285
|
+
? String(err.code ?? '')
|
|
286
|
+
: '';
|
|
287
|
+
return /SQLITE_BUSY|database is locked/i.test(msg) || /SQLITE_BUSY/i.test(code);
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Load session metas for feed outcome enrichment. The sessions index can be
|
|
291
|
+
* locked by a concurrent scanner/daemon — enrichment is best-effort, so a lock
|
|
292
|
+
* degrades to an empty list instead of crashing `agents feed` (RUSH-2006).
|
|
293
|
+
*
|
|
294
|
+
* `load` is injected so tests can throw a real lock-shaped error without
|
|
295
|
+
* mocking the session module.
|
|
296
|
+
*/
|
|
297
|
+
export async function loadSessionMetasForFeedEnrichment(load) {
|
|
298
|
+
try {
|
|
299
|
+
return { metas: await load(), skippedLock: false };
|
|
300
|
+
}
|
|
301
|
+
catch (err) {
|
|
302
|
+
if (isSqliteBusyError(err)) {
|
|
303
|
+
return { metas: [], skippedLock: true };
|
|
304
|
+
}
|
|
305
|
+
throw err;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
256
308
|
export function registerFeedCommand(program) {
|
|
257
309
|
const feed = program
|
|
258
310
|
.command('feed')
|
|
@@ -446,7 +498,18 @@ export function registerFeedCommand(program) {
|
|
|
446
498
|
if (includeLocal) {
|
|
447
499
|
sessions = await getActiveSessions();
|
|
448
500
|
}
|
|
449
|
-
|
|
501
|
+
// discoverSessions touches sessions.db; under concurrent scan pressure it
|
|
502
|
+
// can throw SQLITE_BUSY. Outcome enrichment is best-effort — degrade with
|
|
503
|
+
// a warning rather than crash the whole feed (RUSH-2006).
|
|
504
|
+
let sessionMetas = [];
|
|
505
|
+
if (includeLocal && sessions.length > 0) {
|
|
506
|
+
const loaded = await loadSessionMetasForFeedEnrichment(() => discoverSessions({ all: true, limit: 5000 }));
|
|
507
|
+
if (loaded.skippedLock) {
|
|
508
|
+
console.error(chalk.yellow('Feed: session index is locked; skipping local outcome enrichment'));
|
|
509
|
+
}
|
|
510
|
+
sessionMetas = loaded.metas;
|
|
511
|
+
backfillActiveRowsFromMeta(sessions, new Map(sessionMetas.map((meta) => [meta.id, meta])));
|
|
512
|
+
}
|
|
450
513
|
const localSignals = buildSessionSignals(sessions, sessionMetas);
|
|
451
514
|
if (opts.pause || opts.kill) {
|
|
452
515
|
if (!includeLocal) {
|