cool-workflow 0.1.78
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/.claude-plugin/plugin.json +20 -0
- package/.codex-plugin/mcp.json +10 -0
- package/.codex-plugin/plugin.json +38 -0
- package/.mcp.json +10 -0
- package/LICENSE +24 -0
- package/README.md +638 -0
- package/apps/architecture-review/app.json +51 -0
- package/apps/architecture-review/workflow.js +116 -0
- package/apps/end-to-end-golden-path/app.json +30 -0
- package/apps/end-to-end-golden-path/workflow.js +33 -0
- package/apps/pr-review-fix-ci/app.json +59 -0
- package/apps/pr-review-fix-ci/workflow.js +90 -0
- package/apps/release-cut/app.json +54 -0
- package/apps/release-cut/workflow.js +82 -0
- package/apps/research-synthesis/app.json +50 -0
- package/apps/research-synthesis/workflow.js +76 -0
- package/apps/workflow-app-framework-demo/app.json +29 -0
- package/apps/workflow-app-framework-demo/workflow.js +44 -0
- package/dist/agent-config.js +223 -0
- package/dist/candidate-scoring.js +715 -0
- package/dist/capability-core.js +630 -0
- package/dist/capability-dispatcher.js +86 -0
- package/dist/capability-registry.js +523 -0
- package/dist/cli.js +1276 -0
- package/dist/collaboration.js +727 -0
- package/dist/commit.js +570 -0
- package/dist/contract-migration.js +234 -0
- package/dist/coordinator.js +1163 -0
- package/dist/daemon.js +44 -0
- package/dist/dispatch.js +201 -0
- package/dist/drive.js +503 -0
- package/dist/error-feedback.js +415 -0
- package/dist/evidence-grounding.js +179 -0
- package/dist/evidence-reasoning.js +733 -0
- package/dist/execution-backend.js +1279 -0
- package/dist/harness.js +61 -0
- package/dist/mcp-server.js +1615 -0
- package/dist/multi-agent-eval.js +857 -0
- package/dist/multi-agent-host.js +764 -0
- package/dist/multi-agent-operator-ux.js +537 -0
- package/dist/multi-agent-trust.js +366 -0
- package/dist/multi-agent.js +1173 -0
- package/dist/node-snapshot.js +270 -0
- package/dist/observability.js +922 -0
- package/dist/operator-ux.js +971 -0
- package/dist/orchestrator/audit-operations.js +182 -0
- package/dist/orchestrator/candidate-operations.js +117 -0
- package/dist/orchestrator/cli-options.js +288 -0
- package/dist/orchestrator/collaboration-operations.js +86 -0
- package/dist/orchestrator/feedback-operations.js +81 -0
- package/dist/orchestrator/host-operations.js +78 -0
- package/dist/orchestrator/lifecycle-operations.js +462 -0
- package/dist/orchestrator/migration-operations.js +44 -0
- package/dist/orchestrator/multi-agent-operations.js +362 -0
- package/dist/orchestrator/report.js +369 -0
- package/dist/orchestrator/topology-operations.js +84 -0
- package/dist/orchestrator.js +874 -0
- package/dist/pipeline-contract.js +92 -0
- package/dist/pipeline-runner.js +285 -0
- package/dist/reclamation.js +882 -0
- package/dist/result-normalize.js +194 -0
- package/dist/run-export.js +64 -0
- package/dist/run-registry.js +1347 -0
- package/dist/run-state-schema.js +67 -0
- package/dist/sandbox-profile.js +471 -0
- package/dist/scheduler.js +266 -0
- package/dist/scheduling.js +184 -0
- package/dist/schema-validate.js +98 -0
- package/dist/state-explosion.js +1213 -0
- package/dist/state-migrations.js +463 -0
- package/dist/state-node.js +301 -0
- package/dist/state.js +308 -0
- package/dist/telemetry-attestation.js +156 -0
- package/dist/telemetry-ledger.js +145 -0
- package/dist/topology.js +527 -0
- package/dist/triggers.js +159 -0
- package/dist/trust-audit.js +475 -0
- package/dist/types/blackboard.js +2 -0
- package/dist/types/boundary.js +29 -0
- package/dist/types/candidate.js +2 -0
- package/dist/types/collaboration.js +2 -0
- package/dist/types/core.js +2 -0
- package/dist/types/drive.js +10 -0
- package/dist/types/error-feedback.js +2 -0
- package/dist/types/evidence-reasoning.js +2 -0
- package/dist/types/execution-backend.js +2 -0
- package/dist/types/multi-agent.js +2 -0
- package/dist/types/observability.js +2 -0
- package/dist/types/pipeline.js +2 -0
- package/dist/types/reclamation.js +8 -0
- package/dist/types/result.js +2 -0
- package/dist/types/run-registry.js +2 -0
- package/dist/types/run.js +2 -0
- package/dist/types/sandbox.js +2 -0
- package/dist/types/schedule.js +2 -0
- package/dist/types/state-node.js +2 -0
- package/dist/types/topology.js +2 -0
- package/dist/types/trust.js +2 -0
- package/dist/types/workbench.js +2 -0
- package/dist/types/worker.js +2 -0
- package/dist/types/workflow-app.js +2 -0
- package/dist/types.js +43 -0
- package/dist/verifier-registry.js +46 -0
- package/dist/verifier.js +78 -0
- package/dist/version.js +8 -0
- package/dist/workbench-host.js +172 -0
- package/dist/workbench.js +190 -0
- package/dist/worker-isolation.js +1028 -0
- package/dist/workflow-api.js +98 -0
- package/dist/workflow-app-framework.js +626 -0
- package/docs/agent-delegation-drive.7.md +190 -0
- package/docs/agent-framework.md +176 -0
- package/docs/candidate-scoring.7.md +106 -0
- package/docs/canonical-workflow-apps.7.md +137 -0
- package/docs/capability-topology-registry.7.md +168 -0
- package/docs/cli-mcp-parity.7.md +373 -0
- package/docs/contract-migration-tooling.7.md +123 -0
- package/docs/control-plane-scheduling.7.md +110 -0
- package/docs/coordinator-blackboard.7.md +183 -0
- package/docs/dogfood/architecture-review-cool-workflow.md +16 -0
- package/docs/dogfood-one-real-repo.7.md +168 -0
- package/docs/durable-state-and-locking.7.md +107 -0
- package/docs/end-to-end-golden-path.7.md +117 -0
- package/docs/error-feedback.7.md +153 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +270 -0
- package/docs/execution-backends.7.md +300 -0
- package/docs/getting-started.md +99 -0
- package/docs/index.md +41 -0
- package/docs/mcp-app-surface.7.md +235 -0
- package/docs/multi-agent-cli-mcp-surface.7.md +265 -0
- package/docs/multi-agent-eval-replay-harness.7.md +302 -0
- package/docs/multi-agent-operator-ux.7.md +314 -0
- package/docs/multi-agent-runtime-core.7.md +231 -0
- package/docs/multi-agent-topologies.7.md +103 -0
- package/docs/multi-agent-trust-policy-audit.7.md +154 -0
- package/docs/node-snapshot-diff-replay.7.md +135 -0
- package/docs/observability-cost-accounting.7.md +194 -0
- package/docs/operator-ux.7.md +180 -0
- package/docs/pipeline-runner.7.md +136 -0
- package/docs/project-index.md +261 -0
- package/docs/real-execution-backends.7.md +142 -0
- package/docs/release-and-migration.7.md +280 -0
- package/docs/release-tooling.7.md +159 -0
- package/docs/routines.md +48 -0
- package/docs/run-registry-control-plane.7.md +312 -0
- package/docs/run-retention-reclamation.7.md +191 -0
- package/docs/sandbox-profiles.7.md +137 -0
- package/docs/scheduled-tasks.md +80 -0
- package/docs/security-trust-hardening.7.md +117 -0
- package/docs/state-explosion-management.7.md +264 -0
- package/docs/state-node.7.md +96 -0
- package/docs/team-collaboration.7.md +207 -0
- package/docs/unix-principles.md +192 -0
- package/docs/verifier-gated-commit.7.md +140 -0
- package/docs/web-desktop-workbench.7.md +215 -0
- package/docs/worker-isolation.7.md +167 -0
- package/docs/workflow-app-framework.7.md +274 -0
- package/manifest/README.md +43 -0
- package/manifest/plugin.manifest.json +316 -0
- package/manifest/pricing.policy.json +14 -0
- package/package.json +79 -0
- package/scripts/agents/claude-p-agent.js +104 -0
- package/scripts/agents/claude-p-agent.sh +9 -0
- package/scripts/agents/cw-attest-keygen.js +55 -0
- package/scripts/agents/cw-attest-wrap.js +143 -0
- package/scripts/block-unapproved-tag.sh +39 -0
- package/scripts/bump-version.js +249 -0
- package/scripts/canonical-apps.js +171 -0
- package/scripts/cw.js +4 -0
- package/scripts/dist-drift-check.js +79 -0
- package/scripts/dogfood-architecture-review.js +237 -0
- package/scripts/dogfood-release.js +624 -0
- package/scripts/forward-ref-docs.js +73 -0
- package/scripts/gen-manifests.js +232 -0
- package/scripts/golden-path.js +300 -0
- package/scripts/mcp-server.js +4 -0
- package/scripts/new-feature.js +121 -0
- package/scripts/parity-check.js +213 -0
- package/scripts/release-check.js +118 -0
- package/scripts/release-flow.js +272 -0
- package/scripts/release-gate.sh +85 -0
- package/scripts/sync-project-index.js +387 -0
- package/scripts/validate-run-state-schema.js +126 -0
- package/scripts/verify-container-selfref.js +64 -0
- package/scripts/version-sync-check.js +237 -0
- package/skills/cool-workflow/SKILL.md +162 -0
- package/skills/cool-workflow/references/commands.md +282 -0
- package/tsconfig.json +16 -0
- package/ui/workbench/app.css +76 -0
- package/ui/workbench/app.js +159 -0
- package/ui/workbench/index.html +32 -0
- package/workflows/architecture-review.workflow.js +84 -0
- package/workflows/research-synthesis.workflow.js +47 -0
package/dist/drive.js
ADDED
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Agent Delegation Drive (v0.1.38) — the `run --drive` auto-advance loop.
|
|
3
|
+
//
|
|
4
|
+
// THE GAP THIS CLOSES: CW can plan, isolate, and accept worker output, but nothing
|
|
5
|
+
// SPAWNS the agent that writes each result.md — the last mile was a human/agent
|
|
6
|
+
// hand-writing 14 result.md files out-of-band. This loop wires the EXISTING verbs
|
|
7
|
+
// (plan -> dispatch -> agent-fulfill -> recordWorkerOutput/verify -> commit) and the
|
|
8
|
+
// v0.1.37 scheduler (retryOrPark) into ONE thin orchestrator. It spawns NOTHING
|
|
9
|
+
// itself except through `runBackend({ backendId: "agent" })`; it introduces NO second
|
|
10
|
+
// queue, runner, or scheduler.
|
|
11
|
+
//
|
|
12
|
+
// BSD discipline:
|
|
13
|
+
// - DELEGATE, DON'T EXECUTE: the model runs in the agent's process. The loop only
|
|
14
|
+
// sequences existing verbs + the agent backend; it never imports a model SDK.
|
|
15
|
+
// - FAIL CLOSED [load-bearing]: an unconfigured agent BLOCKS (never fabricates a
|
|
16
|
+
// completion); an agent hop that exits non-zero / writes no result.md / writes an
|
|
17
|
+
// invalid result.md is a FAILED hop. A worker that exhausts the scheduling retry
|
|
18
|
+
// budget PARKS (reuse v0.1.37 retryOrPark) — never silently re-driven forever.
|
|
19
|
+
// - DETERMINISTIC: `now` is injected; the per-worker order is the existing
|
|
20
|
+
// deterministic phase/dispatch order; `--once` advances exactly one step.
|
|
21
|
+
// - REUSE, DON'T FORK: every mutation goes through the existing runner verbs.
|
|
22
|
+
//
|
|
23
|
+
// See docs/agent-delegation-drive.7.md.
|
|
24
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
25
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
26
|
+
};
|
|
27
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
exports.DRIVE_SCHEMA_VERSION = void 0;
|
|
29
|
+
exports.driveStep = driveStep;
|
|
30
|
+
exports.driveConcurrentRound = driveConcurrentRound;
|
|
31
|
+
exports.drive = drive;
|
|
32
|
+
exports.drivePreview = drivePreview;
|
|
33
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
34
|
+
const dispatch_1 = require("./dispatch");
|
|
35
|
+
const execution_backend_1 = require("./execution-backend");
|
|
36
|
+
const worker_isolation_1 = require("./worker-isolation");
|
|
37
|
+
const agent_config_1 = require("./agent-config");
|
|
38
|
+
const scheduling_1 = require("./scheduling");
|
|
39
|
+
const observability_1 = require("./observability");
|
|
40
|
+
exports.DRIVE_SCHEMA_VERSION = 1;
|
|
41
|
+
/** The task the next drive step would advance: a RUNNING (already-dispatched,
|
|
42
|
+
* awaiting fulfillment / retry) task first, else the next PENDING task in the
|
|
43
|
+
* first runnable phase. Deterministic; honors the existing phase gate. */
|
|
44
|
+
function selectDriveTask(run) {
|
|
45
|
+
const phase = (0, dispatch_1.firstRunnablePhase)(run);
|
|
46
|
+
if (!phase)
|
|
47
|
+
return undefined;
|
|
48
|
+
const phaseTasks = run.tasks.filter((task) => phase.taskIds.includes(task.id));
|
|
49
|
+
return phaseTasks.find((task) => task.status === "running") || phaseTasks.find((task) => task.status === "pending");
|
|
50
|
+
}
|
|
51
|
+
function countCompleted(run) {
|
|
52
|
+
return run.tasks.filter((task) => task.status === "completed").length;
|
|
53
|
+
}
|
|
54
|
+
function countParked(run) {
|
|
55
|
+
return run.tasks.filter((task) => task.status === "failed").length;
|
|
56
|
+
}
|
|
57
|
+
function verdictVerifierNodeId(run) {
|
|
58
|
+
const verdict = run.tasks.find((task) => /^verdict[:/]|^synthesis[:/]/i.test(task.id) && task.status === "completed");
|
|
59
|
+
return verdict?.verifierNodeId;
|
|
60
|
+
}
|
|
61
|
+
function exitCodeFromEvidence(evidence) {
|
|
62
|
+
const entry = evidence.find((line) => line.startsWith("exitCode:"));
|
|
63
|
+
if (!entry)
|
|
64
|
+
return null;
|
|
65
|
+
const raw = entry.slice("exitCode:".length);
|
|
66
|
+
return raw === "null" ? null : Number(raw);
|
|
67
|
+
}
|
|
68
|
+
function agentConfigured(config) {
|
|
69
|
+
return Boolean(config.command || config.endpoint);
|
|
70
|
+
}
|
|
71
|
+
/** Opt-in progress to STDERR (stdout stays clean JSON), gated on CW_DRIVE_PROGRESS,
|
|
72
|
+
* so a live multi-minute drive is observable. */
|
|
73
|
+
function emitProgress(message) {
|
|
74
|
+
if (process.env.CW_DRIVE_PROGRESS)
|
|
75
|
+
process.stderr.write(`[drive] ${message}\n`);
|
|
76
|
+
}
|
|
77
|
+
/** Advance exactly ONE deterministic step. Pure-ish: all mutation is through the
|
|
78
|
+
* existing runner verbs + runBackend. */
|
|
79
|
+
function driveStep(ctx) {
|
|
80
|
+
const run = ctx.runner.loadRun(ctx.runId);
|
|
81
|
+
const selected = selectDriveTask(run);
|
|
82
|
+
const gate = terminalOrConfigStep(ctx, run, selected);
|
|
83
|
+
if (gate)
|
|
84
|
+
return gate;
|
|
85
|
+
return processSelectedTask(ctx, selected);
|
|
86
|
+
}
|
|
87
|
+
/** The non-advancing outcomes shared by the serial and concurrent loops: a
|
|
88
|
+
* terminal commit/complete, a blocked phase, or a fail-closed unconfigured
|
|
89
|
+
* agent. Returns undefined when there is a task ready to actually process. */
|
|
90
|
+
function terminalOrConfigStep(ctx, run, selected) {
|
|
91
|
+
const { runner, runId } = ctx;
|
|
92
|
+
// Terminal: no pending/running work remains -> commit (once) then complete.
|
|
93
|
+
if (!selected) {
|
|
94
|
+
const allComplete = run.tasks.every((task) => task.status === "completed");
|
|
95
|
+
if (allComplete) {
|
|
96
|
+
const alreadyCommitted = (run.commits || []).some((commit) => commit.reason && commit.reason.startsWith("agent-delegation-drive"));
|
|
97
|
+
if (!alreadyCommitted) {
|
|
98
|
+
const verifierNodeId = verdictVerifierNodeId(run);
|
|
99
|
+
const commit = runner.commit(runId, {
|
|
100
|
+
reason: "agent-delegation-drive: audited verdict committed",
|
|
101
|
+
...(verifierNodeId ? { verifier: verifierNodeId } : { allowUnverifiedCheckpoint: true })
|
|
102
|
+
});
|
|
103
|
+
return step("commit", "complete", { runId, reason: `committed ${commit.commit.id}` });
|
|
104
|
+
}
|
|
105
|
+
return step("complete", "complete", { runId });
|
|
106
|
+
}
|
|
107
|
+
// Nothing eligible but not all complete: a parked/failed worker blocks the phase.
|
|
108
|
+
return step("blocked", "blocked", { runId, reason: "no eligible worker (a parked/failed worker blocks the phase gate)" });
|
|
109
|
+
}
|
|
110
|
+
// Token budget (Track 3): enforce limits.tokenBudget against RECORDED usage
|
|
111
|
+
// before spawning the next agent. CW does not measure usage — it counts the
|
|
112
|
+
// host-attested records exactly as recorded (deriveUsageTotals, the same
|
|
113
|
+
// aggregation MetricsReport shows), so an unreported hop costs 0 here; an
|
|
114
|
+
// operator who needs every hop counted combines this with the fail-closed
|
|
115
|
+
// telemetry policy (Track 1), which refuses unattested usage at accept time.
|
|
116
|
+
// Exhaustion BLOCKS (refuses to spawn) rather than parks: the task is not
|
|
117
|
+
// bad, the run is out of budget.
|
|
118
|
+
const budget = run.workflow.limits?.tokenBudget;
|
|
119
|
+
if (typeof budget === "number" && budget > 0) {
|
|
120
|
+
const spent = (0, observability_1.deriveUsageTotals)(run).totals.totalTokens;
|
|
121
|
+
if (spent >= budget) {
|
|
122
|
+
return step("blocked", "blocked", {
|
|
123
|
+
runId,
|
|
124
|
+
taskId: selected.id,
|
|
125
|
+
phase: selected.phase,
|
|
126
|
+
reason: `token budget exhausted: ${spent} recorded tokens >= budget ${budget} — refusing to spawn further agents`
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Fail closed: an unconfigured agent cannot fulfill anything.
|
|
131
|
+
if (!agentConfigured(ctx.config)) {
|
|
132
|
+
return step("blocked", "blocked", {
|
|
133
|
+
runId,
|
|
134
|
+
taskId: selected.id,
|
|
135
|
+
phase: selected.phase,
|
|
136
|
+
reason: "agent backend not configured (set CW_AGENT_COMMAND/CW_AGENT_ENDPOINT or pass --agent-command/--agent-endpoint) — refusing rather than fabricating a completion"
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
/** The ONE place a drive execution request is built — serial and concurrent
|
|
142
|
+
* paths share it so the delegation surface cannot drift. task.model overrides
|
|
143
|
+
* the agent-config model for THIS task ({{model}} substitution + stripped-args
|
|
144
|
+
* provenance; never the attested model). task.agentType names the delegating
|
|
145
|
+
* backend driver (default "agent"). */
|
|
146
|
+
function buildAgentRequest(ctx, run, task, manifest, preparedOutcome) {
|
|
147
|
+
return {
|
|
148
|
+
schemaVersion: 1,
|
|
149
|
+
runId: ctx.runId,
|
|
150
|
+
taskId: task.id,
|
|
151
|
+
backendId: task.agentType || "agent",
|
|
152
|
+
cwd: run.cwd,
|
|
153
|
+
sandboxPolicy: manifest.sandboxPolicy || manifest.sandbox?.policy,
|
|
154
|
+
manifest,
|
|
155
|
+
label: task.id,
|
|
156
|
+
timeoutMs: ctx.config.timeoutMs,
|
|
157
|
+
delegation: {
|
|
158
|
+
command: ctx.config.command,
|
|
159
|
+
args: ctx.config.args,
|
|
160
|
+
endpoint: ctx.config.endpoint,
|
|
161
|
+
model: task.model || ctx.config.model
|
|
162
|
+
},
|
|
163
|
+
...(preparedOutcome ? { preparedAgentOutcome: preparedOutcome } : {})
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/** Process ONE ready task end-to-end: dispatch (if pending) -> delegate to the
|
|
167
|
+
* agent backend -> accept its result.md. Factored out of driveStep so the
|
|
168
|
+
* concurrent loop can reuse the IDENTICAL per-worker delegation (red line and
|
|
169
|
+
* fail-closed semantics included), differing only in HOW MANY tasks per round.
|
|
170
|
+
* Track 2: `preparedOutcome` carries a batch-collected child outcome — the
|
|
171
|
+
* fulfill step then settles it through runBackend instead of spawning again,
|
|
172
|
+
* so the concurrent round and the serial step share EVERY envelope/accept
|
|
173
|
+
* branch by construction. */
|
|
174
|
+
function processSelectedTask(ctx, selected, preparedOutcome) {
|
|
175
|
+
const { runner, runId } = ctx;
|
|
176
|
+
let run = runner.loadRun(runId);
|
|
177
|
+
// 1. DISPATCH (only a fresh pending task; a running task is a retry on its scope).
|
|
178
|
+
// task.agentType names the delegating backend driver (default "agent").
|
|
179
|
+
let workerId = selected.workerId;
|
|
180
|
+
let dispatched = false;
|
|
181
|
+
if (selected.status === "pending") {
|
|
182
|
+
const manifest = runner.dispatch(runId, { limit: 1, backend: selected.agentType || "agent" });
|
|
183
|
+
const task = manifest.tasks.find((entry) => entry.id === selected.id) || manifest.tasks[0];
|
|
184
|
+
if (!task || !task.workerId) {
|
|
185
|
+
return step("dispatch", "failed", { runId, taskId: selected.id, phase: selected.phase, reason: "dispatch produced no worker scope" });
|
|
186
|
+
}
|
|
187
|
+
workerId = task.workerId;
|
|
188
|
+
dispatched = true;
|
|
189
|
+
run = runner.loadRun(runId);
|
|
190
|
+
}
|
|
191
|
+
if (!workerId) {
|
|
192
|
+
return step("dispatch", "failed", { runId, taskId: selected.id, phase: selected.phase, reason: "no worker scope for task" });
|
|
193
|
+
}
|
|
194
|
+
// 2. FULFILL — delegate the worker to the agent backend (out-of-process). The
|
|
195
|
+
// agent reads input/manifest and writes result.md; CW captures the child's
|
|
196
|
+
// command/exit/stdout digest (the evidence triple) + the reported model.
|
|
197
|
+
const manifest = runner.showWorkerManifest(runId, workerId);
|
|
198
|
+
// Progress BEFORE the (possibly multi-minute) agent spawn, so a live drive shows
|
|
199
|
+
// immediate activity instead of a long silence on the first worker. task.label
|
|
200
|
+
// is the human-facing display name; the id stays the stable reference.
|
|
201
|
+
emitProgress(`→ ${selected.label || selected.id} (${selected.phase}) — ${dispatched ? "dispatched, " : ""}spawning agent, may take minutes…`);
|
|
202
|
+
const promptDigest = node_fs_1.default.existsSync(manifest.inputPath) ? (0, execution_backend_1.sha256)(node_fs_1.default.readFileSync(manifest.inputPath, "utf8")) : (0, execution_backend_1.sha256)(manifest.prompt || "");
|
|
203
|
+
const envelope = (0, execution_backend_1.runBackend)(buildAgentRequest(ctx, run, selected, manifest, preparedOutcome));
|
|
204
|
+
const handle = envelope.provenance.handle;
|
|
205
|
+
const reportedModel = handle?.metadata?.reportedModel || "unreported";
|
|
206
|
+
const reportedUsage = handle?.metadata?.reportedUsage;
|
|
207
|
+
const usageSignature = handle?.metadata?.usageSignature;
|
|
208
|
+
if (envelope.status !== "completed") {
|
|
209
|
+
return handleHop(ctx, selected, workerId, `agent hop ${envelope.status}: ${envelope.result.summary}`, dispatched);
|
|
210
|
+
}
|
|
211
|
+
// 3. ACCEPT — the SEPARATE recordWorkerOutput layer validates + records result.md.
|
|
212
|
+
// A missing result.md is a failed hop (pre-checked so no terminal side effect);
|
|
213
|
+
// an invalid result.md throws at validation BEFORE any state mutation.
|
|
214
|
+
if (!manifest.resultPath || !node_fs_1.default.existsSync(manifest.resultPath)) {
|
|
215
|
+
return handleHop(ctx, selected, workerId, "agent produced no result.md", dispatched);
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
runner.recordWorkerOutput(runId, workerId, manifest.resultPath, {
|
|
219
|
+
agentDelegation: {
|
|
220
|
+
handle: handle,
|
|
221
|
+
model: reportedModel,
|
|
222
|
+
promptDigest,
|
|
223
|
+
command: handle?.metadata?.command,
|
|
224
|
+
args: handle?.metadata?.args || [],
|
|
225
|
+
exitCode: exitCodeFromEvidence(envelope.evidence),
|
|
226
|
+
// Track 1: thread the agent's self-reported usage + its signature through
|
|
227
|
+
// to the accept layer, with the operator trust key to verify against.
|
|
228
|
+
reportedUsage,
|
|
229
|
+
usageSignature,
|
|
230
|
+
usageTrustPublicKey: ctx.config.attestPublicKey
|
|
231
|
+
},
|
|
232
|
+
// Track 1 fail-closed (opt-in): park a hop whose telemetry isn't attested.
|
|
233
|
+
requireAttestedTelemetry: ctx.config.requireAttestedTelemetry
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
return handleHop(ctx, selected, workerId, `result.md rejected: ${error instanceof Error ? error.message : String(error)}`, dispatched);
|
|
238
|
+
}
|
|
239
|
+
return step("accept", "ok", {
|
|
240
|
+
runId,
|
|
241
|
+
taskId: selected.id,
|
|
242
|
+
phase: selected.phase,
|
|
243
|
+
backendId: "agent",
|
|
244
|
+
handleKind: handle?.kind,
|
|
245
|
+
reportedModel
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
/** Advance ONE concurrent ROUND: fulfill up to `limit` ready tasks in the first
|
|
249
|
+
* runnable phase as a single batch, recording results in DETERMINISTIC task
|
|
250
|
+
* order (the existing phase/dispatch order) regardless of completion order — so
|
|
251
|
+
* replay stays byte-stable. Reuses processSelectedTask per worker, so the red
|
|
252
|
+
* line holds: each task is still an out-of-process agent delegation, never an
|
|
253
|
+
* in-CW model call. Track 2: the batch's agent children run CONCURRENTLY in
|
|
254
|
+
* wall-clock (one batch delegate child; per-job timeout kill), then results are
|
|
255
|
+
* recorded in DETERMINISTIC task order through the same processSelectedTask —
|
|
256
|
+
* collect-all: a failed/hung/dirty hop never aborts its siblings, every hop
|
|
257
|
+
* settles and is recorded (failures park via the same retryOrPark). */
|
|
258
|
+
function driveConcurrentRound(ctx, limit) {
|
|
259
|
+
const run = ctx.runner.loadRun(ctx.runId);
|
|
260
|
+
const selected = selectDriveTask(run);
|
|
261
|
+
const gate = terminalOrConfigStep(ctx, run, selected);
|
|
262
|
+
if (gate)
|
|
263
|
+
return [gate];
|
|
264
|
+
const phase = (0, dispatch_1.firstRunnablePhase)(run);
|
|
265
|
+
const width = Math.max(1, Math.floor(limit) || 1);
|
|
266
|
+
const batch = run.tasks
|
|
267
|
+
.filter((task) => phase.taskIds.includes(task.id) && (task.status === "pending" || task.status === "running"))
|
|
268
|
+
.slice(0, width)
|
|
269
|
+
.map((task) => task.id);
|
|
270
|
+
// Phase A+B: dispatch every batch task (sequential — dispatch mutates state),
|
|
271
|
+
// then collect ALL spawn-style child outcomes in one concurrent window. The
|
|
272
|
+
// token-budget gate ran at round entry; it is NOT re-checked between accepts —
|
|
273
|
+
// the spawns already happened, and refusing to RECORD finished work would
|
|
274
|
+
// discard real results (collect-all + never-claw-back). Overshoot is bounded
|
|
275
|
+
// by the round width; the next round blocks.
|
|
276
|
+
const prepared = prepareConcurrentOutcomes(ctx, batch);
|
|
277
|
+
// Phase C: settle + accept in deterministic batch order, regardless of the
|
|
278
|
+
// wall-clock order the children finished in.
|
|
279
|
+
const steps = [];
|
|
280
|
+
for (const taskId of batch) {
|
|
281
|
+
const failStep = prepared.failSteps.get(taskId);
|
|
282
|
+
if (failStep) {
|
|
283
|
+
steps.push(failStep);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
// Re-read per task: a prior accept in this round mutated state.
|
|
287
|
+
const freshRun = ctx.runner.loadRun(ctx.runId);
|
|
288
|
+
const fresh = freshRun.tasks.find((task) => task.id === taskId);
|
|
289
|
+
if (!fresh || (fresh.status !== "pending" && fresh.status !== "running"))
|
|
290
|
+
continue;
|
|
291
|
+
steps.push(processSelectedTask(ctx, fresh, prepared.outcomes.get(taskId)));
|
|
292
|
+
}
|
|
293
|
+
return steps.length > 0 ? steps : [driveStep(ctx)];
|
|
294
|
+
}
|
|
295
|
+
/** Dispatch each batch task and run every spawn-style agent child concurrently
|
|
296
|
+
* (one batch delegate child, per-job timeout kill). Returns outcomes keyed by
|
|
297
|
+
* task id; endpoint-configured agents get no outcome and settle through the
|
|
298
|
+
* serial (sequential) path inside the accept loop. Dispatch failures become
|
|
299
|
+
* recorded fail steps, exactly what the serial path would emit. */
|
|
300
|
+
function prepareConcurrentOutcomes(ctx, batch) {
|
|
301
|
+
const { runner, runId } = ctx;
|
|
302
|
+
const failSteps = new Map();
|
|
303
|
+
const jobs = [];
|
|
304
|
+
const jobTaskIds = [];
|
|
305
|
+
for (const taskId of batch) {
|
|
306
|
+
const run = runner.loadRun(runId);
|
|
307
|
+
const task = run.tasks.find((candidate) => candidate.id === taskId);
|
|
308
|
+
if (!task || (task.status !== "pending" && task.status !== "running"))
|
|
309
|
+
continue;
|
|
310
|
+
let workerId = task.workerId;
|
|
311
|
+
if (task.status === "pending") {
|
|
312
|
+
const manifest = runner.dispatch(runId, { limit: 1, backend: task.agentType || "agent" });
|
|
313
|
+
const dispatchedTask = manifest.tasks.find((entry) => entry.id === task.id) || manifest.tasks[0];
|
|
314
|
+
if (!dispatchedTask || !dispatchedTask.workerId) {
|
|
315
|
+
failSteps.set(taskId, step("dispatch", "failed", { runId, taskId, phase: task.phase, reason: "dispatch produced no worker scope" }));
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
workerId = dispatchedTask.workerId;
|
|
319
|
+
}
|
|
320
|
+
if (!workerId) {
|
|
321
|
+
failSteps.set(taskId, step("dispatch", "failed", { runId, taskId, phase: task.phase, reason: "no worker scope for task" }));
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
const manifest = runner.showWorkerManifest(runId, workerId);
|
|
325
|
+
const job = (0, execution_backend_1.prepareAgentSpawn)(buildAgentRequest(ctx, run, task, manifest));
|
|
326
|
+
if (job) {
|
|
327
|
+
jobs.push(job);
|
|
328
|
+
jobTaskIds.push(taskId);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (jobs.length) {
|
|
332
|
+
emitProgress(`⇉ concurrent round: ${jobs.length} agent${jobs.length > 1 ? "s" : ""} spawning in parallel, may take minutes…`);
|
|
333
|
+
}
|
|
334
|
+
const settled = (0, execution_backend_1.runAgentBatchOutcomes)(jobs);
|
|
335
|
+
const outcomes = new Map();
|
|
336
|
+
jobTaskIds.forEach((taskId, index) => outcomes.set(taskId, settled[index]));
|
|
337
|
+
return { outcomes, failSteps };
|
|
338
|
+
}
|
|
339
|
+
/** A failed agent hop: charge one attempt and (reuse v0.1.37 retryOrPark) either
|
|
340
|
+
* retry on the SAME worker scope next step, or PARK past the retry budget. */
|
|
341
|
+
function handleHop(ctx, task, workerId, reason, dispatched) {
|
|
342
|
+
const persisted = ctx.runner.showWorker(ctx.runId, workerId).retryCount || 0;
|
|
343
|
+
const prior = Math.max(ctx.attempts.get(task.id) || 0, persisted);
|
|
344
|
+
const entry = {
|
|
345
|
+
schemaVersion: 1,
|
|
346
|
+
id: task.id,
|
|
347
|
+
repo: ctx.runner.loadRun(ctx.runId).cwd,
|
|
348
|
+
priority: 0,
|
|
349
|
+
enqueuedAt: ctx.now,
|
|
350
|
+
status: "ready",
|
|
351
|
+
attempts: prior
|
|
352
|
+
};
|
|
353
|
+
const decided = (0, scheduling_1.retryOrPark)(entry, ctx.policy, ctx.now, reason);
|
|
354
|
+
ctx.attempts.set(task.id, decided.attempts || prior + 1);
|
|
355
|
+
if (decided.status === "parked") {
|
|
356
|
+
const attempts = decided.attempts || prior + 1;
|
|
357
|
+
// Terminal: record the failure so the worker/task carries the park reason and
|
|
358
|
+
// the phase gate stops advancing it. Never silently re-driven.
|
|
359
|
+
ctx.runner.recordWorkerFailure(ctx.runId, workerId, decided.parkedReason || reason, {
|
|
360
|
+
code: "agent-delegation-parked",
|
|
361
|
+
retryable: false,
|
|
362
|
+
retryCount: attempts
|
|
363
|
+
});
|
|
364
|
+
return step("park", "parked", {
|
|
365
|
+
runId: ctx.runId,
|
|
366
|
+
taskId: task.id,
|
|
367
|
+
phase: task.phase,
|
|
368
|
+
backendId: "agent",
|
|
369
|
+
attempts,
|
|
370
|
+
reason: decided.parkedReason || reason
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
// Retryable: leave the task running (scope reused) for the next step.
|
|
374
|
+
void dispatched;
|
|
375
|
+
(0, worker_isolation_1.recordWorkerRetryAttempt)(ctx.runner.loadRun(ctx.runId), workerId, decided.attempts || prior + 1, reason);
|
|
376
|
+
return step("fulfill", "failed", {
|
|
377
|
+
runId: ctx.runId,
|
|
378
|
+
taskId: task.id,
|
|
379
|
+
phase: task.phase,
|
|
380
|
+
backendId: "agent",
|
|
381
|
+
attempts: decided.attempts,
|
|
382
|
+
reason
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
function step(action, status, fields) {
|
|
386
|
+
return { schemaVersion: 1, action, status, ...fields };
|
|
387
|
+
}
|
|
388
|
+
/** Drive a run: `--once` advances exactly one step; otherwise run to completion,
|
|
389
|
+
* park, or a blocked stop. Composes the existing verbs + the agent backend only. */
|
|
390
|
+
function drive(runner, runId, options = {}) {
|
|
391
|
+
const now = options.now || new Date().toISOString();
|
|
392
|
+
const policy = (0, scheduling_1.normalizeSchedulingPolicy)(options.policy || scheduling_1.DEFAULT_SCHEDULING_POLICY);
|
|
393
|
+
const config = options.agentConfig || (0, agent_config_1.resolveAgentConfig)(options.args || {});
|
|
394
|
+
const ctx = { runner, runId, now, policy, config, attempts: new Map() };
|
|
395
|
+
const steps = [];
|
|
396
|
+
const run0 = runner.loadRun(runId);
|
|
397
|
+
const plannedWorkers = run0.tasks.length;
|
|
398
|
+
// Safety bound: every worker, every retry, plus the terminal commit + slack.
|
|
399
|
+
// Each concurrent round retires >=1 worker, so this bounds rounds too.
|
|
400
|
+
const maxIterations = plannedWorkers * (policy.maxAttempts + 1) + 5;
|
|
401
|
+
const concurrency = Math.max(1, Math.floor(options.concurrency || 1));
|
|
402
|
+
// The parallel() on-ramp: a phase authored with mode "parallel" is fulfilled
|
|
403
|
+
// concurrently through EVERY shipping surface (run --drive, quickstart) — no
|
|
404
|
+
// hidden option required. Width = the phase's task count bounded by
|
|
405
|
+
// limits.maxConcurrentAgents; an explicit options.concurrency still overrides
|
|
406
|
+
// (tests, operator tuning). Re-derived per round: phases differ.
|
|
407
|
+
const autoWidth = (run) => {
|
|
408
|
+
const phase = (0, dispatch_1.firstRunnablePhase)(run);
|
|
409
|
+
if (!phase || phase.mode !== "parallel")
|
|
410
|
+
return 1;
|
|
411
|
+
const cap = Math.max(1, Math.floor(run.workflow.limits?.maxConcurrentAgents || 1));
|
|
412
|
+
return Math.max(1, Math.min(cap, phase.taskIds.length));
|
|
413
|
+
};
|
|
414
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
415
|
+
const width = concurrency > 1 ? concurrency : autoWidth(runner.loadRun(runId));
|
|
416
|
+
const roundSteps = width > 1 ? driveConcurrentRound(ctx, width) : [driveStep(ctx)];
|
|
417
|
+
for (const stepResult of roundSteps) {
|
|
418
|
+
steps.push(stepResult);
|
|
419
|
+
emitProgress([
|
|
420
|
+
`${steps.length}.`,
|
|
421
|
+
stepResult.action,
|
|
422
|
+
stepResult.status,
|
|
423
|
+
stepResult.taskId || "",
|
|
424
|
+
stepResult.reportedModel ? `model=${stepResult.reportedModel}` : "",
|
|
425
|
+
stepResult.reason ? `— ${stepResult.reason}` : ""
|
|
426
|
+
]
|
|
427
|
+
.filter(Boolean)
|
|
428
|
+
.join(" "));
|
|
429
|
+
}
|
|
430
|
+
const last = roundSteps[roundSteps.length - 1];
|
|
431
|
+
if (options.once)
|
|
432
|
+
break;
|
|
433
|
+
if (last && (last.status === "complete" || last.status === "parked" || last.status === "blocked"))
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
const run = runner.loadRun(runId);
|
|
437
|
+
const completedWorkers = countCompleted(run);
|
|
438
|
+
const parkedWorkers = countParked(run);
|
|
439
|
+
const committed = (run.commits || []).find((commit) => commit.reason && commit.reason.startsWith("agent-delegation-drive"));
|
|
440
|
+
const last = steps[steps.length - 1];
|
|
441
|
+
const status = options.once
|
|
442
|
+
? completedWorkers === plannedWorkers && committed
|
|
443
|
+
? "complete"
|
|
444
|
+
: last && (last.status === "parked" || last.status === "blocked")
|
|
445
|
+
? last.status
|
|
446
|
+
: "in-progress"
|
|
447
|
+
: parkedWorkers > 0 || (last && last.status === "parked")
|
|
448
|
+
? "parked"
|
|
449
|
+
: last && last.status === "blocked"
|
|
450
|
+
? "blocked"
|
|
451
|
+
: "complete";
|
|
452
|
+
return {
|
|
453
|
+
schemaVersion: 1,
|
|
454
|
+
runId,
|
|
455
|
+
workflowId: run.workflow.id,
|
|
456
|
+
status,
|
|
457
|
+
steps,
|
|
458
|
+
plannedWorkers,
|
|
459
|
+
completedWorkers,
|
|
460
|
+
parkedWorkers,
|
|
461
|
+
commitId: committed?.id,
|
|
462
|
+
reportPath: run.paths.report,
|
|
463
|
+
statePath: run.paths.state
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
/** Read-only, deterministic preview of the NEXT drive step for a run — no mutation,
|
|
467
|
+
* no spawn. Counts come from state; safe for CLI<->MCP payload parity. */
|
|
468
|
+
function drivePreview(runner, runId, args = {}) {
|
|
469
|
+
const run = runner.loadRun(runId);
|
|
470
|
+
const config = (0, agent_config_1.resolveAgentConfig)(args);
|
|
471
|
+
const configured = agentConfigured(config);
|
|
472
|
+
const selected = selectDriveTask(run);
|
|
473
|
+
const plannedWorkers = run.tasks.length;
|
|
474
|
+
const pendingWorkers = run.tasks.filter((task) => task.status === "pending" || task.status === "running").length;
|
|
475
|
+
const completedWorkers = countCompleted(run);
|
|
476
|
+
const parkedWorkers = countParked(run);
|
|
477
|
+
let nextAction;
|
|
478
|
+
if (!selected) {
|
|
479
|
+
nextAction = run.tasks.every((task) => task.status === "completed") ? "commit" : "blocked";
|
|
480
|
+
}
|
|
481
|
+
else if (!configured) {
|
|
482
|
+
nextAction = "blocked";
|
|
483
|
+
}
|
|
484
|
+
else if (selected.status === "pending") {
|
|
485
|
+
nextAction = "dispatch";
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
nextAction = "fulfill";
|
|
489
|
+
}
|
|
490
|
+
return {
|
|
491
|
+
schemaVersion: 1,
|
|
492
|
+
runId,
|
|
493
|
+
workflowId: run.workflow.id,
|
|
494
|
+
plannedWorkers,
|
|
495
|
+
pendingWorkers,
|
|
496
|
+
completedWorkers,
|
|
497
|
+
parkedWorkers,
|
|
498
|
+
nextAction,
|
|
499
|
+
nextTaskId: selected?.id,
|
|
500
|
+
nextPhase: selected?.phase,
|
|
501
|
+
agentConfigured: configured
|
|
502
|
+
};
|
|
503
|
+
}
|