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.
Files changed (193) hide show
  1. package/.claude-plugin/plugin.json +20 -0
  2. package/.codex-plugin/mcp.json +10 -0
  3. package/.codex-plugin/plugin.json +38 -0
  4. package/.mcp.json +10 -0
  5. package/LICENSE +24 -0
  6. package/README.md +638 -0
  7. package/apps/architecture-review/app.json +51 -0
  8. package/apps/architecture-review/workflow.js +116 -0
  9. package/apps/end-to-end-golden-path/app.json +30 -0
  10. package/apps/end-to-end-golden-path/workflow.js +33 -0
  11. package/apps/pr-review-fix-ci/app.json +59 -0
  12. package/apps/pr-review-fix-ci/workflow.js +90 -0
  13. package/apps/release-cut/app.json +54 -0
  14. package/apps/release-cut/workflow.js +82 -0
  15. package/apps/research-synthesis/app.json +50 -0
  16. package/apps/research-synthesis/workflow.js +76 -0
  17. package/apps/workflow-app-framework-demo/app.json +29 -0
  18. package/apps/workflow-app-framework-demo/workflow.js +44 -0
  19. package/dist/agent-config.js +223 -0
  20. package/dist/candidate-scoring.js +715 -0
  21. package/dist/capability-core.js +630 -0
  22. package/dist/capability-dispatcher.js +86 -0
  23. package/dist/capability-registry.js +523 -0
  24. package/dist/cli.js +1276 -0
  25. package/dist/collaboration.js +727 -0
  26. package/dist/commit.js +570 -0
  27. package/dist/contract-migration.js +234 -0
  28. package/dist/coordinator.js +1163 -0
  29. package/dist/daemon.js +44 -0
  30. package/dist/dispatch.js +201 -0
  31. package/dist/drive.js +503 -0
  32. package/dist/error-feedback.js +415 -0
  33. package/dist/evidence-grounding.js +179 -0
  34. package/dist/evidence-reasoning.js +733 -0
  35. package/dist/execution-backend.js +1279 -0
  36. package/dist/harness.js +61 -0
  37. package/dist/mcp-server.js +1615 -0
  38. package/dist/multi-agent-eval.js +857 -0
  39. package/dist/multi-agent-host.js +764 -0
  40. package/dist/multi-agent-operator-ux.js +537 -0
  41. package/dist/multi-agent-trust.js +366 -0
  42. package/dist/multi-agent.js +1173 -0
  43. package/dist/node-snapshot.js +270 -0
  44. package/dist/observability.js +922 -0
  45. package/dist/operator-ux.js +971 -0
  46. package/dist/orchestrator/audit-operations.js +182 -0
  47. package/dist/orchestrator/candidate-operations.js +117 -0
  48. package/dist/orchestrator/cli-options.js +288 -0
  49. package/dist/orchestrator/collaboration-operations.js +86 -0
  50. package/dist/orchestrator/feedback-operations.js +81 -0
  51. package/dist/orchestrator/host-operations.js +78 -0
  52. package/dist/orchestrator/lifecycle-operations.js +462 -0
  53. package/dist/orchestrator/migration-operations.js +44 -0
  54. package/dist/orchestrator/multi-agent-operations.js +362 -0
  55. package/dist/orchestrator/report.js +369 -0
  56. package/dist/orchestrator/topology-operations.js +84 -0
  57. package/dist/orchestrator.js +874 -0
  58. package/dist/pipeline-contract.js +92 -0
  59. package/dist/pipeline-runner.js +285 -0
  60. package/dist/reclamation.js +882 -0
  61. package/dist/result-normalize.js +194 -0
  62. package/dist/run-export.js +64 -0
  63. package/dist/run-registry.js +1347 -0
  64. package/dist/run-state-schema.js +67 -0
  65. package/dist/sandbox-profile.js +471 -0
  66. package/dist/scheduler.js +266 -0
  67. package/dist/scheduling.js +184 -0
  68. package/dist/schema-validate.js +98 -0
  69. package/dist/state-explosion.js +1213 -0
  70. package/dist/state-migrations.js +463 -0
  71. package/dist/state-node.js +301 -0
  72. package/dist/state.js +308 -0
  73. package/dist/telemetry-attestation.js +156 -0
  74. package/dist/telemetry-ledger.js +145 -0
  75. package/dist/topology.js +527 -0
  76. package/dist/triggers.js +159 -0
  77. package/dist/trust-audit.js +475 -0
  78. package/dist/types/blackboard.js +2 -0
  79. package/dist/types/boundary.js +29 -0
  80. package/dist/types/candidate.js +2 -0
  81. package/dist/types/collaboration.js +2 -0
  82. package/dist/types/core.js +2 -0
  83. package/dist/types/drive.js +10 -0
  84. package/dist/types/error-feedback.js +2 -0
  85. package/dist/types/evidence-reasoning.js +2 -0
  86. package/dist/types/execution-backend.js +2 -0
  87. package/dist/types/multi-agent.js +2 -0
  88. package/dist/types/observability.js +2 -0
  89. package/dist/types/pipeline.js +2 -0
  90. package/dist/types/reclamation.js +8 -0
  91. package/dist/types/result.js +2 -0
  92. package/dist/types/run-registry.js +2 -0
  93. package/dist/types/run.js +2 -0
  94. package/dist/types/sandbox.js +2 -0
  95. package/dist/types/schedule.js +2 -0
  96. package/dist/types/state-node.js +2 -0
  97. package/dist/types/topology.js +2 -0
  98. package/dist/types/trust.js +2 -0
  99. package/dist/types/workbench.js +2 -0
  100. package/dist/types/worker.js +2 -0
  101. package/dist/types/workflow-app.js +2 -0
  102. package/dist/types.js +43 -0
  103. package/dist/verifier-registry.js +46 -0
  104. package/dist/verifier.js +78 -0
  105. package/dist/version.js +8 -0
  106. package/dist/workbench-host.js +172 -0
  107. package/dist/workbench.js +190 -0
  108. package/dist/worker-isolation.js +1028 -0
  109. package/dist/workflow-api.js +98 -0
  110. package/dist/workflow-app-framework.js +626 -0
  111. package/docs/agent-delegation-drive.7.md +190 -0
  112. package/docs/agent-framework.md +176 -0
  113. package/docs/candidate-scoring.7.md +106 -0
  114. package/docs/canonical-workflow-apps.7.md +137 -0
  115. package/docs/capability-topology-registry.7.md +168 -0
  116. package/docs/cli-mcp-parity.7.md +373 -0
  117. package/docs/contract-migration-tooling.7.md +123 -0
  118. package/docs/control-plane-scheduling.7.md +110 -0
  119. package/docs/coordinator-blackboard.7.md +183 -0
  120. package/docs/dogfood/architecture-review-cool-workflow.md +16 -0
  121. package/docs/dogfood-one-real-repo.7.md +168 -0
  122. package/docs/durable-state-and-locking.7.md +107 -0
  123. package/docs/end-to-end-golden-path.7.md +117 -0
  124. package/docs/error-feedback.7.md +153 -0
  125. package/docs/evidence-adoption-reasoning-chain.7.md +270 -0
  126. package/docs/execution-backends.7.md +300 -0
  127. package/docs/getting-started.md +99 -0
  128. package/docs/index.md +41 -0
  129. package/docs/mcp-app-surface.7.md +235 -0
  130. package/docs/multi-agent-cli-mcp-surface.7.md +265 -0
  131. package/docs/multi-agent-eval-replay-harness.7.md +302 -0
  132. package/docs/multi-agent-operator-ux.7.md +314 -0
  133. package/docs/multi-agent-runtime-core.7.md +231 -0
  134. package/docs/multi-agent-topologies.7.md +103 -0
  135. package/docs/multi-agent-trust-policy-audit.7.md +154 -0
  136. package/docs/node-snapshot-diff-replay.7.md +135 -0
  137. package/docs/observability-cost-accounting.7.md +194 -0
  138. package/docs/operator-ux.7.md +180 -0
  139. package/docs/pipeline-runner.7.md +136 -0
  140. package/docs/project-index.md +261 -0
  141. package/docs/real-execution-backends.7.md +142 -0
  142. package/docs/release-and-migration.7.md +280 -0
  143. package/docs/release-tooling.7.md +159 -0
  144. package/docs/routines.md +48 -0
  145. package/docs/run-registry-control-plane.7.md +312 -0
  146. package/docs/run-retention-reclamation.7.md +191 -0
  147. package/docs/sandbox-profiles.7.md +137 -0
  148. package/docs/scheduled-tasks.md +80 -0
  149. package/docs/security-trust-hardening.7.md +117 -0
  150. package/docs/state-explosion-management.7.md +264 -0
  151. package/docs/state-node.7.md +96 -0
  152. package/docs/team-collaboration.7.md +207 -0
  153. package/docs/unix-principles.md +192 -0
  154. package/docs/verifier-gated-commit.7.md +140 -0
  155. package/docs/web-desktop-workbench.7.md +215 -0
  156. package/docs/worker-isolation.7.md +167 -0
  157. package/docs/workflow-app-framework.7.md +274 -0
  158. package/manifest/README.md +43 -0
  159. package/manifest/plugin.manifest.json +316 -0
  160. package/manifest/pricing.policy.json +14 -0
  161. package/package.json +79 -0
  162. package/scripts/agents/claude-p-agent.js +104 -0
  163. package/scripts/agents/claude-p-agent.sh +9 -0
  164. package/scripts/agents/cw-attest-keygen.js +55 -0
  165. package/scripts/agents/cw-attest-wrap.js +143 -0
  166. package/scripts/block-unapproved-tag.sh +39 -0
  167. package/scripts/bump-version.js +249 -0
  168. package/scripts/canonical-apps.js +171 -0
  169. package/scripts/cw.js +4 -0
  170. package/scripts/dist-drift-check.js +79 -0
  171. package/scripts/dogfood-architecture-review.js +237 -0
  172. package/scripts/dogfood-release.js +624 -0
  173. package/scripts/forward-ref-docs.js +73 -0
  174. package/scripts/gen-manifests.js +232 -0
  175. package/scripts/golden-path.js +300 -0
  176. package/scripts/mcp-server.js +4 -0
  177. package/scripts/new-feature.js +121 -0
  178. package/scripts/parity-check.js +213 -0
  179. package/scripts/release-check.js +118 -0
  180. package/scripts/release-flow.js +272 -0
  181. package/scripts/release-gate.sh +85 -0
  182. package/scripts/sync-project-index.js +387 -0
  183. package/scripts/validate-run-state-schema.js +126 -0
  184. package/scripts/verify-container-selfref.js +64 -0
  185. package/scripts/version-sync-check.js +237 -0
  186. package/skills/cool-workflow/SKILL.md +162 -0
  187. package/skills/cool-workflow/references/commands.md +282 -0
  188. package/tsconfig.json +16 -0
  189. package/ui/workbench/app.css +76 -0
  190. package/ui/workbench/app.js +159 -0
  191. package/ui/workbench/index.html +32 -0
  192. package/workflows/architecture-review.workflow.js +84 -0
  193. 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
+ }