@phnx-labs/agents-cli 1.20.35 → 1.20.36
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 +91 -0
- package/LICENSE +185 -21
- package/README.md +8 -4
- package/dist/commands/audit.d.ts +14 -0
- package/dist/commands/audit.js +68 -0
- package/dist/commands/browser.js +82 -8
- package/dist/commands/check.d.ts +15 -0
- package/dist/commands/check.js +84 -0
- package/dist/commands/cloud.js +143 -3
- package/dist/commands/computer.js +81 -0
- package/dist/commands/daemon.js +4 -1
- package/dist/commands/doctor.js +1 -89
- package/dist/commands/events.js +3 -3
- package/dist/commands/exec.d.ts +19 -0
- package/dist/commands/exec.js +277 -11
- package/dist/commands/hosts.js +10 -6
- package/dist/commands/inspect.js +8 -9
- package/dist/commands/lock.d.ts +12 -0
- package/dist/commands/lock.js +70 -0
- package/dist/commands/message.d.ts +15 -0
- package/dist/commands/message.js +56 -0
- package/dist/commands/routines.js +101 -5
- package/dist/commands/secrets-migrate.js +106 -57
- package/dist/commands/secrets.d.ts +31 -18
- package/dist/commands/secrets.js +156 -75
- package/dist/commands/serve.d.ts +10 -0
- package/dist/commands/serve.js +37 -0
- package/dist/commands/sessions-inject.d.ts +14 -0
- package/dist/commands/sessions-inject.js +111 -0
- package/dist/commands/sessions-picker.d.ts +2 -0
- package/dist/commands/sessions-picker.js +24 -3
- package/dist/commands/sessions-resume.js +20 -8
- package/dist/commands/sessions.d.ts +71 -1
- package/dist/commands/sessions.js +365 -37
- package/dist/commands/setup.js +4 -2
- package/dist/commands/sync.d.ts +3 -1
- package/dist/commands/sync.js +156 -4
- package/dist/commands/teams.js +217 -0
- package/dist/commands/versions.js +2 -4
- package/dist/commands/watchdog.d.ts +18 -0
- package/dist/commands/watchdog.js +238 -0
- package/dist/index.js +25 -2
- package/dist/lib/audit/log.d.ts +92 -0
- package/dist/lib/audit/log.js +177 -0
- package/dist/lib/auto-pull.js +2 -1
- package/dist/lib/browser/chrome.d.ts +10 -0
- package/dist/lib/browser/chrome.js +18 -7
- package/dist/lib/browser/drivers/ssh.js +2 -1
- package/dist/lib/browser/har.d.ts +84 -0
- package/dist/lib/browser/har.js +77 -0
- package/dist/lib/browser/ipc.js +24 -3
- package/dist/lib/browser/profiles.d.ts +1 -1
- package/dist/lib/browser/profiles.js +8 -10
- package/dist/lib/browser/refs.d.ts +65 -0
- package/dist/lib/browser/refs.js +73 -1
- package/dist/lib/browser/runtime-state.js +1 -0
- package/dist/lib/browser/service.d.ts +38 -2
- package/dist/lib/browser/service.js +112 -8
- package/dist/lib/browser/types.d.ts +14 -1
- package/dist/lib/budget/live-cloud.d.ts +42 -0
- package/dist/lib/budget/live-cloud.js +79 -0
- package/dist/lib/budget/live-team.d.ts +31 -0
- package/dist/lib/budget/live-team.js +115 -0
- package/dist/lib/cloud/codex.js +4 -0
- package/dist/lib/cloud/rush.d.ts +12 -1
- package/dist/lib/cloud/rush.js +13 -3
- package/dist/lib/cloud/types.d.ts +9 -0
- package/dist/lib/computer/dispatch.d.ts +8 -0
- package/dist/lib/computer/dispatch.js +125 -0
- package/dist/lib/computer/loop.d.ts +62 -0
- package/dist/lib/computer/loop.js +98 -0
- package/dist/lib/computer/model.d.ts +44 -0
- package/dist/lib/computer/model.js +157 -0
- package/dist/lib/concurrency.d.ts +19 -0
- package/dist/lib/concurrency.js +33 -0
- package/dist/lib/daemon.d.ts +57 -0
- package/dist/lib/daemon.js +192 -16
- package/dist/lib/devices/registry.d.ts +7 -0
- package/dist/lib/devices/registry.js +24 -0
- package/dist/lib/devices/tailscale.js +1 -1
- package/dist/lib/drift.d.ts +52 -0
- package/dist/lib/drift.js +112 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/events.js +31 -13
- package/dist/lib/exec.d.ts +17 -0
- package/dist/lib/exec.js +79 -13
- package/dist/lib/git.d.ts +27 -0
- package/dist/lib/git.js +56 -1
- package/dist/lib/hooks/cache.d.ts +6 -0
- package/dist/lib/hooks/cache.js +54 -12
- package/dist/lib/hooks.d.ts +27 -0
- package/dist/lib/hooks.js +127 -8
- package/dist/lib/hosts/dispatch.d.ts +15 -0
- package/dist/lib/hosts/dispatch.js +39 -6
- package/dist/lib/hosts/logs.js +30 -1
- package/dist/lib/hosts/option.js +1 -1
- package/dist/lib/hosts/passthrough.js +3 -1
- package/dist/lib/hosts/ready.d.ts +29 -6
- package/dist/lib/hosts/ready.js +66 -15
- package/dist/lib/hosts/registry.d.ts +19 -2
- package/dist/lib/hosts/registry.js +58 -2
- package/dist/lib/hosts/remote-cmd.d.ts +62 -1
- package/dist/lib/hosts/remote-cmd.js +70 -1
- package/dist/lib/hosts/remote-os.d.ts +17 -0
- package/dist/lib/hosts/remote-os.js +30 -0
- package/dist/lib/hosts/session-index.d.ts +34 -0
- package/dist/lib/hosts/session-index.js +56 -0
- package/dist/lib/hosts/tasks.d.ts +14 -0
- package/dist/lib/hosts/tasks.js +15 -0
- package/dist/lib/lock.d.ts +93 -0
- package/dist/lib/lock.js +207 -0
- package/dist/lib/loop.js +16 -1
- package/dist/lib/machine-id.d.ts +21 -0
- package/dist/lib/machine-id.js +26 -0
- package/dist/lib/mailbox-target.d.ts +36 -0
- package/dist/lib/mailbox-target.js +45 -0
- package/dist/lib/mailbox.d.ts +47 -0
- package/dist/lib/mailbox.js +194 -0
- package/dist/lib/mcp.d.ts +5 -0
- package/dist/lib/mcp.js +24 -8
- package/dist/lib/migrate.d.ts +19 -0
- package/dist/lib/migrate.js +134 -26
- package/dist/lib/overdue.js +3 -0
- package/dist/lib/picker.d.ts +2 -0
- package/dist/lib/picker.js +4 -1
- package/dist/lib/platform/exec.d.ts +46 -0
- package/dist/lib/platform/exec.js +74 -0
- package/dist/lib/platform/process.d.ts +31 -0
- package/dist/lib/platform/process.js +34 -1
- package/dist/lib/platform/winpath.js +2 -0
- package/dist/lib/plugins.js +16 -6
- package/dist/lib/profiles.d.ts +25 -0
- package/dist/lib/profiles.js +22 -6
- package/dist/lib/pty-client.js +2 -1
- package/dist/lib/rotate.d.ts +61 -0
- package/dist/lib/rotate.js +52 -0
- package/dist/lib/routines.d.ts +40 -2
- package/dist/lib/routines.js +66 -8
- package/dist/lib/runner.d.ts +11 -2
- package/dist/lib/runner.js +49 -7
- package/dist/lib/scheduler.js +6 -1
- package/dist/lib/secrets/bundles.d.ts +60 -4
- package/dist/lib/secrets/bundles.js +131 -12
- package/dist/lib/secrets/filestore.d.ts +3 -0
- package/dist/lib/secrets/filestore.js +42 -16
- package/dist/lib/secrets/index.d.ts +43 -2
- package/dist/lib/secrets/index.js +102 -3
- package/dist/lib/secrets/mcp.d.ts +93 -0
- package/dist/lib/secrets/mcp.js +205 -0
- package/dist/lib/secrets/remote.js +12 -5
- package/dist/lib/secrets/sync.js +83 -4
- package/dist/lib/secrets/windows.js +14 -3
- package/dist/lib/serve/data.d.ts +81 -0
- package/dist/lib/serve/data.js +91 -0
- package/dist/lib/serve/page.d.ts +7 -0
- package/dist/lib/serve/page.js +140 -0
- package/dist/lib/serve/server.d.ts +46 -0
- package/dist/lib/serve/server.js +115 -0
- package/dist/lib/session/active.d.ts +54 -0
- package/dist/lib/session/active.js +190 -19
- package/dist/lib/session/discover.d.ts +37 -0
- package/dist/lib/session/discover.js +111 -28
- package/dist/lib/session/inject.d.ts +18 -0
- package/dist/lib/session/inject.js +21 -0
- package/dist/lib/session/parse.js +23 -20
- package/dist/lib/session/pid-registry.d.ts +1 -0
- package/dist/lib/session/pid-registry.js +24 -0
- package/dist/lib/session/provenance.d.ts +14 -2
- package/dist/lib/session/provenance.js +39 -8
- package/dist/lib/session/remote-active.js +19 -7
- package/dist/lib/session/remote-list.d.ts +51 -0
- package/dist/lib/session/remote-list.js +213 -0
- package/dist/lib/session/remote.d.ts +7 -1
- package/dist/lib/session/remote.js +16 -2
- package/dist/lib/session/sync/config.d.ts +1 -15
- package/dist/lib/session/sync/config.js +4 -20
- package/dist/lib/session/types.d.ts +17 -0
- package/dist/lib/shims.d.ts +36 -6
- package/dist/lib/shims.js +91 -29
- package/dist/lib/ssh-exec.js +2 -0
- package/dist/lib/ssh-tunnel.js +2 -1
- package/dist/lib/startup/command-registry.d.ts +6 -0
- package/dist/lib/startup/command-registry.js +13 -1
- package/dist/lib/state.d.ts +13 -0
- package/dist/lib/state.js +103 -9
- package/dist/lib/sync-umbrella.d.ts +14 -7
- package/dist/lib/sync-umbrella.js +17 -9
- package/dist/lib/teams/forEach.d.ts +110 -0
- package/dist/lib/teams/forEach.js +186 -0
- package/dist/lib/teams/index.d.ts +1 -0
- package/dist/lib/teams/index.js +1 -0
- package/dist/lib/teams/pr-watch.d.ts +226 -0
- package/dist/lib/teams/pr-watch.js +371 -0
- package/dist/lib/teams/supervisor.d.ts +14 -1
- package/dist/lib/teams/supervisor.js +19 -0
- package/dist/lib/teams/worktree.d.ts +9 -0
- package/dist/lib/teams/worktree.js +32 -0
- package/dist/lib/terminal/backends/index.d.ts +2 -1
- package/dist/lib/terminal/backends/index.js +3 -1
- package/dist/lib/terminal/backends/vscodium-agent.d.ts +31 -0
- package/dist/lib/terminal/backends/vscodium-agent.js +72 -0
- package/dist/lib/terminal/index.d.ts +4 -1
- package/dist/lib/terminal/index.js +4 -1
- package/dist/lib/terminal/inject.d.ts +204 -0
- package/dist/lib/terminal/inject.js +247 -0
- package/dist/lib/terminal/resolve.d.ts +64 -0
- package/dist/lib/terminal/resolve.js +90 -0
- package/dist/lib/terminal/types.d.ts +1 -1
- package/dist/lib/triggers/webhook.d.ts +85 -0
- package/dist/lib/triggers/webhook.js +141 -0
- package/dist/lib/versions.d.ts +23 -0
- package/dist/lib/versions.js +119 -13
- package/dist/lib/watchdog/index.d.ts +3 -0
- package/dist/lib/watchdog/index.js +5 -0
- package/dist/lib/watchdog/read.d.ts +35 -0
- package/dist/lib/watchdog/read.js +149 -0
- package/dist/lib/watchdog/runner.d.ts +127 -0
- package/dist/lib/watchdog/runner.js +322 -0
- package/dist/lib/watchdog/watchdog.d.ts +40 -0
- package/dist/lib/watchdog/watchdog.js +166 -0
- package/dist/lib/watchdog/watchdogTail.d.ts +5 -0
- package/dist/lib/watchdog/watchdogTail.js +154 -0
- package/dist/lib/workflows.d.ts +166 -0
- package/dist/lib/workflows.js +193 -0
- package/package.json +5 -4
package/dist/commands/doctor.js
CHANGED
|
@@ -4,10 +4,8 @@ import { checkAllClis } from '../lib/teams/agents.js';
|
|
|
4
4
|
import { AGENTS, ALL_AGENT_IDS, resolveAgentName, formatAgentError } from '../lib/agents.js';
|
|
5
5
|
import { getGlobalDefault, getVersionHomePath, isVersionInstalled, listInstalledVersions, parseAgentSpec, } from '../lib/versions.js';
|
|
6
6
|
import { loadManifest, isStale } from '../lib/staleness/index.js';
|
|
7
|
-
import { diffVersionCommands, iterCommandsCapableVersions } from '../lib/commands.js';
|
|
8
|
-
import { diffVersionSkills, iterSkillsCapableVersions } from '../lib/skills.js';
|
|
9
|
-
import { iterHooksCapableVersions, listUnmanagedHooksInVersionHome } from '../lib/hooks.js';
|
|
10
7
|
import { diffVersionResources, DOCTOR_ALL_KINDS, } from '../lib/doctor-diff.js';
|
|
8
|
+
import { checkSyncStatus, countOrphans } from '../lib/drift.js';
|
|
11
9
|
import { unifiedDiff, colorizeUnifiedDiff } from '../lib/diff-text.js';
|
|
12
10
|
import { listCliStatus } from '../lib/cli-resources.js';
|
|
13
11
|
import { setHelpSections } from '../lib/help.js';
|
|
@@ -17,33 +15,6 @@ import { terminalWidth, truncateToWidth, stringWidth, padToWidth } from '../lib/
|
|
|
17
15
|
import * as fs from 'fs';
|
|
18
16
|
const AGENT_NAMES = Object.fromEntries(ALL_AGENT_IDS.map((id) => [id, AGENTS[id].name]));
|
|
19
17
|
// ─── overview mode (no target) ────────────────────────────────────────────────
|
|
20
|
-
// Lines naming exactly what's out of sync for a version, plugins prioritized:
|
|
21
|
-
// each divergent plugin gets its own line with specifics (stale mirror version,
|
|
22
|
-
// invalid manifest, or the bundled skills/commands missing from the mirror —
|
|
23
|
-
// the system-repo plugin content that matters most). Other kinds collapse to
|
|
24
|
-
// compact counts so the readout stays scannable.
|
|
25
|
-
function divergenceLines(report) {
|
|
26
|
-
const lines = [];
|
|
27
|
-
for (const p of report.kinds.plugins) {
|
|
28
|
-
if (p.status === 'missing')
|
|
29
|
-
lines.push(`plugin ${p.name} — not installed`);
|
|
30
|
-
else if (p.status === 'diff')
|
|
31
|
-
lines.push(`plugin ${p.name} — ${p.detail ?? 'mirror drifted'}`);
|
|
32
|
-
}
|
|
33
|
-
for (const kind of ['commands', 'skills', 'hooks', 'rules', 'mcp', 'permissions', 'subagents']) {
|
|
34
|
-
const rows = report.kinds[kind];
|
|
35
|
-
const miss = rows.filter((r) => r.status === 'missing').length;
|
|
36
|
-
const dif = rows.filter((r) => r.status === 'diff').length;
|
|
37
|
-
const bits = [];
|
|
38
|
-
if (miss)
|
|
39
|
-
bits.push(`${miss} missing`);
|
|
40
|
-
if (dif)
|
|
41
|
-
bits.push(`${dif} drifted`);
|
|
42
|
-
if (bits.length)
|
|
43
|
-
lines.push(`${kind.padEnd(11)} ${bits.join(' · ')}`);
|
|
44
|
-
}
|
|
45
|
-
return lines;
|
|
46
|
-
}
|
|
47
18
|
function collapseWhitespace(s) {
|
|
48
19
|
return s.replace(/\s+/g, ' ').trim();
|
|
49
20
|
}
|
|
@@ -78,65 +49,6 @@ function printWrappedLine(prefix, text) {
|
|
|
78
49
|
for (const line of wrapLine(prefix, text))
|
|
79
50
|
console.log(chalk.gray(line));
|
|
80
51
|
}
|
|
81
|
-
function checkSyncStatus(cwd) {
|
|
82
|
-
const rows = [];
|
|
83
|
-
// Every installed version, not just the default — a stale NON-default version
|
|
84
|
-
// (e.g. one you launched from yesterday) is exactly the rot that silently
|
|
85
|
-
// serves outdated/invalid resources and that `--fix` now heals. Hiding it here
|
|
86
|
-
// is why that class of bug went unnoticed.
|
|
87
|
-
for (const agent of ALL_AGENT_IDS) {
|
|
88
|
-
const def = getGlobalDefault(agent);
|
|
89
|
-
for (const version of listInstalledVersions(agent)) {
|
|
90
|
-
const manifest = loadManifest(agent, version);
|
|
91
|
-
const status = !manifest
|
|
92
|
-
? 'never-synced'
|
|
93
|
-
: isStale(manifest, agent, version, cwd) ? 'stale' : 'fresh';
|
|
94
|
-
const row = { agent, version, status, isDefault: version === def };
|
|
95
|
-
if (status === 'stale') {
|
|
96
|
-
// Resolve the specifics against non-project layers (the global home is
|
|
97
|
-
// never reconciled against per-cwd project resources).
|
|
98
|
-
const report = diffVersionResources(agent, version, { cwd, excludeProject: true });
|
|
99
|
-
const lines = divergenceLines(report);
|
|
100
|
-
if (lines.length)
|
|
101
|
-
row.divergence = lines;
|
|
102
|
-
}
|
|
103
|
-
rows.push(row);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return rows;
|
|
107
|
-
}
|
|
108
|
-
function countOrphans() {
|
|
109
|
-
const byKey = new Map();
|
|
110
|
-
const ensure = (agent, version) => {
|
|
111
|
-
const key = `${agent}@${version}`;
|
|
112
|
-
let row = byKey.get(key);
|
|
113
|
-
if (!row) {
|
|
114
|
-
row = { agent, version, commands: 0, skills: 0, hooks: 0 };
|
|
115
|
-
byKey.set(key, row);
|
|
116
|
-
}
|
|
117
|
-
return row;
|
|
118
|
-
};
|
|
119
|
-
for (const { agent, version } of iterCommandsCapableVersions()) {
|
|
120
|
-
const diff = diffVersionCommands(agent, version);
|
|
121
|
-
if (diff.orphans.length > 0)
|
|
122
|
-
ensure(agent, version).commands = diff.orphans.length;
|
|
123
|
-
}
|
|
124
|
-
for (const { agent, version } of iterSkillsCapableVersions()) {
|
|
125
|
-
const diff = diffVersionSkills(agent, version);
|
|
126
|
-
if (diff.orphans.length > 0)
|
|
127
|
-
ensure(agent, version).skills = diff.orphans.length;
|
|
128
|
-
}
|
|
129
|
-
// Orphan hooks are scripts in the version home that no agents.yaml/hooks.yaml
|
|
130
|
-
// entry registers — so the registrar never wires them to an event and they
|
|
131
|
-
// never fire. (Distinct from the source-diff `diffVersionHooks().orphans`,
|
|
132
|
-
// which false-flags valid system-sourced registered hooks.)
|
|
133
|
-
for (const { agent, version } of iterHooksCapableVersions()) {
|
|
134
|
-
const dead = listUnmanagedHooksInVersionHome(agent, version);
|
|
135
|
-
if (dead.length > 0)
|
|
136
|
-
ensure(agent, version).hooks = dead.length;
|
|
137
|
-
}
|
|
138
|
-
return Array.from(byKey.values()).filter((r) => r.commands + r.skills + r.hooks > 0);
|
|
139
|
-
}
|
|
140
52
|
function renderOverviewText(clis, syncRows, orphanRows, hostClis) {
|
|
141
53
|
console.log(chalk.bold('Agent CLIs'));
|
|
142
54
|
// Show the fleet you actually run — agents that are ready in PATH, plus any
|
package/dist/commands/events.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import chalk from 'chalk';
|
|
16
16
|
import * as fs from 'fs';
|
|
17
|
-
import { query,
|
|
17
|
+
import { query, getLogsPath } from '../lib/events.js';
|
|
18
18
|
/** Parse `--since`: relative offsets (30s/5m/2h/7d/4w) or an ISO/absolute date. */
|
|
19
19
|
function parseSince(s) {
|
|
20
20
|
const m = s.match(/^(\d+)([smhdw])$/);
|
|
@@ -116,7 +116,7 @@ Examples:
|
|
|
116
116
|
// query() returns newest-first; print oldest-first so a tail reads naturally.
|
|
117
117
|
for (const r of records.slice().reverse())
|
|
118
118
|
console.log(renderRow(r));
|
|
119
|
-
console.log(chalk.gray(`\n${records.length} event(s). Log: ${
|
|
119
|
+
console.log(chalk.gray(`\n${records.length} event(s). Log: ${getLogsPath()}`));
|
|
120
120
|
});
|
|
121
121
|
}
|
|
122
122
|
/** commander repeatable-option collector. */
|
|
@@ -126,7 +126,7 @@ function collect(value, previous) {
|
|
|
126
126
|
/** Tail today's event file, printing new lines as they land. */
|
|
127
127
|
async function followLog() {
|
|
128
128
|
const today = new Date();
|
|
129
|
-
const file = `${
|
|
129
|
+
const file = `${getLogsPath()}/events-${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}.jsonl`;
|
|
130
130
|
let offset = 0;
|
|
131
131
|
try {
|
|
132
132
|
offset = fs.statSync(file).size;
|
package/dist/commands/exec.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* injection, and multi-agent fallback chains for rate-limit resilience.
|
|
7
7
|
*/
|
|
8
8
|
import { type Command } from 'commander';
|
|
9
|
+
import type { ExecEffort } from '../lib/exec.js';
|
|
9
10
|
/**
|
|
10
11
|
* Build the LoopConfig the driver consumes from CLI flags and/or a workflow's
|
|
11
12
|
* `loop:` frontmatter block (issue #332). Returns undefined when neither source
|
|
@@ -26,5 +27,23 @@ export declare function buildLoopConfig(flags: {
|
|
|
26
27
|
}, workflowLoop?: import('../lib/workflows.js').LoopConfigRaw): import('../lib/loop.js').LoopConfig | undefined;
|
|
27
28
|
/** Map a loop stop reason to a process exit code. condition-met/max are clean exits. */
|
|
28
29
|
export declare function loopExitCode(stoppedBy: import('../lib/loop.js').LoopStoppedBy): number;
|
|
30
|
+
/**
|
|
31
|
+
* Drive a workflow's declarative `for_each:` fan-out end-to-end (issue #343).
|
|
32
|
+
*
|
|
33
|
+
* Runs the producer, expands one stage teammate per produced item (respecting
|
|
34
|
+
* `max_items` / `DEFAULT_FOR_EACH_CAP`, surfacing any truncation — never a
|
|
35
|
+
* silent cap), stages them into a fresh team via the existing `runForEach`
|
|
36
|
+
* bridge, then drives the teams supervisor until the DAG drains. When a verify
|
|
37
|
+
* panel is declared, tallies each item's `keep_if` gate from the skeptics'
|
|
38
|
+
* terminal status and logs which items survived.
|
|
39
|
+
*
|
|
40
|
+
* Reuses the teams substrate wholesale — no new orchestration engine. Returns
|
|
41
|
+
* the process exit code (0 on drain, 1 on producer failure / non-drain).
|
|
42
|
+
*/
|
|
43
|
+
export declare function runWorkflowForEach(spec: import('../lib/workflows.js').ForEachSpec, opts: {
|
|
44
|
+
workflowName: string;
|
|
45
|
+
cwd: string;
|
|
46
|
+
effort?: ExecEffort;
|
|
47
|
+
}): Promise<number>;
|
|
29
48
|
/** Register the `agents run <agent> [prompt]` command. */
|
|
30
49
|
export declare function registerRunCommand(program: Command): void;
|
package/dist/commands/exec.js
CHANGED
|
@@ -10,9 +10,11 @@ import chalk from 'chalk';
|
|
|
10
10
|
import { setHelpSections } from '../lib/help.js';
|
|
11
11
|
import { parseLoopInterval } from '../lib/loop.js';
|
|
12
12
|
import { AGENTS } from '../lib/agents.js';
|
|
13
|
+
import { recordDispatchedRun } from '../lib/audit/log.js';
|
|
13
14
|
import * as fs from 'fs';
|
|
14
15
|
import * as path from 'path';
|
|
15
16
|
import * as os from 'os';
|
|
17
|
+
import { randomUUID } from 'crypto';
|
|
16
18
|
/** Type guard that narrows a string to a known AgentId. */
|
|
17
19
|
function isValidAgent(agent) {
|
|
18
20
|
return agent in AGENTS;
|
|
@@ -101,6 +103,100 @@ export function loopExitCode(stoppedBy) {
|
|
|
101
103
|
return 1;
|
|
102
104
|
}
|
|
103
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Drive a workflow's declarative `for_each:` fan-out end-to-end (issue #343).
|
|
108
|
+
*
|
|
109
|
+
* Runs the producer, expands one stage teammate per produced item (respecting
|
|
110
|
+
* `max_items` / `DEFAULT_FOR_EACH_CAP`, surfacing any truncation — never a
|
|
111
|
+
* silent cap), stages them into a fresh team via the existing `runForEach`
|
|
112
|
+
* bridge, then drives the teams supervisor until the DAG drains. When a verify
|
|
113
|
+
* panel is declared, tallies each item's `keep_if` gate from the skeptics'
|
|
114
|
+
* terminal status and logs which items survived.
|
|
115
|
+
*
|
|
116
|
+
* Reuses the teams substrate wholesale — no new orchestration engine. Returns
|
|
117
|
+
* the process exit code (0 on drain, 1 on producer failure / non-drain).
|
|
118
|
+
*/
|
|
119
|
+
export async function runWorkflowForEach(spec, opts) {
|
|
120
|
+
const [{ produceItems, runForEach, tallyForEach }, { expandForEach, DEFAULT_FOR_EACH_CAP }, { AgentManager }, { createTeam }, { runSupervisor }, { ALL_AGENT_IDS }] = await Promise.all([
|
|
121
|
+
import('../lib/teams/forEach.js'),
|
|
122
|
+
import('../lib/workflows.js'),
|
|
123
|
+
import('../lib/teams/agents.js'),
|
|
124
|
+
import('../lib/teams/registry.js'),
|
|
125
|
+
import('../lib/teams/supervisor.js'),
|
|
126
|
+
import('../lib/agents.js'),
|
|
127
|
+
]);
|
|
128
|
+
// The teams substrate spawns teammates as a real harness. When `agent:` names
|
|
129
|
+
// a known harness id, honor it; otherwise fall back to claude (the workflow
|
|
130
|
+
// harness) so a subagent-style name in `agent:` still runs.
|
|
131
|
+
const isHarness = (a) => ALL_AGENT_IDS.includes(a);
|
|
132
|
+
const runSpec = {
|
|
133
|
+
...spec,
|
|
134
|
+
agent: isHarness(spec.agent) ? spec.agent : 'claude',
|
|
135
|
+
...(spec.verify
|
|
136
|
+
? { verify: { ...spec.verify, agent: isHarness(spec.verify.agent) ? spec.verify.agent : 'claude' } }
|
|
137
|
+
: {}),
|
|
138
|
+
};
|
|
139
|
+
if (runSpec.agent !== spec.agent) {
|
|
140
|
+
process.stderr.write(chalk.gray(`[for_each] '${spec.agent}' is not a harness id — staging stage teammates as claude\n`));
|
|
141
|
+
}
|
|
142
|
+
// 1. Producer: run the shell command (or resolve itemsRef) into the item list.
|
|
143
|
+
let items;
|
|
144
|
+
try {
|
|
145
|
+
items = await produceItems(runSpec, { cwd: opts.cwd });
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
console.error(chalk.red(`[for_each] producer failed: ${err.message}`));
|
|
149
|
+
return 1;
|
|
150
|
+
}
|
|
151
|
+
if (items.length === 0) {
|
|
152
|
+
process.stderr.write(chalk.yellow('[for_each] producer emitted no items — nothing to fan out.\n'));
|
|
153
|
+
return 0;
|
|
154
|
+
}
|
|
155
|
+
// 2. Cap accounting (surfaced, never silent — acceptance criterion in #343).
|
|
156
|
+
const cap = runSpec.max_items ?? DEFAULT_FOR_EACH_CAP;
|
|
157
|
+
const { truncated } = expandForEach(runSpec, items);
|
|
158
|
+
if (truncated > 0) {
|
|
159
|
+
process.stderr.write(chalk.yellow(`[for_each] producer emitted ${items.length} items; capping at ${cap} (${truncated} dropped). Raise \`max_items\` to fan out more.\n`));
|
|
160
|
+
}
|
|
161
|
+
process.stderr.write(chalk.gray(`[for_each] ${Math.min(items.length, cap)} stage teammate(s) from ${items.length} produced item(s)\n`));
|
|
162
|
+
// 3. Stage the expanded teammates into a fresh team via the existing bridge.
|
|
163
|
+
const team = `foreach-${opts.workflowName.replace(/[^a-zA-Z0-9_-]/g, '-')}-${Date.now().toString(36)}`;
|
|
164
|
+
await createTeam(team, { description: `for_each fan-out from workflow '${opts.workflowName}'` });
|
|
165
|
+
const mgr = new AgentManager();
|
|
166
|
+
const { teammates } = await runForEach(mgr, team, runSpec, items, {
|
|
167
|
+
cwd: opts.cwd,
|
|
168
|
+
effort: opts.effort,
|
|
169
|
+
concurrency: runSpec.concurrency,
|
|
170
|
+
});
|
|
171
|
+
// 4. Drive the supervisor until the DAG drains (the same loop `teams start
|
|
172
|
+
// --watch` uses; it absorbs the staged teammates via rescanFromDisk).
|
|
173
|
+
const result = await runSupervisor(mgr, {
|
|
174
|
+
team,
|
|
175
|
+
onWave: (s) => {
|
|
176
|
+
const ts = s.timestamp.slice(11, 19);
|
|
177
|
+
process.stderr.write(`[${ts}] [for_each] wave ${s.wave} launched=${s.launched.length} running=${s.running} pending=${s.pending} done=${s.completed} failed=${s.failed}\n`);
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
// 5. keep_if tally: a skeptic that COMPLETED is a keep vote; a failed skeptic
|
|
181
|
+
// votes to drop. Gate each item and report which survived.
|
|
182
|
+
if (runSpec.verify) {
|
|
183
|
+
const loaded = await mgr.listByTask(team);
|
|
184
|
+
const statusByName = new Map(loaded.map((a) => [a.name, a.status]));
|
|
185
|
+
const verdicts = tallyForEach(teammates, (v) => statusByName.get(v.name) === 'completed');
|
|
186
|
+
const kept = verdicts.filter((v) => v.kept);
|
|
187
|
+
process.stderr.write(chalk.gray(`[for_each] keep_if=${runSpec.verify.keep_if}: kept ${kept.length}/${verdicts.length} item(s)\n`));
|
|
188
|
+
for (const v of verdicts) {
|
|
189
|
+
const tag = v.kept ? chalk.green('keep') : chalk.red('drop');
|
|
190
|
+
process.stderr.write(chalk.gray(` [${tag}] ${v.item} (${v.votes.filter(Boolean).length}/${v.votes.length} votes)\n`));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (result.stoppedBy === 'drained') {
|
|
194
|
+
process.stderr.write(chalk.green(`[for_each] drained in ${Math.floor(result.elapsed_ms / 1000)}s (${result.waves} waves). Team: ${team}\n`));
|
|
195
|
+
return 0;
|
|
196
|
+
}
|
|
197
|
+
process.stderr.write(chalk.yellow(`[for_each] stopped by ${result.stoppedBy} after ${result.waves} waves. Team: ${team}\n`));
|
|
198
|
+
return 1;
|
|
199
|
+
}
|
|
104
200
|
/** Register the `agents run <agent> [prompt]` command. */
|
|
105
201
|
export function registerRunCommand(program) {
|
|
106
202
|
const runCmd = program
|
|
@@ -112,6 +208,8 @@ export function registerRunCommand(program) {
|
|
|
112
208
|
.option('--env <key=value>', 'Pass environment variable to the agent (repeatable, e.g., --env DEBUG=1 --env API_KEY=xyz)', (val, prev) => [...prev, val], [])
|
|
113
209
|
.option('--secrets <bundle>', 'Inject a secrets bundle (repeatable). Values resolve from macOS Keychain at run time. See `agents secrets`.', (val, prev) => [...prev, val], [])
|
|
114
210
|
.option('--no-auto-secrets', 'Skip auto-injection of secrets declared by a workflow\'s frontmatter `secrets:` field. Has no effect on bare-agent runs.')
|
|
211
|
+
.option('--secrets-keys <keys>', 'Inject only this comma-separated subset of keys from --secrets bundles (e.g. KEY1,KEY2). Missing keys are an error. Applies to all --secrets bundles on this run.')
|
|
212
|
+
.option('--allow-expired', 'Inject secrets even if their expiry date has passed (overrides the pre-run expiry abort).')
|
|
115
213
|
.option('--cwd <dir>', 'Working directory for the agent (defaults to current directory)')
|
|
116
214
|
.option('--add-dir <dir>', 'Grant access to an additional directory outside the project (Claude only, repeatable)', (val, prev) => [...prev, val], [])
|
|
117
215
|
.option('--json', 'Stream events as JSON lines (for parsing by other tools)')
|
|
@@ -133,7 +231,8 @@ export function registerRunCommand(program) {
|
|
|
133
231
|
.option('--budget <tokens>', 'Loop token hard-cap: stop once cumulative tokens reach this (stoppedBy: budget), enforced outside the agent. Loop only.')
|
|
134
232
|
.option('--until <signal>', 'Loop stop condition. `signal` reads <runDir>/loop-signal.json {continue,reason} each iteration; absent or continue:false stops (fail-closed). Loop only.')
|
|
135
233
|
.option('--interval <dur>', 'Loop delay between iterations ("0" back-to-back, "30m" paces). Loop only.')
|
|
136
|
-
.option('--host <name>', 'Offload this run onto
|
|
234
|
+
.option('--host <name>', 'Offload this run onto another machine over SSH instead of running locally — a device, a registered agent host, or user@host. See `agents devices` / `agents hosts`.')
|
|
235
|
+
.option('--device <name>', 'Alias of --host: offload this run onto a registered device (from `agents devices`).')
|
|
137
236
|
.option('--remote-cwd <dir>', 'Working directory on the host for --host runs.')
|
|
138
237
|
.option('--no-follow', 'With --host, dispatch detached and return immediately (track via `agents hosts ps/logs`).')
|
|
139
238
|
.option('--any', 'With --host <cap> (a capability tag), pick any matching host instead of erroring when several match.')
|
|
@@ -248,10 +347,10 @@ export function registerRunCommand(program) {
|
|
|
248
347
|
}
|
|
249
348
|
// --host/--on/--computer: offload this run onto a registered agent host
|
|
250
349
|
// over SSH instead of running locally. The three flags are aliases.
|
|
251
|
-
const hostGiven = [options.host, options.on, options.computer].filter((v) => !!v);
|
|
350
|
+
const hostGiven = [options.host, options.device, options.on, options.computer].filter((v) => !!v);
|
|
252
351
|
if (hostGiven.length > 0) {
|
|
253
352
|
if (new Set(hostGiven).size > 1) {
|
|
254
|
-
console.error(chalk.red('Conflicting --host/--
|
|
353
|
+
console.error(chalk.red('Conflicting --host/--device values — pass just one.'));
|
|
255
354
|
process.exit(1);
|
|
256
355
|
}
|
|
257
356
|
const hostName = hostGiven[0];
|
|
@@ -261,6 +360,10 @@ export function registerRunCommand(program) {
|
|
|
261
360
|
}
|
|
262
361
|
const { resolveHost, resolveHostByCap } = await import('../lib/hosts/registry.js');
|
|
263
362
|
const { dispatchToHost } = await import('../lib/hosts/dispatch.js');
|
|
363
|
+
const { registerHostSession } = await import('../lib/hosts/session-index.js');
|
|
364
|
+
// A password-auth device throws DeviceOffloadUnsupportedError here; it's
|
|
365
|
+
// printed cleanly by the top-level catch in index.ts (covers every
|
|
366
|
+
// resolveHost caller), so it never falls through to capability routing.
|
|
264
367
|
let host = await resolveHost(hostName);
|
|
265
368
|
if (!host) {
|
|
266
369
|
// Not a host name — try capability routing (e.g. --host gpu). A
|
|
@@ -282,14 +385,31 @@ export function registerRunCommand(program) {
|
|
|
282
385
|
process.exit(1);
|
|
283
386
|
}
|
|
284
387
|
try {
|
|
285
|
-
const
|
|
286
|
-
|
|
388
|
+
const runAgent = agentSpec.split('@')[0];
|
|
389
|
+
// `--resume [id]`: commander yields the string id, or `true` when the
|
|
390
|
+
// flag is passed bare. A bare resume needs the interactive picker,
|
|
391
|
+
// which can't run over a detached remote dispatch — only forward a
|
|
392
|
+
// concrete id.
|
|
393
|
+
const resumeId = typeof options.resume === 'string' ? options.resume : undefined;
|
|
394
|
+
// Mirror the local path (lib/exec.ts): only Claude accepts a forced
|
|
395
|
+
// `--session-id`. Generating it here lets us register the run in the
|
|
396
|
+
// local index and makes it resumable by that id. On resume the remote
|
|
397
|
+
// session keeps its existing id — don't mint a new one.
|
|
398
|
+
const hostSessionId = runAgent === 'claude' && !resumeId ? randomUUID() : undefined;
|
|
399
|
+
const { task, exitCode } = await dispatchToHost(host, {
|
|
400
|
+
agent: runAgent,
|
|
287
401
|
prompt,
|
|
288
402
|
mode: options.mode,
|
|
289
403
|
model: options.model,
|
|
290
404
|
remoteCwd: options.remoteCwd,
|
|
405
|
+
sessionId: hostSessionId,
|
|
406
|
+
resume: resumeId,
|
|
291
407
|
follow: options.follow !== false,
|
|
292
408
|
});
|
|
409
|
+
// Register the dispatched run in the LOCAL session index so it shows
|
|
410
|
+
// up in `agents sessions` and resolves by id, even though its
|
|
411
|
+
// transcript lives on the host. No-op when no session id was captured.
|
|
412
|
+
registerHostSession(task, { cwd: process.cwd(), prompt });
|
|
293
413
|
if (options.follow === false) {
|
|
294
414
|
console.log(chalk.green(`Dispatched to ${host.name}.`) + chalk.gray(' Track: agents hosts ps · Follow: agents hosts logs <id> -f'));
|
|
295
415
|
process.exit(0);
|
|
@@ -382,9 +502,20 @@ export function registerRunCommand(program) {
|
|
|
382
502
|
sessionId: cp.sessionId,
|
|
383
503
|
});
|
|
384
504
|
process.stderr.write(chalk.gray(`[loop] stopped: ${result.stoppedBy} after ${result.iterations} iteration(s), ${result.tokens} tokens\n`));
|
|
385
|
-
|
|
505
|
+
// Governance chokepoint (#347): --resume-checkpoint short-circuits normal
|
|
506
|
+
// dispatch and exits here — record its one audit entry with the agent,
|
|
507
|
+
// version, and cwd reconstructed from the checkpoint.
|
|
508
|
+
const resumeExit = loopExitCode(result.stoppedBy);
|
|
509
|
+
recordDispatchedRun({
|
|
510
|
+
agent: cp.agent,
|
|
511
|
+
version: cp.version ?? 'unknown',
|
|
512
|
+
mode: resumeExec.mode ?? 'auto',
|
|
513
|
+
cwd: resumeExec.cwd ?? process.cwd(),
|
|
514
|
+
exitCode: resumeExit,
|
|
515
|
+
});
|
|
516
|
+
process.exit(resumeExit);
|
|
386
517
|
}
|
|
387
|
-
const [{ buildExecCommand, parseExecEnv, execAgent, runWithFallback, normalizeMode, resolveMode, defaultModeFor, headlessPlanStallCommand, nativeResume, resolveInteractive }, { ALL_AGENT_IDS }, { profileExists, resolveProfileForRun }, { readAndResolveBundleEnv, describeBundle }, { splitBundleRef, resolveSshTarget, remoteResolveEnv }, { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, RUN_STRATEGIES }, { getGlobalDefault, getVersionHomePath, resolveVersion, resolveVersionAlias }, { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion }, { parseWorkflowFrontmatter, resolveWorkflowRef, resolveAllowedSubagents }, { resolveRunDefaults }, { getMcpServersByName, buildWorkflowMcpConfig }, { supports },] = await Promise.all([
|
|
518
|
+
const [{ buildExecCommand, parseExecEnv, execAgent, runWithFallback, normalizeMode, resolveMode, defaultModeFor, headlessPlanStallCommand, nativeResume, resolveInteractive }, { ALL_AGENT_IDS }, { profileExists, resolveProfileForRun }, { readAndResolveBundleEnv, describeBundle, assertRemoteBundleFlagsUnsupported }, { splitBundleRef, resolveSshTarget, remoteResolveEnv }, { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, rotationFailoverChain, shouldArmRotationFailover, RUN_STRATEGIES }, { getGlobalDefault, getVersionHomePath, resolveVersion, resolveVersionAlias }, { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion }, { parseWorkflowFrontmatter, resolveWorkflowRef, resolveAllowedSubagents, pruneStaleWorkflowSubagents }, { resolveRunDefaults }, { getMcpServersByName, buildWorkflowMcpConfig }, { supports },] = await Promise.all([
|
|
388
519
|
import('../lib/exec.js'),
|
|
389
520
|
import('../lib/agents.js'),
|
|
390
521
|
import('../lib/profiles.js'),
|
|
@@ -405,13 +536,23 @@ export function registerRunCommand(program) {
|
|
|
405
536
|
let version = rawVersion || undefined;
|
|
406
537
|
let profileEnv;
|
|
407
538
|
let fromProfile = false;
|
|
539
|
+
let profileFallbackModel;
|
|
408
540
|
let workflowModel;
|
|
409
541
|
// WORKFLOW.md capability scoping, translated to Claude headless flags below.
|
|
410
542
|
let workflowToolsRestrict;
|
|
411
543
|
let workflowMcpConfigPath;
|
|
544
|
+
// Full paths of workflow subagent files THIS run copied into the shared
|
|
545
|
+
// per-agent agents dir. Torn down after the run to restore the shared dir
|
|
546
|
+
// (issue #401), mirroring cleanupWorkflowMcpConfig for the mcp-config.
|
|
547
|
+
const workflowSubagentTargets = [];
|
|
412
548
|
// WORKFLOW.md `loop:` block (issue #332). When a workflow declares it,
|
|
413
549
|
// `agents run <workflow>` honors the loop without a --loop flag.
|
|
414
550
|
let workflowLoop;
|
|
551
|
+
// WORKFLOW.md `for_each:` block (issue #343). When a workflow declares it,
|
|
552
|
+
// `agents run <workflow>` runs the producer, expands one stage teammate per
|
|
553
|
+
// produced item, and drives the teams supervisor to drain — no `teams`
|
|
554
|
+
// subcommands needed.
|
|
555
|
+
let workflowForEach;
|
|
415
556
|
const cwd = options.cwd ?? process.cwd();
|
|
416
557
|
if (isValidAgent(rawAgent)) {
|
|
417
558
|
agent = rawAgent;
|
|
@@ -427,6 +568,7 @@ export function registerRunCommand(program) {
|
|
|
427
568
|
if (!version)
|
|
428
569
|
version = resolved.version;
|
|
429
570
|
profileEnv = resolved.env;
|
|
571
|
+
profileFallbackModel = resolved.fallbackModel;
|
|
430
572
|
fromProfile = true;
|
|
431
573
|
process.stderr.write(chalk.gray(`Resolved profile '${resolved.profileName}' -> ${agent}${version ? `@${version}` : ''}\n`));
|
|
432
574
|
}
|
|
@@ -448,6 +590,7 @@ export function registerRunCommand(program) {
|
|
|
448
590
|
workflowModel = workflowFrontmatter.model.trim();
|
|
449
591
|
}
|
|
450
592
|
workflowLoop = workflowFrontmatter?.loop;
|
|
593
|
+
workflowForEach = workflowFrontmatter?.forEach;
|
|
451
594
|
const resolvedVersion = resolveVersionAlias('claude', version);
|
|
452
595
|
const versionHome = getVersionHomePath('claude', resolvedVersion ?? getGlobalDefault('claude') ?? '');
|
|
453
596
|
const claudeAgentsDir = path.join(versionHome, '.claude', 'agents');
|
|
@@ -469,6 +612,15 @@ export function registerRunCommand(program) {
|
|
|
469
612
|
const allFiles = fs.readdirSync(subagentsDir).filter(f => f.endsWith('.md'));
|
|
470
613
|
const { allowedStems, missing } = resolveAllowedSubagents(allFiles, allowedAgents);
|
|
471
614
|
const allowStemSet = new Set(allowedStems);
|
|
615
|
+
// Fail-closed prune (issue #401, follow-up to #324). A prior
|
|
616
|
+
// unrestricted run may have left workflow subagent files that THIS
|
|
617
|
+
// scoped run does not permit; they linger in the shared dir and stay
|
|
618
|
+
// dispatchable. Remove those no-longer-permitted workflow-managed
|
|
619
|
+
// files BEFORE writing the allowed set — never a user's own subagent.
|
|
620
|
+
const pruned = pruneStaleWorkflowSubagents(claudeAgentsDir, allFiles, allowedStems);
|
|
621
|
+
if (pruned.length > 0) {
|
|
622
|
+
process.stderr.write(chalk.gray(`[workflow] pruned ${pruned.length} stale workflow subagent(s) from shared dir: ${pruned.join(', ')}\n`));
|
|
623
|
+
}
|
|
472
624
|
let copied = 0;
|
|
473
625
|
let skipped = 0;
|
|
474
626
|
for (const file of allFiles) {
|
|
@@ -477,7 +629,9 @@ export function registerRunCommand(program) {
|
|
|
477
629
|
skipped++;
|
|
478
630
|
continue;
|
|
479
631
|
}
|
|
480
|
-
|
|
632
|
+
const dest = path.join(claudeAgentsDir, file);
|
|
633
|
+
fs.copyFileSync(path.join(subagentsDir, file), dest);
|
|
634
|
+
workflowSubagentTargets.push(dest);
|
|
481
635
|
copied++;
|
|
482
636
|
}
|
|
483
637
|
if (allowedAgents !== undefined) {
|
|
@@ -722,6 +876,10 @@ export function registerRunCommand(program) {
|
|
|
722
876
|
}
|
|
723
877
|
const configuredStrategy = getConfiguredRunStrategy(agent, cwd);
|
|
724
878
|
const explicitStrategy = options.strategy ? normalizeRunStrategy(options.strategy) : null;
|
|
879
|
+
// Captured from resolveRunVersion below so mid-run rate-limit failover can
|
|
880
|
+
// synthesize a same-agent fallback chain from the other healthy accounts
|
|
881
|
+
// (issue #348). Stays null unless a non-pinned strategy actually rotated.
|
|
882
|
+
let rotationResult = null;
|
|
725
883
|
if (options.strategy && !explicitStrategy) {
|
|
726
884
|
console.error(chalk.red(`Invalid strategy: ${options.strategy}. Use ${RUN_STRATEGIES.join(', ')}.`));
|
|
727
885
|
process.exit(1);
|
|
@@ -748,6 +906,7 @@ export function registerRunCommand(program) {
|
|
|
748
906
|
const resolved = await resolveRunVersion(agent, strategy, cwd);
|
|
749
907
|
if (resolved.version) {
|
|
750
908
|
version = resolved.version;
|
|
909
|
+
rotationResult = resolved.rotation;
|
|
751
910
|
if (resolved.rotation && !options.quiet) {
|
|
752
911
|
const banner = formatRotationBanner(resolved.rotation, strategy);
|
|
753
912
|
process.stderr.write(chalk.gray(banner + '\n'));
|
|
@@ -836,11 +995,19 @@ export function registerRunCommand(program) {
|
|
|
836
995
|
// Resolve --secrets bundles in flag order. Later bundles override earlier
|
|
837
996
|
// ones. Any resolution failure (missing keychain item, blocked exec ref)
|
|
838
997
|
// aborts before spawn so the agent never sees a partial env.
|
|
998
|
+
const secretsKeysSubset = options.secretsKeys
|
|
999
|
+
? options.secretsKeys.split(',').map((k) => k.trim()).filter(Boolean)
|
|
1000
|
+
: undefined;
|
|
839
1001
|
let secretsEnv = {};
|
|
840
1002
|
for (const bundleRef of options.secrets) {
|
|
841
1003
|
try {
|
|
842
1004
|
const { bundle: bundleName, host } = splitBundleRef(bundleRef);
|
|
843
1005
|
if (host) {
|
|
1006
|
+
// Least-privilege flags (--secrets-keys / --allow-expired) do not
|
|
1007
|
+
// yet cross the SSH resolver — silently applying them would inject
|
|
1008
|
+
// the full remote env or an expired key. Fail loud so the user
|
|
1009
|
+
// can drop the flag or resolve locally instead.
|
|
1010
|
+
assertRemoteBundleFlagsUnsupported(bundleName, host, { keys: secretsKeysSubset, allowExpired: options.allowExpired }, { keysFlag: '--secrets-keys', allowExpiredFlag: '--allow-expired' });
|
|
844
1011
|
// Remote bundle (`bundle@host`): resolve over SSH and inject
|
|
845
1012
|
// ephemerally — values never touch this machine's keychain or disk.
|
|
846
1013
|
const target = await resolveSshTarget(host);
|
|
@@ -849,7 +1016,11 @@ export function registerRunCommand(program) {
|
|
|
849
1016
|
secretsEnv = { ...secretsEnv, ...bundleEnv };
|
|
850
1017
|
}
|
|
851
1018
|
else {
|
|
852
|
-
const { bundle, env: bundleEnv } = readAndResolveBundleEnv(bundleName, {
|
|
1019
|
+
const { bundle, env: bundleEnv } = readAndResolveBundleEnv(bundleName, {
|
|
1020
|
+
caller: `agent ${agent}`,
|
|
1021
|
+
keys: secretsKeysSubset,
|
|
1022
|
+
allowExpired: options.allowExpired,
|
|
1023
|
+
});
|
|
853
1024
|
const entries = describeBundle(bundle);
|
|
854
1025
|
const counts = {};
|
|
855
1026
|
for (const e of entries) {
|
|
@@ -945,6 +1116,50 @@ export function registerRunCommand(program) {
|
|
|
945
1116
|
fallback.push({ agent: fbAgent, version: resolveVersionAlias(fbAgent, fbVersion || undefined) });
|
|
946
1117
|
}
|
|
947
1118
|
}
|
|
1119
|
+
// Profile-declared same-host model swap (issue #325). Inserted BEFORE any
|
|
1120
|
+
// user --fallback entries so a rate limit first tries the cheaper/backup
|
|
1121
|
+
// model on the same provider (auth + base URL preserved via envOverride);
|
|
1122
|
+
// only if THAT still rate-limits do we cascade to a different agent CLI.
|
|
1123
|
+
// Requires a prompt for the same reason --fallback does — headless-only.
|
|
1124
|
+
if (fromProfile && profileFallbackModel && prompt !== undefined && !options.interactive) {
|
|
1125
|
+
fallback.unshift({
|
|
1126
|
+
agent,
|
|
1127
|
+
version,
|
|
1128
|
+
envOverride: { [profileFallbackModel.envKey]: profileFallbackModel.model },
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
// Mid-run rate-limit failover (issue #348). When a pre-flight rotation
|
|
1132
|
+
// picked an account and there are OTHER healthy accounts for the same
|
|
1133
|
+
// agent, synthesize a same-agent fallback chain from them so a 429 mid-run
|
|
1134
|
+
// re-dispatches on the next healthy account via the SAME runWithFallback
|
|
1135
|
+
// path (continuing the session via /continue). Because this injects into
|
|
1136
|
+
// the same `fallback` array `--fallback` uses, it must only arm for run
|
|
1137
|
+
// shapes that accept a fallback chain — shouldArmRotationFailover excludes
|
|
1138
|
+
// acp/loop/resume-checkpoint (which reject a non-empty fallback below),
|
|
1139
|
+
// interactive/no-prompt runs, and runs that already have an explicit or
|
|
1140
|
+
// profile fallback. Pinned/single-account runs stay unchanged because
|
|
1141
|
+
// rotationResult is null or rotationFailoverChain returns []. version is
|
|
1142
|
+
// set here because rotationResult is only populated when resolveRunVersion
|
|
1143
|
+
// picked one.
|
|
1144
|
+
if (shouldArmRotationFailover({
|
|
1145
|
+
hasRotation: !!rotationResult,
|
|
1146
|
+
hasVersion: !!version,
|
|
1147
|
+
hasPrompt: prompt !== undefined,
|
|
1148
|
+
explicitFallback: fallback.length > 0,
|
|
1149
|
+
interactive: !!options.interactive,
|
|
1150
|
+
acp: !!options.acp,
|
|
1151
|
+
loop: !!options.loop,
|
|
1152
|
+
resumeCheckpoint: !!options.resumeCheckpoint,
|
|
1153
|
+
})) {
|
|
1154
|
+
const failover = rotationFailoverChain(rotationResult, version);
|
|
1155
|
+
if (failover.length > 0) {
|
|
1156
|
+
fallback.push(...failover);
|
|
1157
|
+
if (!options.quiet) {
|
|
1158
|
+
const accounts = failover.map(f => `${f.agent}@${f.version}`).join(', ');
|
|
1159
|
+
process.stderr.write(chalk.gray(`[agents] rate-limit failover armed: ${accounts}\n`));
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
948
1163
|
if (options.acp) {
|
|
949
1164
|
if (prompt === undefined) {
|
|
950
1165
|
console.error(chalk.red('--acp requires a prompt. ACP is a programmatic protocol; interactive TUI sessions still use the native CLI.'));
|
|
@@ -968,6 +1183,9 @@ export function registerRunCommand(program) {
|
|
|
968
1183
|
mode,
|
|
969
1184
|
json: options.json ?? false,
|
|
970
1185
|
});
|
|
1186
|
+
// Governance chokepoint (#347): the --acp path exits here, bypassing
|
|
1187
|
+
// the normal finalize below — record its one audit entry.
|
|
1188
|
+
recordDispatchedRun({ agent, version: defaultVersion ?? 'unknown', mode, cwd, exitCode });
|
|
971
1189
|
process.exit(exitCode);
|
|
972
1190
|
}
|
|
973
1191
|
catch (err) {
|
|
@@ -1049,6 +1267,36 @@ export function registerRunCommand(program) {
|
|
|
1049
1267
|
// best-effort: nothing actionable if the temp dir is already gone.
|
|
1050
1268
|
}
|
|
1051
1269
|
};
|
|
1270
|
+
// Restore the shared per-agent agents dir after the run (issue #401):
|
|
1271
|
+
// remove the workflow subagent files THIS run copied in, so a scoped
|
|
1272
|
+
// workflow never leaves definitions behind for the next, unrelated run to
|
|
1273
|
+
// inherit. Mirrors cleanupWorkflowMcpConfig — tear down only what we made.
|
|
1274
|
+
const cleanupWorkflowSubagents = () => {
|
|
1275
|
+
for (const target of workflowSubagentTargets) {
|
|
1276
|
+
try {
|
|
1277
|
+
fs.rmSync(target, { force: true });
|
|
1278
|
+
}
|
|
1279
|
+
catch {
|
|
1280
|
+
// best-effort: nothing actionable if the file is already gone.
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
};
|
|
1284
|
+
// for_each dispatch (issue #343). A workflow that declares `for_each:` is a
|
|
1285
|
+
// declarative dynamic fan-out, not a single-agent run: execute the producer,
|
|
1286
|
+
// expand one stage teammate per produced item (+ optional verify panel),
|
|
1287
|
+
// stage them into a team, then drive the supervisor until the DAG drains.
|
|
1288
|
+
// This is mutually exclusive with the single-agent loop/fallback paths — the
|
|
1289
|
+
// fan-out IS the run.
|
|
1290
|
+
if (workflowForEach) {
|
|
1291
|
+
cleanupWorkflowMcpConfig();
|
|
1292
|
+
cleanupWorkflowSubagents();
|
|
1293
|
+
const exitCode = await runWorkflowForEach(workflowForEach, {
|
|
1294
|
+
workflowName: rawAgent,
|
|
1295
|
+
cwd,
|
|
1296
|
+
effort: options.effort,
|
|
1297
|
+
});
|
|
1298
|
+
process.exit(exitCode);
|
|
1299
|
+
}
|
|
1052
1300
|
// Loop dispatch (issue #332). Active when --loop is passed OR a workflow
|
|
1053
1301
|
// declares a `loop:` block. The loop path runs AFTER the #346 pre-flight
|
|
1054
1302
|
// gate above (which fired once) — the loop's token budget is an ADDITIONAL
|
|
@@ -1088,29 +1336,47 @@ export function registerRunCommand(program) {
|
|
|
1088
1336
|
version,
|
|
1089
1337
|
});
|
|
1090
1338
|
cleanupWorkflowMcpConfig();
|
|
1339
|
+
cleanupWorkflowSubagents();
|
|
1091
1340
|
process.stderr.write(chalk.gray(`[loop] stopped: ${result.stoppedBy} after ${result.iterations} iteration(s), ${result.tokens} tokens (checkpoint: ${path.join(runDir, 'checkpoint.json')})\n`));
|
|
1092
|
-
|
|
1341
|
+
// Governance chokepoint (#347): the --loop path exits here, bypassing
|
|
1342
|
+
// the normal finalize below — record its one audit entry.
|
|
1343
|
+
const loopExit = loopExitCode(result.stoppedBy);
|
|
1344
|
+
recordDispatchedRun({ agent, version: defaultVersion ?? 'unknown', mode, cwd, exitCode: loopExit });
|
|
1345
|
+
process.exit(loopExit);
|
|
1093
1346
|
}
|
|
1094
1347
|
catch (err) {
|
|
1095
1348
|
cleanupWorkflowMcpConfig();
|
|
1349
|
+
cleanupWorkflowSubagents();
|
|
1096
1350
|
console.error(chalk.red(`Loop failed for ${agent}: ${err.message}`));
|
|
1097
1351
|
process.exit(1);
|
|
1098
1352
|
}
|
|
1099
1353
|
}
|
|
1100
1354
|
try {
|
|
1101
1355
|
let exitCode;
|
|
1356
|
+
let ranAgent = agent;
|
|
1357
|
+
let ranVersion = defaultVersion;
|
|
1102
1358
|
if (fallback.length > 0) {
|
|
1103
1359
|
// fallback requires a prompt — enforced above, narrow the type here.
|
|
1104
|
-
|
|
1360
|
+
// The sink reports which chain entry actually executed (may differ from
|
|
1361
|
+
// the primary after a rate-limit handoff) so the audit record is honest.
|
|
1362
|
+
const sink = {};
|
|
1363
|
+
exitCode = await runWithFallback({ ...execOptions, prompt: prompt, fallback, dispatchSink: sink });
|
|
1364
|
+
ranAgent = sink.agent ?? agent;
|
|
1365
|
+
ranVersion = sink.version ?? defaultVersion;
|
|
1105
1366
|
}
|
|
1106
1367
|
else {
|
|
1107
1368
|
exitCode = await execAgent(execOptions);
|
|
1108
1369
|
}
|
|
1109
1370
|
cleanupWorkflowMcpConfig();
|
|
1371
|
+
cleanupWorkflowSubagents();
|
|
1372
|
+
// Governance chokepoint (#347): every dispatched run finalizes here.
|
|
1373
|
+
// ONE tamper-evident audit record per run — non-fatal by contract.
|
|
1374
|
+
recordDispatchedRun({ agent: ranAgent, version: ranVersion ?? 'unknown', mode, cwd, exitCode });
|
|
1110
1375
|
process.exit(exitCode);
|
|
1111
1376
|
}
|
|
1112
1377
|
catch (err) {
|
|
1113
1378
|
cleanupWorkflowMcpConfig();
|
|
1379
|
+
cleanupWorkflowSubagents();
|
|
1114
1380
|
console.error(chalk.red(`Failed to execute ${agent}: ${err.message}`));
|
|
1115
1381
|
process.exit(1);
|
|
1116
1382
|
}
|