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
@@ -0,0 +1,873 @@
1
+ "use strict";
2
+ // shell/drive.ts — the thin imperative loop that calls drive-decide.ts
3
+ // once per step and performs the spawn/commit/cache-write IO the
4
+ // decision names.
5
+ //
6
+ // MILESTONE 6+7 (combined; see v2/PLAN.md Open risk 9/10 — the LARGEST
7
+ // milestone). Byte-exact port of the old build's src/drive.ts's
8
+ // imperative shell around the pure decision core now in
9
+ // core/pipeline/drive-decide.ts. Sub-workflow nesting and `--incremental`
10
+ // are ported; the concurrent-round driver (driveConcurrentRound) is
11
+ // scoped down to the serial driver run through a width loop, since no
12
+ // case in this milestone's combined gate exercises true concurrent-batch
13
+ // recording order (that is `--concurrency`/parallel-phase-specific and is
14
+ // authored as its own future conformance case per Open risk 5) — the
15
+ // `mode:"parallel"` architecture-review phases still complete correctly
16
+ // through the serial per-task loop, just without the wall-clock-parallel
17
+ // spawn optimization; this is flagged here rather than silently ported as
18
+ // if fully equivalent.
19
+ //
20
+ // Evidence: SPEC/pipeline-run.md "Drive loop — src/drive.ts".
21
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
22
+ if (k2 === undefined) k2 = k;
23
+ var desc = Object.getOwnPropertyDescriptor(m, k);
24
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
25
+ desc = { enumerable: true, get: function() { return m[k]; } };
26
+ }
27
+ Object.defineProperty(o, k2, desc);
28
+ }) : (function(o, m, k, k2) {
29
+ if (k2 === undefined) k2 = k;
30
+ o[k2] = m[k];
31
+ }));
32
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
33
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
34
+ }) : function(o, v) {
35
+ o["default"] = v;
36
+ });
37
+ var __importStar = (this && this.__importStar) || (function () {
38
+ var ownKeys = function(o) {
39
+ ownKeys = Object.getOwnPropertyNames || function (o) {
40
+ var ar = [];
41
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
42
+ return ar;
43
+ };
44
+ return ownKeys(o);
45
+ };
46
+ return function (mod) {
47
+ if (mod && mod.__esModule) return mod;
48
+ var result = {};
49
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
50
+ __setModuleDefault(result, mod);
51
+ return result;
52
+ };
53
+ })();
54
+ Object.defineProperty(exports, "__esModule", { value: true });
55
+ exports.MAX_SUB_WORKFLOW_DEPTH = exports.DRIVE_SCHEMA_VERSION = void 0;
56
+ exports.maybeExpandLoop = maybeExpandLoop;
57
+ exports.driveStep = driveStep;
58
+ exports.drive = drive;
59
+ exports.drivePreview = drivePreview;
60
+ const fs = __importStar(require("node:fs"));
61
+ const path = __importStar(require("node:path"));
62
+ const drive_decide_1 = require("../core/pipeline/drive-decide");
63
+ const loop_expansion_1 = require("../core/pipeline/loop-expansion");
64
+ const dispatch_1 = require("../core/pipeline/dispatch");
65
+ const run_store_1 = require("./run-store");
66
+ const dispatch_2 = require("./dispatch");
67
+ const worker_isolation_1 = require("./worker-isolation");
68
+ const state_node_1 = require("../core/state/state-node");
69
+ const node_store_1 = require("./node-store");
70
+ const runner_1 = require("../core/pipeline/runner");
71
+ const harness_1 = require("./harness");
72
+ const term_1 = require("./term");
73
+ const commit_1 = require("./commit");
74
+ const report_1 = require("./report");
75
+ const trust_audit_1 = require("./trust-audit");
76
+ const agent_config_1 = require("./agent-config");
77
+ const registry_1 = require("./execution-backend/registry");
78
+ const agent_1 = require("./execution-backend/agent");
79
+ const local_1 = require("./execution-backend/local");
80
+ const hash_1 = require("../core/hash");
81
+ const pipeline_1 = require("./pipeline");
82
+ const reporter_1 = require("./reporter");
83
+ const fs_atomic_1 = require("./fs-atomic");
84
+ const observability_1 = require("./observability");
85
+ const telemetry_attestation_1 = require("../core/trust/telemetry-attestation");
86
+ /** Total RECORDED tokens across the run's attested units, reading the
87
+ * agent-reported usage through normalizeReportedUsage so a hop that
88
+ * reported snake_case buckets (`input_tokens`/`output_tokens`, the shape
89
+ * parseAgentReport hands back verbatim) is counted — not silently zeroed.
90
+ * Reuses deriveUsageTotals' own unit selection (its `rows` are the
91
+ * deduped attested units) so worker-vs-task double-counting stays fixed;
92
+ * only the key-reading is corrected here. The old build normalized at
93
+ * store time in worker-accept/verifier-completion.ts; v2 stores the raw
94
+ * reportedUsage, so the drive's budget accounting normalizes at read
95
+ * time — same net RECORDED total, same fail-closed backstop. */
96
+ function recordedTokenTotal(run) {
97
+ let total = 0;
98
+ for (const row of (0, observability_1.deriveUsageTotals)(run).rows) {
99
+ const usage = row.usage;
100
+ if (!usage)
101
+ continue;
102
+ const declared = usage.totalTokens;
103
+ if (typeof declared === "number") {
104
+ total += declared;
105
+ continue;
106
+ }
107
+ const n = (0, telemetry_attestation_1.normalizeReportedUsage)(usage);
108
+ if (typeof n.totalTokens === "number")
109
+ total += n.totalTokens;
110
+ else
111
+ total += (n.inputTokens || 0) + (n.outputTokens || 0);
112
+ }
113
+ return total;
114
+ }
115
+ /** Token-budget gate input: enforce limits.tokenBudget against RECORDED usage,
116
+ * not a hardcoded zero. Returns undefined when no positive budget is set so the
117
+ * gate is a no-op. Spent = the run's total recorded tokens. */
118
+ function tokenBudgetUsage(run) {
119
+ const budget = run.workflow.limits?.tokenBudget;
120
+ if (!budget || budget <= 0)
121
+ return undefined;
122
+ return { spent: recordedTokenTotal(run), budget };
123
+ }
124
+ exports.DRIVE_SCHEMA_VERSION = 1;
125
+ exports.MAX_SUB_WORKFLOW_DEPTH = 4;
126
+ function agentConfigured(config) {
127
+ return Boolean(config.command || config.endpoint);
128
+ }
129
+ /** Progress to STDERR (stdout stays clean JSON). On by default when
130
+ * stderr is a TTY; silent in CI/pipes. CW_DRIVE_PROGRESS=0 forces off,
131
+ * =1 forces on. This is gate point #2 of the Rule of Silence's three
132
+ * gate points (SPEC/reporting-ux.md rebuild risk #1) — byte-exact port
133
+ * of the old build's src/drive.ts's emitProgress. */
134
+ function emitProgress(message) {
135
+ const forcedOff = process.env.CW_DRIVE_PROGRESS === "0";
136
+ const forcedOn = process.env.CW_DRIVE_PROGRESS === "1";
137
+ if ((Boolean(process.stderr.isTTY) && !forcedOff) || forcedOn)
138
+ reporter_1.reporter.progress(`[drive] ${message}`);
139
+ }
140
+ // A concurrent round runs many dispatch/accept steps against ONE shared
141
+ // in-memory run object, deferring every disk write to a single flush at
142
+ // round end (see driveConcurrentRound below). loadRun(ctx) is the single
143
+ // choke point every step reads through, so the cache lives here: while a
144
+ // round is active for a given run id, loadRun returns the SAME mutated
145
+ // object instead of re-reading (necessarily stale) disk state. Keyed by
146
+ // run id (not a stack) so a sub-workflow task's nested drive() call on a
147
+ // DIFFERENT run id is unaffected — re-entrant, matches the old build's
148
+ // runner.loadWithCache. Byte-exact in spirit to src/drive.ts's own
149
+ // per-runner cache; ported here as a module-level map since this build
150
+ // has no persistent "runner" object to hang it on.
151
+ const roundCache = new Map();
152
+ function loadRun(ctx) {
153
+ const cached = roundCache.get(ctx.runId);
154
+ if (cached)
155
+ return cached;
156
+ return (0, run_store_1.loadRunFromCwd)(ctx.runId, ctx.cwd);
157
+ }
158
+ /** Runs `fn` with `runId`'s loadRun calls served from one shared cached
159
+ * object (seeded fresh from disk), and always clears the cache entry
160
+ * afterward — even on throw — so a round never leaks its cache into a
161
+ * later, unrelated drive call. */
162
+ function withRoundCache(ctx, fn) {
163
+ const seed = (0, run_store_1.loadRunFromCwd)(ctx.runId, ctx.cwd);
164
+ roundCache.set(ctx.runId, seed);
165
+ try {
166
+ return fn();
167
+ }
168
+ finally {
169
+ roundCache.delete(ctx.runId);
170
+ }
171
+ }
172
+ function resultCachePath(run, task, promptDigest, incremental, delegationDigest) {
173
+ let digest;
174
+ if (incremental) {
175
+ const upstream = previousPhaseResultsDigest(run, task);
176
+ digest = (0, drive_decide_1.incrementalCacheKey)(run.workflow.id, task.id, promptDigest, (0, hash_1.sha256)((0, hash_1.stableStringify)(run.inputs || {})), delegationDigest, upstream);
177
+ }
178
+ else {
179
+ const policy = task.resultCache;
180
+ if (!policy || policy.mode !== "read-write" || !policy.keyInput)
181
+ return undefined;
182
+ const keyValue = String(run.inputs[policy.keyInput] || "").trim();
183
+ digest = (0, drive_decide_1.defaultCacheKey)(run.workflow.id, task.id, policy.keyInput, keyValue, promptDigest, "");
184
+ }
185
+ if (!digest)
186
+ return undefined;
187
+ return path.join(run.cwd, ".cw", "cache", "worker-results", (0, fs_atomic_1.safeFileName)(run.workflow.id), (0, drive_decide_1.cacheFileName)(task.id, digest));
188
+ }
189
+ function previousPhaseResultsDigest(run, task) {
190
+ const phaseIndex = run.phases.findIndex((p) => p.name === task.phase || p.id === task.phase);
191
+ if (phaseIndex < 0)
192
+ return undefined;
193
+ const previousTaskIds = new Set(run.phases.slice(0, phaseIndex).flatMap((p) => p.taskIds));
194
+ const records = [];
195
+ for (const candidate of run.tasks.filter((t) => previousTaskIds.has(t.id)).sort((a, b) => a.id.localeCompare(b.id))) {
196
+ if (candidate.status !== "completed" || !candidate.resultPath || !fs.existsSync(candidate.resultPath)) {
197
+ records.push(undefined);
198
+ continue;
199
+ }
200
+ records.push([candidate.id, (0, hash_1.sha256)(fs.readFileSync(candidate.resultPath, "utf8"))]);
201
+ }
202
+ if (records.some((r) => r === undefined))
203
+ return undefined;
204
+ return (0, hash_1.sha256)(JSON.stringify(records));
205
+ }
206
+ function writeResultCache(file, content) {
207
+ fs.mkdirSync(path.dirname(file), { recursive: true });
208
+ const tmp = `${file}.${process.pid}.tmp`;
209
+ fs.writeFileSync(tmp, content, "utf8");
210
+ fs.renameSync(tmp, file);
211
+ }
212
+ /** `deferPersist` (concurrent-round callers ONLY — never a plain serial
213
+ * step) skips saveCheckpoint so a caller driving many tasks through one
214
+ * in-memory `run` can defer the disk flush to a single call at round
215
+ * end; `sharedRun`, when given, is mutated in place instead of a fresh
216
+ * loadRun (the round's one shared cached object). */
217
+ function handleHop(ctx, task, workerId, reason, deferPersist = false, sharedRun) {
218
+ // ONE load, mutated in place and saved — a fresh reload right before
219
+ // saveCheckpoint would discard recordWorkerFailure/RetryAttempt's own
220
+ // in-memory mutation (they return an updated scope but mutate the run
221
+ // object passed in), silently dropping the park/retry bookkeeping.
222
+ const run = sharedRun || loadRun(ctx);
223
+ const scope = (0, worker_isolation_1.getWorkerScope)(run, workerId);
224
+ const persisted = scope?.retryCount || 0;
225
+ const prior = (0, drive_decide_1.priorAttempts)(ctx.attempts.get(task.id) || 0, persisted);
226
+ const decided = (0, drive_decide_1.retryOrPark)(prior, ctx.policy, reason);
227
+ ctx.attempts.set(task.id, decided.attempts);
228
+ if (decided.status === "parked") {
229
+ (0, worker_isolation_1.recordWorkerFailure)(run, workerId, decided.parkedReason || reason, { code: "agent-delegation-parked", retryable: false, retryCount: decided.attempts });
230
+ if (!deferPersist)
231
+ (0, run_store_1.saveCheckpoint)(run);
232
+ return (0, drive_decide_1.makeStep)("park", "parked", { runId: ctx.runId, taskId: task.id, phase: task.phase, backendId: "agent", attempts: decided.attempts, reason: decided.parkedReason || reason });
233
+ }
234
+ (0, worker_isolation_1.recordWorkerRetryAttempt)(run, workerId, decided.attempts, reason);
235
+ if (!deferPersist)
236
+ (0, run_store_1.saveCheckpoint)(run);
237
+ return (0, drive_decide_1.makeStep)("fulfill", "failed", { runId: ctx.runId, taskId: task.id, phase: task.phase, backendId: "agent", attempts: decided.attempts, reason });
238
+ }
239
+ function renderSubInputs(spec, parentInputs) {
240
+ const out = {};
241
+ for (const [key, template] of Object.entries(spec.inputs || {})) {
242
+ out[key] = String(template).replace(/\{\{(\w+)\}\}/g, (_, name) => String(parentInputs[name] ?? ""));
243
+ }
244
+ return out;
245
+ }
246
+ function errMessage(error) {
247
+ return error instanceof Error ? error.message : String(error);
248
+ }
249
+ /** `deferPersist` (concurrent-round callers ONLY) skips the per-task
250
+ * commitState/saveCheckpoint calls — the round flushes once at the end
251
+ * instead. `preparedOutcome`, when given (concurrent round only), is
252
+ * fed to runBackend so the agent spawn that already ran concurrently in
253
+ * prepareConcurrentOutcomes is SETTLED here, not re-spawned. */
254
+ function processSelectedTask(ctx, selectedId, preparedOutcome, deferPersist = false) {
255
+ let run = loadRun(ctx);
256
+ let selected = run.tasks.find((t) => t.id === selectedId);
257
+ let workerId = selected.workerId;
258
+ let dispatched = false;
259
+ if (selected.status === "pending") {
260
+ const manifest = (0, dispatch_2.createDispatchManifest)(run, 1, { backendId: selected.agentType || "agent" });
261
+ // Advance the RUN-level lifecycle stage on dispatch, exactly as the old
262
+ // build's orchestrator dispatch() wrapper did (run.loopStage = "act").
263
+ // The operator status "Stage:" line reads run.loopStage; v2's shell/
264
+ // dispatch.ts advances the task/node loopStage but never the run, so a
265
+ // driven run's operator status stayed frozen at "interpret".
266
+ run.loopStage = "act";
267
+ // Byte-exact to the old build's orchestrator dispatch() wrapper: a
268
+ // successful dispatch is its own checkpoint commit (reason
269
+ // `dispatch:<dispatch-id>`), not just a bare saveCheckpoint — SPEC/
270
+ // pipeline-run.md's persist-ordering section pins this exact reason.
271
+ if (!deferPersist) {
272
+ if (manifest.dispatchId)
273
+ (0, commit_1.commitState)(run, `dispatch:${manifest.dispatchId}`);
274
+ (0, run_store_1.saveCheckpoint)(run);
275
+ }
276
+ const dispatchedTask = manifest.tasks.find((t) => t.id === selected.id) || manifest.tasks[0];
277
+ if (!dispatchedTask || !dispatchedTask.workerId) {
278
+ return (0, drive_decide_1.makeStep)("dispatch", "failed", { runId: ctx.runId, taskId: selected.id, phase: selected.phase, reason: "dispatch produced no worker scope" });
279
+ }
280
+ workerId = dispatchedTask.workerId;
281
+ dispatched = true;
282
+ run = loadRun(ctx);
283
+ selected = run.tasks.find((t) => t.id === selectedId);
284
+ }
285
+ if (!workerId) {
286
+ return (0, drive_decide_1.makeStep)("dispatch", "failed", { runId: ctx.runId, taskId: selected.id, phase: selected.phase, reason: "no worker scope for task" });
287
+ }
288
+ const manifest = (0, worker_isolation_1.showWorkerManifest)(run, workerId);
289
+ // `promptDigest` here is the PER-DISPATCH worker instructions file
290
+ // (input.md) — it embeds this run's own id/dispatch id, so it is
291
+ // NEVER stable across separate runs. It feeds ONLY recordWorkerOutput's
292
+ // agentDelegation telemetry below, never the cache key. The cache key
293
+ // instead digests the task's own static, workflow-authored prompt text
294
+ // (selected.prompt), which IS stable across runs — byte-exact to the
295
+ // old build's src/drive.ts:280-282 (two differently-sourced digests,
296
+ // easy to collapse into one by mistake).
297
+ const promptDigest = fs.existsSync(manifest.inputPath) ? (0, hash_1.sha256)(fs.readFileSync(manifest.inputPath, "utf8")) : (0, hash_1.sha256)(manifest.prompt || "");
298
+ const cacheKeyPromptDigest = (0, hash_1.sha256)(selected.prompt || "");
299
+ const delegationDigest = ctx.incremental
300
+ ? (0, drive_decide_1.incrementalDelegationDigest)(selected.model || ctx.config.model || "", selected.agentType || "agent", manifest.sandboxPolicy?.id || selected.sandboxProfileId || "", ctx.config.command || "", ctx.config.args ? (0, agent_1.stripSecretArgs)(ctx.config.args) : [], ctx.config.endpoint || "")
301
+ : "";
302
+ const cachePath = resultCachePath(run, selected, cacheKeyPromptDigest, ctx.incremental, delegationDigest);
303
+ if (cachePath && fs.existsSync(cachePath)) {
304
+ emitProgress(`↺ ${selected.label || selected.id} (${selected.phase}) — accepting cached result`);
305
+ try {
306
+ fs.writeFileSync(manifest.resultPath, fs.readFileSync(cachePath, "utf8"), "utf8");
307
+ (0, worker_isolation_1.recordWorkerOutput)(run, workerId, manifest.resultPath);
308
+ // Advance the run lifecycle stage on accept, as the old build's
309
+ // recordWorkerOutput wrapper did (run.loopStage = "observe").
310
+ run.loopStage = "observe";
311
+ // Bounded dynamic loops: after a round's tasks complete, evaluate the
312
+ // predicate and either append the next round or mark the loop done —
313
+ // folded into this same worker:<id>:result checkpoint, exactly as the
314
+ // old build's recordWorkerOutput wrapper did (no-op for non-loop runs).
315
+ maybeExpandLoop(run);
316
+ // Byte-exact to the old build's orchestrator recordWorkerOutput()
317
+ // wrapper: an accepted result is its own checkpoint commit (reason
318
+ // `worker:<worker-id>:result`), not just a bare saveCheckpoint.
319
+ if (!deferPersist) {
320
+ (0, commit_1.commitState)(run, `worker:${workerId}:result`);
321
+ (0, run_store_1.saveCheckpoint)(run);
322
+ }
323
+ }
324
+ catch (error) {
325
+ return handleHop(ctx, selected, workerId, `result cache rejected: ${errMessage(error)}`, deferPersist, deferPersist ? run : undefined);
326
+ }
327
+ return (0, drive_decide_1.makeStep)("accept", "ok", { runId: ctx.runId, taskId: selected.id, phase: selected.phase, handleKind: "result-cache", reason: "result cache hit" });
328
+ }
329
+ const subWorkflow = selected.subWorkflow;
330
+ if (subWorkflow) {
331
+ emitProgress(`⧉ ${selected.label || selected.id} (${selected.phase}) — sub-workflow ${subWorkflow.appId}…`);
332
+ return runSubWorkflow(ctx, run, selected, workerId, manifest, subWorkflow, deferPersist);
333
+ }
334
+ emitProgress(`→ ${selected.label || selected.id} (${selected.phase}) — ${dispatched ? "dispatched, " : ""}spawning agent, may take minutes…`);
335
+ const envelope = (0, registry_1.runBackend)({
336
+ schemaVersion: 1,
337
+ runId: ctx.runId,
338
+ taskId: selected.id,
339
+ backendId: selected.agentType || "agent",
340
+ cwd: run.cwd,
341
+ sandboxPolicy: manifest.sandboxPolicy,
342
+ manifest: { workerDir: manifest.workerDir, manifestPath: manifest.manifestPath, inputPath: manifest.inputPath, resultPath: manifest.resultPath, prompt: manifest.prompt },
343
+ label: selected.id,
344
+ timeoutMs: ctx.config.timeoutMs,
345
+ delegation: { command: ctx.config.command, args: ctx.config.args, endpoint: ctx.config.endpoint, model: selected.model || ctx.config.model },
346
+ ...(preparedOutcome ? { preparedAgentOutcome: preparedOutcome } : {}),
347
+ });
348
+ void dispatched;
349
+ const handle = envelope.provenance.handle;
350
+ const reportedModel = handle?.metadata?.reportedModel || "unreported";
351
+ const reportedUsage = handle?.metadata?.reportedUsage;
352
+ const usageSignature = handle?.metadata?.usageSignature;
353
+ if (envelope.status !== "completed") {
354
+ return handleHop(ctx, selected, workerId, `agent hop ${envelope.status}: ${envelope.result.summary}`, deferPersist, deferPersist ? run : undefined);
355
+ }
356
+ if (!manifest.resultPath || !fs.existsSync(manifest.resultPath)) {
357
+ return handleHop(ctx, selected, workerId, "agent produced no result.md", deferPersist, deferPersist ? run : undefined);
358
+ }
359
+ try {
360
+ (0, worker_isolation_1.recordWorkerOutput)(run, workerId, manifest.resultPath, {
361
+ agentDelegation: {
362
+ handle: handle,
363
+ model: reportedModel,
364
+ promptDigest,
365
+ command: handle?.metadata?.command,
366
+ args: handle?.metadata?.args || [],
367
+ exitCode: (0, drive_decide_1.exitCodeFromEvidence)(envelope.evidence),
368
+ reportedUsage,
369
+ usageSignature,
370
+ usageTrustPublicKey: ctx.config.attestPublicKey,
371
+ },
372
+ requireAttestedTelemetry: ctx.config.requireAttestedTelemetry,
373
+ });
374
+ // Advance the run lifecycle stage on accept (old build: "observe").
375
+ run.loopStage = "observe";
376
+ // Bounded dynamic loops: same round-boundary evaluation the old build's
377
+ // recordWorkerOutput wrapper performed, folded into this checkpoint.
378
+ maybeExpandLoop(run);
379
+ if (!deferPersist) {
380
+ (0, commit_1.commitState)(run, `worker:${workerId}:result`);
381
+ (0, run_store_1.saveCheckpoint)(run);
382
+ }
383
+ }
384
+ catch (error) {
385
+ return handleHop(ctx, selected, workerId, `result.md rejected: ${errMessage(error)}`, deferPersist, deferPersist ? run : undefined);
386
+ }
387
+ if (cachePath && fs.existsSync(manifest.resultPath)) {
388
+ writeResultCache(cachePath, fs.readFileSync(manifest.resultPath, "utf8"));
389
+ }
390
+ return (0, drive_decide_1.makeStep)("accept", "ok", { runId: ctx.runId, taskId: selected.id, phase: selected.phase, backendId: "agent", handleKind: handle?.kind, reportedModel });
391
+ }
392
+ function runSubWorkflow(ctx, run, selected, workerId, manifest, spec, deferPersist = false) {
393
+ const parentApp = run.workflow.id;
394
+ if (ctx.depth + 1 > exports.MAX_SUB_WORKFLOW_DEPTH) {
395
+ return handleHop(ctx, selected, workerId, `sub-workflow depth limit exceeded (> ${exports.MAX_SUB_WORKFLOW_DEPTH})`, deferPersist, deferPersist ? run : undefined);
396
+ }
397
+ if ([...ctx.visitedAppIds, parentApp].includes(spec.appId)) {
398
+ return handleHop(ctx, selected, workerId, `sub-workflow cycle detected: ${[...ctx.visitedAppIds, parentApp, spec.appId].join(" -> ")}`, deferPersist, deferPersist ? run : undefined);
399
+ }
400
+ const childRunId = `sub-${run.id}-${(0, fs_atomic_1.safeFileName)(selected.id)}`;
401
+ const childInputs = {
402
+ repo: run.inputs.repo ?? run.cwd,
403
+ cwd: run.cwd,
404
+ question: run.inputs.question ?? "",
405
+ ...renderSubInputs(spec, run.inputs),
406
+ runId: childRunId,
407
+ };
408
+ let childRun;
409
+ try {
410
+ const { loadWorkflowApp } = require("./workflow-app-loader");
411
+ childRun = (0, pipeline_1.plan)(loadWorkflowApp(spec.appId), childInputs);
412
+ }
413
+ catch (error) {
414
+ return handleHop(ctx, selected, workerId, `sub-workflow plan failed (${spec.appId}): ${errMessage(error)}`, deferPersist, deferPersist ? run : undefined);
415
+ }
416
+ const childResult = drive(childRun.id, childRun.cwd, {
417
+ now: ctx.now,
418
+ agentConfig: ctx.config,
419
+ incremental: ctx.incremental,
420
+ depth: ctx.depth + 1,
421
+ visitedAppIds: [...ctx.visitedAppIds, parentApp],
422
+ policy: ctx.policy,
423
+ });
424
+ if (childResult.status !== "complete") {
425
+ return handleHop(ctx, selected, workerId, `sub-workflow ${spec.appId} did not complete (status: ${childResult.status})`, deferPersist, deferPersist ? run : undefined);
426
+ }
427
+ const finalChild = (0, run_store_1.loadRunFromCwd)(childRun.id, childRun.cwd);
428
+ let childBytes;
429
+ if (spec.bindResult === "verdict-result") {
430
+ const verdict = finalChild.tasks.find((t) => /^verdict[:/]|^synthesis[:/]/i.test(t.id) && t.status === "completed");
431
+ childBytes = verdict?.resultPath && fs.existsSync(verdict.resultPath) ? fs.readFileSync(verdict.resultPath, "utf8") : undefined;
432
+ }
433
+ else {
434
+ childBytes = fs.existsSync(finalChild.paths.report) ? fs.readFileSync(finalChild.paths.report, "utf8") : undefined;
435
+ }
436
+ if (childBytes === undefined) {
437
+ return handleHop(ctx, selected, workerId, `sub-workflow ${spec.appId} produced no ${spec.bindResult || "report"}`, deferPersist, deferPersist ? run : undefined);
438
+ }
439
+ try {
440
+ fs.writeFileSync(manifest.resultPath, childBytes, "utf8");
441
+ (0, worker_isolation_1.recordWorkerOutput)(run, workerId, manifest.resultPath);
442
+ // Cross-link the child run onto the parent's delegate task + a tamper-
443
+ // evident `worker.sub-workflow` trust-audit event (byte-behavior port of
444
+ // the old build's drive sub-workflow cross-link). subRunId/subRunDir let a
445
+ // reader walk from the parent task to the child run; the audit event binds
446
+ // the child report digest + verification into the parent's hash chain.
447
+ selected.subRunId = childRun.id;
448
+ selected.subRunDir = finalChild.paths.runDir;
449
+ try {
450
+ const childAudit = (0, trust_audit_1.verifyTrustAudit)(finalChild);
451
+ (0, trust_audit_1.recordTrustAuditEvent)(run, {
452
+ kind: "worker.sub-workflow",
453
+ decision: "accepted",
454
+ source: "cw-validated",
455
+ workerId,
456
+ taskId: selected.id,
457
+ nodeId: selected.resultNodeId,
458
+ metadata: {
459
+ subWorkflowAppId: spec.appId,
460
+ subRunId: childRun.id,
461
+ childReportDigest: (0, hash_1.sha256)(childBytes),
462
+ childAuditVerified: childAudit.verified,
463
+ bindResult: spec.bindResult || "report",
464
+ },
465
+ });
466
+ }
467
+ catch {
468
+ /* the cross-link is provenance; a failure here must not undo an accepted hop */
469
+ }
470
+ // Advance the run lifecycle stage on accept (old build: "observe").
471
+ run.loopStage = "observe";
472
+ // Bounded dynamic loops: evaluate the round boundary in the same
473
+ // checkpoint (no-op unless this task's phase is a loop round).
474
+ maybeExpandLoop(run);
475
+ if (!deferPersist) {
476
+ (0, commit_1.commitState)(run, `worker:${workerId}:result`);
477
+ (0, run_store_1.saveCheckpoint)(run);
478
+ }
479
+ }
480
+ catch (error) {
481
+ return handleHop(ctx, selected, workerId, `sub-workflow result rejected by parent gate: ${errMessage(error)}`, deferPersist, deferPersist ? run : undefined);
482
+ }
483
+ return (0, drive_decide_1.makeStep)("accept", "ok", { runId: run.id, taskId: selected.id, phase: selected.phase, handleKind: "sub-workflow", reason: `sub-workflow ${spec.appId} → ${childRun.id}` });
484
+ }
485
+ /** Byte-stable string order for anything that flows into a recorded
486
+ * loop-control decision (POLA: deterministic across runs/locales). */
487
+ function compareBytes(a, b) {
488
+ return a < b ? -1 : a > b ? 1 : 0;
489
+ }
490
+ /** Bounded dynamic loop expansion — the shell half of the loop runtime.
491
+ * After a worker result is recorded (mutating `run` in memory), if the
492
+ * just-completed phase is the LATEST round of a loop whose origin is not
493
+ * yet done and all that round's tasks completed, evaluate the pure stop
494
+ * decision (evaluateLoopStop) and either append the next round (clone the
495
+ * round-1 template tasks into a fresh phase, materialized like plan()
496
+ * does — task files + a plan-stage node per new task) or mark the loop
497
+ * done. One deterministic `loop-control` node is recorded per round
498
+ * boundary — the replay source of truth. No-op when the run has no loop
499
+ * phases (POLA). Expands at most ONE loop boundary per call; the next
500
+ * accept handles the next. Byte-exact port of the old build's
501
+ * orchestrator/lifecycle-operations.ts maybeExpandLoop, called there from
502
+ * recordWorkerOutput; v2's recordWorkerOutput (shell/worker-isolation.ts)
503
+ * does not expand, so the drive shell wires it in right after an accept.
504
+ * Exported so the standalone `cw worker output` CLI verb (worker-cli.ts) can
505
+ * run the same round-expansion after a hand-recorded accept, matching the old
506
+ * build's recordWorkerOutput wrapper. */
507
+ function maybeExpandLoop(run) {
508
+ for (const phase of [...run.phases]) {
509
+ const originId = phase.loop ? phase.id : phase.loopOrigin;
510
+ if (!originId)
511
+ continue;
512
+ const origin = run.phases.find((p) => p.id === originId);
513
+ if (!origin || !origin.loop || origin.loopDone)
514
+ continue;
515
+ // Act only from the LATEST round phase of this loop.
516
+ const loopPhases = run.phases.filter((p) => p.id === originId || p.loopOrigin === originId);
517
+ const latest = loopPhases.reduce((a, b) => ((b.loopRound || 1) >= (a.loopRound || 1) ? b : a));
518
+ if (phase.id !== latest.id)
519
+ continue;
520
+ const roundTasks = run.tasks.filter((t) => latest.taskIds.includes(t.id));
521
+ if (roundTasks.length === 0 || !roundTasks.every((t) => t.status === "completed"))
522
+ continue;
523
+ const round = latest.loopRound || 1;
524
+ const ordered = (tasks) => tasks
525
+ .slice()
526
+ .sort((a, b) => compareBytes(a.id, b.id))
527
+ .map((t) => t.result);
528
+ const roundResults = ordered(roundTasks);
529
+ const allLoopTasks = run.tasks.filter((t) => t.status === "completed" && loopPhases.some((p) => p.taskIds.includes(t.id)));
530
+ const allResults = ordered(allLoopTasks);
531
+ const ctx = {
532
+ round,
533
+ roundResults,
534
+ allResults,
535
+ // budget-target scaling counts RECORDED tokens; read them through the
536
+ // same normalizer the CAP gate uses so a snake_case reportedUsage hop
537
+ // is counted (evaluateLoopStop reads usageTotals.totalTokens).
538
+ usageTotals: { totalTokens: recordedTokenTotal(run) },
539
+ inputs: run.inputs,
540
+ };
541
+ const decision = (0, loop_expansion_1.evaluateLoopStop)(origin, round, ctx);
542
+ const until = origin.loop.until;
543
+ // Record the decision under a deterministic id (the replay source of truth).
544
+ (0, node_store_1.appendRunNode)(run, (0, state_node_1.createStateNode)({
545
+ id: (0, loop_expansion_1.loopControlNodeId)(run.id, originId, round),
546
+ kind: "loop-control",
547
+ status: "completed",
548
+ loopStage: "adjust",
549
+ outputs: { round, done: decision.done, atCap: decision.atCap, reason: decision.reason },
550
+ metadata: {
551
+ originPhaseId: originId,
552
+ until: until.kind === "predicate" ? until.ref : `budget-target:${until.target}`,
553
+ round,
554
+ done: decision.done,
555
+ atCap: decision.atCap,
556
+ reason: decision.reason,
557
+ },
558
+ }));
559
+ if (decision.done) {
560
+ origin.loopDone = true;
561
+ return;
562
+ }
563
+ // Expand: clone the ROUND-1 template tasks into a fresh phase appended
564
+ // right after the latest round.
565
+ const nextRound = round + 1;
566
+ const templateTasks = run.tasks.filter((t) => origin.taskIds.includes(t.id));
567
+ const { phase: nextPhase, tasks: newTasks } = (0, loop_expansion_1.cloneLoopRoundTasks)(origin, templateTasks, nextRound);
568
+ const insertAt = run.phases.findIndex((p) => p.id === latest.id);
569
+ run.phases.splice(insertAt + 1, 0, nextPhase);
570
+ run.tasks.push(...newTasks);
571
+ // Materialize: task files + a plan-stage node per new task (mirrors plan()).
572
+ (0, harness_1.writeTaskFiles)(run);
573
+ const inputNodeId = `${run.id}:input`;
574
+ for (const t of newTasks) {
575
+ const result = (0, runner_1.runPipelineStage)(run, "plan", inputNodeId, {
576
+ outputNodeId: `${run.id}:task:${t.id}`,
577
+ outputStatus: "pending",
578
+ loopStage: "interpret",
579
+ artifacts: [{ id: "task", kind: "markdown", path: t.taskPath }],
580
+ metadata: {
581
+ workflowId: run.workflow.id,
582
+ taskId: t.id,
583
+ phase: t.phase,
584
+ taskKind: t.kind,
585
+ requiresEvidence: t.requiresEvidence,
586
+ sandboxProfileId: t.sandboxProfileId,
587
+ },
588
+ }, { persist: false, persistNode: (r, node) => void (0, node_store_1.appendRunNode)(r, node) });
589
+ t.stateNodeId = result.outputNodeId;
590
+ }
591
+ (0, dispatch_1.updatePhaseStatuses)(run);
592
+ return;
593
+ }
594
+ }
595
+ /** One deterministic drive step. */
596
+ function driveStep(ctx) {
597
+ const run = loadRun(ctx);
598
+ const selected = (0, drive_decide_1.selectDriveTask)(run);
599
+ const gate = (0, drive_decide_1.terminalOrConfigStep)(run, selected, agentConfigured(ctx.config), tokenBudgetUsage(run));
600
+ if (gate.kind === "commit") {
601
+ // Terminal commit: advance the run lifecycle stage as the old build's
602
+ // commit() wrapper did (run.loopStage = "checkpoint").
603
+ run.loopStage = "checkpoint";
604
+ const commit = (0, commit_1.commitState)(run, { reason: "agent-delegation-drive: audited verdict committed", ...(gate.verifierNodeId ? { verifierNodeId: gate.verifierNodeId } : { allowUnverifiedCheckpoint: true, verifierGated: false }) });
605
+ (0, report_1.writeReport)(run);
606
+ (0, run_store_1.saveCheckpoint)(run);
607
+ return (0, drive_decide_1.makeStep)("commit", "complete", { runId: run.id, reason: `committed ${commit.id}` });
608
+ }
609
+ if (gate.step)
610
+ return gate.step;
611
+ return processSelectedTask(ctx, selected.id);
612
+ }
613
+ /** Dispatch every batch task (sequential — dispatch mutates state), then
614
+ * collect ALL spawn-style agent child outcomes in one concurrent window
615
+ * (one batch delegate child process, per-job timeout kill). Returns
616
+ * outcomes keyed by task id; a cache-hit or endpoint-configured agent
617
+ * gets no prepared outcome and settles through the serial accept path
618
+ * inside processSelectedTask. Dispatch failures become recorded fail
619
+ * steps up front, exactly what the serial path would emit. Byte-exact
620
+ * to the old build's src/drive.ts's prepareConcurrentOutcomes. */
621
+ function prepareConcurrentOutcomes(ctx, batch) {
622
+ const failSteps = new Map();
623
+ const jobs = [];
624
+ const jobTaskIds = [];
625
+ for (const taskId of batch) {
626
+ const run = loadRun(ctx);
627
+ const task = run.tasks.find((candidate) => candidate.id === taskId);
628
+ if (!task || (task.status !== "pending" && task.status !== "running"))
629
+ continue;
630
+ let workerId = task.workerId;
631
+ if (task.status === "pending") {
632
+ const manifest = (0, dispatch_2.createDispatchManifest)(run, 1, { backendId: task.agentType || "agent" });
633
+ const dispatchedTask = manifest.tasks.find((entry) => entry.id === task.id) || manifest.tasks[0];
634
+ if (!dispatchedTask || !dispatchedTask.workerId) {
635
+ failSteps.set(taskId, (0, drive_decide_1.makeStep)("dispatch", "failed", { runId: ctx.runId, taskId, phase: task.phase, reason: "dispatch produced no worker scope" }));
636
+ continue;
637
+ }
638
+ workerId = dispatchedTask.workerId;
639
+ }
640
+ if (!workerId) {
641
+ failSteps.set(taskId, (0, drive_decide_1.makeStep)("dispatch", "failed", { runId: ctx.runId, taskId, phase: task.phase, reason: "no worker scope for task" }));
642
+ continue;
643
+ }
644
+ const freshRun = loadRun(ctx);
645
+ const manifest = (0, worker_isolation_1.showWorkerManifest)(freshRun, workerId);
646
+ const delegationDigest = ctx.incremental
647
+ ? (0, drive_decide_1.incrementalDelegationDigest)(task.model || ctx.config.model || "", task.agentType || "agent", manifest.sandboxPolicy?.id || task.sandboxProfileId || "", ctx.config.command || "", ctx.config.args ? (0, agent_1.stripSecretArgs)(ctx.config.args) : [], ctx.config.endpoint || "")
648
+ : "";
649
+ const cachePath = resultCachePath(freshRun, task, (0, hash_1.sha256)(task.prompt || ""), ctx.incremental, delegationDigest);
650
+ if (cachePath && fs.existsSync(cachePath))
651
+ continue;
652
+ const job = (0, agent_1.prepareAgentSpawn)({
653
+ schemaVersion: 1,
654
+ runId: ctx.runId,
655
+ taskId: task.id,
656
+ backendId: task.agentType || "agent",
657
+ cwd: freshRun.cwd,
658
+ sandboxPolicy: manifest.sandboxPolicy,
659
+ manifest: { workerDir: manifest.workerDir, manifestPath: manifest.manifestPath, inputPath: manifest.inputPath, resultPath: manifest.resultPath, prompt: manifest.prompt },
660
+ label: task.id,
661
+ timeoutMs: ctx.config.timeoutMs,
662
+ delegation: { command: ctx.config.command, args: ctx.config.args, endpoint: ctx.config.endpoint, model: task.model || ctx.config.model },
663
+ });
664
+ if (job) {
665
+ const sandboxPolicy = manifest.sandboxPolicy;
666
+ if (sandboxPolicy) {
667
+ const filteredEnv = (0, local_1.buildChildEnv)(sandboxPolicy);
668
+ for (const key of Object.keys(process.env)) {
669
+ if (/^(CW_|ANTHROPIC_|OPENAI_|GEMINI_|DEEPSEEK_|CODEX_|GOOGLE_|COHERE_|MISTRAL_|OLLAMA_|AZURE_|AWS_)/i.test(key)) {
670
+ filteredEnv[key] = process.env[key];
671
+ }
672
+ }
673
+ job.env = filteredEnv;
674
+ }
675
+ jobs.push(job);
676
+ jobTaskIds.push(taskId);
677
+ }
678
+ }
679
+ if (jobs.length) {
680
+ emitProgress(`⇉ concurrent round: ${jobs.length} agent${jobs.length > 1 ? "s" : ""} spawning in parallel, may take minutes…`);
681
+ }
682
+ const settled = (0, agent_1.runAgentBatchOutcomes)(jobs);
683
+ const outcomes = new Map();
684
+ jobTaskIds.forEach((taskId, index) => outcomes.set(taskId, settled[index]));
685
+ return { outcomes, failSteps };
686
+ }
687
+ /** One concurrent round inside one cached in-memory run: dispatches every
688
+ * batch task, spawns all spawn-style agent children in one concurrent
689
+ * window, then settles + accepts in DETERMINISTIC batch (task-id) order
690
+ * regardless of wall-clock finish order. At round end it flushes once:
691
+ * commitState(run, "concurrent-round:<n>-tasks") + writeReport +
692
+ * saveCheckpoint. Cache-hit tasks and endpoint-only agents get no
693
+ * prepared outcome and settle through the serial path (still inside
694
+ * this one deferred-persist round). If no step was produced (nothing
695
+ * runnable at round entry — terminal/blocked/token-budget gate) the
696
+ * round degrades to one plain driveStep. Byte-exact to the old build's
697
+ * src/drive.ts's driveConcurrentRound. */
698
+ function driveConcurrentRound(ctx, limit) {
699
+ return withRoundCache(ctx, () => {
700
+ const run = loadRun(ctx);
701
+ const selected = (0, drive_decide_1.selectDriveTask)(run);
702
+ const gate = (0, drive_decide_1.terminalOrConfigStep)(run, selected, agentConfigured(ctx.config), tokenBudgetUsage(run));
703
+ if (gate.kind === "commit" || gate.step)
704
+ return [driveStep(ctx)];
705
+ const phase = (0, dispatch_1.firstRunnablePhase)(run);
706
+ const width = Math.max(1, Math.floor(limit) || 1);
707
+ const batch = run.tasks
708
+ .filter((task) => phase.taskIds.includes(task.id) && (task.status === "pending" || task.status === "running"))
709
+ .slice(0, width)
710
+ .map((task) => task.id);
711
+ const prepared = prepareConcurrentOutcomes(ctx, batch);
712
+ const steps = [];
713
+ for (const taskId of batch) {
714
+ const failStep = prepared.failSteps.get(taskId);
715
+ if (failStep) {
716
+ steps.push(failStep);
717
+ continue;
718
+ }
719
+ // Re-read per task: a prior accept in this round mutated state (the
720
+ // SAME cached object via loadRun's round cache — no disk round-trip
721
+ // until the round-end flush below).
722
+ const freshRun = loadRun(ctx);
723
+ const fresh = freshRun.tasks.find((task) => task.id === taskId);
724
+ if (!fresh || (fresh.status !== "pending" && fresh.status !== "running"))
725
+ continue;
726
+ steps.push(processSelectedTask(ctx, taskId, prepared.outcomes.get(taskId), true));
727
+ }
728
+ if (steps.length > 0) {
729
+ const settledRun = loadRun(ctx);
730
+ (0, commit_1.commitState)(settledRun, `concurrent-round:${batch.length}-tasks`);
731
+ (0, report_1.writeReport)(settledRun);
732
+ (0, run_store_1.saveCheckpoint)(settledRun);
733
+ }
734
+ return steps.length > 0 ? steps : [driveStep(ctx)];
735
+ });
736
+ }
737
+ /** Drive a run: `--once` advances exactly one step; otherwise run to
738
+ * completion, park, or a blocked stop. */
739
+ function drive(runId, cwd, options = {}) {
740
+ const now = options.now || new Date().toISOString();
741
+ const config = options.agentConfig || (0, agent_config_1.resolveAgentConfig)(options.args || {});
742
+ const policy = { ...drive_decide_1.DEFAULT_SCHEDULING_POLICY, ...(options.policy || {}) };
743
+ const ctx = {
744
+ runId,
745
+ cwd,
746
+ now,
747
+ config,
748
+ attempts: new Map(),
749
+ incremental: Boolean(options.incremental),
750
+ depth: Math.max(0, Math.floor(options.depth || 0)),
751
+ visitedAppIds: options.visitedAppIds || [],
752
+ policy,
753
+ };
754
+ const steps = [];
755
+ const run0 = loadRun(ctx);
756
+ const plannedWorkers = run0.tasks.length;
757
+ const maxIter = (0, drive_decide_1.maxIterations)(plannedWorkers, (0, loop_expansion_1.maxLoopExpansion)(run0), policy);
758
+ // Phase-boundary progress (brew-style): announce each phase when it
759
+ // becomes active and when it finishes — `==> Map ✓ (6/6)` / `==> Assess
760
+ // ⇉ (3/6)`. Describes CW's OWN phases (vendor-neutral); goes to stderr
761
+ // via emitProgress so stdout stays clean data. Byte-exact port of the
762
+ // old build's src/drive.ts emitPhaseProgress. term.phaseProgressLine
763
+ // renders the line; this closure decides WHEN to emit each boundary.
764
+ const announcedPhaseComplete = new Set();
765
+ let activePhaseId;
766
+ const titleCase = (s) => (s ? s.charAt(0).toUpperCase() + s.slice(1) : s);
767
+ const emitPhaseProgress = (run) => {
768
+ for (const ph of run.phases || []) {
769
+ const phaseTasks = run.tasks.filter((task) => ph.taskIds.includes(task.id));
770
+ const total = phaseTasks.length;
771
+ if (total === 0)
772
+ continue;
773
+ const done = phaseTasks.filter((task) => task.status === "completed").length;
774
+ const label = titleCase(ph.name || ph.id);
775
+ if (done >= total) {
776
+ if (!announcedPhaseComplete.has(ph.id)) {
777
+ announcedPhaseComplete.add(ph.id);
778
+ emitProgress((0, term_1.phaseProgressLine)(label, done, total, ph.mode, process.stderr));
779
+ }
780
+ continue;
781
+ }
782
+ if (ph.id !== activePhaseId) {
783
+ activePhaseId = ph.id;
784
+ emitProgress((0, term_1.phaseProgressLine)(label, done, total, ph.mode, process.stderr));
785
+ }
786
+ return; // only the first not-yet-complete phase is "active"
787
+ }
788
+ };
789
+ let exhaustedMaxIterations = !options.once;
790
+ for (let i = 0; i < maxIter; i++) {
791
+ const width = (0, drive_decide_1.roundWidth)(loadRun(ctx), options.concurrency);
792
+ // width>1 (an explicit --concurrency>1, or an auto-width parallel
793
+ // phase) runs the whole round through driveConcurrentRound — one or
794
+ // more steps recorded in deterministic batch order, one flush at
795
+ // round end. `--once` still stops after this ONE outer-loop
796
+ // iteration even though a round can yield multiple steps.
797
+ const roundSteps = width > 1 ? driveConcurrentRound(ctx, width) : [driveStep(ctx)];
798
+ for (const stepResult of roundSteps)
799
+ steps.push(stepResult);
800
+ // Brew-style phase boundaries: after each round, announce a
801
+ // newly-active phase and any phase that just finished. Cheap — reuses
802
+ // the run we just advanced; goes to stderr so stdout stays clean.
803
+ emitPhaseProgress(loadRun(ctx));
804
+ const last = roundSteps[roundSteps.length - 1];
805
+ if (options.once) {
806
+ exhaustedMaxIterations = false;
807
+ break;
808
+ }
809
+ if (last && (last.status === "complete" || last.status === "parked" || last.status === "blocked")) {
810
+ exhaustedMaxIterations = false;
811
+ break;
812
+ }
813
+ }
814
+ const run = loadRun(ctx);
815
+ const completedWorkers = (0, drive_decide_1.countCompleted)(run);
816
+ const parkedWorkers = (0, drive_decide_1.countParked)(run);
817
+ const committed = (0, drive_decide_1.hasTerminalCommit)(run);
818
+ const last = steps[steps.length - 1];
819
+ if (exhaustedMaxIterations) {
820
+ steps.push((0, drive_decide_1.makeStep)("blocked", "blocked", { runId, reason: `drive reached max iteration limit (${maxIter}) before a terminal state` }));
821
+ }
822
+ const statusInputs = {
823
+ once: Boolean(options.once),
824
+ completedWorkers,
825
+ plannedWorkers,
826
+ committed,
827
+ lastStepStatus: steps[steps.length - 1]?.status,
828
+ exhaustedMaxIterations,
829
+ parkedWorkers,
830
+ };
831
+ void last;
832
+ void drive_decide_1.verdictVerifierNodeId;
833
+ const status = (0, drive_decide_1.finalDriveStatus)(statusInputs);
834
+ const committedCommit = (run.commits || []).find((c) => c.reason && c.reason.startsWith("agent-delegation-drive"));
835
+ return {
836
+ schemaVersion: 1,
837
+ runId,
838
+ workflowId: run.workflow.id,
839
+ status,
840
+ steps,
841
+ plannedWorkers,
842
+ completedWorkers,
843
+ parkedWorkers,
844
+ commitId: committedCommit?.id,
845
+ reportPath: run.paths.report,
846
+ statePath: run.paths.state,
847
+ agentConfigured: agentConfigured(config),
848
+ };
849
+ }
850
+ function drivePreview(runId, cwd, args = {}) {
851
+ const run = (0, run_store_1.loadRunFromCwd)(runId, cwd);
852
+ const config = (0, agent_config_1.resolveAgentConfig)(args);
853
+ const configured = agentConfigured(config);
854
+ const selected = (0, drive_decide_1.selectDriveTask)(run);
855
+ const plannedWorkers = run.tasks.length;
856
+ const pendingWorkers = run.tasks.filter((t) => t.status === "pending" || t.status === "running").length;
857
+ const completedWorkers = (0, drive_decide_1.countCompleted)(run);
858
+ const parkedWorkers = (0, drive_decide_1.countParked)(run);
859
+ let nextAction;
860
+ if (!selected) {
861
+ nextAction = run.tasks.every((t) => t.status === "completed") ? "commit" : "blocked";
862
+ }
863
+ else if (!configured) {
864
+ nextAction = "blocked";
865
+ }
866
+ else if (selected.status === "pending") {
867
+ nextAction = "dispatch";
868
+ }
869
+ else {
870
+ nextAction = "fulfill";
871
+ }
872
+ return { schemaVersion: 1, runId, workflowId: run.workflow.id, plannedWorkers, pendingWorkers, completedWorkers, parkedWorkers, nextAction, nextTaskId: selected?.id, nextPhase: selected?.phase, agentConfigured: configured };
873
+ }