@phnx-labs/agents-cli 1.22.57 → 1.22.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +294 -0
- package/README.md +29 -0
- package/dist/bootstrap.js +39 -1
- package/dist/commands/accounts.js +7 -3
- package/dist/commands/apply.js +10 -2
- package/dist/commands/fork.d.ts +23 -10
- package/dist/commands/fork.js +115 -58
- package/dist/commands/monitors.js +198 -23
- package/dist/commands/prune.js +5 -3
- package/dist/commands/routines.d.ts +8 -0
- package/dist/commands/routines.js +57 -3
- package/dist/commands/routines.test-fixture.js +5 -0
- package/dist/commands/send.d.ts +2 -1
- package/dist/commands/send.js +7 -5
- package/dist/commands/sessions-picker.d.ts +11 -0
- package/dist/commands/sessions-picker.js +16 -0
- package/dist/commands/sessions-stats.js +37 -5
- package/dist/commands/sessions.js +40 -5
- package/dist/commands/share.d.ts +14 -0
- package/dist/commands/share.js +43 -2
- package/dist/commands/ssh.js +12 -1
- package/dist/commands/status.js +1 -1
- package/dist/commands/sync.js +83 -7
- package/dist/commands/traces.js +7 -0
- package/dist/commands/versions.js +12 -4
- package/dist/commands/view.js +7 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +6 -1
- package/dist/lib/account-registry.d.ts +5 -1
- package/dist/lib/account-registry.js +47 -14
- package/dist/lib/accounting/capacity.d.ts +18 -7
- package/dist/lib/accounting/capacity.js +19 -8
- package/dist/lib/accounting/usage-sync.d.ts +29 -1
- package/dist/lib/accounting/usage-sync.js +76 -2
- package/dist/lib/accounting/usage.js +7 -1
- package/dist/lib/auth-mint.d.ts +11 -1
- package/dist/lib/auth-mint.js +21 -6
- package/dist/lib/auto-pull-worker.js +7 -2
- package/dist/lib/browser/ipc.d.ts +8 -0
- package/dist/lib/browser/ipc.js +87 -0
- package/dist/lib/browser/service.d.ts +19 -0
- package/dist/lib/browser/service.js +96 -11
- package/dist/lib/browser/sessions-list.js +10 -1
- package/dist/lib/cloud/rush.d.ts +7 -0
- package/dist/lib/cloud/rush.js +29 -1
- package/dist/lib/daemon/daemon.d.ts +22 -0
- package/dist/lib/daemon/daemon.js +39 -0
- package/dist/lib/daemon/runner.d.ts +3 -0
- package/dist/lib/daemon/runner.js +86 -45
- package/dist/lib/daemon/session-index-service.js +9 -1
- package/dist/lib/daemon/usage-sync-service.d.ts +3 -3
- package/dist/lib/daemon/usage-sync-service.js +14 -8
- package/dist/lib/daemon-services.js +1 -1
- package/dist/lib/daemon-ticks.d.ts +15 -0
- package/dist/lib/daemon-ticks.js +26 -0
- package/dist/lib/device-config.d.ts +5 -1
- package/dist/lib/device-config.js +2 -2
- package/dist/lib/devices/connect.d.ts +17 -8
- package/dist/lib/devices/connect.js +31 -14
- package/dist/lib/devices/health.js +5 -1
- package/dist/lib/devices/pool.d.ts +25 -2
- package/dist/lib/devices/pool.js +32 -2
- package/dist/lib/devices/stats-cache.d.ts +0 -6
- package/dist/lib/devices/stats-cache.js +2 -9
- package/dist/lib/doctor-diff.d.ts +14 -0
- package/dist/lib/doctor-diff.js +120 -9
- package/dist/lib/fleet/manifest.d.ts +17 -0
- package/dist/lib/fleet/manifest.js +26 -0
- package/dist/lib/git.d.ts +38 -0
- package/dist/lib/git.js +58 -0
- package/dist/lib/hooks/install.d.ts +27 -11
- package/dist/lib/hooks/install.js +42 -17
- package/dist/lib/hosts/ready.d.ts +8 -0
- package/dist/lib/hosts/ready.js +13 -2
- package/dist/lib/hosts/reconnect.d.ts +52 -203
- package/dist/lib/hosts/reconnect.js +64 -284
- package/dist/lib/installations/migrate.d.ts +6 -120
- package/dist/lib/installations/migrate.js +27 -259
- package/dist/lib/installations/shims.d.ts +13 -95
- package/dist/lib/installations/shims.js +22 -139
- package/dist/lib/installations/store.js +1 -1
- package/dist/lib/installations/versions.d.ts +43 -133
- package/dist/lib/installations/versions.js +94 -206
- package/dist/lib/monitors/config.d.ts +71 -3
- package/dist/lib/monitors/config.js +100 -12
- package/dist/lib/monitors/pid-watch.d.ts +35 -0
- package/dist/lib/monitors/pid-watch.js +45 -0
- package/dist/lib/monitors/remote.d.ts +18 -0
- package/dist/lib/monitors/remote.js +11 -0
- package/dist/lib/permissions.js +7 -2
- package/dist/lib/plugins/plugins.d.ts +17 -3
- package/dist/lib/plugins/plugins.js +84 -9
- package/dist/lib/plugins/skills.d.ts +8 -1
- package/dist/lib/plugins/skills.js +18 -2
- package/dist/lib/pty-server.d.ts +14 -0
- package/dist/lib/pty-server.js +49 -5
- package/dist/lib/refresh.d.ts +9 -0
- package/dist/lib/refresh.js +3 -1
- package/dist/lib/routine-readiness.d.ts +15 -1
- package/dist/lib/routine-readiness.js +41 -0
- package/dist/lib/sandbox.d.ts +4 -1
- package/dist/lib/sandbox.js +30 -1
- package/dist/lib/secrets/agent.d.ts +80 -225
- package/dist/lib/secrets/agent.js +139 -401
- package/dist/lib/secrets/bundles.d.ts +73 -222
- package/dist/lib/secrets/bundles.js +168 -467
- package/dist/lib/secrets/drivers/rush.js +5 -0
- package/dist/lib/secrets/reaper.d.ts +28 -70
- package/dist/lib/secrets/reaper.js +30 -85
- package/dist/lib/secrets/remote.d.ts +42 -129
- package/dist/lib/secrets/remote.js +55 -173
- package/dist/lib/self-heal/checks/install-staging.d.ts +4 -0
- package/dist/lib/self-heal/checks/install-staging.js +96 -0
- package/dist/lib/self-heal/registry.js +2 -0
- package/dist/lib/self-heal/types.d.ts +1 -1
- package/dist/lib/self-update.d.ts +65 -0
- package/dist/lib/self-update.js +138 -0
- package/dist/lib/session/active.d.ts +13 -1
- package/dist/lib/session/active.js +2 -0
- package/dist/lib/session/cloud.js +5 -0
- package/dist/lib/session/db.d.ts +51 -6
- package/dist/lib/session/db.js +266 -20
- package/dist/lib/session/fork.d.ts +45 -26
- package/dist/lib/session/fork.js +32 -95
- package/dist/lib/session/tool-calls.d.ts +43 -1
- package/dist/lib/session/tool-calls.js +74 -44
- package/dist/lib/session/tool-store.d.ts +33 -2
- package/dist/lib/session/tool-store.js +56 -3
- package/dist/lib/smart-launch.d.ts +6 -0
- package/dist/lib/smart-launch.js +5 -2
- package/dist/lib/staleness/writers/plugins.js +5 -2
- package/dist/lib/staleness/writers/sources.d.ts +5 -0
- package/dist/lib/staleness/writers/sources.js +2 -1
- package/dist/lib/staleness/writers/subagents.js +13 -3
- package/dist/lib/state.d.ts +7 -4
- package/dist/lib/state.js +7 -4
- package/dist/lib/subagents.js +8 -2
- package/dist/lib/sync-status.d.ts +22 -0
- package/dist/lib/sync-status.js +27 -0
- package/dist/lib/sync-umbrella.d.ts +9 -0
- package/dist/lib/sync-umbrella.js +21 -2
- package/dist/lib/teams/scheduler.d.ts +10 -0
- package/dist/lib/teams/scheduler.js +8 -0
- package/dist/lib/traces/insights.d.ts +47 -14
- package/dist/lib/traces/insights.js +92 -21
- package/dist/lib/traces/phenotype.d.ts +23 -3
- package/dist/lib/traces/phenotype.js +72 -24
- package/dist/lib/traces/sync.d.ts +128 -6
- package/dist/lib/traces/sync.js +294 -35
- package/dist/lib/traces/worker-template.js +154 -1
- package/dist/lib/view-types.d.ts +12 -0
- package/package.json +2 -2
|
@@ -245,12 +245,17 @@ function renderRoutineRows({ jobs, scheduler, overdueSet, link, now, local = tru
|
|
|
245
245
|
: lastStatus === 'missed' ? chalk.magenta
|
|
246
246
|
: chalk.gray;
|
|
247
247
|
const overdueTag = overdueSet.has(job.name) ? chalk.yellow(' (overdue)') : '';
|
|
248
|
+
// Append the concrete reason a routine did not complete (auth_failed, wedged,
|
|
249
|
+
// blocked, missed) right in the Last Status cell, so the list answers "why"
|
|
250
|
+
// without a drill-in. Only for non-completed local runs; peer rows stay blank.
|
|
251
|
+
const reason = latestRun ? runFailureReason(latestRun) : null;
|
|
252
|
+
const reasonTag = reason ? chalk.gray(` — ${reason}`) : '';
|
|
248
253
|
const agentLabelPadded = job.command
|
|
249
254
|
? chalk.magenta('command'.padEnd(10))
|
|
250
255
|
: job.workflow
|
|
251
256
|
? chalk.magenta(`wf:${job.workflow}`.padEnd(10))
|
|
252
257
|
: (job.agent || '').padEnd(10);
|
|
253
|
-
console.log(` ${chalk.cyan(job.name.padEnd(NAME_W))} ${agentLabelPadded} ${repoCell}${' '.repeat(repoPadding)} ${deviceCell}${' '.repeat(devicePad)} ${schedStr.padEnd(SCHED_W)} ${enabledStr}${' '.repeat(enabledPad)} ${chalk.gray(nextStr.padEnd(NEXT_W))} ${statusColor(lastStatus)}${overdueTag}`);
|
|
258
|
+
console.log(` ${chalk.cyan(job.name.padEnd(NAME_W))} ${agentLabelPadded} ${repoCell}${' '.repeat(repoPadding)} ${deviceCell}${' '.repeat(devicePad)} ${schedStr.padEnd(SCHED_W)} ${enabledStr}${' '.repeat(enabledPad)} ${chalk.gray(nextStr.padEnd(NEXT_W))} ${statusColor(lastStatus)}${overdueTag}${reasonTag}`);
|
|
254
259
|
}
|
|
255
260
|
}
|
|
256
261
|
function parseRoutineTrigger(options) {
|
|
@@ -622,6 +627,43 @@ function routineMatchesQuery(job, q) {
|
|
|
622
627
|
.toLowerCase()
|
|
623
628
|
.includes(q);
|
|
624
629
|
}
|
|
630
|
+
/** Friendly one-liners for the claim a `skipped` run lost. */
|
|
631
|
+
const SKIP_REASON_LABEL = {
|
|
632
|
+
active_run: 'wedged: a prior run is still active',
|
|
633
|
+
duplicate_slot: 'duplicate slot (already fired)',
|
|
634
|
+
wrong_owner: 'pinned to another device',
|
|
635
|
+
};
|
|
636
|
+
/**
|
|
637
|
+
* The short, human reason a run did not simply complete — for inline display in
|
|
638
|
+
* the list/detail so "why did it fail" needs no dig into the run dir. Prefers the
|
|
639
|
+
* concrete `errorMessage` (which carries `auth_failed: …`, OAuth-revoked, timeouts),
|
|
640
|
+
* then the readiness block for a `blocked` run, then the mapped skip reason. Returns
|
|
641
|
+
* null for a healthy (`completed`/`running`) run, which needs no annotation.
|
|
642
|
+
*/
|
|
643
|
+
export function runFailureReason(run) {
|
|
644
|
+
if (run.status === 'completed' || run.status === 'running')
|
|
645
|
+
return null;
|
|
646
|
+
const compact = (s) => {
|
|
647
|
+
const one = s.replace(/\s+/g, ' ').trim();
|
|
648
|
+
return one.length > 80 ? one.slice(0, 79) + '…' : one;
|
|
649
|
+
};
|
|
650
|
+
if (run.errorMessage)
|
|
651
|
+
return compact(run.errorMessage);
|
|
652
|
+
if (run.status === 'blocked' && run.readiness) {
|
|
653
|
+
return compact(run.readiness.message || run.readiness.code);
|
|
654
|
+
}
|
|
655
|
+
if (run.skipReason)
|
|
656
|
+
return SKIP_REASON_LABEL[run.skipReason];
|
|
657
|
+
if (run.status === 'missed')
|
|
658
|
+
return 'scheduler was not running when it came due';
|
|
659
|
+
// The most common failure shape: a command/agent body that exited nonzero with
|
|
660
|
+
// no structured errorMessage (the cause is in stdout). Naming the exit code is
|
|
661
|
+
// still more than the bare status word, and tells the reader it ran and threw.
|
|
662
|
+
if ((run.status === 'failed' || run.status === 'timeout') && run.exitCode !== null && run.exitCode !== undefined) {
|
|
663
|
+
return `exit ${run.exitCode}`;
|
|
664
|
+
}
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
625
667
|
/** One compact routine row for the browser list: name · kind · schedule · next · last. */
|
|
626
668
|
function routineBrowserRow(job, scheduler, overdueSet, now) {
|
|
627
669
|
const kind = job.command ? 'command' : job.workflow ? `wf:${job.workflow}` : job.agent ?? '?';
|
|
@@ -685,7 +727,12 @@ function buildRoutineDetail(job, scheduler, now) {
|
|
|
685
727
|
? chalk.red(run.status)
|
|
686
728
|
: chalk.yellow(run.status);
|
|
687
729
|
const dur = run.completedAt ? ` ${formatRunDuration(run.startedAt, run.completedAt)}` : '';
|
|
688
|
-
|
|
730
|
+
// Surface WHY a run did not complete, inline, so "looking at status" does not
|
|
731
|
+
// require digging into the run dir. auth_failed / OAuth-revoked, a wedged
|
|
732
|
+
// active-run skip, or a readiness block all live on the RunMeta already.
|
|
733
|
+
const reason = runFailureReason(run);
|
|
734
|
+
const why = reason ? chalk.gray(` — ${reason}`) : '';
|
|
735
|
+
lines.push(` ${run.startedAt} ${status}${dur}${why}`);
|
|
689
736
|
}
|
|
690
737
|
}
|
|
691
738
|
// 4. Stats
|
|
@@ -1533,7 +1580,9 @@ export function registerRoutinesCommands(program) {
|
|
|
1533
1580
|
: run.status === 'failed'
|
|
1534
1581
|
? chalk.red(run.status)
|
|
1535
1582
|
: chalk.yellow(run.status);
|
|
1536
|
-
|
|
1583
|
+
const reason = runFailureReason(run);
|
|
1584
|
+
const why = reason ? chalk.gray(` — ${reason}`) : '';
|
|
1585
|
+
console.log(` ${run.runId} ${status} ${run.startedAt}${why}`);
|
|
1537
1586
|
}
|
|
1538
1587
|
});
|
|
1539
1588
|
routinesCmd
|
|
@@ -1856,6 +1905,11 @@ export function registerRoutinesCommands(program) {
|
|
|
1856
1905
|
chalk.gray(` ${run.startedAt}`) +
|
|
1857
1906
|
chalk.gray(formatRunDuration(run.startedAt, run.completedAt)) +
|
|
1858
1907
|
(run.exitCode !== null && run.exitCode !== undefined ? chalk.gray(` exit ${run.exitCode}`) : ''));
|
|
1908
|
+
// The structured reason (auth_failed, blocked readiness, wedged skip) — the
|
|
1909
|
+
// report/stdout tail below often buries or omits it, so name it up front.
|
|
1910
|
+
const logsReason = runFailureReason(run);
|
|
1911
|
+
if (logsReason)
|
|
1912
|
+
console.log(chalk.red('reason: ') + chalk.gray(logsReason));
|
|
1859
1913
|
console.log(chalk.gray('─'.repeat(60)));
|
|
1860
1914
|
const reportPath = path.join(getRunDir(name, runId), 'report.md');
|
|
1861
1915
|
if (fs.existsSync(reportPath)) {
|
|
@@ -149,6 +149,11 @@ export function createDaemonHarness(fileSlug) {
|
|
|
149
149
|
// and kills real tmux-wrapped processes every five-minute tick (RUSH-2545).
|
|
150
150
|
AGENTS_HISTORY_DIR: path.join(home, '.agents', '.history'),
|
|
151
151
|
AGENTS_SKIP_MIGRATION: '1',
|
|
152
|
+
// PHNX-2545 test-home tripwire: name the isolated home this daemon must
|
|
153
|
+
// resolve its state dir under. If the HOME override above ever failed to
|
|
154
|
+
// reach the child, runDaemon()'s assertTestDaemonHome() refuses to boot
|
|
155
|
+
// instead of ticking its scheduler against the operator's real host.
|
|
156
|
+
AGENTS_DAEMON_TEST_HOME: home,
|
|
152
157
|
},
|
|
153
158
|
detached: true,
|
|
154
159
|
stdio: 'ignore',
|
package/dist/commands/send.d.ts
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* Compat: positional text still works (`agents send "hi" --channel … --to …`).
|
|
12
12
|
*
|
|
13
13
|
* `agents notify` is the same delivery path with owner defaults
|
|
14
|
-
* (`send --to owner`)
|
|
14
|
+
* (`send --to owner`) and is deprecated — new callers should use
|
|
15
|
+
* `agents feed post`. Not a second stack. Not agent control — use
|
|
15
16
|
* `agents message` / `agents sessions inject` for running agents.
|
|
16
17
|
*
|
|
17
18
|
* Feed / activity are a different plane (record + read); feed.broadcast may
|
package/dist/commands/send.js
CHANGED
|
@@ -47,7 +47,7 @@ async function runSend(positionalText, opts, ownerMode) {
|
|
|
47
47
|
}
|
|
48
48
|
const SHARED_NOTES = `
|
|
49
49
|
Planes (do not mix them up):
|
|
50
|
-
send
|
|
50
|
+
send - DELIVER a message to a recipient (this command)
|
|
51
51
|
feed post - RECORD progress / milestones (optional broadcast may call send)
|
|
52
52
|
activity - READ the activity stream (not a send path)
|
|
53
53
|
message / inject - CONTROL a running agent (mailbox answer or terminal keystroke)
|
|
@@ -97,7 +97,7 @@ export function registerSendCommand(program) {
|
|
|
97
97
|
});
|
|
98
98
|
const notifyCmd = program
|
|
99
99
|
.command('notify [text]')
|
|
100
|
-
.description('Deliver to the owner (alias of send --to owner).
|
|
100
|
+
.description('[DEPRECATED] Deliver to the owner (alias of send --to owner). Use "agents feed post" for new code.')
|
|
101
101
|
.option('--text <text>', 'message body (preferred over positional text)')
|
|
102
102
|
.option('--channel <name>', 'override owner channel')
|
|
103
103
|
.option('--to <target>', 'override owner target (or pass a non-owner dest with --channel)')
|
|
@@ -110,7 +110,7 @@ export function registerSendCommand(program) {
|
|
|
110
110
|
.option('--dry-run', 'resolve + build but do not send');
|
|
111
111
|
setHelpSections(notifyCmd, {
|
|
112
112
|
examples: `
|
|
113
|
-
#
|
|
113
|
+
# Deprecated: prefer "agents feed post --title \"…\" \"…\" --level important"
|
|
114
114
|
agents notify --text "Build finished — PR #1346 is green"
|
|
115
115
|
agents notify "legacy positional still works"
|
|
116
116
|
|
|
@@ -120,13 +120,15 @@ export function registerSendCommand(program) {
|
|
|
120
120
|
agents notify --text "probe" --dry-run --json
|
|
121
121
|
`,
|
|
122
122
|
notes: `
|
|
123
|
-
notify ≡ send --to owner
|
|
124
|
-
|
|
123
|
+
DEPRECATED. notify ≡ send --to owner and still works, but new callers
|
|
124
|
+
should use "agents feed post" (record + optional broadcast) instead.
|
|
125
|
+
Set notify.owner.{channel,to} in agents.yaml once per machine/fleet.
|
|
125
126
|
|
|
126
127
|
${SHARED_NOTES}
|
|
127
128
|
`,
|
|
128
129
|
});
|
|
129
130
|
notifyCmd.action(async (text, opts) => {
|
|
131
|
+
console.error(chalk.yellow('Warning: "agents notify" is deprecated. Use "agents feed post" for progress posts that can also reach the owner.'));
|
|
130
132
|
await runSend(text, opts, true);
|
|
131
133
|
});
|
|
132
134
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { SessionEvent, SessionMeta, TodoProgress } from '../lib/session/types.js';
|
|
2
2
|
import { fetchPeerPreviewDigest } from '../lib/session/remote-list.js';
|
|
3
3
|
import { classifyFileChanges, changeCounts, toolHistogram, detectTestResult } from '../lib/session/digest.js';
|
|
4
|
+
import type { FileChange } from '../lib/session/digest.js';
|
|
4
5
|
import { extractArtifacts, extractHooks, extractLinks, extractSkills } from '../lib/session/highlights.js';
|
|
5
6
|
/**
|
|
6
7
|
* Compact checklist tally for list rows and previews (RUSH-2045).
|
|
@@ -118,6 +119,16 @@ export interface SessionPreviewDigest {
|
|
|
118
119
|
backgroundShellCount?: number;
|
|
119
120
|
toolTags: string[];
|
|
120
121
|
changes: ReturnType<typeof changeCounts>;
|
|
122
|
+
/**
|
|
123
|
+
* The per-file source paths behind `changes`. `changes` keeps the roll-up
|
|
124
|
+
* counts every existing consumer already reads; `changedFiles` carries the
|
|
125
|
+
* real path + op the CLI computed at scan time (via classifyFileChanges) and
|
|
126
|
+
* used to discard — kept so a consumer that renders a per-file diff list (the
|
|
127
|
+
* AGI EXT Fleet detail panel, PHNX-2973) has the paths, not just the totals.
|
|
128
|
+
* Capped so a session that rewrote thousands of files can't bloat the cached
|
|
129
|
+
* digest / JSON payload; the `changes` counts stay the true totals.
|
|
130
|
+
*/
|
|
131
|
+
changedFiles: FileChange[];
|
|
121
132
|
dirs: string[];
|
|
122
133
|
repos: string[];
|
|
123
134
|
artifacts: ReturnType<typeof extractArtifacts>;
|
|
@@ -229,6 +229,14 @@ export function sanitizeRemoteDigest(raw) {
|
|
|
229
229
|
backgroundShellCount: optNum(d.backgroundShellCount),
|
|
230
230
|
toolTags: strList(d.toolTags),
|
|
231
231
|
changes,
|
|
232
|
+
// Per-file paths from an untrusted peer: keep only well-formed {path, op}
|
|
233
|
+
// entries (path scrubbed of terminal escapes, op a known FileOp), bounded
|
|
234
|
+
// like the local build so a hostile peer can't flood the pane.
|
|
235
|
+
changedFiles: objList(d.changedFiles, (f) => {
|
|
236
|
+
const path = str(f.path);
|
|
237
|
+
const op = f.op === 'created' || f.op === 'modified' || f.op === 'deleted' ? f.op : undefined;
|
|
238
|
+
return path && op ? { path, op } : undefined;
|
|
239
|
+
}).slice(0, CHANGED_FILES_MAX),
|
|
232
240
|
dirs: strList(d.dirs),
|
|
233
241
|
repos: strList(d.repos),
|
|
234
242
|
artifacts: objList(d.artifacts, (a) => {
|
|
@@ -655,6 +663,10 @@ const LAST_RESPONSE_MAX_LINES = 15;
|
|
|
655
663
|
const LAST_RESPONSE_MAX_LINES_WITH_TODOS = 8;
|
|
656
664
|
const TODOS_MAX_ITEMS = 5;
|
|
657
665
|
const DIRS_TOUCHED_MAX = 5;
|
|
666
|
+
// Upper bound on the per-file `changedFiles` list carried on the digest. High
|
|
667
|
+
// enough to cover any real session's edits, low enough that a runaway rewrite
|
|
668
|
+
// can't bloat the cached JSON. The `changes` counts stay the true totals.
|
|
669
|
+
const CHANGED_FILES_MAX = 200;
|
|
658
670
|
/** Fold a harness-normalized event stream into the stable preview data model. */
|
|
659
671
|
export function buildSessionPreviewDigest(events, session) {
|
|
660
672
|
let firstUser = '';
|
|
@@ -747,6 +759,10 @@ export function buildSessionPreviewDigest(events, session) {
|
|
|
747
759
|
backgroundShellCount,
|
|
748
760
|
toolTags: [...toolTags],
|
|
749
761
|
changes: chg,
|
|
762
|
+
// Full per-file list the picker used to collapse to `chg` and throw away.
|
|
763
|
+
// Bounded so a mass-rewrite session can't blow up the cached digest; the
|
|
764
|
+
// counts above remain the true totals.
|
|
765
|
+
changedFiles: changes.slice(0, CHANGED_FILES_MAX),
|
|
750
766
|
dirs: directoriesTouched(session, events, changes),
|
|
751
767
|
repos: extractRepos(events, session.cwd),
|
|
752
768
|
artifacts: extractArtifacts(changes),
|
|
@@ -6,6 +6,19 @@ import { discoverPlugins } from '../lib/plugins/plugins.js';
|
|
|
6
6
|
import { setHelpSections } from '../lib/help.js';
|
|
7
7
|
import { terminalWidth, truncateToWidth, padToWidth, stringWidth } from '../lib/session/width.js';
|
|
8
8
|
const DEFAULT_TOP = 20;
|
|
9
|
+
/**
|
|
10
|
+
* The recorded-set the zero-counts must be read against (PHNX-2301). A resource
|
|
11
|
+
* only records an EXPLICIT invocation from a harness that emits the event:
|
|
12
|
+
* `Skill` tool calls come from Claude + Kimi, slash-commands from Claude only
|
|
13
|
+
* (`SKILL_TOOL_NAME_BY_AGENT` in `lib/session/highlights.ts`, slash-command
|
|
14
|
+
* parsing in `lib/session/parse.ts`). A session under any other harness — or an
|
|
15
|
+
* auto-triggered skill, which emits no event on any harness — contributes
|
|
16
|
+
* nothing, so its resources read as 0 whether or not they were used. Keep these
|
|
17
|
+
* in lockstep with the writer: widening them here without a verified transcript
|
|
18
|
+
* tool-name would make the caveat lie about coverage it does not have.
|
|
19
|
+
*/
|
|
20
|
+
const RECORDING_SKILL_HARNESSES = ['claude', 'kimi'];
|
|
21
|
+
const RECORDING_COMMAND_HARNESSES = ['claude'];
|
|
9
22
|
/**
|
|
10
23
|
* The both-ends "dead weight" set: installed resources whose identity
|
|
11
24
|
* (kind + name — name already embeds `plugin:short`, so it matches the stored
|
|
@@ -128,7 +141,7 @@ async function statsAction(cmd) {
|
|
|
128
141
|
const totalInvocations = allInvoked.reduce((s, r) => s + r.invocations, 0);
|
|
129
142
|
if (opts.json) {
|
|
130
143
|
process.stdout.write(JSON.stringify({
|
|
131
|
-
schemaVersion:
|
|
144
|
+
schemaVersion: 2,
|
|
132
145
|
kind: 'sessions-stats',
|
|
133
146
|
generatedAt: new Date().toISOString(),
|
|
134
147
|
filters: {
|
|
@@ -142,9 +155,21 @@ async function statsAction(cmd) {
|
|
|
142
155
|
signal: {
|
|
143
156
|
explicitOnly: true,
|
|
144
157
|
note: 'Explicit invocations only (slash commands + Skill tool calls). Auto-triggered skills emit no event and read as 0. Skill invocations are recorded for Claude and Kimi; slash-commands for Claude only.',
|
|
158
|
+
// Structured recorded-set so a machine consumer can tell a zero from a
|
|
159
|
+
// non-recording harness apart from a genuine non-use (PHNX-2301): a
|
|
160
|
+
// zeroInvoked resource may simply have been auto-triggered, or only
|
|
161
|
+
// used under a harness not in these sets.
|
|
162
|
+
recording: {
|
|
163
|
+
skills: RECORDING_SKILL_HARNESSES,
|
|
164
|
+
commands: RECORDING_COMMAND_HARNESSES,
|
|
165
|
+
},
|
|
145
166
|
},
|
|
146
167
|
coverage: {
|
|
168
|
+
// sessionsWithUsage/sessionsIndexed kept for the SES-IF-4b envelope
|
|
169
|
+
// contract; sessionsScanned is the honest backfill-coverage numerator
|
|
170
|
+
// (ledger rows), distinct from the absolute sessionsWithUsage count.
|
|
147
171
|
sessionsWithUsage: coverage.covered,
|
|
172
|
+
sessionsScanned: coverage.scanned,
|
|
148
173
|
sessionsIndexed: coverage.total,
|
|
149
174
|
},
|
|
150
175
|
totals: {
|
|
@@ -210,8 +235,12 @@ function renderHuman(args) {
|
|
|
210
235
|
out.push(chalk.bold('Resource usage') +
|
|
211
236
|
chalk.gray(` · ${allInvokedCount} invoked · ${totalInvocations} invocation${totalInvocations !== 1 ? 's' : ''}` +
|
|
212
237
|
(scopeBits.length ? ` · ${scopeBits.join(' · ')}` : '')));
|
|
213
|
-
|
|
214
|
-
|
|
238
|
+
// Scan coverage (ledger) is the honest "has the backfill folded in history?"
|
|
239
|
+
// signal and drives the hint; sessionsWithUsage is an ABSOLUTE count of sessions
|
|
240
|
+
// carrying ≥1 explicit invocation, which stays small at full scan coverage
|
|
241
|
+
// because most sessions invoke nothing (PHNX-2301).
|
|
242
|
+
out.push(chalk.gray(` coverage: ${coverage.scanned}/${coverage.total} sessions scanned · ${coverage.covered} carry an explicit invocation`) +
|
|
243
|
+
(coverage.total > 0 && coverage.scanned / coverage.total < 0.5
|
|
215
244
|
? chalk.yellow(' — run `agents sessions backfill resources` to fold in history')
|
|
216
245
|
: ''));
|
|
217
246
|
out.push(chalk.gray(' signal: explicit invocations only (slash commands + Skill tool); auto-triggered skills read as 0; skills from Claude+Kimi, slash-commands Claude-only'));
|
|
@@ -228,8 +257,11 @@ function renderHuman(args) {
|
|
|
228
257
|
}
|
|
229
258
|
out.push('');
|
|
230
259
|
}
|
|
231
|
-
// Zero-invoked section (the dead weight).
|
|
232
|
-
|
|
260
|
+
// Zero-invoked section (the dead weight). "Never invoked" is scoped to the
|
|
261
|
+
// recorded set: never EXPLICITLY invoked under a recording harness (skills
|
|
262
|
+
// Claude+Kimi, commands Claude-only) — it may still have been auto-triggered or
|
|
263
|
+
// used under another harness, which is why this is dead-weight EVIDENCE, not proof.
|
|
264
|
+
out.push(chalk.bold('Installed but never explicitly invoked') + chalk.gray(` (${zeroInvoked.length})`));
|
|
233
265
|
if (zeroInvoked.length === 0) {
|
|
234
266
|
out.push(chalk.gray(' (every installed resource has at least one explicit invocation in this window)'));
|
|
235
267
|
}
|
|
@@ -2061,6 +2061,7 @@ export async function renderSessionPreview(query, scope) {
|
|
|
2061
2061
|
tokenCount: session.tokenCount,
|
|
2062
2062
|
costUsd: session.costUsd,
|
|
2063
2063
|
label: session.label,
|
|
2064
|
+
topic: session.topic,
|
|
2064
2065
|
ticketId: session.ticketId,
|
|
2065
2066
|
prUrl: session.prUrl,
|
|
2066
2067
|
},
|
|
@@ -2315,9 +2316,16 @@ limitSource) {
|
|
|
2315
2316
|
return;
|
|
2316
2317
|
}
|
|
2317
2318
|
// --device / --devices values are merged into options.host for internal routing. A bare `all` / `fleet` sentinel means
|
|
2318
|
-
// "search every peer" — which is already the default
|
|
2319
|
-
//
|
|
2320
|
-
|
|
2319
|
+
// "search every peer" — which is already the default for `--active` and the
|
|
2320
|
+
// interactive listing — so it resolves to no explicit host set rather than
|
|
2321
|
+
// erroring on a device literally named "all".
|
|
2322
|
+
const rawDeviceTargets = [...(options.device ?? []), ...(options.devices ?? [])];
|
|
2323
|
+
// But the HISTORICAL `--json` listing does NOT fan out by default (it stays a
|
|
2324
|
+
// deterministic local slice for scripts), so the sentinel would otherwise be
|
|
2325
|
+
// silently dropped and the fleet never reached (PHNX-2673). Remember it so that
|
|
2326
|
+
// path can sweep every peer, exactly as the interactive listing already does.
|
|
2327
|
+
const fleetWide = rawDeviceTargets.some(t => t.toLowerCase() === 'all' || t.toLowerCase() === 'fleet');
|
|
2328
|
+
const deviceTargets = rawDeviceTargets
|
|
2321
2329
|
.filter(t => t.toLowerCase() !== 'all' && t.toLowerCase() !== 'fleet');
|
|
2322
2330
|
if (deviceTargets.length > 0) {
|
|
2323
2331
|
options.host = [...(options.host ?? []), ...deviceTargets];
|
|
@@ -2774,16 +2782,43 @@ limitSource) {
|
|
|
2774
2782
|
// each peer) would fall to FTS content search and return every transcript
|
|
2775
2783
|
// that merely MENTIONS the id, defeating exact remote resolution. A genuine
|
|
2776
2784
|
// search phrase keeps the ranked metadata+content path.
|
|
2777
|
-
|
|
2785
|
+
let filtered = searchQuery
|
|
2778
2786
|
? resolveSessionQuery(sessions, searchQuery, {
|
|
2779
2787
|
scope: { agent: options.agent, project: options.project, routine: options.routine },
|
|
2780
2788
|
}).matches
|
|
2781
2789
|
: sessions;
|
|
2790
|
+
// `--device all`/`--fleet` on a historical `--json` query fans out to every
|
|
2791
|
+
// peer and merges their rows in — the SAME SSH sweep the interactive listing
|
|
2792
|
+
// below uses, and the whole-fleet twin of the explicit `--device <host>
|
|
2793
|
+
// --json` path (`runRemoteSessionsJson`). Without this the documented
|
|
2794
|
+
// "search the whole fleet" flag returned local rows only (PHNX-2673). Guarded
|
|
2795
|
+
// to the sentinel so a bare `--json` stays a deterministic local slice for
|
|
2796
|
+
// scripts, and skipped under `--local`/the peer recursion guard. Dead peers
|
|
2797
|
+
// are skipped by the fan-out, never fatal — enrichment, not a dependency.
|
|
2798
|
+
const forceLocalJson = options.local === true || process.env[NO_FANOUT_ENV] === '1';
|
|
2799
|
+
if (fleetWide && !forceLocalJson) {
|
|
2800
|
+
const forwarded = ensureWholeIndex(buildForwardedArgs(process.argv, new Set(options.host ?? [])));
|
|
2801
|
+
if (!forwarded.includes('--json'))
|
|
2802
|
+
forwarded.push('--json');
|
|
2803
|
+
const fanSpinner = isInteractiveTerminal() ? ora('Reaching other machines...').start() : null;
|
|
2804
|
+
try {
|
|
2805
|
+
const { sessions: remoteSessions } = await gatherRemoteList(forwarded, undefined);
|
|
2806
|
+
if (remoteSessions.length > 0) {
|
|
2807
|
+
filtered = mergeLocalFirst([...filtered, ...remoteSessions], machineId());
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
catch {
|
|
2811
|
+
// fan-out is an enrichment, never a hard dependency
|
|
2812
|
+
}
|
|
2813
|
+
finally {
|
|
2814
|
+
fanSpinner?.stop();
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2782
2817
|
// JSON is the canonical picker contract: enrich the durable rows once
|
|
2783
2818
|
// from the shared live cache so every consumer gets lifecycle/recovery
|
|
2784
2819
|
// metadata without performing its own join or transcript scan.
|
|
2785
2820
|
const live = await gatherActiveSessions({
|
|
2786
|
-
local:
|
|
2821
|
+
local: forceLocalJson,
|
|
2787
2822
|
hosts: options.host,
|
|
2788
2823
|
});
|
|
2789
2824
|
process.stdout.write(serializeSessionsJson(serializeSessionPickerRows(filtered, live.sessions)));
|
package/dist/commands/share.d.ts
CHANGED
|
@@ -250,6 +250,17 @@ export interface ShareUpdateResult {
|
|
|
250
250
|
templateHash: string;
|
|
251
251
|
baseUrl: string;
|
|
252
252
|
workerName: string;
|
|
253
|
+
/** True when this was a `--check` run: nothing was deployed, the fields below
|
|
254
|
+
* just report whether a deploy WOULD be needed. Used by the release train to
|
|
255
|
+
* decide during preflight — before it verifies deploy creds — whether this
|
|
256
|
+
* release even changes the Worker. */
|
|
257
|
+
checked?: boolean;
|
|
258
|
+
/** Whether the current `worker-template.ts` render differs from what the
|
|
259
|
+
* endpoint last deployed (or `--force`/an endpoint with no recorded hash). */
|
|
260
|
+
deployNeeded?: boolean;
|
|
261
|
+
/** The template hash the endpoint last deployed, per local config. Undefined
|
|
262
|
+
* on an endpoint provisioned before the hash field existed. */
|
|
263
|
+
deployedHash?: string;
|
|
253
264
|
}
|
|
254
265
|
/** Re-deploy the Worker script on an ALREADY-provisioned endpoint to match the
|
|
255
266
|
* current `worker-template.ts`. Reuses the existing account/worker/bucket and
|
|
@@ -264,6 +275,9 @@ export declare function runShareUpdate(opts?: {
|
|
|
264
275
|
account?: string;
|
|
265
276
|
token?: string;
|
|
266
277
|
force?: boolean;
|
|
278
|
+
/** Report whether a deploy is needed and return without deploying. Needs no
|
|
279
|
+
* Cloudflare credentials — change detection is a pure local render+hash. */
|
|
280
|
+
check?: boolean;
|
|
267
281
|
request?: CloudflareRequester;
|
|
268
282
|
/** Bind PHOENIX_ID_BASE even when the configured hostname is not the managed domain. */
|
|
269
283
|
managed?: boolean;
|
package/dist/commands/share.js
CHANGED
|
@@ -820,6 +820,7 @@ Prefer to change visibility without a browser? Use 'agents artifacts share visib
|
|
|
820
820
|
.option('--account <id>', 'Cloudflare account id override (default: the configured endpoint\'s account)')
|
|
821
821
|
.option('--token <t>', 'Cloudflare API token (else read from --bundle)')
|
|
822
822
|
.option('--force', 're-deploy even if the deployed template already matches')
|
|
823
|
+
.option('--check', 'report whether a deploy is needed and exit without deploying (needs no Cloudflare credentials)')
|
|
823
824
|
// Named --update-json, not --json: `share <file>` (the parent) already
|
|
824
825
|
// owns --json for its own publish-time result. Commander resolves an
|
|
825
826
|
// option's long name against the WHOLE ancestor chain, so a same-named
|
|
@@ -832,6 +833,12 @@ Prefer to change visibility without a browser? Use 'agents artifacts share visib
|
|
|
832
833
|
console.log(JSON.stringify(result, null, 2));
|
|
833
834
|
return;
|
|
834
835
|
}
|
|
836
|
+
if (result.checked) {
|
|
837
|
+
console.log(result.deployNeeded
|
|
838
|
+
? chalk.yellow(`Worker '${result.workerName}' template is outdated — deploy needed (current render ${result.templateHash.slice(0, 12)}…).`)
|
|
839
|
+
: chalk.dim(`Worker '${result.workerName}' already matches the current template — no deploy needed.`));
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
835
842
|
if (result.updated) {
|
|
836
843
|
console.log(chalk.green(`Worker '${result.workerName}' updated → template ${result.templateHash.slice(0, 12)}…`));
|
|
837
844
|
}
|
|
@@ -851,11 +858,18 @@ Prefer to change visibility without a browser? Use 'agents artifacts share visib
|
|
|
851
858
|
|
|
852
859
|
# Force a re-deploy even though the template hash already matches
|
|
853
860
|
agents artifacts share update --force
|
|
861
|
+
|
|
862
|
+
# Report whether a deploy is due without deploying (no Cloudflare creds needed)
|
|
863
|
+
agents artifacts share update --check --update-json
|
|
854
864
|
`,
|
|
855
865
|
notes: `
|
|
856
866
|
Reuses the existing account/worker/bucket from 'agents artifacts share status' and the
|
|
857
867
|
existing write token — it never re-provisions a bucket, touches routes, or
|
|
858
868
|
regenerates the token. See 'agents artifacts share status' for whether an update is due.
|
|
869
|
+
|
|
870
|
+
--check renders the current template and compares its hash to what the endpoint last
|
|
871
|
+
deployed, then exits — deploying nothing and reading no Cloudflare credentials. The
|
|
872
|
+
release train uses it to decide, before publish, whether a release changes the Worker.
|
|
859
873
|
`,
|
|
860
874
|
});
|
|
861
875
|
shareCmd
|
|
@@ -1222,6 +1236,27 @@ export async function runShareUpdate(opts = {}) {
|
|
|
1222
1236
|
if (!cfg) {
|
|
1223
1237
|
throw new Error("Not configured. Run 'agents artifacts setup' (to provision) or 'agents artifacts share join' first.");
|
|
1224
1238
|
}
|
|
1239
|
+
// Change detection is a pure local computation (render + sha256), so it needs
|
|
1240
|
+
// no Cloudflare credentials. The release train calls this in `--check` mode
|
|
1241
|
+
// during preflight — BEFORE it has verified the deploy creds — to decide
|
|
1242
|
+
// whether this release changes the Worker at all. `shareTemplateStatus` is the
|
|
1243
|
+
// same comparator `agents artifacts share status` uses; 'unknown' (an endpoint
|
|
1244
|
+
// with no recorded hash) deploys, mirroring `updateWorker`'s undefined-previous
|
|
1245
|
+
// behavior.
|
|
1246
|
+
const worker = renderWorkerBundle();
|
|
1247
|
+
const templateHash = hashWorkerScript(worker.script);
|
|
1248
|
+
const deployNeeded = opts.force === true || shareTemplateStatus(cfg) !== 'current';
|
|
1249
|
+
if (opts.check) {
|
|
1250
|
+
return {
|
|
1251
|
+
updated: false,
|
|
1252
|
+
checked: true,
|
|
1253
|
+
deployNeeded,
|
|
1254
|
+
templateHash,
|
|
1255
|
+
deployedHash: cfg.templateHash,
|
|
1256
|
+
baseUrl: cfg.baseUrl,
|
|
1257
|
+
workerName: cfg.workerName,
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1225
1260
|
const { apiToken, accountId: acctFromBundle } = readCloudflareCreds(opts.bundle ?? DEFAULT_CF_BUNDLE, {
|
|
1226
1261
|
apiToken: opts.token,
|
|
1227
1262
|
accountId: opts.account,
|
|
@@ -1231,7 +1266,6 @@ export async function runShareUpdate(opts = {}) {
|
|
|
1231
1266
|
throw new Error("Share endpoint has no Cloudflare account id — `agents artifacts share update` cannot call the API. Pass --account <id>, or re-run 'agents artifacts share join'.");
|
|
1232
1267
|
}
|
|
1233
1268
|
const writeToken = readWriteToken();
|
|
1234
|
-
const worker = renderWorkerBundle();
|
|
1235
1269
|
const phoenixIdBase = phoenixIdBaseForDeploy({ managed: opts.managed }, cfg);
|
|
1236
1270
|
const provisionOpts = {
|
|
1237
1271
|
...(opts.request ? { request: opts.request } : {}),
|
|
@@ -1242,7 +1276,14 @@ export async function runShareUpdate(opts = {}) {
|
|
|
1242
1276
|
if (!result.skipped) {
|
|
1243
1277
|
writeShareConfig({ ...cfg, accountId, templateHash: result.templateHash });
|
|
1244
1278
|
}
|
|
1245
|
-
return {
|
|
1279
|
+
return {
|
|
1280
|
+
updated: !result.skipped,
|
|
1281
|
+
templateHash: result.templateHash,
|
|
1282
|
+
deployNeeded,
|
|
1283
|
+
deployedHash: cfg.templateHash,
|
|
1284
|
+
baseUrl: cfg.baseUrl,
|
|
1285
|
+
workerName: cfg.workerName,
|
|
1286
|
+
};
|
|
1246
1287
|
}
|
|
1247
1288
|
function cleanHostname(domain) {
|
|
1248
1289
|
const raw = domain?.trim().replace(/\/+$/, '');
|
package/dist/commands/ssh.js
CHANGED
|
@@ -130,7 +130,11 @@ function pctCell(v, width) {
|
|
|
130
130
|
* header — which defeats the scannability this column exists for. */
|
|
131
131
|
const SPEC_WIDTH_MIN = 12;
|
|
132
132
|
/** The static hardware as one compact cell — `12c 64G 1T`: cores, total RAM,
|
|
133
|
-
* total root disk via fmtBytes. `—`
|
|
133
|
+
* total root disk via fmtBytes. `—` covers two cases: no probe has ever seen
|
|
134
|
+
* the box, or the probe answered but yielded no usable core count (`parseNcpu`
|
|
135
|
+
* finding none, or the Windows `ncpu` group failing the finite-and-positive
|
|
136
|
+
* check) — the cell is keyed on `ncpu` because a spec string without it would
|
|
137
|
+
* read as a machine with zero cores.
|
|
134
138
|
*
|
|
135
139
|
* Deliberately NOT gated on `reachable`: hardware does not change while a
|
|
136
140
|
* machine is down, so an offline device keeps rendering the spec from its last
|
|
@@ -2298,6 +2302,13 @@ box; \`agents doctor\` names it.
|
|
|
2298
2302
|
|
|
2299
2303
|
A box whose probe cannot answer (no POSIX shell, e.g. Windows) is reported
|
|
2300
2304
|
\`unverified\` rather than counted as a success.
|
|
2305
|
+
|
|
2306
|
+
The upgrade OWNS its global bin links: after installing, it verifies that
|
|
2307
|
+
\`<prefix>/bin/{agents,ag,browser,computer}\` resolve to the freshly-installed
|
|
2308
|
+
copy and RESTORES any the package manager dropped — the state that once left a
|
|
2309
|
+
box upgraded in place but with every \`agents\` invocation "command not found"
|
|
2310
|
+
(PHNX-2768). A link it cannot make resolve fails the upgrade loud, so that box
|
|
2311
|
+
is reported \`failed\` (exit non-zero), never a stranded \`ok\`.
|
|
2301
2312
|
`)
|
|
2302
2313
|
.action(async (version) => {
|
|
2303
2314
|
let cmd;
|
package/dist/commands/status.js
CHANGED
|
@@ -78,7 +78,7 @@ export function registerStatusCommand(syncCmd) {
|
|
|
78
78
|
// A non-git / partial ~/.agents is its own drift state, not a per-agent
|
|
79
79
|
// "N missing" — surface it distinctly so the real problem isn't buried (PHNX-3301).
|
|
80
80
|
if (status.user.notGitRepo) {
|
|
81
|
-
console.log(` ${'~/.agents (user repo)'.padEnd(28)} ${chalk.yellow('not a git repo — will adopt on next `agents repo sync user`')}`);
|
|
81
|
+
console.log(` ${'~/.agents (user repo)'.padEnd(28)} ${chalk.yellow('not a git repo — will adopt on next `agents sync` (or `agents repo sync user`)')}`);
|
|
82
82
|
}
|
|
83
83
|
if (status.agents.length === 0) {
|
|
84
84
|
console.log(chalk.gray(' (no installed agent versions)'));
|