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