@phnx-labs/agents-cli 1.22.26 → 1.22.28
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 +293 -0
- package/README.md +80 -27
- package/dist/bin/agents +0 -0
- package/dist/commands/bench.d.ts +2 -0
- package/dist/commands/bench.js +101 -0
- package/dist/commands/doctor.js +24 -10
- package/dist/commands/exec.js +71 -26
- package/dist/commands/hosts.js +4 -3
- package/dist/commands/insights.js +39 -5
- package/dist/commands/output.js +100 -25
- package/dist/commands/perf.d.ts +10 -0
- package/dist/commands/perf.js +14 -6
- package/dist/commands/projects.js +24 -10
- package/dist/commands/resume.d.ts +11 -0
- package/dist/commands/resume.js +51 -0
- package/dist/commands/routines.js +31 -4
- package/dist/commands/run-account-picker.d.ts +37 -0
- package/dist/commands/run-account-picker.js +101 -17
- package/dist/commands/secrets.js +86 -2
- package/dist/commands/sessions-picker.js +12 -7
- package/dist/commands/sessions.d.ts +73 -7
- package/dist/commands/sessions.js +391 -49
- package/dist/commands/ssh.js +184 -11
- package/dist/commands/teams.js +22 -11
- package/dist/index.js +16 -20
- package/dist/lib/activity.js +45 -99
- package/dist/lib/auth-health.d.ts +47 -3
- package/dist/lib/auth-health.js +74 -15
- package/dist/lib/bench/index.d.ts +4 -0
- package/dist/lib/bench/index.js +4 -0
- package/dist/lib/bench/runner.d.ts +16 -0
- package/dist/lib/bench/runner.js +111 -0
- package/dist/lib/bench/schema.d.ts +5 -0
- package/dist/lib/bench/schema.js +91 -0
- package/dist/lib/bench/storage.d.ts +5 -0
- package/dist/lib/bench/storage.js +32 -0
- package/dist/lib/bench/types.d.ts +40 -0
- package/dist/lib/bench/types.js +1 -0
- package/dist/lib/claude-account-token.d.ts +2 -0
- package/dist/lib/claude-account-token.js +56 -3
- package/dist/lib/cloud/host.js +1 -0
- package/dist/lib/crabbox/cli.d.ts +2 -0
- package/dist/lib/crabbox/cli.js +2 -0
- package/dist/lib/crabbox/lease.js +7 -1
- package/dist/lib/daemon.js +20 -0
- package/dist/lib/devices/connect.d.ts +2 -0
- package/dist/lib/devices/connect.js +7 -0
- package/dist/lib/devices/doctor-findings.d.ts +4 -1
- package/dist/lib/devices/doctor-findings.js +15 -1
- package/dist/lib/devices/harness-inventory.d.ts +97 -0
- package/dist/lib/devices/harness-inventory.js +0 -0
- package/dist/lib/devices/registry.d.ts +2 -0
- package/dist/lib/devices/resolve-target.d.ts +1 -0
- package/dist/lib/devices/resolve-target.js +9 -2
- package/dist/lib/devices/ssh-config.js +3 -0
- package/dist/lib/devices/windows-ssh-enrollment.d.ts +20 -0
- package/dist/lib/devices/windows-ssh-enrollment.js +98 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/events.js +2 -2
- package/dist/lib/exec.js +18 -8
- package/dist/lib/fleet/apply.js +9 -7
- package/dist/lib/fleet/remote-login.d.ts +4 -3
- package/dist/lib/fleet/remote-login.js +11 -9
- package/dist/lib/gemini-settings.d.ts +0 -1
- package/dist/lib/gemini-settings.js +12 -7
- package/dist/lib/git.d.ts +9 -3
- package/dist/lib/git.js +39 -22
- package/dist/lib/hooks/profile.d.ts +12 -1
- package/dist/lib/hooks/profile.js +6 -1
- package/dist/lib/hooks.d.ts +8 -5
- package/dist/lib/hooks.js +14 -7
- package/dist/lib/hosts/dispatch.d.ts +10 -0
- package/dist/lib/hosts/dispatch.js +85 -25
- package/dist/lib/hosts/logs.js +11 -1
- package/dist/lib/hosts/passthrough.d.ts +27 -0
- package/dist/lib/hosts/passthrough.js +19 -10
- package/dist/lib/hosts/progress.d.ts +12 -1
- package/dist/lib/hosts/progress.js +41 -8
- package/dist/lib/hosts/providers/devices.js +1 -0
- package/dist/lib/hosts/ready.d.ts +45 -4
- package/dist/lib/hosts/ready.js +124 -20
- package/dist/lib/hosts/reconcile.d.ts +2 -1
- package/dist/lib/hosts/reconcile.js +24 -7
- package/dist/lib/hosts/reconnect.d.ts +45 -12
- package/dist/lib/hosts/reconnect.js +94 -36
- package/dist/lib/hosts/registry.d.ts +2 -2
- package/dist/lib/hosts/registry.js +4 -5
- package/dist/lib/hosts/remote-cmd.d.ts +17 -0
- package/dist/lib/hosts/remote-cmd.js +29 -0
- package/dist/lib/hosts/tasks.d.ts +3 -0
- package/dist/lib/hosts/types.d.ts +1 -0
- package/dist/lib/hosts/types.js +3 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +9 -6
- package/dist/lib/menubar/install-menubar.js +20 -9
- package/dist/lib/menubar/snapshot.js +15 -2
- package/dist/lib/models.d.ts +42 -6
- package/dist/lib/models.js +55 -153
- package/dist/lib/perf/db.js +16 -3
- package/dist/lib/perf/types.d.ts +12 -1
- package/dist/lib/pricing/cost.d.ts +9 -0
- package/dist/lib/pricing/cost.js +24 -0
- package/dist/lib/pricing/index.d.ts +1 -1
- package/dist/lib/pricing/index.js +1 -1
- package/dist/lib/project-probe.d.ts +22 -3
- package/dist/lib/project-probe.js +105 -17
- package/dist/lib/project-status.d.ts +9 -0
- package/dist/lib/project-status.js +15 -0
- package/dist/lib/redact.js +8 -3
- package/dist/lib/refresh.js +22 -11
- package/dist/lib/remote-agents-json.d.ts +32 -0
- package/dist/lib/remote-agents-json.js +47 -16
- package/dist/lib/resource-profiles.js +1 -2
- package/dist/lib/rotate.d.ts +22 -0
- package/dist/lib/rotate.js +26 -0
- package/dist/lib/routine-notify-owner.d.ts +102 -0
- package/dist/lib/routine-notify-owner.js +232 -0
- package/dist/lib/routines.d.ts +10 -0
- package/dist/lib/runner.d.ts +9 -1
- package/dist/lib/runner.js +182 -17
- package/dist/lib/sandbox.d.ts +0 -2
- package/dist/lib/sandbox.js +2 -19
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.d.ts +19 -3
- package/dist/lib/secrets/agent.js +61 -22
- package/dist/lib/secrets/audit.d.ts +1 -1
- package/dist/lib/secrets/audit.js +2 -0
- package/dist/lib/secrets/bundles.d.ts +3 -3
- package/dist/lib/secrets/bundles.js +15 -5
- package/dist/lib/secrets/filestore.d.ts +2 -0
- package/dist/lib/secrets/filestore.js +4 -0
- package/dist/lib/secrets/lease.d.ts +25 -0
- package/dist/lib/secrets/lease.js +44 -0
- package/dist/lib/secrets/session-store.d.ts +7 -0
- package/dist/lib/secrets/session-store.js +21 -0
- package/dist/lib/self-update.d.ts +20 -5
- package/dist/lib/self-update.js +93 -16
- package/dist/lib/session/active.d.ts +49 -0
- package/dist/lib/session/active.js +175 -11
- package/dist/lib/session/bash-command.d.ts +16 -0
- package/dist/lib/session/bash-command.js +65 -0
- package/dist/lib/session/db.d.ts +23 -2
- package/dist/lib/session/db.js +86 -20
- package/dist/lib/session/discover.d.ts +21 -1
- package/dist/lib/session/discover.js +124 -32
- package/dist/lib/session/insights.d.ts +19 -0
- package/dist/lib/session/insights.js +78 -10
- package/dist/lib/session/recovery.d.ts +17 -1
- package/dist/lib/session/recovery.js +111 -4
- package/dist/lib/session/remote-bundle.js +1 -1
- package/dist/lib/session/remote-list.d.ts +10 -9
- package/dist/lib/session/remote-list.js +25 -23
- package/dist/lib/session/resume-owner.d.ts +55 -0
- package/dist/lib/session/resume-owner.js +69 -0
- package/dist/lib/session/team-filter.d.ts +65 -0
- package/dist/lib/session/team-filter.js +98 -3
- package/dist/lib/session/types.d.ts +33 -0
- package/dist/lib/smart-launch.d.ts +31 -5
- package/dist/lib/smart-launch.js +43 -7
- package/dist/lib/ssh-exec.d.ts +27 -0
- package/dist/lib/ssh-exec.js +34 -1
- package/dist/lib/ssh-tunnel.d.ts +3 -2
- package/dist/lib/ssh-tunnel.js +25 -16
- package/dist/lib/staleness/detectors/skills.d.ts +2 -0
- package/dist/lib/staleness/detectors/skills.js +17 -2
- package/dist/lib/staleness/index.d.ts +10 -1
- package/dist/lib/staleness/index.js +28 -3
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/teams/agents.d.ts +1 -0
- package/dist/lib/teams/agents.js +38 -11
- package/dist/lib/teams/placement-probe.d.ts +1 -1
- package/dist/lib/teams/placement-probe.js +24 -24
- package/dist/lib/teams/remoteWorktree.d.ts +11 -7
- package/dist/lib/teams/remoteWorktree.js +29 -27
- package/dist/lib/tmux/session.d.ts +8 -0
- package/dist/lib/tmux/session.js +22 -0
- package/dist/lib/types.d.ts +2 -0
- package/dist/lib/versions.d.ts +1 -0
- package/dist/lib/versions.js +40 -22
- package/package.json +3 -1
package/dist/lib/models.d.ts
CHANGED
|
@@ -43,7 +43,7 @@ export interface ModelCatalog {
|
|
|
43
43
|
aliases: Record<string, string>;
|
|
44
44
|
}
|
|
45
45
|
/** How the model catalog source was obtained. */
|
|
46
|
-
export type ModelSourceKind = 'bundle' | 'binary' | '
|
|
46
|
+
export type ModelSourceKind = 'bundle' | 'binary' | 'cli';
|
|
47
47
|
/** Describes the location and extraction strategy for a model catalog source. */
|
|
48
48
|
export interface ModelSource {
|
|
49
49
|
path: string;
|
|
@@ -69,13 +69,49 @@ export declare function locateModelSource(agent: AgentId, version: string): Mode
|
|
|
69
69
|
* - constants: {OPUS_ID:"...",OPUS_NAME:"...",SONNET_ID:"...",...}
|
|
70
70
|
*/
|
|
71
71
|
/**
|
|
72
|
-
* Drop a
|
|
73
|
-
* sibling (`claude-opus-4-8`
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
72
|
+
* Drop a catalog id that is a proper dash-boundary prefix of a more-specific
|
|
73
|
+
* sibling also present in the list (e.g. `claude-opus-4` when `claude-opus-4-8`
|
|
74
|
+
* is present, or `claude-opus-4-1` when `claude-opus-4-1-20250805` is present).
|
|
75
|
+
*
|
|
76
|
+
* Two sources produce these prefix forms in the native binary:
|
|
77
|
+
* - standalone `.includes("claude-opus-4")` prefix-check strings (#1892)
|
|
78
|
+
* - per-cloud metadata field values such as `foundry:"claude-opus-4-1"` next
|
|
79
|
+
* to a real firstParty id `claude-opus-4-1-20250805` (#2233)
|
|
80
|
+
*
|
|
81
|
+
* A genuine bare current id with no longer sibling (e.g. `claude-sonnet-5`) is
|
|
82
|
+
* kept. The dash boundary (`startsWith(id + '-')`) avoids collapsing
|
|
83
|
+
* `claude-opus-4-1` into `claude-opus-4-10`.
|
|
77
84
|
*/
|
|
78
85
|
export declare function dropBareLegacyIds(ids: string[]): string[];
|
|
86
|
+
/**
|
|
87
|
+
* Scan raw binary/bundle text for canonical Claude model ids, then drop bare
|
|
88
|
+
* legacy / cloud-metadata prefixes (#1892, #2233). Two independent guards keep
|
|
89
|
+
* non-model strings out of the catalog:
|
|
90
|
+
*
|
|
91
|
+
* - **Word-boundary anchors on the id regex.** The id must not be glued to a
|
|
92
|
+
* surrounding identifier character, and must not be the truncated prefix of a
|
|
93
|
+
* longer *version* token. `(?<![A-Za-z0-9_])` rejects a glued prefix;
|
|
94
|
+
* `(?![A-Za-z0-9])` rejects a glued alnum suffix; `(?!\.\d)` rejects a
|
|
95
|
+
* dotted-version continuation — so the bare-major prefix of the binary's own
|
|
96
|
+
* "Typo in model ID" troubleshooting string `claude-sonnet-4.6` is not
|
|
97
|
+
* scraped as `claude-sonnet-4`, while a real id followed by an unrelated `.`
|
|
98
|
+
* suffix (`claude-fable-5.md`) still matches. Dash-separated segments only:
|
|
99
|
+
* the dotted form never appears in a genuine id.
|
|
100
|
+
*
|
|
101
|
+
* The id body is captured inside a lookahead (`(?=(...))\1`) so the greedy
|
|
102
|
+
* `-\d+` run matches **atomically**: without it, a suffix-glued token like
|
|
103
|
+
* `claude-opus-4-1x` would fail the trailing anchor on the full match, then
|
|
104
|
+
* backtrack a segment and re-emit the bare `claude-opus-4` — the exact 404-able
|
|
105
|
+
* id this scan exists to suppress (two packed strings can end up glued with no
|
|
106
|
+
* separator in the extracted binary text). The atomic match fails outright
|
|
107
|
+
* instead of degrading to the bare form.
|
|
108
|
+
* - **`dropBareLegacyIds`.** A fully delimited string the anchors cannot tell
|
|
109
|
+
* apart from a real id — whether a bare major (`.includes("claude-opus-4")`)
|
|
110
|
+
* or a bare-minor per-cloud field value (`foundry:"claude-opus-4-1"`) — is
|
|
111
|
+
* dropped when a more-specific sibling is also present; a genuinely bare
|
|
112
|
+
* current id with no sibling (`claude-sonnet-5`) is kept.
|
|
113
|
+
*/
|
|
114
|
+
export declare function scanClaudeCatalogIds(text: string): string[];
|
|
79
115
|
/**
|
|
80
116
|
* Parse `grok models` stdout into a catalog. Exported for unit tests.
|
|
81
117
|
*
|
package/dist/lib/models.js
CHANGED
|
@@ -112,51 +112,8 @@ export function locateModelSource(agent, version) {
|
|
|
112
112
|
}
|
|
113
113
|
return null;
|
|
114
114
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
// 0.42+ inlines the same constants into a minified chunk under @google/gemini-cli/bundle/.
|
|
118
|
-
const modelsJs = path.join(versionDir, 'node_modules', '@google', 'gemini-cli-core', 'dist', 'src', 'config', 'models.js');
|
|
119
|
-
if (fs.existsSync(modelsJs))
|
|
120
|
-
return { path: modelsJs, kind: 'js' };
|
|
121
|
-
const bundleDir = path.join(versionDir, 'node_modules', '@google', 'gemini-cli', 'bundle');
|
|
122
|
-
if (fs.existsSync(bundleDir)) {
|
|
123
|
-
try {
|
|
124
|
-
const entries = fs.readdirSync(bundleDir);
|
|
125
|
-
// The model constants live in a single chunk; pick the first .js file
|
|
126
|
-
// whose contents contain VALID_GEMINI_MODELS. Skip subdirectories.
|
|
127
|
-
for (const name of entries) {
|
|
128
|
-
if (!name.endsWith('.js'))
|
|
129
|
-
continue;
|
|
130
|
-
const full = path.join(bundleDir, name);
|
|
131
|
-
let stat;
|
|
132
|
-
try {
|
|
133
|
-
stat = fs.statSync(full);
|
|
134
|
-
}
|
|
135
|
-
catch {
|
|
136
|
-
continue;
|
|
137
|
-
}
|
|
138
|
-
if (!stat.isFile())
|
|
139
|
-
continue;
|
|
140
|
-
// Most chunks just re-export the constants; only the defining chunk
|
|
141
|
-
// actually has `var VALID_GEMINI_MODELS = ... new Set(`. Match that
|
|
142
|
-
// shape so we pick the chunk that the extractor can parse.
|
|
143
|
-
let body;
|
|
144
|
-
try {
|
|
145
|
-
body = fs.readFileSync(full, 'utf-8');
|
|
146
|
-
}
|
|
147
|
-
catch {
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
if (/var\s+VALID_GEMINI_MODELS\s*=[^\n]*new\s+Set/.test(body) &&
|
|
151
|
-
/var\s+DEFAULT_GEMINI_MODEL\s*=/.test(body)) {
|
|
152
|
-
return { path: full, kind: 'js' };
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
catch { /* unreadable bundle dir */ }
|
|
157
|
-
}
|
|
158
|
-
return null;
|
|
159
|
-
}
|
|
115
|
+
// gemini is hard-deprecated (no launch path left to validate a model
|
|
116
|
+
// against), so its bundle is deliberately not parsed for a catalog here.
|
|
160
117
|
if (agent === 'opencode') {
|
|
161
118
|
// The `opencode` shim under node_modules/.bin dispatches to a platform-
|
|
162
119
|
// specific native binary. We don't parse the 100MB binary; we let the CLI
|
|
@@ -333,17 +290,60 @@ function extractStrings(filePath, minLen = 6) {
|
|
|
333
290
|
* - constants: {OPUS_ID:"...",OPUS_NAME:"...",SONNET_ID:"...",...}
|
|
334
291
|
*/
|
|
335
292
|
/**
|
|
336
|
-
* Drop a
|
|
337
|
-
* sibling (`claude-opus-4-8`
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
293
|
+
* Drop a catalog id that is a proper dash-boundary prefix of a more-specific
|
|
294
|
+
* sibling also present in the list (e.g. `claude-opus-4` when `claude-opus-4-8`
|
|
295
|
+
* is present, or `claude-opus-4-1` when `claude-opus-4-1-20250805` is present).
|
|
296
|
+
*
|
|
297
|
+
* Two sources produce these prefix forms in the native binary:
|
|
298
|
+
* - standalone `.includes("claude-opus-4")` prefix-check strings (#1892)
|
|
299
|
+
* - per-cloud metadata field values such as `foundry:"claude-opus-4-1"` next
|
|
300
|
+
* to a real firstParty id `claude-opus-4-1-20250805` (#2233)
|
|
301
|
+
*
|
|
302
|
+
* A genuine bare current id with no longer sibling (e.g. `claude-sonnet-5`) is
|
|
303
|
+
* kept. The dash boundary (`startsWith(id + '-')`) avoids collapsing
|
|
304
|
+
* `claude-opus-4-1` into `claude-opus-4-10`.
|
|
341
305
|
*/
|
|
342
306
|
export function dropBareLegacyIds(ids) {
|
|
343
|
-
return ids.filter((id) => {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
307
|
+
return ids.filter((id) => !ids.some((other) => other !== id && other.startsWith(`${id}-`)));
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Scan raw binary/bundle text for canonical Claude model ids, then drop bare
|
|
311
|
+
* legacy / cloud-metadata prefixes (#1892, #2233). Two independent guards keep
|
|
312
|
+
* non-model strings out of the catalog:
|
|
313
|
+
*
|
|
314
|
+
* - **Word-boundary anchors on the id regex.** The id must not be glued to a
|
|
315
|
+
* surrounding identifier character, and must not be the truncated prefix of a
|
|
316
|
+
* longer *version* token. `(?<![A-Za-z0-9_])` rejects a glued prefix;
|
|
317
|
+
* `(?![A-Za-z0-9])` rejects a glued alnum suffix; `(?!\.\d)` rejects a
|
|
318
|
+
* dotted-version continuation — so the bare-major prefix of the binary's own
|
|
319
|
+
* "Typo in model ID" troubleshooting string `claude-sonnet-4.6` is not
|
|
320
|
+
* scraped as `claude-sonnet-4`, while a real id followed by an unrelated `.`
|
|
321
|
+
* suffix (`claude-fable-5.md`) still matches. Dash-separated segments only:
|
|
322
|
+
* the dotted form never appears in a genuine id.
|
|
323
|
+
*
|
|
324
|
+
* The id body is captured inside a lookahead (`(?=(...))\1`) so the greedy
|
|
325
|
+
* `-\d+` run matches **atomically**: without it, a suffix-glued token like
|
|
326
|
+
* `claude-opus-4-1x` would fail the trailing anchor on the full match, then
|
|
327
|
+
* backtrack a segment and re-emit the bare `claude-opus-4` — the exact 404-able
|
|
328
|
+
* id this scan exists to suppress (two packed strings can end up glued with no
|
|
329
|
+
* separator in the extracted binary text). The atomic match fails outright
|
|
330
|
+
* instead of degrading to the bare form.
|
|
331
|
+
* - **`dropBareLegacyIds`.** A fully delimited string the anchors cannot tell
|
|
332
|
+
* apart from a real id — whether a bare major (`.includes("claude-opus-4")`)
|
|
333
|
+
* or a bare-minor per-cloud field value (`foundry:"claude-opus-4-1"`) — is
|
|
334
|
+
* dropped when a more-specific sibling is also present; a genuinely bare
|
|
335
|
+
* current id with no sibling (`claude-sonnet-5`) is kept.
|
|
336
|
+
*/
|
|
337
|
+
export function scanClaudeCatalogIds(text) {
|
|
338
|
+
const idRe = /(?<![A-Za-z0-9_])(?=(claude-(?:opus|sonnet|haiku|fable|mythos)-\d+(?:-\d+)*(?:-(?:fast|v\d+))?))\1(?![A-Za-z0-9])(?!\.\d)/g;
|
|
339
|
+
const scanned = new Set();
|
|
340
|
+
let sm;
|
|
341
|
+
while ((sm = idRe.exec(text)) !== null) {
|
|
342
|
+
scanned.add(sm[0]);
|
|
343
|
+
if (sm.index === idRe.lastIndex)
|
|
344
|
+
idRe.lastIndex++;
|
|
345
|
+
}
|
|
346
|
+
return dropBareLegacyIds([...scanned]);
|
|
347
347
|
}
|
|
348
348
|
function extractClaudeCatalog(text) {
|
|
349
349
|
const aliases = {};
|
|
@@ -406,16 +406,7 @@ function extractClaudeCatalog(text) {
|
|
|
406
406
|
// catalog while a newer one still gets a real catalog (incl. fable/mythos and
|
|
407
407
|
// the opus-5/sonnet-5 line) rather than an empty or single-model one.
|
|
408
408
|
if (models.length < 2) {
|
|
409
|
-
const
|
|
410
|
-
// Dash-separated segments only. Real ids are `claude-sonnet-4-6`; the dotted
|
|
411
|
-
// form `claude-sonnet-4.6` appears only inside the binary's own "Typo in
|
|
412
|
-
// model ID" troubleshooting text, so a `.`-permitting pattern would scrape a
|
|
413
|
-
// non-model string as if it were real.
|
|
414
|
-
const idRe = /claude-(?:opus|sonnet|haiku|fable|mythos)-\d+(?:-\d+)*(?:-(?:fast|v\d+))?/g;
|
|
415
|
-
let sm;
|
|
416
|
-
while ((sm = idRe.exec(text)) !== null)
|
|
417
|
-
scanned.add(sm[0]);
|
|
418
|
-
const filtered = dropBareLegacyIds([...scanned]);
|
|
409
|
+
const filtered = scanClaudeCatalogIds(text);
|
|
419
410
|
if (filtered.length >= 2)
|
|
420
411
|
models = build(filtered);
|
|
421
412
|
}
|
|
@@ -460,85 +451,6 @@ function extractCodexCatalog(text) {
|
|
|
460
451
|
}
|
|
461
452
|
return { models, aliases: {} };
|
|
462
453
|
}
|
|
463
|
-
/**
|
|
464
|
-
* Extract Gemini's model catalog from `@google/gemini-cli-core/.../config/models.js`.
|
|
465
|
-
*
|
|
466
|
-
* The module exports a set of named constants (e.g. `DEFAULT_GEMINI_MODEL`,
|
|
467
|
-
* `PREVIEW_GEMINI_FLASH_MODEL`) plus a `VALID_GEMINI_MODELS` Set and a handful
|
|
468
|
-
* of `GEMINI_MODEL_ALIAS_*` strings. We parse it with regex (a JS-module import
|
|
469
|
-
* would pollute the runtime and ES-module interop is awkward from a CJS build).
|
|
470
|
-
*/
|
|
471
|
-
function extractGeminiCatalog(text) {
|
|
472
|
-
// Old (<=0.41) layout used `export const FOO = 'bar';` in models.js.
|
|
473
|
-
// New (0.42+) bundle inlines the same constants as `var FOO = "bar";`.
|
|
474
|
-
const constRe = /(?:export\s+const|var)\s+([A-Z0-9_]+)\s*=\s*['"]([^'"]+)['"]/g;
|
|
475
|
-
const constants = new Map();
|
|
476
|
-
let m;
|
|
477
|
-
while ((m = constRe.exec(text)) !== null) {
|
|
478
|
-
constants.set(m[1], m[2]);
|
|
479
|
-
}
|
|
480
|
-
// The set of ids the CLI accepts as "valid model names". Names (not values)
|
|
481
|
-
// are listed inside `new Set([...])`, so we expand them via the constants map.
|
|
482
|
-
const validIds = new Set();
|
|
483
|
-
const setBlock = text.match(/VALID_GEMINI_MODELS\s*=\s*(?:\/\*[^*]*\*\/\s*)?new\s+Set\(\[([\s\S]*?)\]\)/);
|
|
484
|
-
if (setBlock) {
|
|
485
|
-
const nameRe = /([A-Z0-9_]+)/g;
|
|
486
|
-
let nm;
|
|
487
|
-
while ((nm = nameRe.exec(setBlock[1])) !== null) {
|
|
488
|
-
const id = constants.get(nm[1]);
|
|
489
|
-
if (id)
|
|
490
|
-
validIds.add(id);
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
// Fall back to any gemini-shaped id we saw in the constants map -- useful
|
|
494
|
-
// when the Set shape changes across gemini versions.
|
|
495
|
-
if (validIds.size === 0) {
|
|
496
|
-
for (const [name, value] of constants) {
|
|
497
|
-
if (/^(DEFAULT_|PREVIEW_)/.test(name) && /^gemini-/.test(value)) {
|
|
498
|
-
validIds.add(value);
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
// Aliases are exported as `GEMINI_MODEL_ALIAS_FLASH = 'flash'` etc. The alias
|
|
503
|
-
// is the *value*; the target model is resolved at runtime by `resolveModel()`.
|
|
504
|
-
// We replicate that logic here so callers get a concrete id per alias.
|
|
505
|
-
const defaultId = constants.get('DEFAULT_GEMINI_MODEL');
|
|
506
|
-
const previewPro = constants.get('PREVIEW_GEMINI_MODEL');
|
|
507
|
-
const previewFlash = constants.get('PREVIEW_GEMINI_FLASH_MODEL');
|
|
508
|
-
const flashLite = constants.get('DEFAULT_GEMINI_FLASH_LITE_MODEL');
|
|
509
|
-
const aliases = {};
|
|
510
|
-
if (previewPro) {
|
|
511
|
-
aliases.auto = previewPro;
|
|
512
|
-
aliases.pro = previewPro;
|
|
513
|
-
}
|
|
514
|
-
if (previewFlash)
|
|
515
|
-
aliases.flash = previewFlash;
|
|
516
|
-
if (flashLite)
|
|
517
|
-
aliases['flash-lite'] = flashLite;
|
|
518
|
-
const aliasReverse = {};
|
|
519
|
-
for (const [alias, id] of Object.entries(aliases)) {
|
|
520
|
-
(aliasReverse[id] ||= []).push(alias);
|
|
521
|
-
}
|
|
522
|
-
const defaults = new Set();
|
|
523
|
-
if (defaultId)
|
|
524
|
-
defaults.add(defaultId);
|
|
525
|
-
if (previewPro)
|
|
526
|
-
defaults.add(previewPro); // auto/pro alias resolves here
|
|
527
|
-
const displayNameFor = (id) => {
|
|
528
|
-
// Gemini has a `getDisplayString` for some aliases but the canonical id
|
|
529
|
-
// is human-readable enough ("gemini-3-pro-preview") -- no separate map.
|
|
530
|
-
return undefined;
|
|
531
|
-
};
|
|
532
|
-
const models = Array.from(validIds)
|
|
533
|
-
.sort()
|
|
534
|
-
.map((id) => ({
|
|
535
|
-
id,
|
|
536
|
-
displayName: displayNameFor(id),
|
|
537
|
-
alias: aliasReverse[id]?.[0],
|
|
538
|
-
isDefault: defaults.has(id),
|
|
539
|
-
}));
|
|
540
|
-
return { models, aliases };
|
|
541
|
-
}
|
|
542
454
|
/**
|
|
543
455
|
* Extract OpenCode's catalog by invoking `opencode models --verbose`. The
|
|
544
456
|
* output is a sequence of `<provider>/<id>\n{json}` blocks -- we parse every
|
|
@@ -1042,16 +954,6 @@ export function getModelCatalog(agent, version) {
|
|
|
1042
954
|
: agent === 'codex' ? extractCodexCatalog(text)
|
|
1043
955
|
: { models: [], aliases: {} });
|
|
1044
956
|
}
|
|
1045
|
-
else if (src.kind === 'js') {
|
|
1046
|
-
try {
|
|
1047
|
-
const text = fs.readFileSync(src.path, 'utf-8');
|
|
1048
|
-
if (agent === 'gemini')
|
|
1049
|
-
({ models, aliases } = extractGeminiCatalog(text));
|
|
1050
|
-
}
|
|
1051
|
-
catch {
|
|
1052
|
-
/* unreadable */
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
957
|
else if (src.kind === 'cli') {
|
|
1056
958
|
if (agent === 'opencode')
|
|
1057
959
|
({ models, aliases } = extractOpenCodeCatalog(src.path));
|
package/dist/lib/perf/db.js
CHANGED
|
@@ -244,7 +244,7 @@ export function aggregateSamples(opts = {}) {
|
|
|
244
244
|
const key = `${r.kind}\0${r.label}`;
|
|
245
245
|
let b = map.get(key);
|
|
246
246
|
if (!b) {
|
|
247
|
-
b = { kind: r.kind, label: r.label, durations: [], hits: 0, stale: 0, misses: 0, errors: 0, timeouts: 0 };
|
|
247
|
+
b = { kind: r.kind, label: r.label, durations: [], hits: 0, stale: 0, misses: 0, errors: 0, blocks: 0, timeouts: 0 };
|
|
248
248
|
map.set(key, b);
|
|
249
249
|
}
|
|
250
250
|
b.durations.push(Number(r.duration_ms));
|
|
@@ -254,10 +254,19 @@ export function aggregateSamples(opts = {}) {
|
|
|
254
254
|
b.stale++;
|
|
255
255
|
else if (r.cache === 'miss' || r.cache === 'none')
|
|
256
256
|
b.misses++;
|
|
257
|
+
// Exit classes (Claude/Codex PreToolUse convention):
|
|
258
|
+
// 0 → allowed
|
|
259
|
+
// 2 → intentional deny/block (not a crash)
|
|
260
|
+
// 1 / other nonzero → real error
|
|
261
|
+
// Timeouts are recorded via status, not exit_code, so they don't double-count.
|
|
257
262
|
if (r.status === 'timeout')
|
|
258
263
|
b.timeouts++;
|
|
259
|
-
else if (typeof r.exit_code === 'number'
|
|
260
|
-
|
|
264
|
+
else if (typeof r.exit_code === 'number') {
|
|
265
|
+
if (r.exit_code === 2)
|
|
266
|
+
b.blocks++;
|
|
267
|
+
else if (r.exit_code !== 0)
|
|
268
|
+
b.errors++;
|
|
269
|
+
}
|
|
261
270
|
}
|
|
262
271
|
const out = [];
|
|
263
272
|
for (const b of map.values()) {
|
|
@@ -286,6 +295,10 @@ export function aggregateSamples(opts = {}) {
|
|
|
286
295
|
row.errorCount = b.errors;
|
|
287
296
|
row.errorRate = Math.round((b.errors / n) * 1000) / 1000;
|
|
288
297
|
}
|
|
298
|
+
if (b.blocks > 0) {
|
|
299
|
+
row.blockCount = b.blocks;
|
|
300
|
+
row.blockRate = Math.round((b.blocks / n) * 1000) / 1000;
|
|
301
|
+
}
|
|
289
302
|
if (b.timeouts > 0)
|
|
290
303
|
row.timeoutRate = Math.round((b.timeouts / n) * 1000) / 1000;
|
|
291
304
|
if (opts.project)
|
package/dist/lib/perf/types.d.ts
CHANGED
|
@@ -35,8 +35,19 @@ export interface PerfAggregateRow {
|
|
|
35
35
|
cacheStalePct?: number;
|
|
36
36
|
cacheMissPct?: number;
|
|
37
37
|
errorCount?: number;
|
|
38
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* Fraction (0-1) of samples with a real crash exit (exit 1 / other nonzero
|
|
40
|
+
* except the intentional PreToolUse deny code 2). Exit 2 is blockRate.
|
|
41
|
+
*/
|
|
39
42
|
errorRate?: number;
|
|
43
|
+
/**
|
|
44
|
+
* Count of intentional deny/block exits (Claude/Codex PreToolUse exit 2).
|
|
45
|
+
* Not an error — deny-by-design guards (ask-user-question-guard, git-guard,
|
|
46
|
+
* plan-html-reminder) exit 2 when they block.
|
|
47
|
+
*/
|
|
48
|
+
blockCount?: number;
|
|
49
|
+
/** Fraction (0-1) of samples with exit code 2 (intentional deny/block). */
|
|
50
|
+
blockRate?: number;
|
|
40
51
|
/** Fraction (0-1) of samples with status:'timeout'. */
|
|
41
52
|
timeoutRate?: number;
|
|
42
53
|
/** Project key (see project-key.ts) the row is scoped to — set only when
|
|
@@ -14,6 +14,15 @@ export interface TokenUsage {
|
|
|
14
14
|
* convention for models that don't publish a separate cache price).
|
|
15
15
|
*/
|
|
16
16
|
export declare function costOfUsage(u: TokenUsage): number;
|
|
17
|
+
/**
|
|
18
|
+
* USD cost of one usage record priced as if caching were OFF: cache-read and
|
|
19
|
+
* cache-write tokens are billed at the model's full INPUT rate rather than their
|
|
20
|
+
* discounted cache rates. This is the "what would this have cost with no prompt
|
|
21
|
+
* caching?" scenario that `agents output --pricing no-cache` models. Output and
|
|
22
|
+
* uncached input are unchanged — only the cache tokens are repriced. Returns 0
|
|
23
|
+
* for a missing/unpriced model, exactly like {@link costOfUsage}.
|
|
24
|
+
*/
|
|
25
|
+
export declare function costOfUsageNoCache(u: TokenUsage): number;
|
|
17
26
|
/** Sum the USD cost of every usage record in a session. */
|
|
18
27
|
export declare function costOfSession(usages: TokenUsage[]): number;
|
|
19
28
|
/**
|
package/dist/lib/pricing/cost.js
CHANGED
|
@@ -31,6 +31,30 @@ export function costOfUsage(u) {
|
|
|
31
31
|
cacheRead * cacheReadRate +
|
|
32
32
|
cacheWrite * cacheWriteRate);
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* USD cost of one usage record priced as if caching were OFF: cache-read and
|
|
36
|
+
* cache-write tokens are billed at the model's full INPUT rate rather than their
|
|
37
|
+
* discounted cache rates. This is the "what would this have cost with no prompt
|
|
38
|
+
* caching?" scenario that `agents output --pricing no-cache` models. Output and
|
|
39
|
+
* uncached input are unchanged — only the cache tokens are repriced. Returns 0
|
|
40
|
+
* for a missing/unpriced model, exactly like {@link costOfUsage}.
|
|
41
|
+
*/
|
|
42
|
+
export function costOfUsageNoCache(u) {
|
|
43
|
+
if (!u.model)
|
|
44
|
+
return 0;
|
|
45
|
+
const pricing = getModelPricing(u.model);
|
|
46
|
+
if (!pricing)
|
|
47
|
+
return 0;
|
|
48
|
+
const input = u.inputTokens ?? 0;
|
|
49
|
+
const output = u.outputTokens ?? 0;
|
|
50
|
+
const cacheRead = u.cacheReadTokens ?? 0;
|
|
51
|
+
const cacheWrite = u.cacheCreationTokens ?? 0;
|
|
52
|
+
// Cache tokens repriced at the input rate — no caching discount applied.
|
|
53
|
+
return (input * pricing.inputPerToken +
|
|
54
|
+
output * pricing.outputPerToken +
|
|
55
|
+
cacheRead * pricing.inputPerToken +
|
|
56
|
+
cacheWrite * pricing.inputPerToken);
|
|
57
|
+
}
|
|
34
58
|
/** Sum the USD cost of every usage record in a session. */
|
|
35
59
|
export function costOfSession(usages) {
|
|
36
60
|
let total = 0;
|
|
@@ -5,4 +5,4 @@
|
|
|
5
5
|
* enforcement) imports against — keep it stable.
|
|
6
6
|
*/
|
|
7
7
|
export { type ModelPricing, PRICING_VERSION, getModelPricing, listPricedModels, } from './table.js';
|
|
8
|
-
export { type TokenUsage, costOfUsage, costOfSession, formatUsd, estimateCost, actualCost, isModelPriced, } from './cost.js';
|
|
8
|
+
export { type TokenUsage, costOfUsage, costOfUsageNoCache, costOfSession, formatUsd, estimateCost, actualCost, isModelPriced, } from './cost.js';
|
|
@@ -5,4 +5,4 @@
|
|
|
5
5
|
* enforcement) imports against — keep it stable.
|
|
6
6
|
*/
|
|
7
7
|
export { PRICING_VERSION, getModelPricing, listPricedModels, } from './table.js';
|
|
8
|
-
export { costOfUsage, costOfSession, formatUsd, estimateCost, actualCost, isModelPriced, } from './cost.js';
|
|
8
|
+
export { costOfUsage, costOfUsageNoCache, costOfSession, formatUsd, estimateCost, actualCost, isModelPriced, } from './cost.js';
|
|
@@ -73,6 +73,16 @@ export declare function formatWorkspaceLine(s: RepoWorkspaceStatus): string;
|
|
|
73
73
|
* `fleet` row label.
|
|
74
74
|
*/
|
|
75
75
|
export declare function formatFleetWorkspaces(statuses: HostWorkspaceStatus[]): string[];
|
|
76
|
+
/**
|
|
77
|
+
* One-line fleet health summary — `6/13 clean · 4 behind · 4 dirty · 1 missing`.
|
|
78
|
+
* Sits ABOVE the per-host {@link formatFleetWorkspaces} table so the card is
|
|
79
|
+
* scannable without reading every host cell; the table keeps the per-host branch
|
|
80
|
+
* and drift detail. Zero buckets are omitted. `behind` colours red when any host
|
|
81
|
+
* is ≥10 behind (matching the footer's critical threshold), else yellow; a host
|
|
82
|
+
* that is both behind and dirty counts in both. Each host×path row is one unit,
|
|
83
|
+
* the same unit the table renders. Pure — chalk styling only.
|
|
84
|
+
*/
|
|
85
|
+
export declare function formatFleetSummary(statuses: HostWorkspaceStatus[]): string;
|
|
76
86
|
/** Severity for a workspace/repo warning on the project card. */
|
|
77
87
|
export type WorkspaceWarningSeverity = 'critical' | 'continue';
|
|
78
88
|
export interface WorkspaceWarning {
|
|
@@ -81,13 +91,22 @@ export interface WorkspaceWarning {
|
|
|
81
91
|
remediation?: string;
|
|
82
92
|
}
|
|
83
93
|
/**
|
|
84
|
-
* Turn probed workspace rows into card-footer warnings
|
|
94
|
+
* Turn probed workspace rows into card-footer warnings, GROUPED by root cause so
|
|
95
|
+
* a fleet where eight hosts drift is a few lines, not sixteen (each with its own
|
|
96
|
+
* repeated remediation).
|
|
85
97
|
*
|
|
86
98
|
* - missing / unreadable git → critical (agents there cannot share a tree)
|
|
87
|
-
* - behind upstream → critical when ≥10 commits, continue
|
|
99
|
+
* - behind upstream → critical when ANY host is ≥10 commits behind, else continue
|
|
88
100
|
* - dirty tree → continue (local work is fine; just note it)
|
|
89
101
|
* - ahead-only is not a warning (that is progress waiting to push)
|
|
90
102
|
*
|
|
91
|
-
*
|
|
103
|
+
* Within one probed path, all behind hosts collapse to one warning listing each
|
|
104
|
+
* host with its count (`4 hosts behind origin/main — mac-mini ↓172, …`) plus one
|
|
105
|
+
* shared remediation; a lone host keeps its full sentence. Missing and dirty
|
|
106
|
+
* collapse the same way. `error` stays per-host (each message is distinct).
|
|
107
|
+
* Grouping is per path so two different repos never merge into one count. Unlike
|
|
108
|
+
* doctor's `emitGroup`, the list names EVERY host, not the first two — each
|
|
109
|
+
* host's drift count differs and is individually actionable. Pure — chalk only.
|
|
110
|
+
* Caller decides whether the rows came from `--fleet` or a local probe.
|
|
92
111
|
*/
|
|
93
112
|
export declare function workspaceWarnings(statuses: HostWorkspaceStatus[]): WorkspaceWarning[];
|
|
@@ -159,46 +159,134 @@ export function formatFleetWorkspaces(statuses) {
|
|
|
159
159
|
});
|
|
160
160
|
}
|
|
161
161
|
/**
|
|
162
|
-
*
|
|
162
|
+
* One-line fleet health summary — `6/13 clean · 4 behind · 4 dirty · 1 missing`.
|
|
163
|
+
* Sits ABOVE the per-host {@link formatFleetWorkspaces} table so the card is
|
|
164
|
+
* scannable without reading every host cell; the table keeps the per-host branch
|
|
165
|
+
* and drift detail. Zero buckets are omitted. `behind` colours red when any host
|
|
166
|
+
* is ≥10 behind (matching the footer's critical threshold), else yellow; a host
|
|
167
|
+
* that is both behind and dirty counts in both. Each host×path row is one unit,
|
|
168
|
+
* the same unit the table renders. Pure — chalk styling only.
|
|
169
|
+
*/
|
|
170
|
+
export function formatFleetSummary(statuses) {
|
|
171
|
+
const total = statuses.length;
|
|
172
|
+
let clean = 0;
|
|
173
|
+
let behind = 0;
|
|
174
|
+
let dirty = 0;
|
|
175
|
+
let missing = 0;
|
|
176
|
+
let hardBehind = false;
|
|
177
|
+
for (const s of statuses) {
|
|
178
|
+
if (!s.present) {
|
|
179
|
+
missing++;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (s.error)
|
|
183
|
+
continue; // unreadable — neither clean nor a drift bucket (surfaced in the footer)
|
|
184
|
+
const isBehind = s.behind !== undefined && s.behind > 0;
|
|
185
|
+
const isDirty = s.dirty !== undefined && s.dirty > 0;
|
|
186
|
+
if (isBehind) {
|
|
187
|
+
behind++;
|
|
188
|
+
if (s.behind >= 10)
|
|
189
|
+
hardBehind = true;
|
|
190
|
+
}
|
|
191
|
+
if (isDirty)
|
|
192
|
+
dirty++;
|
|
193
|
+
if (!isBehind && !isDirty)
|
|
194
|
+
clean++;
|
|
195
|
+
}
|
|
196
|
+
const parts = [chalk.green(`${clean}/${total} clean`)];
|
|
197
|
+
if (behind)
|
|
198
|
+
parts.push((hardBehind ? chalk.red : chalk.yellow)(`${behind} behind`));
|
|
199
|
+
if (dirty)
|
|
200
|
+
parts.push(chalk.yellow(`${dirty} dirty`));
|
|
201
|
+
if (missing)
|
|
202
|
+
parts.push(chalk.red(`${missing} missing`));
|
|
203
|
+
return parts.join(chalk.dim(' · '));
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Turn probed workspace rows into card-footer warnings, GROUPED by root cause so
|
|
207
|
+
* a fleet where eight hosts drift is a few lines, not sixteen (each with its own
|
|
208
|
+
* repeated remediation).
|
|
163
209
|
*
|
|
164
210
|
* - missing / unreadable git → critical (agents there cannot share a tree)
|
|
165
|
-
* - behind upstream → critical when ≥10 commits, continue
|
|
211
|
+
* - behind upstream → critical when ANY host is ≥10 commits behind, else continue
|
|
166
212
|
* - dirty tree → continue (local work is fine; just note it)
|
|
167
213
|
* - ahead-only is not a warning (that is progress waiting to push)
|
|
168
214
|
*
|
|
169
|
-
*
|
|
215
|
+
* Within one probed path, all behind hosts collapse to one warning listing each
|
|
216
|
+
* host with its count (`4 hosts behind origin/main — mac-mini ↓172, …`) plus one
|
|
217
|
+
* shared remediation; a lone host keeps its full sentence. Missing and dirty
|
|
218
|
+
* collapse the same way. `error` stays per-host (each message is distinct).
|
|
219
|
+
* Grouping is per path so two different repos never merge into one count. Unlike
|
|
220
|
+
* doctor's `emitGroup`, the list names EVERY host, not the first two — each
|
|
221
|
+
* host's drift count differs and is individually actionable. Pure — chalk only.
|
|
222
|
+
* Caller decides whether the rows came from `--fleet` or a local probe.
|
|
170
223
|
*/
|
|
171
224
|
export function workspaceWarnings(statuses) {
|
|
172
225
|
const out = [];
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
226
|
+
const where = (s) => (s.host ? s.host : 'local');
|
|
227
|
+
const paths = [...new Set(statuses.map((s) => s.path))].sort((a, b) => a.localeCompare(b));
|
|
228
|
+
for (const path of paths) {
|
|
229
|
+
const pathBit = path ? ` (${path})` : '';
|
|
230
|
+
const rows = statuses.filter((s) => s.path === path);
|
|
231
|
+
const present = rows.filter((s) => s.present && !s.error);
|
|
232
|
+
// Missing checkout — grouped critical, a lone host keeps its full sentence.
|
|
233
|
+
const missing = rows.filter((s) => !s.present).sort((a, b) => where(a).localeCompare(where(b)));
|
|
234
|
+
if (missing.length === 1) {
|
|
177
235
|
out.push({
|
|
178
236
|
severity: 'critical',
|
|
179
|
-
text: `${where}: checkout missing${pathBit}`,
|
|
237
|
+
text: `${where(missing[0])}: checkout missing${pathBit}`,
|
|
180
238
|
remediation: 'clone or sync the project root on that host before landing agents there',
|
|
181
239
|
});
|
|
182
|
-
continue;
|
|
183
240
|
}
|
|
184
|
-
if (
|
|
241
|
+
else if (missing.length > 1) {
|
|
185
242
|
out.push({
|
|
186
243
|
severity: 'critical',
|
|
187
|
-
text: `${
|
|
244
|
+
text: `${missing.length} hosts missing checkout${pathBit} — ${missing.map(where).join(', ')}`,
|
|
245
|
+
remediation: 'clone or sync the project root on those hosts before landing agents there',
|
|
188
246
|
});
|
|
189
|
-
continue;
|
|
190
247
|
}
|
|
191
|
-
|
|
248
|
+
// Unreadable git — one per host, since each error message is distinct.
|
|
249
|
+
for (const s of rows.filter((s) => s.present && s.error).sort((a, b) => where(a).localeCompare(where(b)))) {
|
|
250
|
+
out.push({ severity: 'critical', text: `${where(s)}: ${s.error}${pathBit}` });
|
|
251
|
+
}
|
|
252
|
+
// Behind upstream — grouped, worst count first, one shared remediation.
|
|
253
|
+
const behind = present
|
|
254
|
+
.filter((s) => s.behind !== undefined && s.behind > 0)
|
|
255
|
+
.sort((a, b) => (b.behind ?? 0) - (a.behind ?? 0) || where(a).localeCompare(where(b)));
|
|
256
|
+
if (behind.length === 1) {
|
|
257
|
+
const s = behind[0];
|
|
192
258
|
out.push({
|
|
193
|
-
severity: s.behind >= 10 ? 'critical' : 'continue',
|
|
194
|
-
text: `${where} is ${s.behind} commit${s.behind === 1 ? '' : 's'} behind ${s.upstream ?? 'upstream'}${pathBit}`,
|
|
259
|
+
severity: (s.behind ?? 0) >= 10 ? 'critical' : 'continue',
|
|
260
|
+
text: `${where(s)} is ${s.behind} commit${s.behind === 1 ? '' : 's'} behind ${s.upstream ?? 'upstream'}${pathBit}`,
|
|
195
261
|
remediation: 'pull (or rebase) before agents on this host open PRs against a stale base',
|
|
196
262
|
});
|
|
197
263
|
}
|
|
198
|
-
if (
|
|
264
|
+
else if (behind.length > 1) {
|
|
265
|
+
const upstreams = new Set(behind.map((s) => s.upstream ?? 'upstream'));
|
|
266
|
+
const upstream = upstreams.size === 1 ? [...upstreams][0] : 'upstream';
|
|
267
|
+
const list = behind.map((s) => `${where(s)} ↓${s.behind}`).join(', ');
|
|
268
|
+
out.push({
|
|
269
|
+
severity: behind.some((s) => (s.behind ?? 0) >= 10) ? 'critical' : 'continue',
|
|
270
|
+
text: `${behind.length} hosts behind ${upstream}${pathBit} — ${list}`,
|
|
271
|
+
remediation: 'pull (or rebase) before agents on these hosts open PRs against a stale base',
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
// Dirty tree — grouped, most changes first, no remediation (local work is fine).
|
|
275
|
+
const dirty = present
|
|
276
|
+
.filter((s) => s.dirty !== undefined && s.dirty > 0)
|
|
277
|
+
.sort((a, b) => (b.dirty ?? 0) - (a.dirty ?? 0) || where(a).localeCompare(where(b)));
|
|
278
|
+
if (dirty.length === 1) {
|
|
279
|
+
const s = dirty[0];
|
|
280
|
+
out.push({
|
|
281
|
+
severity: 'continue',
|
|
282
|
+
text: `${where(s)} has ${s.dirty} uncommitted change${s.dirty === 1 ? '' : 's'}${pathBit}`,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
else if (dirty.length > 1) {
|
|
286
|
+
const list = dirty.map((s) => `${where(s)} ${s.dirty}`).join(', ');
|
|
199
287
|
out.push({
|
|
200
288
|
severity: 'continue',
|
|
201
|
-
text: `${
|
|
289
|
+
text: `${dirty.length} hosts with uncommitted changes${pathBit} — ${list}`,
|
|
202
290
|
});
|
|
203
291
|
}
|
|
204
292
|
}
|
|
@@ -87,6 +87,15 @@ export interface LiveDeadSplit {
|
|
|
87
87
|
* crashed sessions is itself a thing to go fix.
|
|
88
88
|
*/
|
|
89
89
|
export declare function liveDeadSplit(byStatus: Partial<Record<ActiveStatus, number>>): LiveDeadSplit;
|
|
90
|
+
/**
|
|
91
|
+
* The `dead` row body: `41 crashed` when every dead session shares one status,
|
|
92
|
+
* else `12 finished or lost (8 crashed, 4 closed)`. The generic "finished or
|
|
93
|
+
* lost" only earns its keep when the statuses actually differ — with a single
|
|
94
|
+
* status it just hides which one behind a parenthetical that repeats the count.
|
|
95
|
+
* Pure — chalk styling only; the caller adds the `dead` label. Assumes
|
|
96
|
+
* `split.dead > 0` (the caller gates on it).
|
|
97
|
+
*/
|
|
98
|
+
export declare function formatDeadSummary(split: LiveDeadSplit): string;
|
|
90
99
|
/** Sort members for the card: running first, then idle, then the rest; agent name asc within a state. */
|
|
91
100
|
export declare function sortProjectMembers(members: ProjectMember[]): ProjectMember[];
|
|
92
101
|
/** Cap for the members line before it collapses to `+N more`. */
|