@phnx-labs/agents-cli 1.22.59 → 1.22.61
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 +71 -0
- package/dist/cli/command-registry.d.ts +1 -0
- package/dist/cli/command-registry.js +2 -0
- package/dist/commands/browser.js +9 -4
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/exec.js +35 -1
- package/dist/commands/harness-hooks.d.ts +55 -0
- package/dist/commands/harness-hooks.js +104 -0
- package/dist/commands/harness-wizard.d.ts +33 -14
- package/dist/commands/harness-wizard.js +53 -23
- package/dist/commands/harness.d.ts +14 -0
- package/dist/commands/harness.js +86 -5
- package/dist/commands/perf.js +10 -0
- package/dist/commands/reminders.d.ts +9 -0
- package/dist/commands/reminders.js +49 -0
- package/dist/commands/run-account-picker.d.ts +14 -0
- package/dist/commands/run-account-picker.js +13 -0
- package/dist/commands/sessions-picker.d.ts +13 -0
- package/dist/commands/sessions-picker.js +17 -8
- package/dist/commands/sessions.js +13 -11
- package/dist/commands/teams-picker.js +20 -6
- package/dist/commands/teams.d.ts +3 -3
- package/dist/commands/teams.js +86 -24
- package/dist/index.js +9 -0
- package/dist/lib/accounting/rotate.d.ts +63 -0
- package/dist/lib/accounting/rotate.js +240 -16
- package/dist/lib/accounting/usage-sync.d.ts +12 -2
- package/dist/lib/accounting/usage-sync.js +34 -6
- package/dist/lib/browser/drivers/local.d.ts +11 -0
- package/dist/lib/browser/drivers/local.js +26 -0
- package/dist/lib/browser/profiles.js +8 -6
- package/dist/lib/browser/service.d.ts +12 -8
- package/dist/lib/browser/service.js +38 -10
- package/dist/lib/claude-statusline.d.ts +14 -1
- package/dist/lib/claude-statusline.js +27 -2
- package/dist/lib/daemon/runner.js +17 -2
- package/dist/lib/devices/doctor-findings.d.ts +1 -1
- package/dist/lib/devices/doctor-findings.js +22 -4
- package/dist/lib/doctor-diff.d.ts +21 -5
- package/dist/lib/doctor-diff.js +242 -76
- package/dist/lib/feed/events.d.ts +1 -1
- package/dist/lib/feed/events.js +28 -15
- package/dist/lib/github/gh-overload.d.ts +58 -0
- package/dist/lib/github/gh-overload.js +246 -0
- package/dist/lib/github/rest.d.ts +64 -0
- package/dist/lib/github/rest.js +111 -0
- package/dist/lib/harness-connection-test.d.ts +57 -0
- package/dist/lib/harness-connection-test.js +80 -0
- package/dist/lib/heal.js +8 -3
- package/dist/lib/installations/shims.d.ts +22 -0
- package/dist/lib/installations/shims.js +104 -0
- package/dist/lib/linear-project-counts.js +8 -0
- package/dist/lib/linear-rate-limit.d.ts +26 -0
- package/dist/lib/linear-rate-limit.js +163 -0
- package/dist/lib/mcp.d.ts +9 -0
- package/dist/lib/mcp.js +37 -1
- package/dist/lib/open-url.js +5 -3
- package/dist/lib/perf/db.d.ts +1 -1
- package/dist/lib/perf/db.js +53 -2
- package/dist/lib/perf/types.d.ts +14 -0
- package/dist/lib/permissions.d.ts +28 -0
- package/dist/lib/permissions.js +156 -1
- package/dist/lib/refresh.js +9 -1
- package/dist/lib/reminders.d.ts +29 -0
- package/dist/lib/reminders.js +88 -0
- package/dist/lib/resource-content-diff.d.ts +33 -0
- package/dist/lib/resource-content-diff.js +103 -0
- package/dist/lib/rules/compile.d.ts +7 -0
- package/dist/lib/rules/compile.js +7 -1
- package/dist/lib/session/active.d.ts +41 -4
- package/dist/lib/session/active.js +58 -7
- package/dist/lib/session/host-link.d.ts +22 -0
- package/dist/lib/session/host-link.js +40 -4
- package/dist/lib/session/live-metadata.js +3 -3
- package/dist/lib/session/trajectory.d.ts +42 -0
- package/dist/lib/session/trajectory.js +46 -27
- package/dist/lib/ssh-exec.d.ts +30 -0
- package/dist/lib/ssh-exec.js +37 -5
- package/dist/lib/startup/command-registry.js +1 -1
- package/dist/lib/subagents-registry.d.ts +18 -0
- package/dist/lib/subagents-registry.js +79 -0
- package/dist/lib/teams/agents.d.ts +12 -0
- package/dist/lib/teams/agents.js +51 -0
- package/dist/lib/teams/api.d.ts +8 -0
- package/dist/lib/teams/api.js +50 -6
- package/dist/lib/teams/delivery.d.ts +14 -4
- package/dist/lib/teams/delivery.js +15 -5
- package/dist/lib/traces/schema2-build.d.ts +85 -0
- package/dist/lib/traces/schema2-build.js +637 -0
- package/dist/lib/traces/schema2-danger.d.ts +36 -0
- package/dist/lib/traces/schema2-danger.js +185 -0
- package/dist/lib/traces/schema2.d.ts +149 -0
- package/dist/lib/traces/schema2.js +20 -0
- package/dist/lib/traces/sync.d.ts +93 -0
- package/dist/lib/traces/sync.js +75 -22
- package/dist/lib/traces/worker-template.js +5 -0
- package/dist/lib/uninstall.js +10 -1
- package/dist/lib/workflows.d.ts +11 -0
- package/dist/lib/workflows.js +67 -8
- package/package.json +1 -1
package/dist/commands/harness.js
CHANGED
|
@@ -17,7 +17,9 @@ import { listProfiles, readProfile, writeProfile, deleteProfile, profileExists,
|
|
|
17
17
|
import { listPresets, getPreset } from '../lib/profiles-presets.js';
|
|
18
18
|
import { AGENTS, ALL_AGENT_IDS, resolveAgentName } from '../lib/agents.js';
|
|
19
19
|
import { findAccount } from '../lib/account-registry.js';
|
|
20
|
-
import { runWizardSteps, createSteps, editSteps, defaultWizardIO, } from './harness-wizard.js';
|
|
20
|
+
import { runWizardSteps, createSteps, editSteps, defaultWizardIO, runConnectionTest, } from './harness-wizard.js';
|
|
21
|
+
import { harnessHooks } from './harness-hooks.js';
|
|
22
|
+
import { CONNECTION_TEST_PROMPT } from '../lib/harness-connection-test.js';
|
|
21
23
|
/** Short capability summary for a native harness — its supported run modes. */
|
|
22
24
|
function nativeModes(id) {
|
|
23
25
|
const modes = AGENTS[id]?.capabilities?.modes ?? [];
|
|
@@ -196,6 +198,73 @@ export function addNeedsWizard(name, opts) {
|
|
|
196
198
|
return true;
|
|
197
199
|
return !getPreset(name);
|
|
198
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* Resolve the tri-state connection-test gate (RUSH-2221), pure so the branching
|
|
203
|
+
* is unit-tested with no prompt or spawn. `--test` forces it on, `--no-test`
|
|
204
|
+
* forces it off, and with neither flag a TTY is asked (default yes) while a
|
|
205
|
+
* non-interactive caller (`--key-stdin`, piped, CI) skips it — so scripting stays
|
|
206
|
+
* non-interactive unless it opts in with `--test`.
|
|
207
|
+
*/
|
|
208
|
+
export function connectionTestGate(testFlag, interactive) {
|
|
209
|
+
if (testFlag === true)
|
|
210
|
+
return 'on';
|
|
211
|
+
if (testFlag === false)
|
|
212
|
+
return 'off';
|
|
213
|
+
return interactive ? 'ask' : 'off';
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Pre-save connection test (RUSH-2221). The harness is already on disk (the test
|
|
217
|
+
* drives the real `agents run <name>` path, so it must be), so this runs a
|
|
218
|
+
* classified smoke test and — on a TTY, when it fails — offers to keep it, edit
|
|
219
|
+
* it, or delete-and-cancel. A test is never blocking on its own: a save is only
|
|
220
|
+
* discarded when the user explicitly chooses to, so a `--test` failure in a
|
|
221
|
+
* non-interactive shell warns and keeps rather than exiting non-zero.
|
|
222
|
+
*/
|
|
223
|
+
async function preSaveConnectionTest(name, testFlag) {
|
|
224
|
+
const interactive = isInteractiveTerminal();
|
|
225
|
+
const gate = connectionTestGate(testFlag, interactive);
|
|
226
|
+
let shouldTest;
|
|
227
|
+
if (gate === 'on')
|
|
228
|
+
shouldTest = true;
|
|
229
|
+
else if (gate === 'off')
|
|
230
|
+
shouldTest = false;
|
|
231
|
+
else {
|
|
232
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
233
|
+
shouldTest = await confirm({ message: `Test the connection for '${name}' now?`, default: true });
|
|
234
|
+
}
|
|
235
|
+
if (!shouldTest)
|
|
236
|
+
return;
|
|
237
|
+
console.log(chalk.gray(`Testing '${name}' — sending "${CONNECTION_TEST_PROMPT}" through agents run…`));
|
|
238
|
+
const result = await runConnectionTest({ mode: 'create', name }, harnessHooks());
|
|
239
|
+
if (!result)
|
|
240
|
+
return;
|
|
241
|
+
if (result.ok) {
|
|
242
|
+
console.log(chalk.green(`✓ ${result.message}`));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
console.log(chalk.yellow(`✗ ${result.message}`));
|
|
246
|
+
if (!interactive) {
|
|
247
|
+
console.log(chalk.gray(`Kept anyway. Fix and retest with: agents harness edit ${name}`));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const { select } = await import('@inquirer/prompts');
|
|
251
|
+
const action = await select({
|
|
252
|
+
message: 'The connection test failed. What would you like to do?',
|
|
253
|
+
choices: [
|
|
254
|
+
{ name: 'Keep it (the endpoint may just be down right now)', value: 'keep' },
|
|
255
|
+
{ name: 'Edit it now', value: 'edit' },
|
|
256
|
+
{ name: 'Delete it and cancel', value: 'delete' },
|
|
257
|
+
],
|
|
258
|
+
});
|
|
259
|
+
if (action === 'edit') {
|
|
260
|
+
await runEditWizard(name, {});
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (action === 'delete') {
|
|
264
|
+
deleteProfile(name);
|
|
265
|
+
throw new Error(`Harness '${name}' deleted after a failed connection test.`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
199
268
|
/**
|
|
200
269
|
* Shared build+persist flow for a fork — used by `agents harness fork`'s
|
|
201
270
|
* flag-driven path AND by the wizard (both for `fork` and, when it falls back
|
|
@@ -223,6 +292,7 @@ async function runForkFlow(source, name, opts) {
|
|
|
223
292
|
writeProfile(forked);
|
|
224
293
|
console.log(chalk.green(`Harness '${name}' forked from ${source}.`));
|
|
225
294
|
console.log(chalk.gray(`Try: agents run ${name} "hello"`));
|
|
295
|
+
await preSaveConnectionTest(name, opts.test);
|
|
226
296
|
}
|
|
227
297
|
/**
|
|
228
298
|
* Interactive `agents harness add`/`fork` wizard — runs when required info is
|
|
@@ -234,7 +304,7 @@ async function runForkFlow(source, name, opts) {
|
|
|
234
304
|
*/
|
|
235
305
|
async function runCreateWizard() {
|
|
236
306
|
const io = await defaultWizardIO();
|
|
237
|
-
const draft = await runWizardSteps(createSteps(), { mode: 'create' }, io);
|
|
307
|
+
const draft = await runWizardSteps(createSteps(), { mode: 'create' }, io, harnessHooks());
|
|
238
308
|
return {
|
|
239
309
|
source: draft.source,
|
|
240
310
|
name: draft.name,
|
|
@@ -292,7 +362,7 @@ async function runEditWizard(name, cliOpts) {
|
|
|
292
362
|
}
|
|
293
363
|
const original = readProfile(name);
|
|
294
364
|
const io = await defaultWizardIO();
|
|
295
|
-
const draft = await runWizardSteps(editSteps(original), { mode: 'edit', original, host: original.host.agent, name }, io);
|
|
365
|
+
const draft = await runWizardSteps(editSteps(original), { mode: 'edit', original, host: original.host.agent, name }, io, harnessHooks());
|
|
296
366
|
const opts = { ...draftToEditOptions(draft, original), keyStdin: cliOpts.keyStdin };
|
|
297
367
|
if (!hasEditFlags(opts)) {
|
|
298
368
|
console.log(chalk.gray(`No changes made to '${name}'.`));
|
|
@@ -308,6 +378,9 @@ async function runEditWizard(name, cliOpts) {
|
|
|
308
378
|
writeProfile(edited);
|
|
309
379
|
console.log(chalk.green(`Harness '${name}' updated.`));
|
|
310
380
|
console.log(chalk.gray(`Model: ${profileModelLabel(edited)}`));
|
|
381
|
+
// An edit that touched host/model/endpoint/auth is exactly what can break the
|
|
382
|
+
// harness, so re-test after saving (RUSH-2221). cliOpts carries the --test flag.
|
|
383
|
+
await preSaveConnectionTest(name, cliOpts.test);
|
|
311
384
|
}
|
|
312
385
|
export function registerHarnessCommands(program) {
|
|
313
386
|
const cmd = program
|
|
@@ -362,6 +435,8 @@ Examples:
|
|
|
362
435
|
.option('--from-secrets <bundle>[:<key>]', 'Removed: import the value with agents accounts add, then use --account')
|
|
363
436
|
.option('--key-stdin', 'Read API key from stdin instead of prompting (for scripts/CI)')
|
|
364
437
|
.option('--force', 'Overwrite an existing harness with the same name')
|
|
438
|
+
.option('--test', 'Run a connection test before saving (default: ask on a terminal)')
|
|
439
|
+
.option('--no-test', 'Skip the pre-save connection test')
|
|
365
440
|
.action(async (name, opts) => {
|
|
366
441
|
try {
|
|
367
442
|
if (opts.authProvider || opts.fromSecrets)
|
|
@@ -371,10 +446,11 @@ Examples:
|
|
|
371
446
|
throw new Error("'agents harness add' needs --preset or --host + --model (or a name and an interactive terminal for the wizard).");
|
|
372
447
|
}
|
|
373
448
|
const wiz = await runCreateWizard();
|
|
374
|
-
await runForkFlow(wiz.source, wiz.name, { ...wiz.opts, force: opts.force, keyStdin: opts.keyStdin });
|
|
449
|
+
await runForkFlow(wiz.source, wiz.name, { ...wiz.opts, force: opts.force, keyStdin: opts.keyStdin, test: opts.test });
|
|
375
450
|
return;
|
|
376
451
|
}
|
|
377
452
|
await addProfile(name, opts, 'Harness');
|
|
453
|
+
await preSaveConnectionTest(name, opts.test);
|
|
378
454
|
}
|
|
379
455
|
catch (err) {
|
|
380
456
|
console.error(chalk.red(err.message));
|
|
@@ -393,6 +469,8 @@ Examples:
|
|
|
393
469
|
.option('--from-secrets <bundle>[:<key>]', 'Removed: import the value with agents accounts add, then use --account')
|
|
394
470
|
.option('--key-stdin', 'Read the API key from stdin instead of prompting (for scripts/CI)')
|
|
395
471
|
.option('--force', 'Overwrite an existing harness with the same name')
|
|
472
|
+
.option('--test', 'Run a connection test before saving (default: ask on a terminal)')
|
|
473
|
+
.option('--no-test', 'Skip the pre-save connection test')
|
|
396
474
|
.addHelpText('after', `
|
|
397
475
|
Examples:
|
|
398
476
|
# Fork OpenCode into a harness pinned to a DeepSeek model on OpenRouter
|
|
@@ -417,7 +495,7 @@ Examples:
|
|
|
417
495
|
throw new Error("'agents harness fork' needs <source> and <name> (or an interactive terminal for the wizard).");
|
|
418
496
|
}
|
|
419
497
|
const wiz = await runCreateWizard();
|
|
420
|
-
await runForkFlow(wiz.source, wiz.name, { ...wiz.opts, force: opts.force, keyStdin: opts.keyStdin });
|
|
498
|
+
await runForkFlow(wiz.source, wiz.name, { ...wiz.opts, force: opts.force, keyStdin: opts.keyStdin, test: opts.test });
|
|
421
499
|
return;
|
|
422
500
|
}
|
|
423
501
|
await runForkFlow(source, name, opts);
|
|
@@ -439,6 +517,8 @@ Examples:
|
|
|
439
517
|
.option('--fallback-model <id>', 'Secondary model retried on the same host on a rate limit (pass an empty string to clear it)')
|
|
440
518
|
.option('--from-secrets <bundle>[:<key>]', 'Removed: import the value with agents accounts add, then use --account')
|
|
441
519
|
.option('--key-stdin', 'Read the API key from stdin instead of prompting (for scripts/CI)')
|
|
520
|
+
.option('--test', 'Run a connection test after saving (default: ask on a terminal)')
|
|
521
|
+
.option('--no-test', 'Skip the connection test')
|
|
442
522
|
.addHelpText('after', `
|
|
443
523
|
Examples:
|
|
444
524
|
# Swap the pinned model
|
|
@@ -479,6 +559,7 @@ Examples:
|
|
|
479
559
|
writeProfile(edited);
|
|
480
560
|
console.log(chalk.green(`Harness '${name}' updated.`));
|
|
481
561
|
console.log(chalk.gray(`Model: ${profileModelLabel(edited)}`));
|
|
562
|
+
await preSaveConnectionTest(name, opts.test);
|
|
482
563
|
}
|
|
483
564
|
catch (err) {
|
|
484
565
|
console.error(chalk.red(err.message));
|
package/dist/commands/perf.js
CHANGED
|
@@ -132,6 +132,16 @@ function renderLabelTable(title, rows, warnMs, limit) {
|
|
|
132
132
|
formatMs(r.maxMs),
|
|
133
133
|
formatRateColumn(r),
|
|
134
134
|
]), sliced.map((r) => r.p99Ms > warnMs));
|
|
135
|
+
// Sub-phase break-out (agent.run's `startup` = spawn overhead before the child
|
|
136
|
+
// runs, PHNX-3468). Indented under the row so a slow boot is visible next to
|
|
137
|
+
// the total without adding columns that are empty for phaseless labels.
|
|
138
|
+
for (const r of sliced) {
|
|
139
|
+
if (!r.phases)
|
|
140
|
+
continue;
|
|
141
|
+
for (const [name, ph] of Object.entries(r.phases)) {
|
|
142
|
+
console.log(chalk.gray(` └ ${name}: p50 ${formatMs(ph.p50Ms)} p90 ${formatMs(ph.p90Ms)} (n=${ph.n})`));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
135
145
|
}
|
|
136
146
|
/**
|
|
137
147
|
* Prefer SQLite samples; fall back to the legacy daily JSONL so existing
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents reminders` — list your personal operating reminders.
|
|
3
|
+
*
|
|
4
|
+
* The reminders live in `~/.agents/reminders/reminders.yaml` and are surfaced
|
|
5
|
+
* succinctly in the Claude statusline — one per session, chosen from the session
|
|
6
|
+
* id. This command shows the full set (and the file to edit).
|
|
7
|
+
*/
|
|
8
|
+
import type { Command } from 'commander';
|
|
9
|
+
export declare function registerRemindersCommand(program: Command): void;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { loadReminders, remindersFilePath } from '../lib/reminders.js';
|
|
3
|
+
import { setHelpSections } from '../lib/help.js';
|
|
4
|
+
const EXAMPLES = [
|
|
5
|
+
'agents reminders # list your operating reminders',
|
|
6
|
+
'agents reminders --json # machine-readable output',
|
|
7
|
+
].join('\n');
|
|
8
|
+
const NOTES = [
|
|
9
|
+
"Reminders live in ~/.agents/reminders/reminders.yaml and sync across the fleet via 'agents repo push'.",
|
|
10
|
+
'One shows succinctly in the Claude statusline per session, chosen deterministically from the session id,',
|
|
11
|
+
'so concurrent agents each show a different one and it stays stable within a session.',
|
|
12
|
+
].join('\n');
|
|
13
|
+
export function registerRemindersCommand(program) {
|
|
14
|
+
const cmd = program
|
|
15
|
+
.command('reminders')
|
|
16
|
+
.description('Personal operating reminders shown in the Claude statusline')
|
|
17
|
+
.option('--json', 'output as JSON')
|
|
18
|
+
.action((opts) => {
|
|
19
|
+
const file = remindersFilePath();
|
|
20
|
+
let reminders;
|
|
21
|
+
try {
|
|
22
|
+
reminders = loadReminders();
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
console.error(chalk.red(`Could not read reminders: ${err instanceof Error ? err.message : String(err)}`));
|
|
26
|
+
process.exitCode = 1;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (opts.json) {
|
|
30
|
+
process.stdout.write(`${JSON.stringify({ file, reminders }, null, 2)}\n`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (reminders.length === 0) {
|
|
34
|
+
console.log(`No reminders yet. Add them to ${chalk.cyan(file)}`);
|
|
35
|
+
console.log(chalk.dim('Each shows succinctly in the Claude statusline — one per session.'));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const count = `${reminders.length} reminder${reminders.length === 1 ? '' : 's'}`;
|
|
39
|
+
console.log(chalk.dim(`${count} · ${file}`));
|
|
40
|
+
console.log(chalk.dim('One shows in the Claude statusline per session, chosen from the session id.\n'));
|
|
41
|
+
for (const reminder of reminders) {
|
|
42
|
+
console.log(` ${chalk.cyan('◆')} ${chalk.bold(reminder.short)}`);
|
|
43
|
+
if (reminder.full && reminder.full !== reminder.short) {
|
|
44
|
+
console.log(` ${chalk.dim(reminder.full)}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
setHelpSections(cmd, { examples: EXAMPLES, notes: NOTES });
|
|
49
|
+
}
|
|
@@ -52,6 +52,20 @@ export declare function signInLaunchDecision(input: {
|
|
|
52
52
|
tty: boolean;
|
|
53
53
|
json: boolean;
|
|
54
54
|
}): 'launch' | 'fail-loud';
|
|
55
|
+
/**
|
|
56
|
+
* How a `balanced`/`available` run reacts when every account's usage is stale and
|
|
57
|
+
* none is verified (PHNX-2526). A human present at a real terminal gets the
|
|
58
|
+
* account `picker` — they can choose knowing the numbers are stale — while every
|
|
59
|
+
* unattended shape (`--headless`, `--json`, or no TTY) `fail-loud`s with
|
|
60
|
+
* NO_VERIFIED_USAGE rather than silently guess on a stale snapshot. `headless`
|
|
61
|
+
* joins the gate because a routine/machine dispatch can carry a TTY yet have no
|
|
62
|
+
* human to answer a picker; the split mirrors `signInLaunchDecision`.
|
|
63
|
+
*/
|
|
64
|
+
export declare function noVerifiedUsageDecision(input: {
|
|
65
|
+
tty: boolean;
|
|
66
|
+
json: boolean;
|
|
67
|
+
headless: boolean;
|
|
68
|
+
}): 'picker' | 'fail-loud';
|
|
55
69
|
/**
|
|
56
70
|
* Choose which installed version to launch so the user can authenticate, when a
|
|
57
71
|
* strategy found zero healthy accounts but at least one is merely signed out
|
|
@@ -221,6 +221,19 @@ export function signInLaunchDecision(input) {
|
|
|
221
221
|
const humanPresent = input.tty && !input.json;
|
|
222
222
|
return input.recoverable > 0 && humanPresent ? 'launch' : 'fail-loud';
|
|
223
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* How a `balanced`/`available` run reacts when every account's usage is stale and
|
|
226
|
+
* none is verified (PHNX-2526). A human present at a real terminal gets the
|
|
227
|
+
* account `picker` — they can choose knowing the numbers are stale — while every
|
|
228
|
+
* unattended shape (`--headless`, `--json`, or no TTY) `fail-loud`s with
|
|
229
|
+
* NO_VERIFIED_USAGE rather than silently guess on a stale snapshot. `headless`
|
|
230
|
+
* joins the gate because a routine/machine dispatch can carry a TTY yet have no
|
|
231
|
+
* human to answer a picker; the split mirrors `signInLaunchDecision`.
|
|
232
|
+
*/
|
|
233
|
+
export function noVerifiedUsageDecision(input) {
|
|
234
|
+
const humanPresent = input.tty && !input.json && !input.headless;
|
|
235
|
+
return humanPresent ? 'picker' : 'fail-loud';
|
|
236
|
+
}
|
|
224
237
|
/**
|
|
225
238
|
* Choose which installed version to launch so the user can authenticate, when a
|
|
226
239
|
* strategy found zero healthy accounts but at least one is merely signed out
|
|
@@ -3,6 +3,19 @@ import { fetchPeerPreviewDigest } from '../lib/session/remote-list.js';
|
|
|
3
3
|
import { classifyFileChanges, changeCounts, toolHistogram, detectTestResult } from '../lib/session/digest.js';
|
|
4
4
|
import type { FileChange } from '../lib/session/digest.js';
|
|
5
5
|
import { extractArtifacts, extractHooks, extractLinks, extractSkills } from '../lib/session/highlights.js';
|
|
6
|
+
/** A session whose transcript FILE is on another machine: its `filePath` is on
|
|
7
|
+
* that peer's disk, so the preview can't parse it locally — it fetches the
|
|
8
|
+
* peer's digest over SSH and shows metadata + a "resume there" note until that
|
|
9
|
+
* lands. `_remote` is an explicit signal; a peer `machine` plus no readable
|
|
10
|
+
* local file covers host-dispatch index rows and live-registry rows that were
|
|
11
|
+
* synthesized outside the fan-out. A locally-readable synced mirror stays
|
|
12
|
+
* local even when its recorded owner is another machine.
|
|
13
|
+
*
|
|
14
|
+
* This is the READ rule and only the read rule. Whether a session may be
|
|
15
|
+
* RESUMED here is a different question with a different answer — a mirror is
|
|
16
|
+
* readable but not resumable — and `sessionOwnerDevice`
|
|
17
|
+
* (lib/session/resume-owner.ts) is the single place that answers it (RUSH-2022). */
|
|
18
|
+
export declare function transcriptOnPeerOf(session: SessionMeta): string | undefined;
|
|
6
19
|
/**
|
|
7
20
|
* Compact checklist tally for list rows and previews (RUSH-2045).
|
|
8
21
|
* Example: `✓6/8 · A5 wiring runner`. Empty string when there is no list.
|
|
@@ -25,19 +25,28 @@ import { createMemoryCache } from '../lib/memory-cache.js';
|
|
|
25
25
|
import { classifyFileChanges, changeCounts, toolHistogram, detectTestResult } from '../lib/session/digest.js';
|
|
26
26
|
import { extractArtifacts, extractHooks, extractLinks, extractRepos, extractSkills, harnessTracksBackgroundShells, isBackgroundShellStart, isSubAgentTool, } from '../lib/session/highlights.js';
|
|
27
27
|
import { getSessionPlugins, readSessionPreviewCache, writeSessionPreviewCache, readSessionContent, readArchivedSessionPreview } from '../lib/session/db.js';
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
* preview can't parse it locally — it fetches the
|
|
31
|
-
* shows metadata + a "resume there" note until that
|
|
32
|
-
*
|
|
33
|
-
* file
|
|
28
|
+
import { machineId } from '../lib/session/sync/config.js';
|
|
29
|
+
/** A session whose transcript FILE is on another machine: its `filePath` is on
|
|
30
|
+
* that peer's disk, so the preview can't parse it locally — it fetches the
|
|
31
|
+
* peer's digest over SSH and shows metadata + a "resume there" note until that
|
|
32
|
+
* lands. `_remote` is an explicit signal; a peer `machine` plus no readable
|
|
33
|
+
* local file covers host-dispatch index rows and live-registry rows that were
|
|
34
|
+
* synthesized outside the fan-out. A locally-readable synced mirror stays
|
|
35
|
+
* local even when its recorded owner is another machine.
|
|
34
36
|
*
|
|
35
37
|
* This is the READ rule and only the read rule. Whether a session may be
|
|
36
38
|
* RESUMED here is a different question with a different answer — a mirror is
|
|
37
39
|
* readable but not resumable — and `sessionOwnerDevice`
|
|
38
40
|
* (lib/session/resume-owner.ts) is the single place that answers it (RUSH-2022). */
|
|
39
|
-
function transcriptOnPeerOf(session) {
|
|
40
|
-
|
|
41
|
+
export function transcriptOnPeerOf(session) {
|
|
42
|
+
if (session._remote)
|
|
43
|
+
return session.machine;
|
|
44
|
+
if (session.machine
|
|
45
|
+
&& session.machine !== machineId()
|
|
46
|
+
&& (!session.filePath || !fs.existsSync(session.filePath))) {
|
|
47
|
+
return session.machine;
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
41
50
|
}
|
|
42
51
|
/**
|
|
43
52
|
* Compact checklist tally for list rows and previews (RUSH-2045).
|
|
@@ -55,7 +55,7 @@ import { listInstalledVersions, resolveVersionAliasLoose } from '../lib/installa
|
|
|
55
55
|
import { getAgentsInvocation } from '../lib/daemon/daemon.js';
|
|
56
56
|
import { sessionRecoveryRunArgs } from '../lib/session/recovery.js';
|
|
57
57
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
58
|
-
import { sessionPicker, buildPreview, loadSessionPreviewDigest, formatTodoCompact, githubRepoUrlFromCwd, } from './sessions-picker.js';
|
|
58
|
+
import { sessionPicker, buildPreview, loadSessionPreviewDigest, transcriptOnPeerOf, formatTodoCompact, githubRepoUrlFromCwd, } from './sessions-picker.js';
|
|
59
59
|
import { setHelpSections } from '../lib/help.js';
|
|
60
60
|
import { registerSessionsTailCommand } from './sessions-tail.js';
|
|
61
61
|
import { registerSessionsResumeCommand } from './sessions-resume.js';
|
|
@@ -2017,13 +2017,14 @@ export async function renderSessionPreview(query, scope) {
|
|
|
2017
2017
|
return;
|
|
2018
2018
|
}
|
|
2019
2019
|
const session = outcome.session;
|
|
2020
|
-
|
|
2020
|
+
const transcriptPeer = transcriptOnPeerOf(session);
|
|
2021
|
+
if (transcriptPeer) {
|
|
2021
2022
|
const args = ['sessions', 'preview', session.id, '--local'];
|
|
2022
2023
|
if (scope.json)
|
|
2023
2024
|
args.push('--json');
|
|
2024
|
-
const rendered = await runOnPeer(args,
|
|
2025
|
+
const rendered = await runOnPeer(args, transcriptPeer);
|
|
2025
2026
|
if (rendered === 'no-target') {
|
|
2026
|
-
console.error(chalk.red(`Session ${session.id} is on ${
|
|
2027
|
+
console.error(chalk.red(`Session ${session.id} is on ${transcriptPeer}, but that device is not reachable.`));
|
|
2027
2028
|
process.exitCode = 1;
|
|
2028
2029
|
}
|
|
2029
2030
|
return;
|
|
@@ -3873,11 +3874,11 @@ export async function handlePickedSession(picked) {
|
|
|
3873
3874
|
}
|
|
3874
3875
|
// Reading and resuming are on DIFFERENT machines' terms, and conflating them
|
|
3875
3876
|
// is what RUSH-2022 was. Reading follows the FILE: a synced mirror sits on
|
|
3876
|
-
// this disk, so
|
|
3877
|
-
// peer. Resuming follows the HARNESS STATE, which is on the
|
|
3878
|
-
// whatever the transcript's location — a mirror included.
|
|
3877
|
+
// this disk, so a peer-owned row with no readable local transcript has to be
|
|
3878
|
+
// read on the peer. Resuming follows the HARNESS STATE, which is on the
|
|
3879
|
+
// owning machine whatever the transcript's location — a mirror included.
|
|
3879
3880
|
if (picked.action === 'view') {
|
|
3880
|
-
const readFrom = picked.session
|
|
3881
|
+
const readFrom = transcriptOnPeerOf(picked.session);
|
|
3881
3882
|
if (readFrom) {
|
|
3882
3883
|
const rc = await runOnPeer(['sessions', picked.session.shortId, '--markdown'], readFrom);
|
|
3883
3884
|
if (rc === 'no-target')
|
|
@@ -4503,7 +4504,8 @@ async function renderOneSession(query, mode, scope) {
|
|
|
4503
4504
|
}
|
|
4504
4505
|
if (outcome.kind === 'resolved') {
|
|
4505
4506
|
const resolved = outcome.session;
|
|
4506
|
-
|
|
4507
|
+
const transcriptPeer = transcriptOnPeerOf(resolved);
|
|
4508
|
+
if (transcriptPeer) {
|
|
4507
4509
|
const args = ['sessions', resolved.id, '--local'];
|
|
4508
4510
|
const flag = modeFlag(mode);
|
|
4509
4511
|
if (flag)
|
|
@@ -4518,9 +4520,9 @@ async function renderOneSession(query, mode, scope) {
|
|
|
4518
4520
|
args.push('--last', String(scope.filter.last));
|
|
4519
4521
|
if (scope.redact === false)
|
|
4520
4522
|
args.push('--no-redact');
|
|
4521
|
-
const rendered = await runOnPeer(args,
|
|
4523
|
+
const rendered = await runOnPeer(args, transcriptPeer);
|
|
4522
4524
|
if (rendered === 'no-target') {
|
|
4523
|
-
console.error(chalk.red(`Session ${resolved.id} is on ${
|
|
4525
|
+
console.error(chalk.red(`Session ${resolved.id} is on ${transcriptPeer}, but that device is not reachable.`));
|
|
4524
4526
|
process.exit(1);
|
|
4525
4527
|
}
|
|
4526
4528
|
return;
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import chalk from 'chalk';
|
|
10
10
|
import { relTime, truncate, humanDuration } from '../lib/format.js';
|
|
11
11
|
import { itemPicker } from '../lib/picker.js';
|
|
12
|
+
import { deliveryDisplayLabel, deliveryColorKey } from '../lib/teams/delivery.js';
|
|
12
13
|
const AGENT_LABEL = {
|
|
13
14
|
claude: 'Claude',
|
|
14
15
|
codex: 'Codex',
|
|
@@ -21,6 +22,7 @@ function statusColor(status) {
|
|
|
21
22
|
case 'pending': return chalk.blue;
|
|
22
23
|
case 'running': return chalk.yellow;
|
|
23
24
|
case 'completed': return chalk.green;
|
|
25
|
+
case 'stranded': return chalk.yellow;
|
|
24
26
|
case 'failed': return chalk.red;
|
|
25
27
|
case 'stopped': return chalk.gray;
|
|
26
28
|
default: return chalk.white;
|
|
@@ -61,10 +63,12 @@ function displayAgent(agent, version) {
|
|
|
61
63
|
function statusCell(t) {
|
|
62
64
|
if (t.agent_count === 0)
|
|
63
65
|
return chalk.gray('empty');
|
|
66
|
+
const done = Math.max(0, t.completed - (t.stranded ?? 0));
|
|
64
67
|
const states = [
|
|
65
68
|
[t.pending, chalk.blue, 'pending'],
|
|
66
69
|
[t.running, chalk.yellow, 'working'],
|
|
67
|
-
[
|
|
70
|
+
[done, chalk.green, 'done'],
|
|
71
|
+
[t.stranded ?? 0, chalk.yellow, 'stranded'],
|
|
68
72
|
[t.failed, chalk.red, 'failed'],
|
|
69
73
|
[t.stopped, chalk.gray, 'stopped'],
|
|
70
74
|
];
|
|
@@ -76,13 +80,16 @@ function statusCell(t) {
|
|
|
76
80
|
return nonzero.map(([n, color, label]) => color(`${n} ${label}`)).join(chalk.gray(' · '));
|
|
77
81
|
}
|
|
78
82
|
function statusSummaryParts(t) {
|
|
83
|
+
const done = Math.max(0, t.completed - (t.stranded ?? 0));
|
|
79
84
|
const parts = [];
|
|
80
85
|
if (t.pending)
|
|
81
86
|
parts.push(chalk.blue(`${t.pending} pending`));
|
|
82
87
|
if (t.running)
|
|
83
88
|
parts.push(chalk.yellow(`${t.running} working`));
|
|
84
|
-
if (
|
|
85
|
-
parts.push(chalk.green(`${
|
|
89
|
+
if (done)
|
|
90
|
+
parts.push(chalk.green(`${done} done`));
|
|
91
|
+
if (t.stranded)
|
|
92
|
+
parts.push(chalk.yellow(`${t.stranded} stranded`));
|
|
86
93
|
if (t.failed)
|
|
87
94
|
parts.push(chalk.red(`${t.failed} failed`));
|
|
88
95
|
if (t.stopped)
|
|
@@ -173,7 +180,12 @@ export function buildTeamPreview(row) {
|
|
|
173
180
|
const nameW = Math.max(6, ...row.agents.map((a) => displayHandle(a).length));
|
|
174
181
|
const agentW = Math.max(8, ...row.agents.map((a) => displayAgent(a.agent_type, a.version).length));
|
|
175
182
|
for (const a of row.agents) {
|
|
176
|
-
const
|
|
183
|
+
const delivery = a.delivery;
|
|
184
|
+
const colorKey = delivery ? deliveryColorKey(delivery, a.status) : a.status;
|
|
185
|
+
const label = delivery
|
|
186
|
+
? deliveryDisplayLabel(delivery, a.status)
|
|
187
|
+
: a.status.toUpperCase();
|
|
188
|
+
const stat = statusColor(colorKey)(label);
|
|
177
189
|
const metaParts = [];
|
|
178
190
|
if (a.task_type)
|
|
179
191
|
metaParts.push(chalk.magenta(a.task_type));
|
|
@@ -244,8 +256,8 @@ function searchHaystack(row) {
|
|
|
244
256
|
parts.push(a.status);
|
|
245
257
|
}
|
|
246
258
|
// Also include team-level status words exposed in the row ("done",
|
|
247
|
-
// "working", "pending", "failed", "stopped", "empty") so the
|
|
248
|
-
// what the user sees on screen.
|
|
259
|
+
// "working", "pending", "stranded", "failed", "stopped", "empty") so the
|
|
260
|
+
// search matches what the user sees on screen.
|
|
249
261
|
const t = row.team;
|
|
250
262
|
if (t.agent_count === 0)
|
|
251
263
|
parts.push('empty');
|
|
@@ -255,6 +267,8 @@ function searchHaystack(row) {
|
|
|
255
267
|
parts.push('working', 'running');
|
|
256
268
|
if (t.completed)
|
|
257
269
|
parts.push('done', 'completed');
|
|
270
|
+
if (t.stranded)
|
|
271
|
+
parts.push('stranded');
|
|
258
272
|
if (t.failed)
|
|
259
273
|
parts.push('failed');
|
|
260
274
|
if (t.stopped)
|
package/dist/commands/teams.d.ts
CHANGED
|
@@ -108,7 +108,7 @@ export declare function staleRepoError(params: {
|
|
|
108
108
|
* the teammate itself so we don't need the original --cloud CLI args.
|
|
109
109
|
*/
|
|
110
110
|
export declare function wireCloudDispatcher(mgr: AgentManager): void;
|
|
111
|
-
export declare function cloudDispatchOptions(agent: Pick<AgentProcess, 'prompt' | 'agentType' | 'cloudRepo' | 'cloudBranch' | 'model'>): DispatchOptions;
|
|
111
|
+
export declare function cloudDispatchOptions(agent: Pick<AgentProcess, 'prompt' | 'agentType' | 'cloudRepo' | 'cloudBranch' | 'model' | 'mode'>): DispatchOptions;
|
|
112
112
|
/**
|
|
113
113
|
* Single-wave start used by `teams start` without --watch. startReady()
|
|
114
114
|
* terminalizes a placement/spawn/cloud/dependency failure to FAILED with
|
|
@@ -120,11 +120,11 @@ export declare function cloudDispatchOptions(agent: Pick<AgentProcess, 'prompt'
|
|
|
120
120
|
export declare function runOneWave(mgr: AgentManager, team: string, json: boolean): Promise<void>;
|
|
121
121
|
export declare function buildTeamRowsFromSnapshots(registry: TeamRegistry, agents: TeamListAgentSnapshot[],
|
|
122
122
|
/** team name -> the session that spawned it (see teamSpawners). */
|
|
123
|
-
spawners?: Map<string, TeamSpawner>): {
|
|
123
|
+
spawners?: Map<string, TeamSpawner>): Promise<{
|
|
124
124
|
rows: TeamRow[];
|
|
125
125
|
teams: TaskInfo[];
|
|
126
126
|
names: string[];
|
|
127
|
-
}
|
|
127
|
+
}>;
|
|
128
128
|
/**
|
|
129
129
|
* `teams add --remote-cwd` is a no-op trap. `--remote-cwd` comes from the shared
|
|
130
130
|
* `--device` option family (option.ts) and is meaningful for commands that *route*
|