cool-workflow 0.1.98 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (306) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/README.md +11 -2
  4. package/apps/architecture-review/app.json +1 -1
  5. package/apps/architecture-review-fast/app.json +1 -1
  6. package/apps/end-to-end-golden-path/app.json +1 -1
  7. package/apps/pr-review-fix-ci/app.json +1 -1
  8. package/apps/release-cut/app.json +1 -1
  9. package/apps/research-synthesis/app.json +1 -1
  10. package/dist/cli/dispatch.js +236 -0
  11. package/dist/cli/entry.js +120 -0
  12. package/dist/cli/io.js +21 -4
  13. package/dist/cli/parseargv.js +157 -0
  14. package/dist/cli.js +6 -33
  15. package/dist/core/capability-table.js +3534 -0
  16. package/dist/core/format/help.js +314 -0
  17. package/dist/{state-explosion/format.js → core/format/state-explosion-text.js} +10 -1
  18. package/dist/core/hash.js +137 -0
  19. package/dist/core/multi-agent/candidate-scoring.js +219 -0
  20. package/dist/core/multi-agent/collaboration.js +481 -0
  21. package/dist/core/multi-agent/coordinator.js +515 -0
  22. package/dist/core/multi-agent/eval-replay.js +306 -0
  23. package/dist/core/multi-agent/runtime.js +929 -0
  24. package/dist/core/multi-agent/topology.js +298 -0
  25. package/dist/core/multi-agent/trust-policy.js +197 -0
  26. package/dist/core/pipeline/commit-gate.js +320 -0
  27. package/dist/{pipeline-contract.js → core/pipeline/contract.js} +24 -37
  28. package/dist/core/pipeline/dispatch.js +103 -0
  29. package/dist/core/pipeline/drive-decide.js +227 -0
  30. package/dist/core/pipeline/error-feedback.js +161 -0
  31. package/dist/core/pipeline/loop-expansion.js +124 -0
  32. package/dist/{result-normalize.js → core/pipeline/result-normalize.js} +14 -37
  33. package/dist/core/pipeline/runner.js +230 -0
  34. package/dist/{contract-migration.js → core/state/contract-migration.js} +68 -92
  35. package/dist/{state-migrations.js → core/state/migrations.js} +103 -96
  36. package/dist/core/state/node-projection.js +95 -0
  37. package/dist/core/state/node-snapshot.js +230 -0
  38. package/dist/core/state/run-paths.js +93 -0
  39. package/dist/{schema-validate.js → core/state/schema-validate.js} +35 -28
  40. package/dist/core/state/schema.js +50 -0
  41. package/dist/core/state/state-explosion/digest.js +243 -0
  42. package/dist/core/state/state-explosion/graph.js +527 -0
  43. package/dist/{state-explosion → core/state/state-explosion}/helpers.js +34 -3
  44. package/dist/core/state/state-explosion/report.js +187 -0
  45. package/dist/{state-explosion → core/state/state-explosion}/size.js +25 -7
  46. package/dist/{state-node.js → core/state/state-node.js} +90 -113
  47. package/dist/core/state/types.js +19 -0
  48. package/dist/{validation.js → core/state/validation.js} +64 -131
  49. package/dist/core/trust/evidence-grounding.js +134 -0
  50. package/dist/core/trust/ledger.js +199 -0
  51. package/dist/{telemetry-attestation.js → core/trust/telemetry-attestation.js} +94 -68
  52. package/dist/core/trust/telemetry-ledger.js +127 -0
  53. package/dist/core/types.js +20 -0
  54. package/dist/core/version.js +16 -0
  55. package/dist/{workflow-app-framework.js → core/workflow-apps/app-schema.js} +337 -426
  56. package/dist/mcp/dispatch.js +104 -0
  57. package/dist/mcp/server.js +144 -0
  58. package/dist/mcp-server.js +6 -84
  59. package/dist/{agent-config.js → shell/agent-config.js} +118 -71
  60. package/dist/shell/app-run-cli.js +88 -0
  61. package/dist/shell/audit-cli.js +259 -0
  62. package/dist/shell/audit-provenance.js +83 -0
  63. package/dist/shell/candidate-scoring-io.js +459 -0
  64. package/dist/shell/collaboration-io.js +264 -0
  65. package/dist/shell/commit-summary.js +104 -0
  66. package/dist/shell/commit.js +290 -0
  67. package/dist/shell/coordinator-io.js +476 -0
  68. package/dist/shell/demo-cli.js +19 -0
  69. package/dist/shell/dispatch.js +162 -0
  70. package/dist/{doctor.js → shell/doctor.js} +123 -56
  71. package/dist/shell/drive.js +873 -0
  72. package/dist/shell/error-feedback-io.js +305 -0
  73. package/dist/shell/eval-io.js +473 -0
  74. package/dist/{multi-agent-eval/format.js → shell/eval-text.js} +78 -49
  75. package/dist/{evidence-reasoning.js → shell/evidence-reasoning.js} +124 -255
  76. package/dist/shell/exec-backend-cli.js +88 -0
  77. package/dist/shell/execution-backend/agent.js +475 -0
  78. package/dist/shell/execution-backend/ci.js +15 -0
  79. package/dist/shell/execution-backend/container.js +69 -0
  80. package/dist/shell/execution-backend/envelopes.js +55 -0
  81. package/dist/shell/execution-backend/local.js +113 -0
  82. package/dist/shell/execution-backend/probes.js +175 -0
  83. package/dist/shell/execution-backend/registry.js +402 -0
  84. package/dist/shell/execution-backend/remote.js +128 -0
  85. package/dist/shell/execution-backend/types.js +11 -0
  86. package/dist/shell/feedback-cli.js +81 -0
  87. package/dist/shell/feedback-operations.js +48 -0
  88. package/dist/shell/fs-atomic.js +276 -0
  89. package/dist/shell/harness.js +98 -0
  90. package/dist/shell/ledger-cli.js +212 -0
  91. package/dist/shell/ledger-io.js +169 -0
  92. package/dist/shell/man-cli.js +89 -0
  93. package/dist/shell/metrics-cli.js +98 -0
  94. package/dist/shell/multi-agent-cli.js +1002 -0
  95. package/dist/shell/multi-agent-host.js +563 -0
  96. package/dist/shell/multi-agent-io.js +387 -0
  97. package/dist/{multi-agent-operator-ux.js → shell/multi-agent-operator-ux.js} +234 -191
  98. package/dist/shell/node-store.js +124 -0
  99. package/dist/{observability/format.js → shell/observability-format.js} +7 -1
  100. package/dist/{observability/intake.js → shell/observability-intake.js} +79 -56
  101. package/dist/{observability.js → shell/observability.js} +159 -332
  102. package/dist/{onramp.js → shell/onramp.js} +11 -0
  103. package/dist/{operator-ux/format.js → shell/operator-ux-text.js} +250 -239
  104. package/dist/shell/operator-ux.js +431 -0
  105. package/dist/shell/orchestrator.js +231 -0
  106. package/dist/shell/pipeline-cli.js +667 -0
  107. package/dist/shell/pipeline.js +217 -0
  108. package/dist/shell/reclamation-io.js +1366 -0
  109. package/dist/shell/registry-cli.js +329 -0
  110. package/dist/{remote-source.js → shell/remote-source.js} +2 -2
  111. package/dist/shell/report-cli.js +101 -0
  112. package/dist/shell/report-view-cli.js +117 -0
  113. package/dist/{orchestrator → shell}/report.js +289 -282
  114. package/dist/shell/reporter.js +62 -0
  115. package/dist/shell/run-export-cli.js +106 -0
  116. package/dist/shell/run-export.js +680 -0
  117. package/dist/shell/run-registry-io.js +1014 -0
  118. package/dist/shell/run-store.js +164 -0
  119. package/dist/{sandbox-profile.js → shell/sandbox-profile.js} +134 -95
  120. package/dist/{scheduler.js → shell/scheduler-io.js} +248 -48
  121. package/dist/shell/scheduling-io.js +311 -0
  122. package/dist/shell/state-cli.js +181 -0
  123. package/dist/shell/state-explosion-cli.js +197 -0
  124. package/dist/shell/telemetry-cli.js +85 -0
  125. package/dist/{telemetry-demo.js → shell/telemetry-demo.js} +149 -119
  126. package/dist/shell/telemetry-ledger-io.js +132 -0
  127. package/dist/{term.js → shell/term.js} +36 -46
  128. package/dist/shell/topology-io.js +361 -0
  129. package/dist/shell/trust-audit.js +471 -0
  130. package/dist/{multi-agent-trust.js → shell/trust-policy-io.js} +67 -205
  131. package/dist/shell/verifier.js +48 -0
  132. package/dist/shell/workbench-host.js +250 -0
  133. package/dist/shell/workbench-text.js +18 -0
  134. package/dist/shell/workbench.js +175 -0
  135. package/dist/shell/worker-cli.js +124 -0
  136. package/dist/shell/worker-isolation.js +852 -0
  137. package/dist/shell/workflow-app-loader.js +650 -0
  138. package/docs/agent-delegation-drive.7.md +2 -0
  139. package/docs/cli-mcp-parity.7.md +280 -219
  140. package/docs/contract-migration-tooling.7.md +2 -0
  141. package/docs/control-plane-scheduling.7.md +2 -0
  142. package/docs/durable-state-and-locking.7.md +2 -0
  143. package/docs/evidence-adoption-reasoning-chain.7.md +2 -0
  144. package/docs/execution-backends.7.md +2 -0
  145. package/docs/multi-agent-cli-mcp-surface.7.md +2 -0
  146. package/docs/multi-agent-eval-replay-harness.7.md +2 -0
  147. package/docs/multi-agent-operator-ux.7.md +2 -0
  148. package/docs/node-snapshot-diff-replay.7.md +2 -0
  149. package/docs/observability-cost-accounting.7.md +2 -0
  150. package/docs/project-index.md +132 -71
  151. package/docs/real-execution-backends.7.md +2 -0
  152. package/docs/release-and-migration.7.md +2 -0
  153. package/docs/release-tooling.7.md +2 -0
  154. package/docs/run-registry-control-plane.7.md +2 -0
  155. package/docs/run-retention-reclamation.7.md +23 -0
  156. package/docs/state-explosion-management.7.md +2 -0
  157. package/docs/team-collaboration.7.md +2 -0
  158. package/docs/web-desktop-workbench.7.md +2 -0
  159. package/manifest/plugin.manifest.json +1 -1
  160. package/manifest/source-context-profiles.json +9 -13
  161. package/package.json +1 -1
  162. package/scripts/agents/cw-attest-wrap.js +2 -2
  163. package/scripts/bump-version.js +4 -3
  164. package/scripts/canonical-apps.js +4 -4
  165. package/scripts/dogfood-architecture-review.js +2 -3
  166. package/scripts/dogfood-release.js +3 -3
  167. package/scripts/gen-parity-doc.js +15 -2
  168. package/scripts/golden-path.js +4 -4
  169. package/scripts/onramp-check.js +1 -1
  170. package/scripts/parity-check.js +38 -21
  171. package/scripts/release-flow.js +2 -2
  172. package/scripts/sync-project-index.js +51 -27
  173. package/scripts/validate-run-state-schema.js +2 -2
  174. package/scripts/version-sync-check.js +30 -30
  175. package/dist/candidate-scoring.js +0 -729
  176. package/dist/capability-core.js +0 -1189
  177. package/dist/capability-registry.js +0 -885
  178. package/dist/cli/command-surface.js +0 -494
  179. package/dist/cli/format.js +0 -56
  180. package/dist/cli/handlers/audit.js +0 -82
  181. package/dist/cli/handlers/blackboard.js +0 -81
  182. package/dist/cli/handlers/candidate.js +0 -40
  183. package/dist/cli/handlers/clones.js +0 -34
  184. package/dist/cli/handlers/collaboration.js +0 -61
  185. package/dist/cli/handlers/eval.js +0 -40
  186. package/dist/cli/handlers/ledger.js +0 -169
  187. package/dist/cli/handlers/maintenance.js +0 -107
  188. package/dist/cli/handlers/multi-agent.js +0 -165
  189. package/dist/cli/handlers/node.js +0 -41
  190. package/dist/cli/handlers/operational.js +0 -155
  191. package/dist/cli/handlers/operator.js +0 -146
  192. package/dist/cli/handlers/registry.js +0 -68
  193. package/dist/cli/handlers/run.js +0 -153
  194. package/dist/cli/handlers/scheduling.js +0 -132
  195. package/dist/cli/handlers/workbench.js +0 -41
  196. package/dist/cli/handlers/worker.js +0 -45
  197. package/dist/cli/run-summary.js +0 -45
  198. package/dist/clones.js +0 -162
  199. package/dist/collaboration.js +0 -726
  200. package/dist/commit.js +0 -592
  201. package/dist/compare.js +0 -18
  202. package/dist/coordinator/classify.js +0 -45
  203. package/dist/coordinator/paths.js +0 -42
  204. package/dist/coordinator/util.js +0 -126
  205. package/dist/coordinator.js +0 -990
  206. package/dist/daemon.js +0 -44
  207. package/dist/dispatch.js +0 -250
  208. package/dist/drive.js +0 -864
  209. package/dist/error-feedback.js +0 -419
  210. package/dist/evidence-grounding.js +0 -184
  211. package/dist/execution-backend/agent.js +0 -354
  212. package/dist/execution-backend/probes.js +0 -112
  213. package/dist/execution-backend/util.js +0 -47
  214. package/dist/execution-backend.js +0 -961
  215. package/dist/gates.js +0 -48
  216. package/dist/harness.js +0 -61
  217. package/dist/ledger.js +0 -313
  218. package/dist/loop-expansion.js +0 -60
  219. package/dist/mcp/tool-call.js +0 -470
  220. package/dist/mcp/tool-definitions.js +0 -1066
  221. package/dist/mcp-surface.js +0 -30
  222. package/dist/multi-agent/graph.js +0 -84
  223. package/dist/multi-agent/helpers.js +0 -141
  224. package/dist/multi-agent/ids.js +0 -20
  225. package/dist/multi-agent/paths.js +0 -22
  226. package/dist/multi-agent-eval/normalize.js +0 -51
  227. package/dist/multi-agent-eval.js +0 -678
  228. package/dist/multi-agent-host.js +0 -777
  229. package/dist/multi-agent.js +0 -984
  230. package/dist/node-projection.js +0 -59
  231. package/dist/node-snapshot.js +0 -260
  232. package/dist/operator-ux.js +0 -631
  233. package/dist/orchestrator/app-operations.js +0 -211
  234. package/dist/orchestrator/audit-operations.js +0 -182
  235. package/dist/orchestrator/candidate-operations.js +0 -117
  236. package/dist/orchestrator/cli-options.js +0 -294
  237. package/dist/orchestrator/collaboration-operations.js +0 -86
  238. package/dist/orchestrator/feedback-operations.js +0 -81
  239. package/dist/orchestrator/host-operations.js +0 -78
  240. package/dist/orchestrator/lifecycle-operations.js +0 -650
  241. package/dist/orchestrator/migration-operations.js +0 -44
  242. package/dist/orchestrator/multi-agent-operations.js +0 -362
  243. package/dist/orchestrator/topology-operations.js +0 -84
  244. package/dist/orchestrator.js +0 -925
  245. package/dist/pipeline-runner.js +0 -285
  246. package/dist/reclamation/hash.js +0 -72
  247. package/dist/reclamation.js +0 -812
  248. package/dist/reporter.js +0 -67
  249. package/dist/run-export.js +0 -815
  250. package/dist/run-registry/derive.js +0 -175
  251. package/dist/run-registry/format.js +0 -124
  252. package/dist/run-registry/gc.js +0 -251
  253. package/dist/run-registry/policy.js +0 -16
  254. package/dist/run-registry/queue.js +0 -115
  255. package/dist/run-registry.js +0 -850
  256. package/dist/run-state-schema.js +0 -68
  257. package/dist/scheduling.js +0 -184
  258. package/dist/state-explosion.js +0 -1014
  259. package/dist/state.js +0 -367
  260. package/dist/telemetry-ledger.js +0 -196
  261. package/dist/topology.js +0 -565
  262. package/dist/triggers.js +0 -184
  263. package/dist/trust-audit.js +0 -644
  264. package/dist/types/blackboard.js +0 -2
  265. package/dist/types/candidate.js +0 -2
  266. package/dist/types/collaboration.js +0 -2
  267. package/dist/types/core.js +0 -2
  268. package/dist/types/drive.js +0 -10
  269. package/dist/types/error-feedback.js +0 -2
  270. package/dist/types/evidence-reasoning.js +0 -2
  271. package/dist/types/execution-backend.js +0 -2
  272. package/dist/types/multi-agent.js +0 -2
  273. package/dist/types/observability.js +0 -2
  274. package/dist/types/pipeline.js +0 -2
  275. package/dist/types/reclamation.js +0 -8
  276. package/dist/types/report-bundle.js +0 -6
  277. package/dist/types/result.js +0 -2
  278. package/dist/types/run-registry.js +0 -2
  279. package/dist/types/run.js +0 -2
  280. package/dist/types/sandbox.js +0 -2
  281. package/dist/types/schedule.js +0 -2
  282. package/dist/types/state-node.js +0 -2
  283. package/dist/types/topology.js +0 -2
  284. package/dist/types/trust.js +0 -2
  285. package/dist/types/workbench.js +0 -2
  286. package/dist/types/worker.js +0 -2
  287. package/dist/types/workflow-app.js +0 -2
  288. package/dist/types.js +0 -44
  289. package/dist/util/fingerprint.js +0 -19
  290. package/dist/util/fingerprint.test.js +0 -27
  291. package/dist/verifier.js +0 -78
  292. package/dist/version.js +0 -8
  293. package/dist/workbench-host.js +0 -192
  294. package/dist/workbench.js +0 -192
  295. package/dist/worker-accept/acceptance.js +0 -114
  296. package/dist/worker-accept/blackboard-fanout.js +0 -80
  297. package/dist/worker-accept/blackboard-linkage.js +0 -19
  298. package/dist/worker-accept/context.js +0 -2
  299. package/dist/worker-accept/telemetry-ledger.js +0 -126
  300. package/dist/worker-accept/validation.js +0 -77
  301. package/dist/worker-accept/verifier-completion.js +0 -73
  302. package/dist/worker-isolation/helpers.js +0 -51
  303. package/dist/worker-isolation/paths.js +0 -46
  304. package/dist/worker-isolation.js +0 -656
  305. package/dist/workflow-api.js +0 -131
  306. /package/dist/{types → core/types}/boundary.js +0 -0
package/dist/drive.js DELETED
@@ -1,864 +0,0 @@
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.MAX_SUB_WORKFLOW_DEPTH = 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 node_path_1 = __importDefault(require("node:path"));
35
- const term_1 = require("./term");
36
- const reporter_1 = require("./reporter");
37
- const dispatch_1 = require("./dispatch");
38
- const execution_backend_1 = require("./execution-backend");
39
- const worker_isolation_1 = require("./worker-isolation");
40
- const agent_config_1 = require("./agent-config");
41
- const scheduling_1 = require("./scheduling");
42
- const observability_1 = require("./observability");
43
- const loop_expansion_1 = require("./loop-expansion");
44
- const telemetry_attestation_1 = require("./telemetry-attestation");
45
- const state_1 = require("./state");
46
- const commit_1 = require("./commit");
47
- const report_1 = require("./orchestrator/report");
48
- const trust_audit_1 = require("./trust-audit");
49
- const compare_1 = require("./compare");
50
- exports.DRIVE_SCHEMA_VERSION = 1;
51
- /** Hard cap on inline sub-workflow nesting depth (fail-closed bound on recursion). */
52
- exports.MAX_SUB_WORKFLOW_DEPTH = 4;
53
- /** The task the next drive step would advance: a RUNNING (already-dispatched,
54
- * awaiting fulfillment / retry) task first, else the next PENDING task in the
55
- * first runnable phase. Deterministic; honors the existing phase gate. */
56
- function selectDriveTask(run) {
57
- const phase = (0, dispatch_1.firstRunnablePhase)(run);
58
- if (!phase)
59
- return undefined;
60
- const phaseTasks = run.tasks.filter((task) => phase.taskIds.includes(task.id));
61
- return phaseTasks.find((task) => task.status === "running") || phaseTasks.find((task) => task.status === "pending");
62
- }
63
- function countCompleted(run) {
64
- return run.tasks.filter((task) => task.status === "completed").length;
65
- }
66
- function countParked(run) {
67
- return run.tasks.filter((task) => task.status === "failed").length;
68
- }
69
- function verdictVerifierNodeId(run) {
70
- const verdict = run.tasks.find((task) => /^verdict[:/]|^synthesis[:/]/i.test(task.id) && task.status === "completed");
71
- return verdict?.verifierNodeId;
72
- }
73
- function exitCodeFromEvidence(evidence) {
74
- const entry = evidence.find((line) => line.startsWith("exitCode:"));
75
- if (!entry)
76
- return null;
77
- const raw = entry.slice("exitCode:".length);
78
- return raw === "null" ? null : Number(raw);
79
- }
80
- function agentConfigured(config) {
81
- return Boolean(config.command || config.endpoint);
82
- }
83
- /** Progress to STDERR (stdout stays clean JSON). On by default when stderr is a
84
- * TTY; silent in CI/pipes. CW_DRIVE_PROGRESS=0 forces off, =1 forces on. */
85
- function emitProgress(message) {
86
- const forcedOff = process.env.CW_DRIVE_PROGRESS === "0";
87
- const forcedOn = process.env.CW_DRIVE_PROGRESS === "1";
88
- if ((Boolean(process.stderr.isTTY) && !forcedOff) || forcedOn)
89
- reporter_1.reporter.progress(`[drive] ${message}`);
90
- }
91
- /** Advance exactly ONE deterministic step. Pure-ish: all mutation is through the
92
- * existing runner verbs + runBackend. */
93
- function driveStep(ctx) {
94
- const run = ctx.runner.loadRun(ctx.runId);
95
- const selected = selectDriveTask(run);
96
- const gate = terminalOrConfigStep(ctx, run, selected);
97
- if (gate)
98
- return gate;
99
- return processSelectedTask(ctx, selected);
100
- }
101
- /** The non-advancing outcomes shared by the serial and concurrent loops: a
102
- * terminal commit/complete, a blocked phase, or a fail-closed unconfigured
103
- * agent. Returns undefined when there is a task ready to actually process. */
104
- function terminalOrConfigStep(ctx, run, selected) {
105
- const { runner, runId } = ctx;
106
- // Terminal: no pending/running work remains -> commit (once) then complete.
107
- if (!selected) {
108
- const allComplete = run.tasks.every((task) => task.status === "completed");
109
- if (allComplete) {
110
- const alreadyCommitted = (run.commits || []).some((commit) => commit.reason && commit.reason.startsWith("agent-delegation-drive"));
111
- if (!alreadyCommitted) {
112
- const verifierNodeId = verdictVerifierNodeId(run);
113
- const commit = runner.commit(runId, {
114
- reason: "agent-delegation-drive: audited verdict committed",
115
- ...(verifierNodeId ? { verifier: verifierNodeId } : { allowUnverifiedCheckpoint: true })
116
- });
117
- return step("commit", "complete", { runId, reason: `committed ${commit.commit.id}` });
118
- }
119
- return step("complete", "complete", { runId });
120
- }
121
- // Nothing eligible but not all complete: a parked/failed worker blocks the phase.
122
- return step("blocked", "blocked", { runId, reason: "no eligible worker (a parked/failed worker blocks the phase gate)" });
123
- }
124
- // Token budget (Track 3): enforce limits.tokenBudget against RECORDED usage
125
- // before spawning the next agent. CW does not measure usage — it counts the
126
- // host-attested records exactly as recorded (deriveUsageTotals, the same
127
- // aggregation MetricsReport shows), so an unreported hop costs 0 here; an
128
- // operator who needs every hop counted combines this with the fail-closed
129
- // telemetry policy (Track 1), which refuses unattested usage at accept time.
130
- // Exhaustion BLOCKS (refuses to spawn) rather than parks: the task is not
131
- // bad, the run is out of budget.
132
- const budget = run.workflow.limits?.tokenBudget;
133
- if (typeof budget === "number" && budget > 0) {
134
- const spent = (0, observability_1.deriveUsageTotals)(run).totals.totalTokens;
135
- if (spent >= budget) {
136
- return step("blocked", "blocked", {
137
- runId,
138
- taskId: selected.id,
139
- phase: selected.phase,
140
- reason: `token budget exhausted: ${spent} recorded tokens >= budget ${budget} — refusing to spawn further agents`
141
- });
142
- }
143
- }
144
- // Fail closed: an unconfigured agent cannot fulfill anything.
145
- if (!agentConfigured(ctx.config)) {
146
- return step("blocked", "blocked", {
147
- runId,
148
- taskId: selected.id,
149
- phase: selected.phase,
150
- reason: "agent backend not configured (set CW_AGENT_COMMAND/CW_AGENT_ENDPOINT or pass --agent-command/--agent-endpoint) — refusing rather than fabricating a completion"
151
- });
152
- }
153
- return undefined;
154
- }
155
- /** The ONE place a drive execution request is built — serial and concurrent
156
- * paths share it so the delegation surface cannot drift. task.model overrides
157
- * the agent-config model for THIS task ({{model}} substitution + stripped-args
158
- * provenance; never the attested model). task.agentType names the delegating
159
- * backend driver (default "agent"). */
160
- function buildAgentRequest(ctx, run, task, manifest, preparedOutcome) {
161
- return {
162
- schemaVersion: 1,
163
- runId: ctx.runId,
164
- taskId: task.id,
165
- backendId: task.agentType || "agent",
166
- cwd: run.cwd,
167
- sandboxPolicy: manifest.sandboxPolicy || manifest.sandbox?.policy,
168
- manifest,
169
- label: task.id,
170
- timeoutMs: ctx.config.timeoutMs,
171
- delegation: {
172
- command: ctx.config.command,
173
- args: ctx.config.args,
174
- endpoint: ctx.config.endpoint,
175
- model: task.model || ctx.config.model
176
- },
177
- ...(preparedOutcome ? { preparedAgentOutcome: preparedOutcome } : {})
178
- };
179
- }
180
- /** Process ONE ready task end-to-end: dispatch (if pending) -> delegate to the
181
- * agent backend -> accept its result.md. Factored out of driveStep so the
182
- * concurrent loop can reuse the IDENTICAL per-worker delegation (red line and
183
- * fail-closed semantics included), differing only in HOW MANY tasks per round.
184
- * Track 2: `preparedOutcome` carries a batch-collected child outcome — the
185
- * fulfill step then settles it through runBackend instead of spawning again,
186
- * so the concurrent round and the serial step share EVERY envelope/accept
187
- * branch by construction. */
188
- function processSelectedTask(ctx, selected, preparedOutcome, deferPersist = false) {
189
- const { runner, runId } = ctx;
190
- let run = runner.loadRun(runId);
191
- // 1. DISPATCH (only a fresh pending task; a running task is a retry on its scope).
192
- // task.agentType names the delegating backend driver (default "agent").
193
- let workerId = selected.workerId;
194
- let dispatched = false;
195
- if (selected.status === "pending") {
196
- const manifest = runner.dispatch(runId, { limit: 1, backend: selected.agentType || "agent", ...(deferPersist ? { persistState: false } : {}) });
197
- const task = manifest.tasks.find((entry) => entry.id === selected.id) || manifest.tasks[0];
198
- if (!task || !task.workerId) {
199
- return step("dispatch", "failed", { runId, taskId: selected.id, phase: selected.phase, reason: "dispatch produced no worker scope" });
200
- }
201
- workerId = task.workerId;
202
- dispatched = true;
203
- run = runner.loadRun(runId);
204
- }
205
- if (!workerId) {
206
- return step("dispatch", "failed", { runId, taskId: selected.id, phase: selected.phase, reason: "no worker scope for task" });
207
- }
208
- // 2. FULFILL — delegate the worker to the agent backend (out-of-process). The
209
- // agent reads input/manifest and writes result.md; CW captures the child's
210
- // command/exit/stdout digest (the evidence triple) + the reported model.
211
- const manifest = runner.showWorkerManifest(runId, workerId);
212
- // Progress BEFORE the (possibly multi-minute) agent spawn, so a live drive shows
213
- // immediate activity instead of a long silence on the first worker. task.label
214
- // is the human-facing display name; the id stays the stable reference.
215
- 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 || "");
216
- const cachePath = resultCachePath(run, selected, (0, execution_backend_1.sha256)(selected.prompt), ctx.incremental, ctx.incremental ? incrementalDelegationDigest(selected, manifest, ctx.config) : "");
217
- if (cachePath && node_fs_1.default.existsSync(cachePath)) {
218
- emitProgress(`↺ ${selected.label || selected.id} (${selected.phase}) — accepting cached result`);
219
- try {
220
- node_fs_1.default.writeFileSync(manifest.resultPath, node_fs_1.default.readFileSync(cachePath, "utf8"), "utf8");
221
- runner.recordWorkerOutput(runId, workerId, manifest.resultPath, deferPersist ? { persistState: false } : {});
222
- }
223
- catch (error) {
224
- return handleHop(ctx, selected, workerId, `result cache rejected: ${error instanceof Error ? error.message : String(error)}`, deferPersist);
225
- }
226
- return step("accept", "ok", {
227
- runId,
228
- taskId: selected.id,
229
- phase: selected.phase,
230
- handleKind: "result-cache",
231
- reason: "result cache hit"
232
- });
233
- }
234
- // Sub-workflow fulfillment (alternative to the agent backend): plan + drive a
235
- // CHILD run and bind its report back as this task's result. Leaf work is still
236
- // external-agent delegation at every level; CW imports no model SDK here.
237
- if (selected.subWorkflow) {
238
- return runSubWorkflow(ctx, run, selected, workerId, manifest, deferPersist);
239
- }
240
- emitProgress(`→ ${selected.label || selected.id} (${selected.phase}) — ${dispatched ? "dispatched, " : ""}spawning agent, may take minutes…`);
241
- const envelope = (0, execution_backend_1.runBackend)(buildAgentRequest(ctx, run, selected, manifest, preparedOutcome));
242
- const handle = envelope.provenance.handle;
243
- const reportedModel = handle?.metadata?.reportedModel || "unreported";
244
- const reportedUsage = handle?.metadata?.reportedUsage;
245
- const usageSignature = handle?.metadata?.usageSignature;
246
- if (envelope.status !== "completed") {
247
- return handleHop(ctx, selected, workerId, `agent hop ${envelope.status}: ${envelope.result.summary}`, deferPersist);
248
- }
249
- // 3. ACCEPT — the SEPARATE recordWorkerOutput layer validates + records result.md.
250
- // A missing result.md is a failed hop (pre-checked so no terminal side effect);
251
- // an invalid result.md throws at validation BEFORE any state mutation.
252
- if (!manifest.resultPath || !node_fs_1.default.existsSync(manifest.resultPath)) {
253
- return handleHop(ctx, selected, workerId, "agent produced no result.md", deferPersist);
254
- }
255
- try {
256
- runner.recordWorkerOutput(runId, workerId, manifest.resultPath, {
257
- ...(deferPersist ? { persistState: false } : {}),
258
- agentDelegation: {
259
- handle: handle,
260
- model: reportedModel,
261
- promptDigest,
262
- command: handle?.metadata?.command,
263
- args: handle?.metadata?.args || [],
264
- exitCode: exitCodeFromEvidence(envelope.evidence),
265
- // Track 1: thread the agent's self-reported usage + its signature through
266
- // to the accept layer, with the operator trust key to verify against.
267
- reportedUsage,
268
- usageSignature,
269
- usageTrustPublicKey: ctx.config.attestPublicKey
270
- },
271
- // Track 1 fail-closed (opt-in): park a hop whose telemetry isn't attested.
272
- requireAttestedTelemetry: ctx.config.requireAttestedTelemetry
273
- });
274
- }
275
- catch (error) {
276
- return handleHop(ctx, selected, workerId, `result.md rejected: ${error instanceof Error ? error.message : String(error)}`, deferPersist);
277
- }
278
- if (cachePath && manifest.resultPath && node_fs_1.default.existsSync(manifest.resultPath)) {
279
- writeResultCache(cachePath, node_fs_1.default.readFileSync(manifest.resultPath, "utf8"));
280
- }
281
- return step("accept", "ok", {
282
- runId,
283
- taskId: selected.id,
284
- phase: selected.phase,
285
- backendId: "agent",
286
- handleKind: handle?.kind,
287
- reportedModel
288
- });
289
- }
290
- function cacheFilePath(run, task, digest) {
291
- return node_path_1.default.join(run.cwd, ".cw", "cache", "worker-results", (0, state_1.safeFileName)(run.workflow.id), `${(0, state_1.safeFileName)(task.id)}-${digest.replace(/^sha256:/, "").slice(0, 32)}.md`);
292
- }
293
- /** Digest of the per-task DELEGATION config that determines a result but is NOT
294
- * carried by the prompt or run.inputs: the resolved model (task override OR the
295
- * global agent-config model), the agent IDENTITY (which binary/endpoint actually
296
- * produces the bytes — `command`/`args`/`endpoint`), the backend driver, and the
297
- * resolved sandbox PROFILE ID. All of these are operator flags/env (`--agent-model`,
298
- * `--agent-command`, `--agent-endpoint`, ...) stripped from run.inputs by
299
- * DRIVE_RUNTIME_KEYS, so they must be folded here or swapping the model/agent/
300
- * endpoint would serve a stale result (and attest the wrong producer). The sandbox
301
- * PROFILE ID (not the full resolved policy) is used because the policy's read/write
302
- * paths embed the per-run worker dir, so the full policy is NOT stable across runs
303
- * (it would defeat all reuse); the id is stable and changes on a profile swap. Args
304
- * are secret-stripped (no credential lands in the digest input). `config.args` is
305
- * the un-substituted template (e.g. `{{result}}`), so it is stable across runs.
306
- * Deterministic: stableStringify, no clock/random. */
307
- function incrementalDelegationDigest(task, manifest, config) {
308
- return (0, execution_backend_1.sha256)((0, telemetry_attestation_1.stableStringify)({
309
- model: task.model || config.model || "",
310
- agentType: task.agentType || "agent",
311
- sandboxProfileId: manifest.sandboxPolicy?.id || task.sandboxProfileId || "",
312
- command: config.command || "",
313
- args: config.args ? (0, execution_backend_1.stripSecretArgs)(config.args) : [],
314
- endpoint: config.endpoint || ""
315
- }));
316
- }
317
- function resultCachePath(run, task, promptDigest, incremental, delegationDigest) {
318
- // Incremental resume (opt-in, run-level): EVERY task is keyed by content so a
319
- // re-run reuses the longest unchanged prefix. The key folds the rendered prompt,
320
- // the full run.inputs, the per-task DELEGATION config (model/backend/sandbox —
321
- // result-determining but NOT carried by prompt/inputs, so editing the model must
322
- // invalidate), and the UPSTREAM RESULT digests (not just prompts: a CW prompt is
323
- // rendered from run.inputs and does NOT carry an upstream task's result bytes, so
324
- // a changed/nondeterministic upstream result must invalidate downstream — which
325
- // keying on upstream result bytes does). A changed prompt/input/model perturbs
326
- // that task's key, and its changed result perturbs every downstream key, so the
327
- // "first changed task and everything after" re-run falls out for free. The phase
328
- // barrier guarantees every upstream task is `completed` before this task runs, so
329
- // its result bytes are available to digest. schemaVersion:2 never collides with
330
- // the opt-in schemaVersion:1 cache below.
331
- if (incremental) {
332
- const upstreamResultsDigest = previousPhaseResultsDigest(run, task);
333
- if (upstreamResultsDigest === undefined)
334
- return undefined;
335
- const digest = (0, execution_backend_1.sha256)((0, telemetry_attestation_1.stableStringify)({
336
- schemaVersion: 2,
337
- workflowId: run.workflow.id,
338
- taskId: task.id,
339
- promptDigest,
340
- runInputsDigest: (0, execution_backend_1.sha256)((0, telemetry_attestation_1.stableStringify)(run.inputs || {})),
341
- delegationDigest,
342
- upstreamResultsDigest
343
- }));
344
- return cacheFilePath(run, task, digest);
345
- }
346
- // Default: the per-task OPT-IN cache (unchanged — POLA).
347
- const policy = task.resultCache;
348
- if (!policy || policy.mode !== "read-write")
349
- return undefined;
350
- const keyInput = policy.keyInput;
351
- const keyValue = keyInput ? String(run.inputs[keyInput] || "").trim() : "";
352
- if (!keyInput || !keyValue)
353
- return undefined;
354
- const completedResultsDigest = completedResultsCacheDigest(run, task);
355
- if (completedResultsDigest === undefined)
356
- return undefined;
357
- const digest = (0, execution_backend_1.sha256)(JSON.stringify({
358
- schemaVersion: 1,
359
- workflowId: run.workflow.id,
360
- taskId: task.id,
361
- keyInput,
362
- keyValue,
363
- promptDigest,
364
- completedResultsDigest
365
- }));
366
- return cacheFilePath(run, task, digest);
367
- }
368
- /** Digest of the result bytes of every task in strictly-earlier phases (deterministic
369
- * id order). `undefined` when any such task is not yet completed/readable — so a
370
- * caller never keys on partial upstream state. */
371
- function previousPhaseResultsDigest(run, task) {
372
- const phaseIndex = run.phases.findIndex((phase) => phase.name === task.phase || phase.id === task.phase);
373
- if (phaseIndex < 0)
374
- return undefined;
375
- const previousTaskIds = new Set(run.phases.slice(0, phaseIndex).flatMap((phase) => phase.taskIds));
376
- const records = run.tasks
377
- .filter((candidate) => previousTaskIds.has(candidate.id))
378
- .sort((a, b) => (0, compare_1.compareBytes)(a.id, b.id))
379
- .map((candidate) => {
380
- if (candidate.status !== "completed" || !candidate.resultPath || !node_fs_1.default.existsSync(candidate.resultPath))
381
- return undefined;
382
- return [candidate.id, (0, execution_backend_1.sha256)(node_fs_1.default.readFileSync(candidate.resultPath, "utf8"))];
383
- });
384
- if (records.some((record) => record === undefined))
385
- return undefined;
386
- return (0, execution_backend_1.sha256)(JSON.stringify(records));
387
- }
388
- function completedResultsCacheDigest(run, task) {
389
- if (task.resultCache?.includeCompletedResults !== "previous-phases")
390
- return "";
391
- return previousPhaseResultsDigest(run, task);
392
- }
393
- function writeResultCache(file, content) {
394
- node_fs_1.default.mkdirSync(node_path_1.default.dirname(file), { recursive: true });
395
- const tmp = `${file}.${process.pid}.tmp`;
396
- node_fs_1.default.writeFileSync(tmp, content, "utf8");
397
- node_fs_1.default.renameSync(tmp, file);
398
- }
399
- /** Advance ONE concurrent ROUND: fulfill up to `limit` ready tasks in the first
400
- * runnable phase as a single batch, recording results in DETERMINISTIC task
401
- * order (the existing phase/dispatch order) regardless of completion order — so
402
- * replay stays byte-stable. Reuses processSelectedTask per worker, so the red
403
- * line holds: each task is still an out-of-process agent delegation, never an
404
- * in-CW model call. Track 2: the batch's agent children run CONCURRENTLY in
405
- * wall-clock (one batch delegate child; per-job timeout kill), then results are
406
- * recorded in DETERMINISTIC task order through the same processSelectedTask —
407
- * collect-all: a failed/hung/dirty hop never aborts its siblings, every hop
408
- * settles and is recorded (failures park via the same retryOrPark). */
409
- function driveConcurrentRound(ctx, limit) {
410
- // The whole round runs inside ONE cached in-memory run object (loadWithCache —
411
- // reentrant, so a sub-workflow task's recursive drive() call cannot clobber this
412
- // scope's cache). Every dispatch/accept in the round defers its own state.json
413
- // write (persistState:false / deferPersist) and mutates that SAME shared object;
414
- // the round flushes to disk exactly ONCE at the end instead of once per task —
415
- // was O(N) full-state durable rewrites per round (measured: dominates wall time
416
- // at scale), now O(1). A crash mid-round loses that round's dispatch/accept
417
- // bookkeeping (bounded by the round width, i.e. limits.maxConcurrentAgents) and
418
- // forces a safe-but-wasteful re-dispatch/re-spawn on the next drive — never
419
- // disk corruption or double-counting, since the atomic-rename+fsync write
420
- // itself is untouched; only the write FREQUENCY changed.
421
- return ctx.runner.loadWithCache(() => {
422
- const run = ctx.runner.loadRun(ctx.runId);
423
- const selected = selectDriveTask(run);
424
- const gate = terminalOrConfigStep(ctx, run, selected);
425
- if (gate)
426
- return [gate];
427
- const phase = (0, dispatch_1.firstRunnablePhase)(run);
428
- const width = Math.max(1, Math.floor(limit) || 1);
429
- const batch = run.tasks
430
- .filter((task) => phase.taskIds.includes(task.id) && (task.status === "pending" || task.status === "running"))
431
- .slice(0, width)
432
- .map((task) => task.id);
433
- // Phase A+B: dispatch every batch task (sequential — dispatch mutates state),
434
- // then collect ALL spawn-style child outcomes in one concurrent window. The
435
- // token-budget gate ran at round entry; it is NOT re-checked between accepts —
436
- // the spawns already happened, and refusing to RECORD finished work would
437
- // discard real results (collect-all + never-claw-back). Overshoot is bounded
438
- // by the round width; the next round blocks.
439
- const prepared = prepareConcurrentOutcomes(ctx, batch);
440
- // Phase C: settle + accept in deterministic batch order, regardless of the
441
- // wall-clock order the children finished in.
442
- const steps = [];
443
- for (const taskId of batch) {
444
- const failStep = prepared.failSteps.get(taskId);
445
- if (failStep) {
446
- steps.push(failStep);
447
- continue;
448
- }
449
- // Re-read per task: a prior accept in this round mutated state (the SAME
450
- // cached object — no disk round-trip until the round-end flush below).
451
- const freshRun = ctx.runner.loadRun(ctx.runId);
452
- const fresh = freshRun.tasks.find((task) => task.id === taskId);
453
- if (!fresh || (fresh.status !== "pending" && fresh.status !== "running"))
454
- continue;
455
- steps.push(processSelectedTask(ctx, fresh, prepared.outcomes.get(taskId), true));
456
- }
457
- if (steps.length > 0) {
458
- const settledRun = ctx.runner.loadRun(ctx.runId);
459
- (0, commit_1.commitState)(settledRun, `concurrent-round:${batch.length}-tasks`);
460
- (0, report_1.writeReport)(settledRun);
461
- (0, state_1.saveCheckpoint)(settledRun);
462
- }
463
- return steps.length > 0 ? steps : [driveStep(ctx)];
464
- });
465
- }
466
- /** Dispatch each batch task and run every spawn-style agent child concurrently
467
- * (one batch delegate child, per-job timeout kill). Returns outcomes keyed by
468
- * task id; endpoint-configured agents get no outcome and settle through the
469
- * serial (sequential) path inside the accept loop. Dispatch failures become
470
- * recorded fail steps, exactly what the serial path would emit. */
471
- function prepareConcurrentOutcomes(ctx, batch) {
472
- const { runner, runId } = ctx;
473
- const failSteps = new Map();
474
- const jobs = [];
475
- const jobTaskIds = [];
476
- for (const taskId of batch) {
477
- const run = runner.loadRun(runId);
478
- const task = run.tasks.find((candidate) => candidate.id === taskId);
479
- if (!task || (task.status !== "pending" && task.status !== "running"))
480
- continue;
481
- let workerId = task.workerId;
482
- if (task.status === "pending") {
483
- const manifest = runner.dispatch(runId, { limit: 1, backend: task.agentType || "agent", persistState: false });
484
- const dispatchedTask = manifest.tasks.find((entry) => entry.id === task.id) || manifest.tasks[0];
485
- if (!dispatchedTask || !dispatchedTask.workerId) {
486
- failSteps.set(taskId, step("dispatch", "failed", { runId, taskId, phase: task.phase, reason: "dispatch produced no worker scope" }));
487
- continue;
488
- }
489
- workerId = dispatchedTask.workerId;
490
- }
491
- if (!workerId) {
492
- failSteps.set(taskId, step("dispatch", "failed", { runId, taskId, phase: task.phase, reason: "no worker scope for task" }));
493
- continue;
494
- }
495
- const manifest = runner.showWorkerManifest(runId, workerId);
496
- const cachePath = resultCachePath(run, task, (0, execution_backend_1.sha256)(task.prompt), ctx.incremental, ctx.incremental ? incrementalDelegationDigest(task, manifest, ctx.config) : "");
497
- if (cachePath && node_fs_1.default.existsSync(cachePath))
498
- continue;
499
- const job = (0, execution_backend_1.prepareAgentSpawn)(buildAgentRequest(ctx, run, task, manifest));
500
- if (job) {
501
- const sandboxPolicy = manifest.sandboxPolicy;
502
- if (sandboxPolicy) {
503
- const filteredEnv = (0, execution_backend_1.buildChildEnv)(sandboxPolicy);
504
- for (const key of Object.keys(process.env)) {
505
- if (/^(CW_|ANTHROPIC_|OPENAI_|GEMINI_|DEEPSEEK_|CODEX_|GOOGLE_|COHERE_|MISTRAL_|OLLAMA_|AZURE_|AWS_)/i.test(key)) {
506
- filteredEnv[key] = process.env[key];
507
- }
508
- }
509
- job.env = filteredEnv;
510
- }
511
- jobs.push(job);
512
- jobTaskIds.push(taskId);
513
- }
514
- }
515
- if (jobs.length) {
516
- emitProgress(`⇉ concurrent round: ${jobs.length} agent${jobs.length > 1 ? "s" : ""} spawning in parallel, may take minutes…`);
517
- }
518
- const settled = (0, execution_backend_1.runAgentBatchOutcomes)(jobs);
519
- const outcomes = new Map();
520
- jobTaskIds.forEach((taskId, index) => outcomes.set(taskId, settled[index]));
521
- return { outcomes, failSteps };
522
- }
523
- /** A failed agent hop: charge one attempt and (reuse v0.1.37 retryOrPark) either
524
- * retry on the SAME worker scope next step, or PARK past the retry budget. */
525
- function handleHop(ctx, task, workerId, reason, deferPersist = false) {
526
- const persisted = ctx.runner.showWorker(ctx.runId, workerId).retryCount || 0;
527
- const prior = Math.max(ctx.attempts.get(task.id) || 0, persisted);
528
- const entry = {
529
- schemaVersion: 1,
530
- id: task.id,
531
- repo: ctx.runner.loadRun(ctx.runId).cwd,
532
- priority: 0,
533
- enqueuedAt: ctx.now,
534
- status: "ready",
535
- attempts: prior
536
- };
537
- const decided = (0, scheduling_1.retryOrPark)(entry, ctx.policy, ctx.now, reason);
538
- ctx.attempts.set(task.id, decided.attempts || prior + 1);
539
- if (decided.status === "parked") {
540
- const attempts = decided.attempts || prior + 1;
541
- // Terminal: record the failure so the worker/task carries the park reason and
542
- // the phase gate stops advancing it. Never silently re-driven.
543
- ctx.runner.recordWorkerFailure(ctx.runId, workerId, decided.parkedReason || reason, {
544
- code: "agent-delegation-parked",
545
- retryable: false,
546
- retryCount: attempts,
547
- ...(deferPersist ? { persistState: false } : {})
548
- });
549
- return step("park", "parked", {
550
- runId: ctx.runId,
551
- taskId: task.id,
552
- phase: task.phase,
553
- backendId: "agent",
554
- attempts,
555
- reason: decided.parkedReason || reason
556
- });
557
- }
558
- // Retryable: leave the task running (scope reused) for the next step.
559
- (0, worker_isolation_1.recordWorkerRetryAttempt)(ctx.runner.loadRun(ctx.runId), workerId, decided.attempts || prior + 1, reason, deferPersist ? { persist: false } : {});
560
- return step("fulfill", "failed", {
561
- runId: ctx.runId,
562
- taskId: task.id,
563
- phase: task.phase,
564
- backendId: "agent",
565
- attempts: decided.attempts,
566
- reason
567
- });
568
- }
569
- function step(action, status, fields) {
570
- return { schemaVersion: 1, action, status, ...fields };
571
- }
572
- function errMessage(error) {
573
- return error instanceof Error ? error.message : String(error);
574
- }
575
- /** Render a sub-workflow's input templates against the PARENT run's inputs, so the
576
- * child inputs are a pure function of recorded parent inputs (deterministic). */
577
- function renderSubInputs(spec, parentInputs) {
578
- const out = {};
579
- for (const [key, template] of Object.entries(spec.inputs || {})) {
580
- out[key] = String(template).replace(/\{\{(\w+)\}\}/g, (_, name) => String(parentInputs[name] ?? ""));
581
- }
582
- return out;
583
- }
584
- /** Fulfill a sub-workflow task: plan + drive a CHILD run, then bind its report (or
585
- * verdict result) back as the parent task's result through the SAME accept path, so
586
- * the parent's verifier/schema/evidence gate and downstream tasks treat it like any
587
- * other result. Fail-closed: bounded recursion + cycle detection BEFORE any child
588
- * state is minted; a child that does not complete parks the parent hop. The child's
589
- * own telemetry/audit live in the child run; the parent records ONE honest
590
- * `worker.sub-workflow` cross-link (child run id + report digest + child audit
591
- * verdict) — nothing is summed or fabricated. */
592
- function runSubWorkflow(ctx, run, selected, workerId, manifest, deferPersist = false) {
593
- const spec = selected.subWorkflow;
594
- const parentApp = run.workflow.id;
595
- // Fail-closed BEFORE planning a child (no child state minted when refused).
596
- if (ctx.depth + 1 > exports.MAX_SUB_WORKFLOW_DEPTH) {
597
- return handleHop(ctx, selected, workerId, `sub-workflow depth limit exceeded (> ${exports.MAX_SUB_WORKFLOW_DEPTH})`, deferPersist);
598
- }
599
- // Include the CURRENT app on the path, so a direct self-cycle (A→A) is caught at
600
- // depth 0 — before any child run dir is minted.
601
- if ([...ctx.visitedAppIds, parentApp].includes(spec.appId)) {
602
- return handleHop(ctx, selected, workerId, `sub-workflow cycle detected: ${[...ctx.visitedAppIds, parentApp, spec.appId].join(" -> ")}`, deferPersist);
603
- }
604
- // Deterministic child run id derived from the parent run + task (no clock/random).
605
- const childRunId = `sub-${run.id}-${(0, state_1.safeFileName)(selected.id)}`;
606
- const childInputs = {
607
- repo: run.inputs.repo ?? run.cwd,
608
- cwd: run.cwd,
609
- question: run.inputs.question ?? "",
610
- ...renderSubInputs(spec, run.inputs),
611
- runId: childRunId
612
- };
613
- emitProgress(`⧉ ${selected.label || selected.id} (${selected.phase}) — sub-workflow ${spec.appId}…`);
614
- let childRun;
615
- try {
616
- childRun = ctx.runner.plan(spec.appId, childInputs);
617
- }
618
- catch (error) {
619
- return handleHop(ctx, selected, workerId, `sub-workflow plan failed (${spec.appId}): ${errMessage(error)}`, deferPersist);
620
- }
621
- const childResult = drive(ctx.runner, childRun.id, {
622
- now: ctx.now,
623
- agentConfig: ctx.config,
624
- policy: ctx.policy,
625
- incremental: ctx.incremental,
626
- depth: ctx.depth + 1,
627
- visitedAppIds: [...ctx.visitedAppIds, parentApp]
628
- });
629
- if (childResult.status !== "complete") {
630
- return handleHop(ctx, selected, workerId, `sub-workflow ${spec.appId} did not complete (status: ${childResult.status})`, deferPersist);
631
- }
632
- // Bind the child's bytes: the rendered report (default) or the verdict result.
633
- const finalChild = ctx.runner.loadRun(childRun.id);
634
- let childBytes;
635
- if (spec.bindResult === "verdict-result") {
636
- const verdict = finalChild.tasks.find((t) => /^verdict[:/]|^synthesis[:/]/i.test(t.id) && t.status === "completed");
637
- childBytes = verdict?.resultPath && node_fs_1.default.existsSync(verdict.resultPath) ? node_fs_1.default.readFileSync(verdict.resultPath, "utf8") : undefined;
638
- }
639
- else {
640
- childBytes = node_fs_1.default.existsSync(finalChild.paths.report) ? node_fs_1.default.readFileSync(finalChild.paths.report, "utf8") : undefined;
641
- }
642
- if (childBytes === undefined) {
643
- return handleHop(ctx, selected, workerId, `sub-workflow ${spec.appId} produced no ${spec.bindResult || "report"}`, deferPersist);
644
- }
645
- // Accept through the SAME path as any other result (verifier/schema/evidence gate).
646
- try {
647
- node_fs_1.default.writeFileSync(manifest.resultPath, childBytes, "utf8");
648
- ctx.runner.recordWorkerOutput(run.id, workerId, manifest.resultPath, deferPersist ? { persistState: false } : {});
649
- }
650
- catch (error) {
651
- return handleHop(ctx, selected, workerId, `sub-workflow result rejected by parent gate: ${errMessage(error)}`, deferPersist);
652
- }
653
- // Honest cross-link (provenance only — never fails the accepted hop): one
654
- // worker.sub-workflow audit event on the parent pins the child run + report digest
655
- // + the child's own audit-chain verdict, and the task points at the child run dir.
656
- try {
657
- const afterAccept = ctx.runner.loadRun(run.id);
658
- const task = afterAccept.tasks.find((t) => t.id === selected.id);
659
- const childAudit = (0, trust_audit_1.verifyTrustAudit)(finalChild);
660
- (0, trust_audit_1.recordTrustAuditEvent)(afterAccept, {
661
- kind: "worker.sub-workflow",
662
- decision: "recorded",
663
- source: "runtime-derived",
664
- workerId,
665
- taskId: selected.id,
666
- nodeId: task?.resultNodeId,
667
- metadata: {
668
- subWorkflowAppId: spec.appId,
669
- subRunId: childRun.id,
670
- childReportDigest: (0, execution_backend_1.sha256)(childBytes),
671
- childAuditVerified: childAudit.verified,
672
- bindResult: spec.bindResult || "report"
673
- }
674
- });
675
- if (task) {
676
- task.subRunId = childRun.id;
677
- task.subRunDir = finalChild.paths.runDir;
678
- }
679
- (0, state_1.saveCheckpoint)(afterAccept);
680
- }
681
- catch {
682
- /* the cross-link is provenance; a failure here must not undo an accepted hop */
683
- }
684
- return step("accept", "ok", {
685
- runId: run.id,
686
- taskId: selected.id,
687
- phase: selected.phase,
688
- handleKind: "sub-workflow",
689
- reason: `sub-workflow ${spec.appId} → ${childRun.id}`
690
- });
691
- }
692
- /** Drive a run: `--once` advances exactly one step; otherwise run to completion,
693
- * park, or a blocked stop. Composes the existing verbs + the agent backend only. */
694
- function drive(runner, runId, options = {}) {
695
- const now = options.now || new Date().toISOString();
696
- const policy = (0, scheduling_1.normalizeSchedulingPolicy)(options.policy || scheduling_1.DEFAULT_SCHEDULING_POLICY);
697
- const config = options.agentConfig || (0, agent_config_1.resolveAgentConfig)(options.args || {});
698
- const ctx = {
699
- runner, runId, now, policy, config, attempts: new Map(),
700
- incremental: Boolean(options.incremental),
701
- depth: Math.max(0, Math.floor(options.depth || 0)),
702
- visitedAppIds: options.visitedAppIds || []
703
- };
704
- const steps = [];
705
- const run0 = runner.loadRun(runId);
706
- const plannedWorkers = run0.tasks.length;
707
- // Safety bound: every worker, every retry, plus the terminal commit + slack. Each
708
- // concurrent round retires >=1 worker, so this bounds rounds too. A bounded dynamic
709
- // loop can append up to (maxRounds-1)×templateTasks MORE tasks at runtime, so the
710
- // iteration bound (NOT plannedWorkers, which stays the initial count for status) adds
711
- // the worst-case expansion derived STATICALLY from the declaration — a pure function
712
- // of the workflow, never of runtime results — so the bound is replay-stable and the
713
- // drive is provably terminating; it reduces to the original value when there are no
714
- // loop phases.
715
- const maxIterations = (plannedWorkers + (0, loop_expansion_1.maxLoopExpansion)(run0)) * (policy.maxAttempts + 1) + 5;
716
- const concurrency = Math.max(1, Math.floor(options.concurrency || 1));
717
- // The parallel() on-ramp: a phase authored with mode "parallel" is fulfilled
718
- // concurrently through EVERY shipping surface (run --drive, quickstart) — no
719
- // hidden option required. Width = the phase's task count bounded by
720
- // limits.maxConcurrentAgents; an explicit options.concurrency still overrides
721
- // (tests, operator tuning). Re-derived per round: phases differ.
722
- const autoWidth = (run) => {
723
- const phase = (0, dispatch_1.firstRunnablePhase)(run);
724
- if (!phase || phase.mode !== "parallel")
725
- return 1;
726
- const cap = Math.max(1, Math.floor(run.workflow.limits?.maxConcurrentAgents || 1));
727
- return Math.max(1, Math.min(cap, phase.taskIds.length));
728
- };
729
- // Phase-boundary progress (brew-style): announce each phase when it becomes active
730
- // and when it finishes — `==> Map ✓ (6/6)` / `==> Assess … (3/6)`. Describes CW's OWN
731
- // phases (vendor-neutral); goes to stderr via emitProgress so stdout stays clean data.
732
- const announcedPhaseComplete = new Set();
733
- let activePhaseId;
734
- const titleCase = (s) => (s ? s.charAt(0).toUpperCase() + s.slice(1) : s);
735
- const emitPhaseProgress = (run) => {
736
- for (const ph of run.phases || []) {
737
- const phaseTasks = run.tasks.filter((task) => ph.taskIds.includes(task.id));
738
- const total = phaseTasks.length;
739
- if (total === 0)
740
- continue;
741
- const done = phaseTasks.filter((task) => task.status === "completed").length;
742
- const label = titleCase(ph.name || ph.id);
743
- if (done >= total) {
744
- if (!announcedPhaseComplete.has(ph.id)) {
745
- announcedPhaseComplete.add(ph.id);
746
- emitProgress((0, term_1.phaseProgressLine)(label, done, total, ph.mode, process.stderr));
747
- }
748
- continue;
749
- }
750
- if (ph.id !== activePhaseId) {
751
- activePhaseId = ph.id;
752
- emitProgress((0, term_1.phaseProgressLine)(label, done, total, ph.mode, process.stderr));
753
- }
754
- return; // only the first not-yet-complete phase is "active"
755
- }
756
- };
757
- let exhaustedMaxIterations = !options.once;
758
- for (let i = 0; i < maxIterations; i++) {
759
- const width = concurrency > 1 ? concurrency : autoWidth(runner.loadRun(runId));
760
- const roundSteps = width > 1 ? driveConcurrentRound(ctx, width) : [driveStep(ctx)];
761
- for (const stepResult of roundSteps) {
762
- steps.push(stepResult);
763
- emitProgress([
764
- `${steps.length}.`,
765
- stepResult.action,
766
- stepResult.status,
767
- stepResult.taskId || "",
768
- stepResult.reportedModel ? `model=${stepResult.reportedModel}` : "",
769
- stepResult.reason ? `— ${stepResult.reason}` : ""
770
- ]
771
- .filter(Boolean)
772
- .join(" "));
773
- }
774
- // Brew-style phase boundaries: after each round, announce a newly-active phase and
775
- // any phase that just finished (`==> Map ✓ (6/6)` / `==> Assess … (3/6)`). Cheap —
776
- // reuses the run we just advanced; goes to stderr via emitProgress so stdout is clean.
777
- emitPhaseProgress(runner.loadRun(runId));
778
- const last = roundSteps[roundSteps.length - 1];
779
- if (options.once) {
780
- exhaustedMaxIterations = false;
781
- break;
782
- }
783
- if (last && (last.status === "complete" || last.status === "parked" || last.status === "blocked")) {
784
- exhaustedMaxIterations = false;
785
- break;
786
- }
787
- }
788
- const run = runner.loadRun(runId);
789
- const completedWorkers = countCompleted(run);
790
- const parkedWorkers = countParked(run);
791
- const committed = (run.commits || []).find((commit) => commit.reason && commit.reason.startsWith("agent-delegation-drive"));
792
- const last = steps[steps.length - 1];
793
- if (exhaustedMaxIterations) {
794
- steps.push(step("blocked", "blocked", {
795
- runId,
796
- reason: `drive reached max iteration limit (${maxIterations}) before a terminal state`
797
- }));
798
- }
799
- const status = options.once
800
- ? completedWorkers === plannedWorkers && committed
801
- ? "complete"
802
- : last && (last.status === "parked" || last.status === "blocked")
803
- ? last.status
804
- : "in-progress"
805
- : exhaustedMaxIterations
806
- ? "blocked"
807
- : parkedWorkers > 0 || (last && last.status === "parked")
808
- ? "parked"
809
- : last && last.status === "blocked"
810
- ? "blocked"
811
- : "complete";
812
- return {
813
- schemaVersion: 1,
814
- runId,
815
- workflowId: run.workflow.id,
816
- status,
817
- steps,
818
- plannedWorkers,
819
- completedWorkers,
820
- parkedWorkers,
821
- commitId: committed?.id,
822
- reportPath: run.paths.report,
823
- statePath: run.paths.state,
824
- agentConfigured: agentConfigured(config)
825
- };
826
- }
827
- /** Read-only, deterministic preview of the NEXT drive step for a run — no mutation,
828
- * no spawn. Counts come from state; safe for CLI<->MCP payload parity. */
829
- function drivePreview(runner, runId, args = {}) {
830
- const run = runner.loadRun(runId);
831
- const config = (0, agent_config_1.resolveAgentConfig)(args);
832
- const configured = agentConfigured(config);
833
- const selected = selectDriveTask(run);
834
- const plannedWorkers = run.tasks.length;
835
- const pendingWorkers = run.tasks.filter((task) => task.status === "pending" || task.status === "running").length;
836
- const completedWorkers = countCompleted(run);
837
- const parkedWorkers = countParked(run);
838
- let nextAction;
839
- if (!selected) {
840
- nextAction = run.tasks.every((task) => task.status === "completed") ? "commit" : "blocked";
841
- }
842
- else if (!configured) {
843
- nextAction = "blocked";
844
- }
845
- else if (selected.status === "pending") {
846
- nextAction = "dispatch";
847
- }
848
- else {
849
- nextAction = "fulfill";
850
- }
851
- return {
852
- schemaVersion: 1,
853
- runId,
854
- workflowId: run.workflow.id,
855
- plannedWorkers,
856
- pendingWorkers,
857
- completedWorkers,
858
- parkedWorkers,
859
- nextAction,
860
- nextTaskId: selected?.id,
861
- nextPhase: selected?.phase,
862
- agentConfigured: configured
863
- };
864
- }