cool-workflow 0.1.98 → 0.2.1

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