@phnx-labs/agents-cli 1.21.2 → 1.22.0
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 +101 -0
- package/README.md +32 -3
- package/dist/bin/agents +0 -0
- package/dist/commands/computer-actions.d.ts +4 -0
- package/dist/commands/computer-actions.js +35 -0
- package/dist/commands/computer.js +4 -2
- package/dist/commands/exec.d.ts +27 -0
- package/dist/commands/exec.js +123 -6
- package/dist/commands/models.js +36 -1
- package/dist/commands/perf.d.ts +16 -0
- package/dist/commands/perf.js +11 -1
- package/dist/commands/projects.d.ts +11 -1
- package/dist/commands/projects.js +38 -4
- package/dist/commands/sessions-backfill.d.ts +32 -0
- package/dist/commands/sessions-backfill.js +186 -0
- package/dist/commands/sessions-picker.js +17 -2
- package/dist/commands/sessions.d.ts +22 -1
- package/dist/commands/sessions.js +331 -18
- package/dist/commands/teams.js +1 -1
- package/dist/commands/worktree.d.ts +3 -3
- package/dist/commands/worktree.js +35 -4
- package/dist/index.js +8 -0
- package/dist/lib/browser/service.js +13 -0
- package/dist/lib/computer/dispatch.d.ts +3 -1
- package/dist/lib/computer/dispatch.js +10 -2
- package/dist/lib/daemon.d.ts +5 -1
- package/dist/lib/daemon.js +63 -14
- package/dist/lib/devices/resolve-target.d.ts +6 -0
- package/dist/lib/devices/resolve-target.js +9 -3
- package/dist/lib/event-stream.d.ts +2 -0
- package/dist/lib/event-stream.js +3 -0
- package/dist/lib/events.d.ts +3 -1
- package/dist/lib/events.js +4 -2
- package/dist/lib/exec.js +39 -8
- package/dist/lib/git.d.ts +14 -0
- package/dist/lib/git.js +36 -0
- package/dist/lib/hooks/profile.js +1 -14
- package/dist/lib/hosts/dispatch.d.ts +12 -0
- package/dist/lib/hosts/dispatch.js +23 -6
- package/dist/lib/hosts/reconnect.d.ts +38 -0
- package/dist/lib/hosts/reconnect.js +85 -4
- package/dist/lib/hosts/run-target.js +14 -2
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/model-tiers.d.ts +54 -0
- package/dist/lib/model-tiers.js +229 -0
- package/dist/lib/models.d.ts +3 -0
- package/dist/lib/models.js +44 -7
- package/dist/lib/percentile.d.ts +12 -0
- package/dist/lib/percentile.js +24 -0
- package/dist/lib/perf/db.d.ts +1 -2
- package/dist/lib/perf/db.js +2 -14
- package/dist/lib/plugins.js +12 -1
- package/dist/lib/pricing/prices.json +16 -1
- package/dist/lib/project-focus.d.ts +42 -0
- package/dist/lib/project-focus.js +80 -0
- package/dist/lib/project-schedule.d.ts +75 -0
- package/dist/lib/project-schedule.js +110 -0
- package/dist/lib/redact.d.ts +2 -0
- package/dist/lib/redact.js +22 -0
- package/dist/lib/remote-agents-json.d.ts +2 -0
- package/dist/lib/remote-agents-json.js +3 -3
- package/dist/lib/resources.d.ts +16 -0
- package/dist/lib/resources.js +25 -14
- package/dist/lib/rotate.d.ts +84 -1
- package/dist/lib/rotate.js +155 -5
- package/dist/lib/routines.js +1 -14
- package/dist/lib/runner.d.ts +4 -2
- package/dist/lib/runner.js +21 -5
- 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/session/bash-command.js +60 -9
- package/dist/lib/session/db.d.ts +22 -1
- package/dist/lib/session/db.js +516 -24
- package/dist/lib/session/discover.d.ts +68 -7
- package/dist/lib/session/discover.js +186 -84
- package/dist/lib/session/highlights.d.ts +24 -4
- package/dist/lib/session/highlights.js +52 -7
- package/dist/lib/session/parse.d.ts +8 -1
- package/dist/lib/session/parse.js +102 -35
- package/dist/lib/session/prompt.d.ts +19 -0
- package/dist/lib/session/prompt.js +43 -0
- package/dist/lib/session/remote-list.d.ts +71 -0
- package/dist/lib/session/remote-list.js +410 -2
- package/dist/lib/session/shell-programs.d.ts +15 -0
- package/dist/lib/session/shell-programs.js +359 -0
- package/dist/lib/session/tool-calls.d.ts +88 -0
- package/dist/lib/session/tool-calls.js +612 -0
- package/dist/lib/session/tool-index.d.ts +100 -0
- package/dist/lib/session/tool-index.js +773 -0
- package/dist/lib/session/tool-store.d.ts +15 -0
- package/dist/lib/session/tool-store.js +198 -0
- package/dist/lib/session/types.d.ts +49 -0
- package/dist/lib/state.d.ts +10 -1
- package/dist/lib/state.js +11 -2
- package/dist/lib/teams/remoteWorktree.d.ts +3 -4
- package/dist/lib/teams/remoteWorktree.js +3 -4
- package/dist/lib/teams/worktree.d.ts +11 -1
- package/dist/lib/teams/worktree.js +42 -4
- package/dist/lib/types.d.ts +31 -0
- package/dist/lib/types.js +17 -0
- package/package.json +3 -1
package/dist/lib/plugins.js
CHANGED
|
@@ -14,7 +14,7 @@ import * as yaml from 'yaml';
|
|
|
14
14
|
import { execFileSync } from 'child_process';
|
|
15
15
|
import { getPluginsDir, getTrashPluginsDir, getExtraPluginsDir, getProjectPluginsDir, getSystemPluginsDir } from './state.js';
|
|
16
16
|
import { IS_WINDOWS, isWindowsAbsolutePath, homeDir } from './platform/index.js';
|
|
17
|
-
import { assertSafeGitTransport } from './git.js';
|
|
17
|
+
import { assertSafeGitTransport, resolveSnapshotSha } from './git.js';
|
|
18
18
|
import { listInstalledVersions, getVersionHomePath } from './versions.js';
|
|
19
19
|
import { AGENTS, agentConfigDirName } from './agents.js';
|
|
20
20
|
import { capableAgents, isCapable } from './capabilities.js';
|
|
@@ -90,6 +90,13 @@ export function discoverPlugins(opts = {}) {
|
|
|
90
90
|
return out;
|
|
91
91
|
}
|
|
92
92
|
export function buildDiscoveredPlugin(pluginRoot, manifest, spec = { kind: 'user' }) {
|
|
93
|
+
// Every marketplace kind lays plugins out as `<repo>/plugins/<name>`, so the
|
|
94
|
+
// repo root is always the grandparent of pluginRoot — true for user
|
|
95
|
+
// (~/.agents), system (~/.agents/.system), each extra repo, and the project
|
|
96
|
+
// repo (<cwd>/.agents) alike. Deriving it here means every caller of
|
|
97
|
+
// buildDiscoveredPlugin (discoverPluginsInDir, inspectPluginCapabilities, …)
|
|
98
|
+
// gets provenance for free with no signature change.
|
|
99
|
+
const repoRoot = path.dirname(path.dirname(pluginRoot));
|
|
93
100
|
return {
|
|
94
101
|
name: manifest.name,
|
|
95
102
|
root: pluginRoot,
|
|
@@ -107,6 +114,10 @@ export function buildDiscoveredPlugin(pluginRoot, manifest, spec = { kind: 'user
|
|
|
107
114
|
monitors: discoverPluginMonitors(pluginRoot),
|
|
108
115
|
hasMcp: fs.existsSync(path.join(pluginRoot, '.mcp.json')),
|
|
109
116
|
hasSettings: pluginHasNonPermissionSettings(pluginRoot),
|
|
117
|
+
repoRoot,
|
|
118
|
+
get snapshotSha() {
|
|
119
|
+
return resolveSnapshotSha(repoRoot);
|
|
120
|
+
},
|
|
110
121
|
};
|
|
111
122
|
}
|
|
112
123
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "2026-
|
|
2
|
+
"version": "2026-08-03",
|
|
3
3
|
"models": {
|
|
4
4
|
"claude-opus-4": {
|
|
5
5
|
"inputPerToken": 0.000005,
|
|
@@ -49,6 +49,21 @@
|
|
|
49
49
|
"cacheReadPerToken": 0.0000015,
|
|
50
50
|
"cacheWritePerToken": 0.00001875
|
|
51
51
|
},
|
|
52
|
+
"gpt-5.6-sol": {
|
|
53
|
+
"inputPerToken": 0.000005,
|
|
54
|
+
"outputPerToken": 0.00003,
|
|
55
|
+
"cacheReadPerToken": 0.0000005
|
|
56
|
+
},
|
|
57
|
+
"gpt-5.6-terra": {
|
|
58
|
+
"inputPerToken": 0.0000025,
|
|
59
|
+
"outputPerToken": 0.000015,
|
|
60
|
+
"cacheReadPerToken": 0.00000025
|
|
61
|
+
},
|
|
62
|
+
"gpt-5.6-luna": {
|
|
63
|
+
"inputPerToken": 0.000001,
|
|
64
|
+
"outputPerToken": 0.000006,
|
|
65
|
+
"cacheReadPerToken": 0.0000001
|
|
66
|
+
},
|
|
52
67
|
"gpt-5.5": {
|
|
53
68
|
"inputPerToken": 0.000005,
|
|
54
69
|
"outputPerToken": 0.00003,
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a project's work actually went, from the local git history.
|
|
3
|
+
*
|
|
4
|
+
* The card can say how many agents are running and how many PRs merged, but not
|
|
5
|
+
* *what was worked on*. That answer is already sitting in the checkout — every
|
|
6
|
+
* merged commit names the files it touched — so it costs no API call, no
|
|
7
|
+
* credential, and no rate-limit budget. Measured at 0.23s on this repo's own
|
|
8
|
+
* 7-day window (897 commits), which is why it runs unconditionally rather than
|
|
9
|
+
* behind a flag.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately NOT from `gh`: the GitHub API would spend a request per PR to
|
|
12
|
+
* learn what `git log --name-only` already knows locally, and it would be wrong
|
|
13
|
+
* on a monorepo whose interesting unit is a subdirectory rather than a repo.
|
|
14
|
+
*/
|
|
15
|
+
/** One directory and how many file-touches landed in it during the window. */
|
|
16
|
+
export interface FocusArea {
|
|
17
|
+
path: string;
|
|
18
|
+
touches: number;
|
|
19
|
+
}
|
|
20
|
+
/** Areas shown on the card before the tail is dropped. */
|
|
21
|
+
export declare const FOCUS_LIMIT = 4;
|
|
22
|
+
/**
|
|
23
|
+
* Bucket a file path to its area. Files shallower than {@link DEPTH} bucket to
|
|
24
|
+
* their own directory, so a repo-root `README.md` does not vanish.
|
|
25
|
+
*/
|
|
26
|
+
export declare function focusBucket(file: string): string | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Rank areas by file-touches, descending, ties broken by path so the order is
|
|
29
|
+
* stable across runs. Pure — the caller supplies the file list, so this is
|
|
30
|
+
* testable without a git repo.
|
|
31
|
+
*/
|
|
32
|
+
export declare function rankFocusAreas(files: string[], limit?: number): FocusArea[];
|
|
33
|
+
/**
|
|
34
|
+
* Read the window's changed files from a checkout. Best-effort in the same shape
|
|
35
|
+
* as the rest of the card's enrichment: a missing checkout, a shallow clone, or
|
|
36
|
+
* a repo with no commits in the window yields an empty list, never a throw.
|
|
37
|
+
*
|
|
38
|
+
* Reads the LOCAL default branch ref rather than fetching — a status command
|
|
39
|
+
* must not mutate the repo it is describing, so the answer is only as fresh as
|
|
40
|
+
* the user's last fetch, which is the correct trade for a read-only card.
|
|
41
|
+
*/
|
|
42
|
+
export declare function readFocusAreas(root: string, windowDays: number): Promise<FocusArea[]>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a project's work actually went, from the local git history.
|
|
3
|
+
*
|
|
4
|
+
* The card can say how many agents are running and how many PRs merged, but not
|
|
5
|
+
* *what was worked on*. That answer is already sitting in the checkout — every
|
|
6
|
+
* merged commit names the files it touched — so it costs no API call, no
|
|
7
|
+
* credential, and no rate-limit budget. Measured at 0.23s on this repo's own
|
|
8
|
+
* 7-day window (897 commits), which is why it runs unconditionally rather than
|
|
9
|
+
* behind a flag.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately NOT from `gh`: the GitHub API would spend a request per PR to
|
|
12
|
+
* learn what `git log --name-only` already knows locally, and it would be wrong
|
|
13
|
+
* on a monorepo whose interesting unit is a subdirectory rather than a repo.
|
|
14
|
+
*/
|
|
15
|
+
import { execFile } from 'child_process';
|
|
16
|
+
import { promisify } from 'util';
|
|
17
|
+
const execFileAsync = promisify(execFile);
|
|
18
|
+
/** How deep a bucket goes: `apps/cli/src`, not `apps` and not every leaf file. */
|
|
19
|
+
const DEPTH = 3;
|
|
20
|
+
/** Areas shown on the card before the tail is dropped. */
|
|
21
|
+
export const FOCUS_LIMIT = 4;
|
|
22
|
+
/**
|
|
23
|
+
* Paths whose churn is process, not engineering.
|
|
24
|
+
*
|
|
25
|
+
* This repo files one changelog fragment per PR, so `.changelog` ranks second by
|
|
26
|
+
* raw file-touches — presenting it as an "area of focus" would read as a signal
|
|
27
|
+
* while measuring nothing but the number of PRs. Same for the generated
|
|
28
|
+
* CHANGELOG and lockfiles.
|
|
29
|
+
*/
|
|
30
|
+
const NOISE = /(^|\/)(\.changelog|CHANGELOG\.md|bun\.lock|package-lock\.json|yarn\.lock)(\/|$)/;
|
|
31
|
+
/**
|
|
32
|
+
* Bucket a file path to its area. Files shallower than {@link DEPTH} bucket to
|
|
33
|
+
* their own directory, so a repo-root `README.md` does not vanish.
|
|
34
|
+
*/
|
|
35
|
+
export function focusBucket(file) {
|
|
36
|
+
if (NOISE.test(file))
|
|
37
|
+
return undefined;
|
|
38
|
+
const parts = file.split('/').filter(Boolean);
|
|
39
|
+
if (parts.length === 0)
|
|
40
|
+
return undefined;
|
|
41
|
+
if (parts.length === 1)
|
|
42
|
+
return parts[0];
|
|
43
|
+
return parts.slice(0, Math.min(DEPTH, parts.length - 1)).join('/');
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Rank areas by file-touches, descending, ties broken by path so the order is
|
|
47
|
+
* stable across runs. Pure — the caller supplies the file list, so this is
|
|
48
|
+
* testable without a git repo.
|
|
49
|
+
*/
|
|
50
|
+
export function rankFocusAreas(files, limit = FOCUS_LIMIT) {
|
|
51
|
+
const counts = new Map();
|
|
52
|
+
for (const f of files) {
|
|
53
|
+
const bucket = focusBucket(f.trim());
|
|
54
|
+
if (!bucket)
|
|
55
|
+
continue;
|
|
56
|
+
counts.set(bucket, (counts.get(bucket) ?? 0) + 1);
|
|
57
|
+
}
|
|
58
|
+
return [...counts.entries()]
|
|
59
|
+
.map(([path, touches]) => ({ path, touches }))
|
|
60
|
+
.sort((a, b) => b.touches - a.touches || a.path.localeCompare(b.path))
|
|
61
|
+
.slice(0, Math.max(1, limit));
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Read the window's changed files from a checkout. Best-effort in the same shape
|
|
65
|
+
* as the rest of the card's enrichment: a missing checkout, a shallow clone, or
|
|
66
|
+
* a repo with no commits in the window yields an empty list, never a throw.
|
|
67
|
+
*
|
|
68
|
+
* Reads the LOCAL default branch ref rather than fetching — a status command
|
|
69
|
+
* must not mutate the repo it is describing, so the answer is only as fresh as
|
|
70
|
+
* the user's last fetch, which is the correct trade for a read-only card.
|
|
71
|
+
*/
|
|
72
|
+
export async function readFocusAreas(root, windowDays) {
|
|
73
|
+
try {
|
|
74
|
+
const { stdout } = await execFileAsync('git', ['-C', root, 'log', `--since=${windowDays} days ago`, '--name-only', '--pretty=format:'], { timeout: 5000, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 });
|
|
75
|
+
return rankFocusAreas(stdout.split('\n').filter((l) => l.trim().length > 0));
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a project's milestone dates prove about its schedule — and nothing more.
|
|
3
|
+
*
|
|
4
|
+
* The tempting version of this file computes "on track / at risk". It cannot be
|
|
5
|
+
* written honestly against this data. Probed live against a real workspace:
|
|
6
|
+
*
|
|
7
|
+
* health: null projectUpdates: []
|
|
8
|
+
* startDate: null targetDate: null
|
|
9
|
+
* scopeHistory: [] completedScopeHistory: []
|
|
10
|
+
*
|
|
11
|
+
* "Behind schedule" needs either start+target dates to interpolate an expected
|
|
12
|
+
* progress line, or a history series to extrapolate a finish date. Both are
|
|
13
|
+
* absent, so any on-track/at-risk chip would be invented — and a confident wrong
|
|
14
|
+
* answer on a status card is worse than a blank one, because it is unfalsifiable
|
|
15
|
+
* from the card itself.
|
|
16
|
+
*
|
|
17
|
+
* So every verdict here is arithmetic on a stored date or a count:
|
|
18
|
+
*
|
|
19
|
+
* overdue a milestone's targetDate has passed and it is unfinished
|
|
20
|
+
* due-soon the next one lands within DUE_SOON_DAYS
|
|
21
|
+
* untracked milestones exist but nothing is filed against any of them,
|
|
22
|
+
* so their progress is not measurable
|
|
23
|
+
* scheduled dated milestones ahead, none due soon, work is filed
|
|
24
|
+
* no-dates milestones exist, none carries a date
|
|
25
|
+
* none the project declares no milestones
|
|
26
|
+
*
|
|
27
|
+
* `declared` relays Linear's OWN health when a human has posted one. It is
|
|
28
|
+
* passed through and attributed, never synthesized — if the user starts posting
|
|
29
|
+
* project updates, their answer wins over anything derived here.
|
|
30
|
+
*/
|
|
31
|
+
import type { LinearMilestone } from './linear-project-counts.js';
|
|
32
|
+
/** How far ahead counts as "due soon" — one sprint's notice. */
|
|
33
|
+
export declare const DUE_SOON_DAYS = 14;
|
|
34
|
+
/** A verdict about the schedule, as a tagged union so `--json` stays stable. */
|
|
35
|
+
export type ProjectVerdict = {
|
|
36
|
+
kind: 'declared';
|
|
37
|
+
health: string;
|
|
38
|
+
} | {
|
|
39
|
+
kind: 'overdue';
|
|
40
|
+
milestone: string;
|
|
41
|
+
days: number;
|
|
42
|
+
} | {
|
|
43
|
+
kind: 'due-soon';
|
|
44
|
+
milestone: string;
|
|
45
|
+
days: number;
|
|
46
|
+
} | {
|
|
47
|
+
kind: 'untracked';
|
|
48
|
+
milestones: number;
|
|
49
|
+
} | {
|
|
50
|
+
kind: 'scheduled';
|
|
51
|
+
milestone: string;
|
|
52
|
+
days: number;
|
|
53
|
+
} | {
|
|
54
|
+
kind: 'no-dates';
|
|
55
|
+
milestones: number;
|
|
56
|
+
} | {
|
|
57
|
+
kind: 'none';
|
|
58
|
+
};
|
|
59
|
+
/** Whole days from `nowMs` to a `YYYY-MM-DD` date, compared at LOCAL midnight. */
|
|
60
|
+
export declare function daysUntil(targetDate: string, nowMs: number): number | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* Decide what the dates prove. Precedence is time-sensitivity first:
|
|
63
|
+
*
|
|
64
|
+
* declared > overdue > due-soon > untracked > no-dates > scheduled
|
|
65
|
+
*
|
|
66
|
+
* An overdue milestone outranks an approaching one, and both outrank the
|
|
67
|
+
* observation that nothing is filed — a deadline moves, that observation does
|
|
68
|
+
* not. `declared` overrides everything, because a human said it.
|
|
69
|
+
*/
|
|
70
|
+
export declare function scheduleVerdict(milestones: LinearMilestone[], nowMs: number, declaredHealth?: string | null): ProjectVerdict;
|
|
71
|
+
/** One line for the card. Returns undefined for `none` — an empty row says nothing. */
|
|
72
|
+
export declare function formatVerdict(v: ProjectVerdict): {
|
|
73
|
+
text: string;
|
|
74
|
+
warn: boolean;
|
|
75
|
+
} | undefined;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a project's milestone dates prove about its schedule — and nothing more.
|
|
3
|
+
*
|
|
4
|
+
* The tempting version of this file computes "on track / at risk". It cannot be
|
|
5
|
+
* written honestly against this data. Probed live against a real workspace:
|
|
6
|
+
*
|
|
7
|
+
* health: null projectUpdates: []
|
|
8
|
+
* startDate: null targetDate: null
|
|
9
|
+
* scopeHistory: [] completedScopeHistory: []
|
|
10
|
+
*
|
|
11
|
+
* "Behind schedule" needs either start+target dates to interpolate an expected
|
|
12
|
+
* progress line, or a history series to extrapolate a finish date. Both are
|
|
13
|
+
* absent, so any on-track/at-risk chip would be invented — and a confident wrong
|
|
14
|
+
* answer on a status card is worse than a blank one, because it is unfalsifiable
|
|
15
|
+
* from the card itself.
|
|
16
|
+
*
|
|
17
|
+
* So every verdict here is arithmetic on a stored date or a count:
|
|
18
|
+
*
|
|
19
|
+
* overdue a milestone's targetDate has passed and it is unfinished
|
|
20
|
+
* due-soon the next one lands within DUE_SOON_DAYS
|
|
21
|
+
* untracked milestones exist but nothing is filed against any of them,
|
|
22
|
+
* so their progress is not measurable
|
|
23
|
+
* scheduled dated milestones ahead, none due soon, work is filed
|
|
24
|
+
* no-dates milestones exist, none carries a date
|
|
25
|
+
* none the project declares no milestones
|
|
26
|
+
*
|
|
27
|
+
* `declared` relays Linear's OWN health when a human has posted one. It is
|
|
28
|
+
* passed through and attributed, never synthesized — if the user starts posting
|
|
29
|
+
* project updates, their answer wins over anything derived here.
|
|
30
|
+
*/
|
|
31
|
+
/** How far ahead counts as "due soon" — one sprint's notice. */
|
|
32
|
+
export const DUE_SOON_DAYS = 14;
|
|
33
|
+
/** Whole days from `nowMs` to a `YYYY-MM-DD` date, compared at LOCAL midnight. */
|
|
34
|
+
export function daysUntil(targetDate, nowMs) {
|
|
35
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(targetDate.trim());
|
|
36
|
+
if (!m)
|
|
37
|
+
return undefined;
|
|
38
|
+
const due = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
|
|
39
|
+
if (Number.isNaN(due.getTime()))
|
|
40
|
+
return undefined;
|
|
41
|
+
const now = new Date(nowMs);
|
|
42
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
43
|
+
return Math.round((due.getTime() - today.getTime()) / 86_400_000);
|
|
44
|
+
}
|
|
45
|
+
const unfinished = (m) => m.total === 0 || m.done < m.total;
|
|
46
|
+
/**
|
|
47
|
+
* Decide what the dates prove. Precedence is time-sensitivity first:
|
|
48
|
+
*
|
|
49
|
+
* declared > overdue > due-soon > untracked > no-dates > scheduled
|
|
50
|
+
*
|
|
51
|
+
* An overdue milestone outranks an approaching one, and both outrank the
|
|
52
|
+
* observation that nothing is filed — a deadline moves, that observation does
|
|
53
|
+
* not. `declared` overrides everything, because a human said it.
|
|
54
|
+
*/
|
|
55
|
+
export function scheduleVerdict(milestones, nowMs, declaredHealth) {
|
|
56
|
+
if (declaredHealth)
|
|
57
|
+
return { kind: 'declared', health: declaredHealth };
|
|
58
|
+
if (milestones.length === 0)
|
|
59
|
+
return { kind: 'none' };
|
|
60
|
+
const open = milestones.filter(unfinished);
|
|
61
|
+
const dated = open
|
|
62
|
+
.map((m) => ({ m, days: m.targetDate ? daysUntil(m.targetDate, nowMs) : undefined }))
|
|
63
|
+
.filter((x) => x.days !== undefined)
|
|
64
|
+
.sort((a, b) => a.days - b.days);
|
|
65
|
+
const worst = dated[0];
|
|
66
|
+
if (worst && worst.days < 0)
|
|
67
|
+
return { kind: 'overdue', milestone: worst.m.name, days: -worst.days };
|
|
68
|
+
// A date bearing down is time-sensitive; "nothing is filed" is a standing
|
|
69
|
+
// condition that will still be true tomorrow. So an approaching deadline is
|
|
70
|
+
// reported even when the milestone has no issues against it — reversing these
|
|
71
|
+
// hid a milestone due in two days behind "3 milestones, no issues filed".
|
|
72
|
+
if (worst && worst.days <= DUE_SOON_DAYS)
|
|
73
|
+
return { kind: 'due-soon', milestone: worst.m.name, days: worst.days };
|
|
74
|
+
// Nothing is filed against ANY milestone, so no progress can be computed for
|
|
75
|
+
// them — the useful thing to say, and the actual state of a project whose
|
|
76
|
+
// milestones were created before its issues. Checked against the full list,
|
|
77
|
+
// not just the open ones: a COMPLETED milestone necessarily has issues, so a
|
|
78
|
+
// project with one cannot honestly be called untracked.
|
|
79
|
+
if (milestones.every((m) => m.total === 0))
|
|
80
|
+
return { kind: 'untracked', milestones: milestones.length };
|
|
81
|
+
if (!worst)
|
|
82
|
+
return { kind: 'no-dates', milestones: open.length };
|
|
83
|
+
return { kind: 'scheduled', milestone: worst.m.name, days: worst.days };
|
|
84
|
+
}
|
|
85
|
+
/** One line for the card. Returns undefined for `none` — an empty row says nothing. */
|
|
86
|
+
export function formatVerdict(v) {
|
|
87
|
+
switch (v.kind) {
|
|
88
|
+
case 'declared':
|
|
89
|
+
// Attributed, so nobody mistakes a human's call for a derived one.
|
|
90
|
+
return { text: `per Linear: ${v.health}`, warn: v.health.toLowerCase() !== 'ontrack' };
|
|
91
|
+
case 'overdue':
|
|
92
|
+
return { text: `${v.milestone} overdue by ${v.days} day${v.days === 1 ? '' : 's'}`, warn: true };
|
|
93
|
+
case 'due-soon':
|
|
94
|
+
return {
|
|
95
|
+
text: `${v.milestone} due ${v.days === 0 ? 'today' : v.days === 1 ? 'tomorrow' : `in ${v.days} days`}`,
|
|
96
|
+
warn: false,
|
|
97
|
+
};
|
|
98
|
+
case 'untracked':
|
|
99
|
+
return {
|
|
100
|
+
text: `${v.milestones} milestone${v.milestones === 1 ? '' : 's'}, no issues filed against any — progress is not measurable`,
|
|
101
|
+
warn: true,
|
|
102
|
+
};
|
|
103
|
+
case 'scheduled':
|
|
104
|
+
return { text: `${v.milestone} in ${v.days} days`, warn: false };
|
|
105
|
+
case 'no-dates':
|
|
106
|
+
return { text: `${v.milestones} open milestone${v.milestones === 1 ? '' : 's'}, none dated`, warn: false };
|
|
107
|
+
case 'none':
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
}
|
package/dist/lib/redact.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared redaction helpers for text that may be exported or logged.
|
|
3
3
|
*/
|
|
4
|
+
/** Remove terminal control sequences from untrusted text before storage or display. */
|
|
5
|
+
export declare function sanitizeForTerminal(text: string): string;
|
|
4
6
|
/**
|
|
5
7
|
* Scrub secrets from `text`. Two passes: format-based patterns (above), then a
|
|
6
8
|
* value-aware pass that masks any `knownValues` verbatim — a credential we
|
package/dist/lib/redact.js
CHANGED
|
@@ -20,9 +20,31 @@ const SECRET_PATTERNS = [
|
|
|
20
20
|
[/\bxapp-[A-Za-z0-9-]{10,}\b/g, '[REDACTED_SLACK_TOKEN]'],
|
|
21
21
|
[/\bnpm_[A-Za-z0-9]{36}\b/g, '[REDACTED_NPM_TOKEN]'],
|
|
22
22
|
[/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, '[REDACTED_JWT]'],
|
|
23
|
+
// Headers frequently appear inside shell arguments. Consume a quoted header
|
|
24
|
+
// as one unit so cookie attributes after a space do not survive redaction.
|
|
25
|
+
[/(^|\s)(["'])(Cookie|Set-Cookie|Authorization|Proxy-Authorization)\s*:\s*.*?\2/gi, '$1$2$3: [REDACTED]$2'],
|
|
26
|
+
[/(^|\s)(["'])(Cookie|Set-Cookie|Authorization|Proxy-Authorization)\s*:\s*(?:(?!\2).)*$/gi, '$1$2$3: [REDACTED]'],
|
|
23
27
|
[/Bearer\s+\S+/gi, 'Bearer [REDACTED]'],
|
|
28
|
+
[/\b((?:Cookie|Set-Cookie)\s*:\s*)\S+/gi, '$1[REDACTED]'],
|
|
29
|
+
[/\b(Authorization\s*:\s*)(?!Bearer\s+\[REDACTED\])\S+(?:\s+\S+)?/gi, '$1[REDACTED]'],
|
|
30
|
+
// Structured secret fields can arrive as raw JSON output rather than an
|
|
31
|
+
// argument object, so the object walker alone is not sufficient.
|
|
32
|
+
[/(^[,{\s]|["'])([A-Z0-9_-]*(?:TOKEN|KEY|SECRET|PASSWORD|AUTHORIZATION|COOKIE)[A-Z0-9_-]*["']?\s*:\s*)(["'][^"']*["']|[^,}\]\s]+)/gim, '$1$2"[REDACTED]"'],
|
|
33
|
+
[/(\s--?(?:password|token|secret|api[_-]?key)(?:=|\s+))(["']?)[^\s"']+\2/gi, '$1[REDACTED]'],
|
|
34
|
+
[/(\s--user(?:=|\s+))(["']?)[^\s"']+\2/gi, '$1[REDACTED]'],
|
|
35
|
+
[/(\s-u\s+)(["']?)[^\s"']+\2/g, '$1[REDACTED]'],
|
|
36
|
+
[/(\s--proxy-user(?:=|\s+))(["']?)[^\s"']+\2/gi, '$1[REDACTED]'],
|
|
37
|
+
[/(\s-U\s+)(["']?)[^\s"']+\2/g, '$1[REDACTED]'],
|
|
38
|
+
[/(https?:\/\/)[^/\s:@]+:[^@\s/]+@/gi, '$1[REDACTED]@'],
|
|
24
39
|
[/\b([A-Z0-9_]*(?:TOKEN|KEY|SECRET|PASSWORD)[A-Z0-9_]*)=("[^"]*"|'[^']*'|\S+)/gi, '$1=[REDACTED]'],
|
|
25
40
|
];
|
|
41
|
+
const TERMINAL_ESCAPE_REGEX = /\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x9d[^\x07\x9c]*(?:\x07|\x9c)|\x1b\[[0-?]*[ -/]*[@-~]|\x9b[0-?]*[ -/]*[@-~]|\x1b[@-_]|[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/g;
|
|
42
|
+
/** Remove terminal control sequences from untrusted text before storage or display. */
|
|
43
|
+
export function sanitizeForTerminal(text) {
|
|
44
|
+
if (!text)
|
|
45
|
+
return text;
|
|
46
|
+
return text.replace(TERMINAL_ESCAPE_REGEX, '');
|
|
47
|
+
}
|
|
26
48
|
/** Env vars whose NAME marks their VALUE as a credential worth masking literally. */
|
|
27
49
|
const SECRET_ENV_NAME = /(?:TOKEN|KEY|SECRET|PASSWORD)/i;
|
|
28
50
|
/** Don't literal-mask trivially short values — they collide with ordinary text. */
|
|
@@ -10,6 +10,8 @@ export interface RemoteAgentsJsonOptions<T> {
|
|
|
10
10
|
* printing a line per offline box above its output. Never a silent drop.
|
|
11
11
|
*/
|
|
12
12
|
quiet?: boolean;
|
|
13
|
+
/** Per-peer deadline. Long-running maintenance commands override 12 seconds. */
|
|
14
|
+
timeoutMs?: number;
|
|
13
15
|
}
|
|
14
16
|
export interface RemoteAgentsJsonParseResult<T> {
|
|
15
17
|
items: T[];
|
|
@@ -32,7 +32,7 @@ export function remoteAgentsJsonCommand(args, noFanoutEnv, os) {
|
|
|
32
32
|
const inner = `${noFanoutEnv}=1 agents ${args.map(shellQuote).join(' ')}`;
|
|
33
33
|
return `bash -lc ${shellQuote(inner)}`;
|
|
34
34
|
}
|
|
35
|
-
function sshCapture(target, remoteCmd) {
|
|
35
|
+
function sshCapture(target, remoteCmd, timeoutMs) {
|
|
36
36
|
assertValidSshTarget(target);
|
|
37
37
|
return new Promise((resolve) => {
|
|
38
38
|
const args = [...SSH_OPTS, ...controlOpts(), target, remoteCmd];
|
|
@@ -49,7 +49,7 @@ function sshCapture(target, remoteCmd) {
|
|
|
49
49
|
const timer = setTimeout(() => {
|
|
50
50
|
child.kill('SIGKILL');
|
|
51
51
|
done(null);
|
|
52
|
-
},
|
|
52
|
+
}, timeoutMs);
|
|
53
53
|
child.stdout.on('data', (data) => { stdout += data.toString(); });
|
|
54
54
|
child.on('error', () => done(null));
|
|
55
55
|
child.on('close', (code) => done(code));
|
|
@@ -102,7 +102,7 @@ export async function gatherRemoteAgentsJson(options) {
|
|
|
102
102
|
const parseFailed = [];
|
|
103
103
|
const results = await Promise.all(targets.map(async (target) => {
|
|
104
104
|
const command = remoteAgentsJsonCommand(options.args, options.noFanoutEnv, target.os);
|
|
105
|
-
const result = await sshCapture(target.target, command);
|
|
105
|
+
const result = await sshCapture(target.target, command, options.timeoutMs ?? REMOTE_TIMEOUT_MS);
|
|
106
106
|
if (result.code !== 0) {
|
|
107
107
|
skipped.push(target.name);
|
|
108
108
|
if (!options.quiet) {
|
package/dist/lib/resources.d.ts
CHANGED
|
@@ -16,6 +16,22 @@ export interface ResolvedResource {
|
|
|
16
16
|
* or the alias name (e.g. 'rush') for extra repos registered in agents.yaml.
|
|
17
17
|
*/
|
|
18
18
|
source: string;
|
|
19
|
+
/**
|
|
20
|
+
* Absolute path to the DotAgents repo root this resource resolved from (the
|
|
21
|
+
* project/user/system/extra-repo dir — one level above the `kind`
|
|
22
|
+
* subdirectory). DotAgents repos are git-tracked (plugins.ts), so this pairs
|
|
23
|
+
* with {@link snapshotSha} to answer "which commit of which repo".
|
|
24
|
+
*/
|
|
25
|
+
repoRoot: string;
|
|
26
|
+
/**
|
|
27
|
+
* Short HEAD sha of `repoRoot`'s git checkout, lazily resolved (a getter,
|
|
28
|
+
* not computed at construction) and memoized per repoRoot
|
|
29
|
+
* (`git.ts` `resolveSnapshotSha`) — a caller that never inspects provenance
|
|
30
|
+
* never pays for the git shell-out, and resolving many resources from the
|
|
31
|
+
* same repo pays for exactly one. `undefined` when `repoRoot` isn't a git
|
|
32
|
+
* repo (or has no commits).
|
|
33
|
+
*/
|
|
34
|
+
readonly snapshotSha: string | undefined;
|
|
19
35
|
}
|
|
20
36
|
/**
|
|
21
37
|
* True when `rawName` (a filename with its extension already stripped) names a
|
package/dist/lib/resources.js
CHANGED
|
@@ -15,6 +15,16 @@ import { WorkflowsHandler } from './resources/workflows.js';
|
|
|
15
15
|
import { isCapable } from './capabilities.js';
|
|
16
16
|
import { getProjectAgentsDir, getUserAgentsDir, getSystemAgentsDir, getEnabledExtraRepos, } from './state.js';
|
|
17
17
|
import { isNameActiveInResourceProfile } from './resource-profiles.js';
|
|
18
|
+
import { resolveSnapshotSha } from './git.js';
|
|
19
|
+
/** Build a ResolvedResource with a lazy, memoized `snapshotSha` getter. */
|
|
20
|
+
function withProvenance(base) {
|
|
21
|
+
return {
|
|
22
|
+
...base,
|
|
23
|
+
get snapshotSha() {
|
|
24
|
+
return resolveSnapshotSha(base.repoRoot);
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
18
28
|
function profiledKind(kind) {
|
|
19
29
|
switch (kind) {
|
|
20
30
|
case 'commands':
|
|
@@ -70,19 +80,19 @@ export function resolveResource(kind, name, cwd) {
|
|
|
70
80
|
const projectDir = getProjectAgentsDir(cwd);
|
|
71
81
|
const extraRepos = getEnabledExtraRepos();
|
|
72
82
|
const candidates = [
|
|
73
|
-
...(projectDir ? [[path.join(projectDir, kind), 'project']] : []),
|
|
74
|
-
[path.join(getUserAgentsDir(), kind), 'user'],
|
|
75
|
-
[path.join(getSystemAgentsDir(), kind), 'system'],
|
|
76
|
-
...extraRepos.map((e) => [path.join(e.dir, kind), e.alias]),
|
|
83
|
+
...(projectDir ? [[path.join(projectDir, kind), 'project', projectDir]] : []),
|
|
84
|
+
[path.join(getUserAgentsDir(), kind), 'user', getUserAgentsDir()],
|
|
85
|
+
[path.join(getSystemAgentsDir(), kind), 'system', getSystemAgentsDir()],
|
|
86
|
+
...extraRepos.map((e) => [path.join(e.dir, kind), e.alias, e.dir]),
|
|
77
87
|
];
|
|
78
|
-
for (const [dir, source] of candidates) {
|
|
88
|
+
for (const [dir, source, repoRoot] of candidates) {
|
|
79
89
|
if (!fs.existsSync(dir))
|
|
80
90
|
continue;
|
|
81
91
|
// Try exact name (for directories like skills/subagents)
|
|
82
92
|
const exactPath = path.join(dir, name);
|
|
83
93
|
if (fs.existsSync(exactPath)) {
|
|
84
94
|
if (resourceIsActive(kind, name, source)) {
|
|
85
|
-
return { name, path: exactPath, source };
|
|
95
|
+
return withProvenance({ name, path: exactPath, source, repoRoot });
|
|
86
96
|
}
|
|
87
97
|
continue;
|
|
88
98
|
}
|
|
@@ -94,7 +104,7 @@ export function resolveResource(kind, name, cwd) {
|
|
|
94
104
|
const withExt = exactPath + ext;
|
|
95
105
|
if (fs.existsSync(withExt)) {
|
|
96
106
|
if (resourceIsActive(kind, name, source)) {
|
|
97
|
-
return { name, path: withExt, source };
|
|
107
|
+
return withProvenance({ name, path: withExt, source, repoRoot });
|
|
98
108
|
}
|
|
99
109
|
continue;
|
|
100
110
|
}
|
|
@@ -113,12 +123,12 @@ export function listResources(kind, cwd) {
|
|
|
113
123
|
const projectDir = getProjectAgentsDir(cwd);
|
|
114
124
|
const extraRepos = getEnabledExtraRepos();
|
|
115
125
|
const roots = [
|
|
116
|
-
...(projectDir ? [[path.join(projectDir, kind), 'project']] : []),
|
|
117
|
-
[path.join(getUserAgentsDir(), kind), 'user'],
|
|
118
|
-
[path.join(getSystemAgentsDir(), kind), 'system'],
|
|
119
|
-
...extraRepos.map((e) => [path.join(e.dir, kind), e.alias]),
|
|
126
|
+
...(projectDir ? [[path.join(projectDir, kind), 'project', projectDir]] : []),
|
|
127
|
+
[path.join(getUserAgentsDir(), kind), 'user', getUserAgentsDir()],
|
|
128
|
+
[path.join(getSystemAgentsDir(), kind), 'system', getSystemAgentsDir()],
|
|
129
|
+
...extraRepos.map((e) => [path.join(e.dir, kind), e.alias, e.dir]),
|
|
120
130
|
];
|
|
121
|
-
for (const [dir, source] of roots) {
|
|
131
|
+
for (const [dir, source, repoRoot] of roots) {
|
|
122
132
|
if (!fs.existsSync(dir))
|
|
123
133
|
continue;
|
|
124
134
|
let entries;
|
|
@@ -143,11 +153,12 @@ export function listResources(kind, cwd) {
|
|
|
143
153
|
if (!resourceIsActive(kind, rawName, source))
|
|
144
154
|
continue;
|
|
145
155
|
seen.add(rawName);
|
|
146
|
-
results.push({
|
|
156
|
+
results.push(withProvenance({
|
|
147
157
|
name: rawName,
|
|
148
158
|
path: path.join(dir, entry.name),
|
|
149
159
|
source,
|
|
150
|
-
|
|
160
|
+
repoRoot,
|
|
161
|
+
}));
|
|
151
162
|
}
|
|
152
163
|
}
|
|
153
164
|
return results;
|
package/dist/lib/rotate.d.ts
CHANGED
|
@@ -174,7 +174,81 @@ export declare function capacityWeight(usedPercent: number | null, minutesToLimi
|
|
|
174
174
|
* usage headroom.
|
|
175
175
|
*/
|
|
176
176
|
export declare function pickAvailableCandidate(candidates: RotateCandidate[], preferredVersion?: string | null, nowMs?: number): RotateResult | null;
|
|
177
|
+
/**
|
|
178
|
+
* Per-harness routing summary for `agents run auto` — the cross-harness layer
|
|
179
|
+
* that sits above `pickBalancedCandidate` (which is strictly per-harness).
|
|
180
|
+
*/
|
|
181
|
+
export interface HarnessSummary {
|
|
182
|
+
agent: AgentId;
|
|
183
|
+
/** Every installed account slot probed for this harness. */
|
|
184
|
+
candidates: RotateCandidate[];
|
|
185
|
+
/** Healthy accounts after identity dedupe, sorted by headroom. */
|
|
186
|
+
healthy: RotateCandidate[];
|
|
187
|
+
/** The account this harness would route to (best verified headroom). Null when the harness is excluded. */
|
|
188
|
+
best: RotateCandidate | null;
|
|
189
|
+
/** Routing used% of `best` (max across non-session windows); null when unknown. */
|
|
190
|
+
bestUsedPercent: number | null;
|
|
191
|
+
/** Why the harness was excluded, e.g. ['2 rate_limited', '1 signed_out']. Empty when healthy. */
|
|
192
|
+
exclusionReasons: string[];
|
|
193
|
+
}
|
|
194
|
+
export interface HarnessPickResult {
|
|
195
|
+
/** The harness picked for this run. */
|
|
196
|
+
picked: HarnessSummary;
|
|
197
|
+
/** Harnesses with ≥1 healthy account (including the picked one). */
|
|
198
|
+
healthy: HarnessSummary[];
|
|
199
|
+
/** Harnesses with zero healthy accounts — excluded, not down-weighted. */
|
|
200
|
+
excluded: HarnessSummary[];
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Classify every harness's candidates into healthy (with a representative
|
|
204
|
+
* best account) vs excluded (with per-reason counts). Pure — the pick and the
|
|
205
|
+
* zero-healthy error message both read this, so they can never disagree.
|
|
206
|
+
*
|
|
207
|
+
* Health uses the exact account-layer gate (`isRotationEligible`: signed in
|
|
208
|
+
* AND not maxed on ANY blocking window, weekly included). The representative
|
|
209
|
+
* best account honors `preferVerified`: confirmed headroom beats apparent
|
|
210
|
+
* headroom, the same freshness rule the account layer routes on.
|
|
211
|
+
*/
|
|
212
|
+
export declare function classifyHarnessCandidates(byHarness: ReadonlyMap<AgentId, RotateCandidate[]>, nowMs?: number): HarnessSummary[];
|
|
213
|
+
/**
|
|
214
|
+
* Pick a harness for `agents run auto` using weighted random by best-account
|
|
215
|
+
* headroom (RUSH-2132).
|
|
216
|
+
*
|
|
217
|
+
* A harness's capacity is `100 − min(routingUsed% across its healthy accounts)`
|
|
218
|
+
* — its best account's headroom. The pick reuses `weightedRandomByCapacity` on
|
|
219
|
+
* the representative best accounts, so host/harness/account layers all share
|
|
220
|
+
* one sampling behavior. Harnesses with zero healthy accounts are EXCLUDED,
|
|
221
|
+
* not down-weighted. Returns null when no harness has any healthy account;
|
|
222
|
+
* call `classifyHarnessCandidates` for the exclusion detail to message with.
|
|
223
|
+
*/
|
|
224
|
+
export declare function pickHarnessWeighted(byHarness: ReadonlyMap<AgentId, RotateCandidate[]>, nowMs?: number): HarnessPickResult | null;
|
|
225
|
+
/** One-line banner naming the auto-picked harness and why (headroom). */
|
|
226
|
+
export declare function formatHarnessPickBanner(result: HarnessPickResult): string;
|
|
227
|
+
/**
|
|
228
|
+
* The earliest FUTURE window reset across these candidates' usage snapshots —
|
|
229
|
+
* when the first exhausted account becomes usable again. Null when no snapshot
|
|
230
|
+
* carries a reset timestamp.
|
|
231
|
+
*/
|
|
232
|
+
export declare function earliestResetAcross(candidates: RotateCandidate[], nowMs?: number): Date | null;
|
|
233
|
+
/**
|
|
234
|
+
* The zero-healthy-account error (RUSH-2132). EXACT contract — the Factory
|
|
235
|
+
* watchdog tail-detects this text: it must contain the literal `no healthy`
|
|
236
|
+
* and `resets <time>` (parsed for the rotate cooldown). Do not deviate.
|
|
237
|
+
*/
|
|
238
|
+
export declare function formatNoHealthyAccountError(agent: AgentId, strategy: RunStrategy, excluded: RotateCandidate[], nowMs?: number): string;
|
|
239
|
+
/**
|
|
240
|
+
* The zero-healthy-harness error for `agents run auto` — names each harness's
|
|
241
|
+
* exclusion reason plus the earliest reset across all snapshots.
|
|
242
|
+
*/
|
|
243
|
+
export declare function formatNoHealthyHarnessError(summaries: HarnessSummary[], nowMs?: number): string;
|
|
177
244
|
export declare function collectRunCandidates(agent: AgentId): Promise<RotateCandidate[]>;
|
|
245
|
+
/**
|
|
246
|
+
* Collect run candidates for every harness with ≥1 installed version — the
|
|
247
|
+
* probe `agents run auto` routes on (the same per-harness account probe
|
|
248
|
+
* `agents view` aggregates). Harnesses with nothing installed are absent from
|
|
249
|
+
* the map: not a candidate at all, rather than an excluded one.
|
|
250
|
+
*/
|
|
251
|
+
export declare function collectHarnessCandidates(agentIds?: AgentId[]): Promise<Map<AgentId, RotateCandidate[]>>;
|
|
178
252
|
/**
|
|
179
253
|
* Resolve an account identity to the installed version slot that holds it, over
|
|
180
254
|
* an already-collected candidate list. Pure — no I/O — so it is unit-tested
|
|
@@ -207,9 +281,18 @@ export declare function resolveAccountVersion(agent: AgentId, account: string):
|
|
|
207
281
|
export declare function selectBalancedVersion(agent: AgentId): Promise<RotateResult | null>;
|
|
208
282
|
/** Select the configured version if available, otherwise another available version. */
|
|
209
283
|
export declare function selectAvailableVersion(agent: AgentId, preferredVersion?: string | null): Promise<RotateResult | null>;
|
|
210
|
-
export declare function resolveRunVersion(agent: AgentId, strategy: RunStrategy, cwd?: string): Promise<{
|
|
284
|
+
export declare function resolveRunVersion(agent: AgentId, strategy: RunStrategy, cwd?: string, collect?: (agent: AgentId) => Promise<RotateCandidate[]>): Promise<{
|
|
211
285
|
version: string | null;
|
|
212
286
|
rotation: RotateResult | null;
|
|
287
|
+
/**
|
|
288
|
+
* Set when a non-pinned strategy found ZERO healthy candidates among the
|
|
289
|
+
* installed versions: the full excluded set, so callers fail loud with
|
|
290
|
+
* per-account reasons instead of launching the exhausted pinned default
|
|
291
|
+
* (RUSH-2132). Undefined for pinned, for successful picks, and when no
|
|
292
|
+
* version is installed at all (the pre-existing not-installed path — there
|
|
293
|
+
* is no account to be "unhealthy").
|
|
294
|
+
*/
|
|
295
|
+
exhausted?: RotateCandidate[];
|
|
213
296
|
}>;
|
|
214
297
|
/**
|
|
215
298
|
* Cap on the number of healthy accounts a single run will re-dispatch through
|