@phnx-labs/agents-cli 1.20.88 → 1.20.90
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 +503 -0
- package/README.md +15 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/commands.js +7 -7
- package/dist/commands/exec.js +7 -1
- package/dist/commands/factory.js +26 -2
- package/dist/commands/funnel.js +16 -1
- package/dist/commands/harness.d.ts +27 -0
- package/dist/commands/harness.js +120 -13
- package/dist/commands/menubar.js +117 -34
- package/dist/commands/profiles.d.ts +3 -0
- package/dist/commands/profiles.js +1 -1
- package/dist/commands/routines.d.ts +19 -0
- package/dist/commands/routines.js +51 -7
- package/dist/commands/secrets-rotate-passphrase.d.ts +17 -0
- package/dist/commands/secrets-rotate-passphrase.js +96 -0
- package/dist/commands/secrets.d.ts +10 -1
- package/dist/commands/secrets.js +20 -6
- package/dist/commands/sessions-browser.d.ts +4 -0
- package/dist/commands/sessions-browser.js +51 -9
- package/dist/commands/sessions-favorite.d.ts +20 -0
- package/dist/commands/sessions-favorite.js +120 -0
- package/dist/commands/sessions.d.ts +110 -21
- package/dist/commands/sessions.js +395 -74
- package/dist/commands/setup-secrets.d.ts +7 -0
- package/dist/commands/setup-secrets.js +12 -9
- package/dist/commands/versions.js +12 -4
- package/dist/commands/view.d.ts +14 -1
- package/dist/commands/view.js +103 -128
- package/dist/commands/webhook.js +7 -2
- package/dist/lib/agents.d.ts +4 -2
- package/dist/lib/agents.js +21 -6
- package/dist/lib/commands.js +9 -1
- package/dist/lib/daemon.d.ts +29 -0
- package/dist/lib/daemon.js +58 -4
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/factory/snapshot.d.ts +78 -0
- package/dist/lib/factory/snapshot.js +209 -0
- package/dist/lib/fs-atomic.d.ts +14 -1
- package/dist/lib/fs-atomic.js +35 -3
- package/dist/lib/funnel.d.ts +1 -0
- package/dist/lib/funnel.js +8 -0
- package/dist/lib/hosts/dispatch.js +19 -1
- package/dist/lib/hq/floor.js +12 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +2 -2
- package/dist/lib/menubar/install-menubar.d.ts +53 -2
- package/dist/lib/menubar/install-menubar.js +183 -28
- package/dist/lib/picker.d.ts +27 -2
- package/dist/lib/picker.js +71 -7
- package/dist/lib/platform/process.d.ts +2 -0
- package/dist/lib/platform/process.js +5 -3
- package/dist/lib/profiles.d.ts +48 -0
- package/dist/lib/profiles.js +67 -0
- package/dist/lib/resources.d.ts +8 -0
- package/dist/lib/resources.js +34 -1
- package/dist/lib/rotate.d.ts +24 -2
- package/dist/lib/rotate.js +63 -6
- package/dist/lib/routines-placement.d.ts +2 -1
- package/dist/lib/routines-placement.js +8 -4
- package/dist/lib/routines.d.ts +57 -1
- package/dist/lib/routines.js +74 -1
- package/dist/lib/runner.d.ts +2 -0
- package/dist/lib/runner.js +21 -8
- 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/bundles.js +9 -34
- package/dist/lib/secrets/filestore.d.ts +152 -34
- package/dist/lib/secrets/filestore.js +676 -123
- package/dist/lib/session/active.d.ts +109 -3
- package/dist/lib/session/active.js +269 -13
- package/dist/lib/session/db.d.ts +14 -0
- package/dist/lib/session/db.js +35 -0
- package/dist/lib/session/favorites.d.ts +39 -0
- package/dist/lib/session/favorites.js +101 -0
- package/dist/lib/session/host-link.d.ts +68 -0
- package/dist/lib/session/host-link.js +64 -0
- package/dist/lib/session/presence.d.ts +85 -0
- package/dist/lib/session/presence.js +150 -0
- package/dist/lib/session/remote-active.d.ts +4 -1
- package/dist/lib/session/remote-active.js +8 -2
- package/dist/lib/session/remote-list.d.ts +10 -0
- package/dist/lib/session/remote-list.js +47 -9
- package/dist/lib/session/viewing-in.d.ts +31 -0
- package/dist/lib/session/viewing-in.js +47 -0
- package/dist/lib/state.d.ts +17 -0
- package/dist/lib/state.js +30 -2
- package/dist/lib/tmux/binary.d.ts +7 -0
- package/dist/lib/tmux/binary.js +11 -1
- package/dist/lib/triggers/handlers.d.ts +95 -0
- package/dist/lib/triggers/handlers.js +384 -0
- package/dist/lib/triggers/webhook.d.ts +10 -2
- package/dist/lib/triggers/webhook.js +65 -11
- package/dist/lib/types.d.ts +4 -3
- package/dist/lib/usage-backoff.d.ts +29 -0
- package/dist/lib/usage-backoff.js +165 -0
- package/dist/lib/usage.d.ts +112 -5
- package/dist/lib/usage.js +464 -46
- package/dist/lib/watchdog/runner.d.ts +13 -0
- package/dist/lib/watchdog/runner.js +16 -1
- package/package.json +1 -1
package/dist/lib/profiles.d.ts
CHANGED
|
@@ -29,6 +29,18 @@ export interface Profile {
|
|
|
29
29
|
description?: string;
|
|
30
30
|
preset?: string;
|
|
31
31
|
provider?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Human-facing label for the harness — what `agents view` prints as the
|
|
34
|
+
* agent-type header, the same slot `AGENTS[id].name` fills for a native
|
|
35
|
+
* harness. Defaults to the profile name when unset.
|
|
36
|
+
*/
|
|
37
|
+
label?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Name of the harness this one was forked from — either a native agent id
|
|
40
|
+
* (`claude`, `opencode`) or another custom harness. Display-only lineage:
|
|
41
|
+
* the fork is a full copy, so deleting the source never affects it.
|
|
42
|
+
*/
|
|
43
|
+
forkedFrom?: string;
|
|
32
44
|
/**
|
|
33
45
|
* Optional secondary model retried on the same host when the primary model
|
|
34
46
|
* env value hits a rate limit. Reuses the `--fallback` cascade in
|
|
@@ -45,12 +57,19 @@ export interface Profile {
|
|
|
45
57
|
*/
|
|
46
58
|
export interface ProfileSummary {
|
|
47
59
|
name: string;
|
|
60
|
+
/** Human-facing header label — `label` when set, else the profile name. */
|
|
61
|
+
label: string;
|
|
48
62
|
agent: AgentId;
|
|
49
63
|
host: string;
|
|
64
|
+
/** Host version pin, or null when the harness follows the host's default. */
|
|
65
|
+
hostVersion: string | null;
|
|
50
66
|
provider: string;
|
|
51
67
|
model: string;
|
|
52
68
|
auth: string;
|
|
53
69
|
path: string;
|
|
70
|
+
description: string | null;
|
|
71
|
+
/** Native agent id or custom harness this one was forked from, if recorded. */
|
|
72
|
+
forkedFrom: string | null;
|
|
54
73
|
}
|
|
55
74
|
/** Get the directory where profile YAML files are stored. */
|
|
56
75
|
export declare function getProfilesDir(): string;
|
|
@@ -91,6 +110,11 @@ export declare function profileModelEnvKey(profile: Profile): string | null;
|
|
|
91
110
|
* - No auth at all: provider only.
|
|
92
111
|
*/
|
|
93
112
|
export declare function profileAuthLabel(profile: Profile): string;
|
|
113
|
+
/**
|
|
114
|
+
* Header label for the harness — the slot `AGENTS[id].name` fills for a native
|
|
115
|
+
* harness, so `agents view` can print custom and native harnesses the same way.
|
|
116
|
+
*/
|
|
117
|
+
export declare function profileLabel(profile: Profile): string;
|
|
94
118
|
/** Build a stable, machine-readable summary for list and view surfaces. */
|
|
95
119
|
export declare function profileSummary(profile: Profile): ProfileSummary;
|
|
96
120
|
/**
|
|
@@ -115,6 +139,8 @@ export interface HostModelOptions {
|
|
|
115
139
|
/** Env var the host reads its auth token from; pair with `provider` to attach keychain auth. */
|
|
116
140
|
authEnvVar?: string;
|
|
117
141
|
description?: string;
|
|
142
|
+
/** Human-facing header label; defaults to the harness name. */
|
|
143
|
+
label?: string;
|
|
118
144
|
}
|
|
119
145
|
/**
|
|
120
146
|
* Build a custom-harness profile from a host CLI + model in one shot, without a
|
|
@@ -123,6 +149,28 @@ export interface HostModelOptions {
|
|
|
123
149
|
* (hosts that manage their own login — e.g. opencode — need neither).
|
|
124
150
|
*/
|
|
125
151
|
export declare function profileFromHostModel(name: string, host: AgentId, model: string, opts?: HostModelOptions): Profile;
|
|
152
|
+
/** Overrides applied on top of the source when forking a harness. */
|
|
153
|
+
export interface ForkProfileOptions {
|
|
154
|
+
/** Swap the pinned model. Written onto the source's model env key when it has
|
|
155
|
+
* one, else onto the host's canonical model var. */
|
|
156
|
+
model?: string;
|
|
157
|
+
/** Swap the endpoint. Only applied for hosts with a known base-URL var. */
|
|
158
|
+
baseUrl?: string;
|
|
159
|
+
/** Repoint auth at a different provider's keychain item. */
|
|
160
|
+
provider?: string;
|
|
161
|
+
/** Env var the host reads its token from; pair with `provider`. */
|
|
162
|
+
authEnvVar?: string;
|
|
163
|
+
/** Re-pin (or unpin, with an empty string) the host CLI version. */
|
|
164
|
+
version?: string;
|
|
165
|
+
label?: string;
|
|
166
|
+
description?: string;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Copy an existing harness under a new name, applying overrides. The fork is a
|
|
170
|
+
* full copy — env, auth binding, and fallback model all carry over — so the two
|
|
171
|
+
* diverge from here and deleting the source never affects the fork.
|
|
172
|
+
*/
|
|
173
|
+
export declare function forkProfile(source: Profile, name: string, opts?: ForkProfileOptions): Profile;
|
|
126
174
|
/**
|
|
127
175
|
* Resolve a profile into the env block that should be injected into the
|
|
128
176
|
* spawned agent process. Reads the token from keychain at exec time so the
|
package/dist/lib/profiles.js
CHANGED
|
@@ -197,16 +197,27 @@ export function profileAuthLabel(profile) {
|
|
|
197
197
|
}
|
|
198
198
|
return provider;
|
|
199
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Header label for the harness — the slot `AGENTS[id].name` fills for a native
|
|
202
|
+
* harness, so `agents view` can print custom and native harnesses the same way.
|
|
203
|
+
*/
|
|
204
|
+
export function profileLabel(profile) {
|
|
205
|
+
return profile.label || profile.name;
|
|
206
|
+
}
|
|
200
207
|
/** Build a stable, machine-readable summary for list and view surfaces. */
|
|
201
208
|
export function profileSummary(profile) {
|
|
202
209
|
return {
|
|
203
210
|
name: profile.name,
|
|
211
|
+
label: profileLabel(profile),
|
|
204
212
|
agent: profile.host.agent,
|
|
205
213
|
host: profileHostLabel(profile),
|
|
214
|
+
hostVersion: profile.host.version ?? null,
|
|
206
215
|
provider: profileProviderLabel(profile),
|
|
207
216
|
model: profileModelLabel(profile),
|
|
208
217
|
auth: profileAuthLabel(profile),
|
|
209
218
|
path: getProfilePath(profile.name),
|
|
219
|
+
description: profile.description ?? null,
|
|
220
|
+
forkedFrom: profile.forkedFrom ?? null,
|
|
210
221
|
};
|
|
211
222
|
}
|
|
212
223
|
/**
|
|
@@ -227,6 +238,7 @@ export function profileFromPreset(profileName, preset, version) {
|
|
|
227
238
|
description: preset.description,
|
|
228
239
|
preset: preset.name,
|
|
229
240
|
provider: preset.provider,
|
|
241
|
+
forkedFrom: preset.host,
|
|
230
242
|
};
|
|
231
243
|
}
|
|
232
244
|
/**
|
|
@@ -285,13 +297,68 @@ export function profileFromHostModel(name, host, model, opts = {}) {
|
|
|
285
297
|
env,
|
|
286
298
|
description: opts.description ?? `Custom harness: ${host} + ${model}`,
|
|
287
299
|
provider: opts.provider ?? host,
|
|
300
|
+
forkedFrom: host,
|
|
288
301
|
};
|
|
302
|
+
if (opts.label)
|
|
303
|
+
profile.label = opts.label;
|
|
289
304
|
if (opts.provider && opts.authEnvVar) {
|
|
290
305
|
profile.auth = { envVar: opts.authEnvVar, keychainItem: keychainItemName(opts.provider) };
|
|
291
306
|
profile.authOptional = false;
|
|
292
307
|
}
|
|
293
308
|
return profile;
|
|
294
309
|
}
|
|
310
|
+
/**
|
|
311
|
+
* Copy an existing harness under a new name, applying overrides. The fork is a
|
|
312
|
+
* full copy — env, auth binding, and fallback model all carry over — so the two
|
|
313
|
+
* diverge from here and deleting the source never affects the fork.
|
|
314
|
+
*/
|
|
315
|
+
export function forkProfile(source, name, opts = {}) {
|
|
316
|
+
validateProfileName(name);
|
|
317
|
+
const host = source.host.agent;
|
|
318
|
+
const env = { ...source.env };
|
|
319
|
+
if (opts.model) {
|
|
320
|
+
env[profileModelEnvKey(source) ?? modelEnvKeyForHost(host)] = opts.model;
|
|
321
|
+
}
|
|
322
|
+
if (opts.baseUrl) {
|
|
323
|
+
const key = baseUrlEnvKeyForHost(host);
|
|
324
|
+
if (!key) {
|
|
325
|
+
throw new Error(`Host '${host}' has no known base-URL env var; drop --base-url or fork onto a claude/codex host.`);
|
|
326
|
+
}
|
|
327
|
+
env[key] = opts.baseUrl;
|
|
328
|
+
}
|
|
329
|
+
const forked = {
|
|
330
|
+
...source,
|
|
331
|
+
name,
|
|
332
|
+
host: { agent: host, ...(opts.version ? { version: opts.version } : source.host.version ? { version: source.host.version } : {}) },
|
|
333
|
+
env,
|
|
334
|
+
// The source's description names the source's model, so inheriting it
|
|
335
|
+
// across a model swap would describe the fork wrongly.
|
|
336
|
+
description: opts.description ?? (opts.model ? `Forked from ${source.name}: ${opts.model}` : source.description),
|
|
337
|
+
forkedFrom: source.name,
|
|
338
|
+
};
|
|
339
|
+
// `label` is the header `agents view` prints, so an inherited one would make
|
|
340
|
+
// the fork and its source visually identical — the ambiguity a per-harness
|
|
341
|
+
// block exists to remove. A fork carries a label only when it is given one;
|
|
342
|
+
// otherwise `profileLabel` falls back to the fork's own name.
|
|
343
|
+
if (opts.label)
|
|
344
|
+
forked.label = opts.label;
|
|
345
|
+
else
|
|
346
|
+
delete forked.label;
|
|
347
|
+
// A fork that repoints the model or endpoint is no longer that preset — keep
|
|
348
|
+
// the preset link only while the fork still matches what the preset defines.
|
|
349
|
+
if (opts.model || opts.baseUrl)
|
|
350
|
+
delete forked.preset;
|
|
351
|
+
if (opts.provider) {
|
|
352
|
+
const envVar = opts.authEnvVar ?? source.auth?.envVar ?? authEnvKeyForHost(host);
|
|
353
|
+
if (!envVar) {
|
|
354
|
+
throw new Error(`Host '${host}' has no known auth env var; --provider cannot be attached to this fork.`);
|
|
355
|
+
}
|
|
356
|
+
forked.provider = opts.provider;
|
|
357
|
+
forked.auth = { envVar, keychainItem: keychainItemName(opts.provider) };
|
|
358
|
+
forked.authOptional = source.authOptional ?? false;
|
|
359
|
+
}
|
|
360
|
+
return forked;
|
|
361
|
+
}
|
|
295
362
|
/**
|
|
296
363
|
* Resolve a profile into the env block that should be injected into the
|
|
297
364
|
* spawned agent process. Reads the token from keychain at exec time so the
|
package/dist/lib/resources.d.ts
CHANGED
|
@@ -17,6 +17,14 @@ export interface ResolvedResource {
|
|
|
17
17
|
*/
|
|
18
18
|
source: string;
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* True when `rawName` (a filename with its extension already stripped) names a
|
|
22
|
+
* directory doc rather than a resource of `kind`. Exported so every enumerator
|
|
23
|
+
* shares one definition — `listCentralCommands` and `discoverCommands` in
|
|
24
|
+
* `commands.ts` do their own `readdirSync` scans, and without this they would
|
|
25
|
+
* list a `README` that `resolveResource` then refuses to open.
|
|
26
|
+
*/
|
|
27
|
+
export declare function isDirectoryDoc(kind: ResourceKind, rawName: string): boolean;
|
|
20
28
|
/**
|
|
21
29
|
* Resolve a single resource by kind + name using project > user > system precedence.
|
|
22
30
|
* For file-based resources the path ends in `.md`, `.yaml`, or `.yml` as appropriate.
|
package/dist/lib/resources.js
CHANGED
|
@@ -35,6 +35,30 @@ function resourceIsActive(kind, name, source) {
|
|
|
35
35
|
const activeKind = profiledKind(kind);
|
|
36
36
|
return activeKind ? isNameActiveInResourceProfile(activeKind, name, source) : true;
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Documentation filenames that live *beside* resources, describing the directory
|
|
40
|
+
* rather than being a resource in it. A DotAgents repo keeps a `README.md` (for
|
|
41
|
+
* humans) and an `AGENTS.md` (for agents) in each resource dir, with
|
|
42
|
+
* `CLAUDE.md`/`GEMINI.md` symlinked to the latter. Without this filter every one
|
|
43
|
+
* of them materializes as a resource — `commands/README.md` installs a bogus
|
|
44
|
+
* `/README` slash command into every agent home.
|
|
45
|
+
*
|
|
46
|
+
* `rules` is exempt: there `AGENTS.md` IS the resource (the composed ruleset that
|
|
47
|
+
* syncs as each agent's memory file), not documentation about the directory.
|
|
48
|
+
*/
|
|
49
|
+
const DOC_BASENAMES = new Set(['readme', 'agents', 'claude', 'gemini']);
|
|
50
|
+
/**
|
|
51
|
+
* True when `rawName` (a filename with its extension already stripped) names a
|
|
52
|
+
* directory doc rather than a resource of `kind`. Exported so every enumerator
|
|
53
|
+
* shares one definition — `listCentralCommands` and `discoverCommands` in
|
|
54
|
+
* `commands.ts` do their own `readdirSync` scans, and without this they would
|
|
55
|
+
* list a `README` that `resolveResource` then refuses to open.
|
|
56
|
+
*/
|
|
57
|
+
export function isDirectoryDoc(kind, rawName) {
|
|
58
|
+
if (kind === 'rules')
|
|
59
|
+
return false;
|
|
60
|
+
return DOC_BASENAMES.has(rawName.toLowerCase());
|
|
61
|
+
}
|
|
38
62
|
/**
|
|
39
63
|
* Resolve a single resource by kind + name using project > user > system precedence.
|
|
40
64
|
* For file-based resources the path ends in `.md`, `.yaml`, or `.yml` as appropriate.
|
|
@@ -62,7 +86,10 @@ export function resolveResource(kind, name, cwd) {
|
|
|
62
86
|
}
|
|
63
87
|
continue;
|
|
64
88
|
}
|
|
65
|
-
// Try with common file extensions
|
|
89
|
+
// Try with common file extensions. A directory doc (README/AGENTS/CLAUDE/
|
|
90
|
+
// GEMINI) describes the directory and is never itself a resource.
|
|
91
|
+
if (isDirectoryDoc(kind, name))
|
|
92
|
+
continue;
|
|
66
93
|
for (const ext of ['.md', '.yaml', '.yml']) {
|
|
67
94
|
const withExt = exactPath + ext;
|
|
68
95
|
if (fs.existsSync(withExt)) {
|
|
@@ -105,6 +132,12 @@ export function listResources(kind, cwd) {
|
|
|
105
132
|
if (entry.name.startsWith('.'))
|
|
106
133
|
continue;
|
|
107
134
|
const rawName = entry.name.replace(/\.(md|yaml|yml)$/, '');
|
|
135
|
+
// Not isFile(): a Dirent for a symlink reports isFile() === false, and
|
|
136
|
+
// CLAUDE.md/GEMINI.md are symlinks to AGENTS.md by convention. Anything
|
|
137
|
+
// that is not a directory is a candidate doc; a resource directory that
|
|
138
|
+
// happens to be named `agents/` is still a real resource.
|
|
139
|
+
if (!entry.isDirectory() && isDirectoryDoc(kind, rawName))
|
|
140
|
+
continue;
|
|
108
141
|
if (seen.has(rawName))
|
|
109
142
|
continue;
|
|
110
143
|
if (!resourceIsActive(kind, rawName, source))
|
package/dist/lib/rotate.d.ts
CHANGED
|
@@ -35,6 +35,12 @@ export interface RotateResult {
|
|
|
35
35
|
healthy: RotateCandidate[];
|
|
36
36
|
/** Candidates excluded (not signed in, or out of credits). */
|
|
37
37
|
excluded: RotateCandidate[];
|
|
38
|
+
/**
|
|
39
|
+
* True when NO candidate on this machine had usage data fresh enough to decide
|
|
40
|
+
* on, so the pick was made from unverified snapshots. Callers surface it —
|
|
41
|
+
* routing blind is a fact the operator needs, not an internal detail.
|
|
42
|
+
*/
|
|
43
|
+
usageUnverified?: boolean;
|
|
38
44
|
}
|
|
39
45
|
export declare const RUN_STRATEGIES: RunStrategy[];
|
|
40
46
|
/**
|
|
@@ -60,6 +66,22 @@ export declare function getProjectRunStrategy(agent: AgentId, startPath: string)
|
|
|
60
66
|
export declare function getConfiguredRunStrategy(agent: AgentId, startPath?: string): RunStrategy;
|
|
61
67
|
/** Persist the global run strategy used by bare `agents run <agent>`. */
|
|
62
68
|
export declare function setGlobalRunStrategy(agent: AgentId, strategy: RunStrategy): void;
|
|
69
|
+
/**
|
|
70
|
+
* How old a usage snapshot may be and still settle a routing DECISION.
|
|
71
|
+
*
|
|
72
|
+
* Deliberately far tighter than the 24h stale-while-revalidate window the
|
|
73
|
+
* display paths use (`USAGE_CACHE_SWR_MS`): `agents view` rendering a slightly
|
|
74
|
+
* old bar costs nothing, but the router choosing an account from one costs the
|
|
75
|
+
* whole run. Measured case — `yosemite-s1` held snapshots 26h to 2.7 days old
|
|
76
|
+
* with a failing refresh, so balanced read `muqsit@getrush.ai` as 48% used and
|
|
77
|
+
* launched into it while the account was actually at its weekly cap.
|
|
78
|
+
*/
|
|
79
|
+
export declare const USAGE_DECISION_MAX_AGE_MS: number;
|
|
80
|
+
/**
|
|
81
|
+
* Whether this candidate's usage number is recent enough to route on. A missing
|
|
82
|
+
* snapshot is unverified by definition — there is no number to trust.
|
|
83
|
+
*/
|
|
84
|
+
export declare function isUsageVerified(candidate: RotateCandidate, nowMs?: number): boolean;
|
|
63
85
|
/**
|
|
64
86
|
* Whether a specific account can serve a run right now, and — when it can't —
|
|
65
87
|
* why. `signed_out` covers a missing usable credential; `rate_limited` and
|
|
@@ -118,13 +140,13 @@ export declare function checkRunAccountReadiness(agent: AgentId, version: string
|
|
|
118
140
|
* Returns null if no candidate is eligible — callers fall back to the pinned
|
|
119
141
|
* version so behavior stays predictable.
|
|
120
142
|
*/
|
|
121
|
-
export declare function pickBalancedCandidate(candidates: RotateCandidate[]): RotateResult | null;
|
|
143
|
+
export declare function pickBalancedCandidate(candidates: RotateCandidate[], nowMs?: number): RotateResult | null;
|
|
122
144
|
/**
|
|
123
145
|
* Pick an available candidate. Prefers the configured pinned version when that
|
|
124
146
|
* version has usage available; otherwise routes to the candidate with the most
|
|
125
147
|
* usage headroom.
|
|
126
148
|
*/
|
|
127
|
-
export declare function pickAvailableCandidate(candidates: RotateCandidate[], preferredVersion?: string | null): RotateResult | null;
|
|
149
|
+
export declare function pickAvailableCandidate(candidates: RotateCandidate[], preferredVersion?: string | null, nowMs?: number): RotateResult | null;
|
|
128
150
|
export declare function collectRunCandidates(agent: AgentId): Promise<RotateCandidate[]>;
|
|
129
151
|
/**
|
|
130
152
|
* Resolve an account identity to the installed version slot that holds it, over
|
package/dist/lib/rotate.js
CHANGED
|
@@ -70,6 +70,27 @@ function isRotationEligible(candidate) {
|
|
|
70
70
|
function isAvailableEligible(candidate) {
|
|
71
71
|
return isRotationEligible(candidate);
|
|
72
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* How old a usage snapshot may be and still settle a routing DECISION.
|
|
75
|
+
*
|
|
76
|
+
* Deliberately far tighter than the 24h stale-while-revalidate window the
|
|
77
|
+
* display paths use (`USAGE_CACHE_SWR_MS`): `agents view` rendering a slightly
|
|
78
|
+
* old bar costs nothing, but the router choosing an account from one costs the
|
|
79
|
+
* whole run. Measured case — `yosemite-s1` held snapshots 26h to 2.7 days old
|
|
80
|
+
* with a failing refresh, so balanced read `muqsit@getrush.ai` as 48% used and
|
|
81
|
+
* launched into it while the account was actually at its weekly cap.
|
|
82
|
+
*/
|
|
83
|
+
export const USAGE_DECISION_MAX_AGE_MS = 5 * 60 * 1000;
|
|
84
|
+
/**
|
|
85
|
+
* Whether this candidate's usage number is recent enough to route on. A missing
|
|
86
|
+
* snapshot is unverified by definition — there is no number to trust.
|
|
87
|
+
*/
|
|
88
|
+
export function isUsageVerified(candidate, nowMs = Date.now()) {
|
|
89
|
+
const capturedAt = candidate.usageSnapshot?.capturedAt;
|
|
90
|
+
if (!capturedAt)
|
|
91
|
+
return false;
|
|
92
|
+
return nowMs - capturedAt.getTime() <= USAGE_DECISION_MAX_AGE_MS;
|
|
93
|
+
}
|
|
73
94
|
function hasUsageAvailable(candidate) {
|
|
74
95
|
const snapshot = candidate.usageSnapshot;
|
|
75
96
|
if (snapshot && snapshot.windows.length > 0) {
|
|
@@ -199,7 +220,7 @@ function dedupeAndSortCandidates(candidates) {
|
|
|
199
220
|
* Returns null if no candidate is eligible — callers fall back to the pinned
|
|
200
221
|
* version so behavior stays predictable.
|
|
201
222
|
*/
|
|
202
|
-
export function pickBalancedCandidate(candidates) {
|
|
223
|
+
export function pickBalancedCandidate(candidates, nowMs = Date.now()) {
|
|
203
224
|
const healthy = [];
|
|
204
225
|
const excluded = [];
|
|
205
226
|
for (const c of candidates) {
|
|
@@ -217,8 +238,31 @@ export function pickBalancedCandidate(candidates) {
|
|
|
217
238
|
if (!deduped.has(c))
|
|
218
239
|
excluded.push(c);
|
|
219
240
|
}
|
|
220
|
-
const picked =
|
|
221
|
-
return { picked, healthy: sorted, excluded };
|
|
241
|
+
const { picked, usageUnverified } = preferVerified(sorted, nowMs, weightedRandomByCapacity);
|
|
242
|
+
return { picked, healthy: sorted, excluded, usageUnverified };
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Choose from the VERIFIED candidates when any exist, else from the whole pool.
|
|
246
|
+
*
|
|
247
|
+
* An eligible account whose usage we could not confirm is a guess, not a green
|
|
248
|
+
* light: the snapshot reads "48% used" with equal confidence whether it was
|
|
249
|
+
* captured a minute or three days ago, and a box whose refresh is failing stays
|
|
250
|
+
* wrong indefinitely. Confirmed headroom therefore beats apparent headroom, even
|
|
251
|
+
* when the unconfirmed number looks better.
|
|
252
|
+
*
|
|
253
|
+
* `healthy` deliberately keeps every eligible candidate rather than just the
|
|
254
|
+
* verified ones. Declining to *pick* an account on stale data and declining to
|
|
255
|
+
* *fail over to* it after the primary has already hit a 429 are different risks:
|
|
256
|
+
* by then the alternative is not launching at all, so the failover chain
|
|
257
|
+
* (rotationFailoverChain, which reads `healthy`) keeps its full safety net —
|
|
258
|
+
* exactly on the machines this guard is protecting.
|
|
259
|
+
*/
|
|
260
|
+
function preferVerified(pool, nowMs, choose) {
|
|
261
|
+
const verified = pool.filter((c) => isUsageVerified(c, nowMs));
|
|
262
|
+
return {
|
|
263
|
+
picked: choose(verified.length > 0 ? verified : pool),
|
|
264
|
+
usageUnverified: verified.length === 0,
|
|
265
|
+
};
|
|
222
266
|
}
|
|
223
267
|
/**
|
|
224
268
|
* Pick one candidate from `sorted` using weights proportional to remaining
|
|
@@ -250,7 +294,7 @@ function weightedRandomByCapacity(sorted) {
|
|
|
250
294
|
* version has usage available; otherwise routes to the candidate with the most
|
|
251
295
|
* usage headroom.
|
|
252
296
|
*/
|
|
253
|
-
export function pickAvailableCandidate(candidates, preferredVersion) {
|
|
297
|
+
export function pickAvailableCandidate(candidates, preferredVersion, nowMs = Date.now()) {
|
|
254
298
|
const healthy = [];
|
|
255
299
|
const excluded = [];
|
|
256
300
|
for (const c of candidates) {
|
|
@@ -268,10 +312,17 @@ export function pickAvailableCandidate(candidates, preferredVersion) {
|
|
|
268
312
|
if (!deduped.has(c))
|
|
269
313
|
excluded.push(c);
|
|
270
314
|
}
|
|
315
|
+
// `available` sorts by apparent headroom and takes the front of the list, so an
|
|
316
|
+
// unconfirmed "48% used" outranks an accurate "90% used" — the same inversion
|
|
317
|
+
// that put a launch on an exhausted account under `balanced`. It routes on the
|
|
318
|
+
// same cache, so it gets the same rule: confirmed headroom first.
|
|
319
|
+
const { picked: bestVerified, usageUnverified } = preferVerified(sorted, nowMs, (from) => from[0]);
|
|
320
|
+
// An explicit version preference is an instruction, not a ranking signal, so it
|
|
321
|
+
// still wins — but only while that version is actually eligible.
|
|
271
322
|
const preferred = preferredVersion
|
|
272
323
|
? sorted.find((candidate) => candidate.version === preferredVersion)
|
|
273
324
|
: undefined;
|
|
274
|
-
return { picked: preferred ??
|
|
325
|
+
return { picked: preferred ?? bestVerified, healthy: sorted, excluded, usageUnverified };
|
|
275
326
|
}
|
|
276
327
|
export async function collectRunCandidates(agent) {
|
|
277
328
|
const versions = listInstalledVersions(agent);
|
|
@@ -299,12 +350,18 @@ export async function collectRunCandidates(agent) {
|
|
|
299
350
|
lastActive: info.lastActive,
|
|
300
351
|
};
|
|
301
352
|
}));
|
|
353
|
+
// These candidates feed a routing decision, so cap how stale their usage may
|
|
354
|
+
// be (see USAGE_DECISION_MAX_AGE_MS). Past that the fetch blocks on a live
|
|
355
|
+
// read instead of serving the cache — one bounded, parallel round trip per
|
|
356
|
+
// account, and none at all inside the 2-minute fresh window that back-to-back
|
|
357
|
+
// launches hit. A failed read still falls back to the cache; the pick then
|
|
358
|
+
// routes around it via isUsageVerified rather than trusting the old number.
|
|
302
359
|
const { usageByKey } = await getUsageInfoByIdentity(rows.map(({ home, info, version }) => ({
|
|
303
360
|
agentId: agent,
|
|
304
361
|
home,
|
|
305
362
|
cliVersion: version,
|
|
306
363
|
info,
|
|
307
|
-
})));
|
|
364
|
+
})), { maxAgeMs: USAGE_DECISION_MAX_AGE_MS });
|
|
308
365
|
return rows.map(({ home: _home, info, ...candidate }) => {
|
|
309
366
|
const usageKey = getUsageLookupKey(info);
|
|
310
367
|
const usage = usageKey ? usageByKey.get(usageKey) : undefined;
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* pin for fleet/host/cloud strategies (applied at add/sync time).
|
|
11
11
|
*/
|
|
12
12
|
import type { JobConfig } from './routines.js';
|
|
13
|
+
import { type DevicePlatform } from './devices/registry.js';
|
|
13
14
|
export type PlacementTarget = {
|
|
14
15
|
mode: 'local';
|
|
15
16
|
} | {
|
|
@@ -30,7 +31,7 @@ export type PlacementTarget = {
|
|
|
30
31
|
* the double-fire pin (`devices: [self]`) would collapse fleet placement to
|
|
31
32
|
* always-local. Control / offline / no-address devices are never chosen.
|
|
32
33
|
*/
|
|
33
|
-
export declare function pickFleetDevice(_config?: Pick<JobConfig, 'devices'
|
|
34
|
+
export declare function pickFleetDevice(_config?: Pick<JobConfig, 'devices'>, platform?: DevicePlatform): string | null;
|
|
34
35
|
/**
|
|
35
36
|
* Resolve where a fired job's body should execute.
|
|
36
37
|
* Throws a human-readable Error when placement cannot be satisfied.
|
|
@@ -25,7 +25,7 @@ import { planFleetTargets } from './devices/fleet.js';
|
|
|
25
25
|
* the double-fire pin (`devices: [self]`) would collapse fleet placement to
|
|
26
26
|
* always-local. Control / offline / no-address devices are never chosen.
|
|
27
27
|
*/
|
|
28
|
-
export function pickFleetDevice(_config) {
|
|
28
|
+
export function pickFleetDevice(_config, platform) {
|
|
29
29
|
let reg;
|
|
30
30
|
try {
|
|
31
31
|
reg = loadDevicesSync();
|
|
@@ -34,11 +34,15 @@ export function pickFleetDevice(_config) {
|
|
|
34
34
|
return null;
|
|
35
35
|
}
|
|
36
36
|
const planned = planFleetTargets(reg);
|
|
37
|
-
const candidates = planned
|
|
37
|
+
const candidates = planned
|
|
38
|
+
.filter((t) => !t.skip && (!platform || t.device.platform === platform))
|
|
39
|
+
.map((t) => t.device.name);
|
|
38
40
|
if (candidates.length === 0) {
|
|
39
41
|
// No registry / nothing online: fall back to self so a single-box fleet
|
|
40
|
-
// without a registry entry still runs locally.
|
|
41
|
-
|
|
42
|
+
// without a registry entry still runs locally. Only when no platform filter
|
|
43
|
+
// was requested — an unmet filter must fail loud so e.g. `fleet/linux` never
|
|
44
|
+
// silently lands on a macOS box.
|
|
45
|
+
return platform ? null : machineId();
|
|
42
46
|
}
|
|
43
47
|
const self = machineId();
|
|
44
48
|
const selfMatch = candidates.find((n) => normalizeHost(n) === self);
|
package/dist/lib/routines.d.ts
CHANGED
|
@@ -78,6 +78,10 @@ export interface LinearJobTrigger {
|
|
|
78
78
|
teamKey?: string;
|
|
79
79
|
/** Required issue label name. */
|
|
80
80
|
label?: string;
|
|
81
|
+
/** Current Linear state name that must match (e.g. `Plan`). */
|
|
82
|
+
stateTo?: string;
|
|
83
|
+
/** Previous Linear state name that must match (e.g. `Triage`). */
|
|
84
|
+
stateFrom?: string;
|
|
81
85
|
}
|
|
82
86
|
export type JobTrigger = GithubJobTrigger | LinearJobTrigger;
|
|
83
87
|
/**
|
|
@@ -118,6 +122,12 @@ export interface JobConfig {
|
|
|
118
122
|
* overdue; everywhere else it is inert and `run` refuses with a pointer.
|
|
119
123
|
*/
|
|
120
124
|
devices?: string[];
|
|
125
|
+
/**
|
|
126
|
+
* Environment variables injected into the spawned run, on top of the sandbox
|
|
127
|
+
* overlay's own. Merged by `buildSpawnEnv`, so it applies to both the
|
|
128
|
+
* foreground and detached execution paths.
|
|
129
|
+
*/
|
|
130
|
+
env?: Record<string, string>;
|
|
121
131
|
/**
|
|
122
132
|
* Execution placement — run the job body on this machine over SSH (a
|
|
123
133
|
* registered host, device, capability tag, or user@host) instead of locally.
|
|
@@ -360,8 +370,54 @@ export declare function oneShotScheduleFireDate(schedule: string | undefined | n
|
|
|
360
370
|
export declare function isPastOneShotRoutine(config: Pick<JobConfig, 'schedule' | 'runOnce' | 'timezone'>, now?: Date): boolean;
|
|
361
371
|
export declare function hasCompletedOneShotRun(config: Pick<JobConfig, 'name' | 'schedule' | 'runOnce' | 'timezone'>, now?: Date): boolean;
|
|
362
372
|
export declare function shouldPurgeCompletedOneShotRoutine(config: Pick<JobConfig, 'name' | 'schedule' | 'runOnce' | 'timezone'>, now?: Date): boolean;
|
|
373
|
+
/**
|
|
374
|
+
* Context passed to `resolveJobPrompt` when a job is fired by a webhook. Lets
|
|
375
|
+
* prompts use `{{issue.identifier}}`, `{{updatedFrom.state.name}}`, etc.
|
|
376
|
+
*/
|
|
377
|
+
export interface WebhookContext {
|
|
378
|
+
source: string;
|
|
379
|
+
event: string;
|
|
380
|
+
action?: string;
|
|
381
|
+
issue?: unknown;
|
|
382
|
+
updatedFrom?: unknown;
|
|
383
|
+
pull_request?: unknown;
|
|
384
|
+
repository?: unknown;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Substitute `{{dotted.path}}` placeholders in a string using a webhook context.
|
|
388
|
+
* Missing values are replaced with an empty string.
|
|
389
|
+
*/
|
|
390
|
+
export declare function substituteWebhookPrompt(prompt: string, context: WebhookContext): string;
|
|
391
|
+
/**
|
|
392
|
+
* Substitute `{{dotted.path}}` placeholders in a string destined for a SHELL,
|
|
393
|
+
* quoting every substituted value so payload content cannot break out of it.
|
|
394
|
+
*
|
|
395
|
+
* `run.command` is executed through a shell, and its context is built from an
|
|
396
|
+
* external webhook payload — `issue.title`, `issue.description`, and the GitHub
|
|
397
|
+
* `pull_request` fields are free text any outside contributor can set. Pasting
|
|
398
|
+
* those in raw (as {@link substituteWebhookPrompt} does, correctly, for prompts)
|
|
399
|
+
* turns an operator's `echo {{issue.title}}` into a command-injection sink.
|
|
400
|
+
*
|
|
401
|
+
* The template itself is operator-authored and stays unquoted, so pipes,
|
|
402
|
+
* redirects, and `&&` in the configured command keep working. Only the
|
|
403
|
+
* interpolated values are quoted.
|
|
404
|
+
*
|
|
405
|
+
* POSIX `sh` quoting: wrap in single quotes and close/escape/reopen for any
|
|
406
|
+
* embedded single quote. `exec` uses `cmd.exe` on Windows, which does not
|
|
407
|
+
* honour these rules — see `assertShellSubstitutionSupported`.
|
|
408
|
+
*/
|
|
409
|
+
export declare function substituteWebhookCommand(command: string, context: WebhookContext): string;
|
|
410
|
+
/**
|
|
411
|
+
* Refuse a `run.command` carrying placeholders on a platform whose shell we
|
|
412
|
+
* cannot safely quote for. `child_process.exec` runs through `cmd.exe` on
|
|
413
|
+
* Windows, where POSIX single-quoting is not a quoting mechanism at all, so
|
|
414
|
+
* {@link substituteWebhookCommand} would not contain a hostile value.
|
|
415
|
+
*
|
|
416
|
+
* Fail loud rather than execute something we cannot prove is safe.
|
|
417
|
+
*/
|
|
418
|
+
export declare function assertShellSubstitutionSupported(command: string, platform?: NodeJS.Platform): void;
|
|
363
419
|
/** Expand built-in and user-defined template variables in a job's prompt string. */
|
|
364
|
-
export declare function resolveJobPrompt(config: JobConfig): string;
|
|
420
|
+
export declare function resolveJobPrompt(config: JobConfig, context?: WebhookContext): string;
|
|
365
421
|
/** Parse a human-readable timeout string (e.g. "10m", "2h", "1h30m", "3d", "1w") into milliseconds.
|
|
366
422
|
* Accepts combinations of w (weeks), d (days), h (hours), m (minutes).
|
|
367
423
|
* Returns null if the string is empty, matches nothing, totals zero, or exceeds 1 week.
|
package/dist/lib/routines.js
CHANGED
|
@@ -569,6 +569,12 @@ export function validateTrigger(trigger) {
|
|
|
569
569
|
if (linear.label !== undefined && typeof linear.label !== 'string') {
|
|
570
570
|
errors.push('trigger.label must be a string');
|
|
571
571
|
}
|
|
572
|
+
if (linear.stateTo !== undefined && typeof linear.stateTo !== 'string') {
|
|
573
|
+
errors.push('trigger.stateTo must be a string');
|
|
574
|
+
}
|
|
575
|
+
if (linear.stateFrom !== undefined && typeof linear.stateFrom !== 'string') {
|
|
576
|
+
errors.push('trigger.stateFrom must be a string');
|
|
577
|
+
}
|
|
572
578
|
return errors;
|
|
573
579
|
}
|
|
574
580
|
function isParseableDate(value) {
|
|
@@ -702,8 +708,71 @@ export function hasCompletedOneShotRun(config, now = new Date()) {
|
|
|
702
708
|
export function shouldPurgeCompletedOneShotRoutine(config, now = new Date()) {
|
|
703
709
|
return hasCompletedOneShotRun(config, now);
|
|
704
710
|
}
|
|
711
|
+
function getPath(obj, path) {
|
|
712
|
+
const parts = path.split('.');
|
|
713
|
+
let current = obj;
|
|
714
|
+
for (const part of parts) {
|
|
715
|
+
if (current === null || current === undefined)
|
|
716
|
+
return undefined;
|
|
717
|
+
current = current[part];
|
|
718
|
+
}
|
|
719
|
+
return current;
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Substitute `{{dotted.path}}` placeholders in a string using a webhook context.
|
|
723
|
+
* Missing values are replaced with an empty string.
|
|
724
|
+
*/
|
|
725
|
+
export function substituteWebhookPrompt(prompt, context) {
|
|
726
|
+
return prompt.replace(/\{\{([^{}]+)\}\}/g, (_, rawPath) => {
|
|
727
|
+
const value = getPath(context, rawPath.trim());
|
|
728
|
+
if (value === undefined || value === null)
|
|
729
|
+
return '';
|
|
730
|
+
return String(value);
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Substitute `{{dotted.path}}` placeholders in a string destined for a SHELL,
|
|
735
|
+
* quoting every substituted value so payload content cannot break out of it.
|
|
736
|
+
*
|
|
737
|
+
* `run.command` is executed through a shell, and its context is built from an
|
|
738
|
+
* external webhook payload — `issue.title`, `issue.description`, and the GitHub
|
|
739
|
+
* `pull_request` fields are free text any outside contributor can set. Pasting
|
|
740
|
+
* those in raw (as {@link substituteWebhookPrompt} does, correctly, for prompts)
|
|
741
|
+
* turns an operator's `echo {{issue.title}}` into a command-injection sink.
|
|
742
|
+
*
|
|
743
|
+
* The template itself is operator-authored and stays unquoted, so pipes,
|
|
744
|
+
* redirects, and `&&` in the configured command keep working. Only the
|
|
745
|
+
* interpolated values are quoted.
|
|
746
|
+
*
|
|
747
|
+
* POSIX `sh` quoting: wrap in single quotes and close/escape/reopen for any
|
|
748
|
+
* embedded single quote. `exec` uses `cmd.exe` on Windows, which does not
|
|
749
|
+
* honour these rules — see `assertShellSubstitutionSupported`.
|
|
750
|
+
*/
|
|
751
|
+
export function substituteWebhookCommand(command, context) {
|
|
752
|
+
return command.replace(/\{\{([^{}]+)\}\}/g, (_, rawPath) => {
|
|
753
|
+
const value = getPath(context, rawPath.trim());
|
|
754
|
+
if (value === undefined || value === null)
|
|
755
|
+
return "''";
|
|
756
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Refuse a `run.command` carrying placeholders on a platform whose shell we
|
|
761
|
+
* cannot safely quote for. `child_process.exec` runs through `cmd.exe` on
|
|
762
|
+
* Windows, where POSIX single-quoting is not a quoting mechanism at all, so
|
|
763
|
+
* {@link substituteWebhookCommand} would not contain a hostile value.
|
|
764
|
+
*
|
|
765
|
+
* Fail loud rather than execute something we cannot prove is safe.
|
|
766
|
+
*/
|
|
767
|
+
export function assertShellSubstitutionSupported(command, platform = process.platform) {
|
|
768
|
+
if (platform === 'win32' && /\{\{[^{}]+\}\}/.test(command)) {
|
|
769
|
+
throw new Error('run.command with {{…}} placeholders is not supported on Windows: the values come from an ' +
|
|
770
|
+
'untrusted webhook payload and cmd.exe cannot be quoted safely. Use run.prompt, or a ' +
|
|
771
|
+
'command with no placeholders.');
|
|
772
|
+
}
|
|
773
|
+
}
|
|
705
774
|
/** Expand built-in and user-defined template variables in a job's prompt string. */
|
|
706
|
-
export function resolveJobPrompt(config) {
|
|
775
|
+
export function resolveJobPrompt(config, context) {
|
|
707
776
|
const now = new Date();
|
|
708
777
|
const tz = config.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
709
778
|
const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
@@ -725,6 +794,10 @@ export function resolveJobPrompt(config) {
|
|
|
725
794
|
prompt = prompt.replace(new RegExp(`\\{${key}\\}`, 'g'), value);
|
|
726
795
|
}
|
|
727
796
|
}
|
|
797
|
+
// Webhook-driven variables ({{issue.identifier}}, {{updatedFrom.state.name}}, ...)
|
|
798
|
+
if (context) {
|
|
799
|
+
prompt = substituteWebhookPrompt(prompt, context);
|
|
800
|
+
}
|
|
728
801
|
// Last report (special handling). Only a COMPLETED run's report is injected —
|
|
729
802
|
// a failed run's report.md is the agent's error text (e.g. a login prompt on
|
|
730
803
|
// an auth failure), and feeding that into the next prompt poisons every
|
package/dist/lib/runner.d.ts
CHANGED
|
@@ -26,6 +26,8 @@ export interface RunResult {
|
|
|
26
26
|
/** Agents the daemon can actually run, derived from the command table above
|
|
27
27
|
* so the `--agent` help and any validation can never drift from it. */
|
|
28
28
|
export declare const ROUTINE_AGENT_IDS: readonly string[];
|
|
29
|
+
/** Stable working directory for routine children, independent of the daemon's launch cwd. */
|
|
30
|
+
export declare function routineSpawnCwd(config: Pick<JobConfig, 'repo'>, configuredRoot?: string | undefined): string;
|
|
29
31
|
/** Build the full CLI argv for executing a job, applying mode, model, and permission flags. */
|
|
30
32
|
export declare function buildJobCommand(config: JobConfig, resolvedPrompt: string): string[];
|
|
31
33
|
export declare function archiveRoutineTranscripts(meta: Pick<RunMeta, 'jobName' | 'runId' | 'agent'>, runDir: string, overlayHome?: string): void;
|