cool-workflow 0.1.98 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (306) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/README.md +11 -2
  4. package/apps/architecture-review/app.json +1 -1
  5. package/apps/architecture-review-fast/app.json +1 -1
  6. package/apps/end-to-end-golden-path/app.json +1 -1
  7. package/apps/pr-review-fix-ci/app.json +1 -1
  8. package/apps/release-cut/app.json +1 -1
  9. package/apps/research-synthesis/app.json +1 -1
  10. package/dist/cli/dispatch.js +236 -0
  11. package/dist/cli/entry.js +120 -0
  12. package/dist/cli/io.js +21 -4
  13. package/dist/cli/parseargv.js +157 -0
  14. package/dist/cli.js +6 -33
  15. package/dist/core/capability-table.js +3534 -0
  16. package/dist/core/format/help.js +314 -0
  17. package/dist/{state-explosion/format.js → core/format/state-explosion-text.js} +10 -1
  18. package/dist/core/hash.js +137 -0
  19. package/dist/core/multi-agent/candidate-scoring.js +219 -0
  20. package/dist/core/multi-agent/collaboration.js +481 -0
  21. package/dist/core/multi-agent/coordinator.js +515 -0
  22. package/dist/core/multi-agent/eval-replay.js +306 -0
  23. package/dist/core/multi-agent/runtime.js +929 -0
  24. package/dist/core/multi-agent/topology.js +298 -0
  25. package/dist/core/multi-agent/trust-policy.js +197 -0
  26. package/dist/core/pipeline/commit-gate.js +320 -0
  27. package/dist/{pipeline-contract.js → core/pipeline/contract.js} +24 -37
  28. package/dist/core/pipeline/dispatch.js +103 -0
  29. package/dist/core/pipeline/drive-decide.js +227 -0
  30. package/dist/core/pipeline/error-feedback.js +161 -0
  31. package/dist/core/pipeline/loop-expansion.js +124 -0
  32. package/dist/{result-normalize.js → core/pipeline/result-normalize.js} +14 -37
  33. package/dist/core/pipeline/runner.js +230 -0
  34. package/dist/{contract-migration.js → core/state/contract-migration.js} +68 -92
  35. package/dist/{state-migrations.js → core/state/migrations.js} +103 -96
  36. package/dist/core/state/node-projection.js +95 -0
  37. package/dist/core/state/node-snapshot.js +230 -0
  38. package/dist/core/state/run-paths.js +93 -0
  39. package/dist/{schema-validate.js → core/state/schema-validate.js} +35 -28
  40. package/dist/core/state/schema.js +50 -0
  41. package/dist/core/state/state-explosion/digest.js +243 -0
  42. package/dist/core/state/state-explosion/graph.js +527 -0
  43. package/dist/{state-explosion → core/state/state-explosion}/helpers.js +34 -3
  44. package/dist/core/state/state-explosion/report.js +187 -0
  45. package/dist/{state-explosion → core/state/state-explosion}/size.js +25 -7
  46. package/dist/{state-node.js → core/state/state-node.js} +90 -113
  47. package/dist/core/state/types.js +19 -0
  48. package/dist/{validation.js → core/state/validation.js} +64 -131
  49. package/dist/core/trust/evidence-grounding.js +134 -0
  50. package/dist/core/trust/ledger.js +199 -0
  51. package/dist/{telemetry-attestation.js → core/trust/telemetry-attestation.js} +94 -68
  52. package/dist/core/trust/telemetry-ledger.js +127 -0
  53. package/dist/core/types.js +20 -0
  54. package/dist/core/version.js +16 -0
  55. package/dist/{workflow-app-framework.js → core/workflow-apps/app-schema.js} +337 -426
  56. package/dist/mcp/dispatch.js +104 -0
  57. package/dist/mcp/server.js +144 -0
  58. package/dist/mcp-server.js +6 -84
  59. package/dist/{agent-config.js → shell/agent-config.js} +118 -71
  60. package/dist/shell/app-run-cli.js +88 -0
  61. package/dist/shell/audit-cli.js +259 -0
  62. package/dist/shell/audit-provenance.js +83 -0
  63. package/dist/shell/candidate-scoring-io.js +459 -0
  64. package/dist/shell/collaboration-io.js +264 -0
  65. package/dist/shell/commit-summary.js +104 -0
  66. package/dist/shell/commit.js +290 -0
  67. package/dist/shell/coordinator-io.js +476 -0
  68. package/dist/shell/demo-cli.js +19 -0
  69. package/dist/shell/dispatch.js +162 -0
  70. package/dist/{doctor.js → shell/doctor.js} +123 -56
  71. package/dist/shell/drive.js +873 -0
  72. package/dist/shell/error-feedback-io.js +305 -0
  73. package/dist/shell/eval-io.js +473 -0
  74. package/dist/{multi-agent-eval/format.js → shell/eval-text.js} +78 -49
  75. package/dist/{evidence-reasoning.js → shell/evidence-reasoning.js} +124 -255
  76. package/dist/shell/exec-backend-cli.js +88 -0
  77. package/dist/shell/execution-backend/agent.js +475 -0
  78. package/dist/shell/execution-backend/ci.js +15 -0
  79. package/dist/shell/execution-backend/container.js +69 -0
  80. package/dist/shell/execution-backend/envelopes.js +55 -0
  81. package/dist/shell/execution-backend/local.js +113 -0
  82. package/dist/shell/execution-backend/probes.js +175 -0
  83. package/dist/shell/execution-backend/registry.js +402 -0
  84. package/dist/shell/execution-backend/remote.js +128 -0
  85. package/dist/shell/execution-backend/types.js +11 -0
  86. package/dist/shell/feedback-cli.js +81 -0
  87. package/dist/shell/feedback-operations.js +48 -0
  88. package/dist/shell/fs-atomic.js +276 -0
  89. package/dist/shell/harness.js +98 -0
  90. package/dist/shell/ledger-cli.js +212 -0
  91. package/dist/shell/ledger-io.js +169 -0
  92. package/dist/shell/man-cli.js +89 -0
  93. package/dist/shell/metrics-cli.js +98 -0
  94. package/dist/shell/multi-agent-cli.js +1002 -0
  95. package/dist/shell/multi-agent-host.js +563 -0
  96. package/dist/shell/multi-agent-io.js +387 -0
  97. package/dist/{multi-agent-operator-ux.js → shell/multi-agent-operator-ux.js} +234 -191
  98. package/dist/shell/node-store.js +124 -0
  99. package/dist/{observability/format.js → shell/observability-format.js} +7 -1
  100. package/dist/{observability/intake.js → shell/observability-intake.js} +79 -56
  101. package/dist/{observability.js → shell/observability.js} +159 -332
  102. package/dist/{onramp.js → shell/onramp.js} +11 -0
  103. package/dist/{operator-ux/format.js → shell/operator-ux-text.js} +250 -239
  104. package/dist/shell/operator-ux.js +431 -0
  105. package/dist/shell/orchestrator.js +231 -0
  106. package/dist/shell/pipeline-cli.js +667 -0
  107. package/dist/shell/pipeline.js +217 -0
  108. package/dist/shell/reclamation-io.js +1366 -0
  109. package/dist/shell/registry-cli.js +329 -0
  110. package/dist/{remote-source.js → shell/remote-source.js} +2 -2
  111. package/dist/shell/report-cli.js +101 -0
  112. package/dist/shell/report-view-cli.js +117 -0
  113. package/dist/{orchestrator → shell}/report.js +289 -282
  114. package/dist/shell/reporter.js +62 -0
  115. package/dist/shell/run-export-cli.js +106 -0
  116. package/dist/shell/run-export.js +680 -0
  117. package/dist/shell/run-registry-io.js +1014 -0
  118. package/dist/shell/run-store.js +164 -0
  119. package/dist/{sandbox-profile.js → shell/sandbox-profile.js} +134 -95
  120. package/dist/{scheduler.js → shell/scheduler-io.js} +248 -48
  121. package/dist/shell/scheduling-io.js +311 -0
  122. package/dist/shell/state-cli.js +181 -0
  123. package/dist/shell/state-explosion-cli.js +197 -0
  124. package/dist/shell/telemetry-cli.js +85 -0
  125. package/dist/{telemetry-demo.js → shell/telemetry-demo.js} +149 -119
  126. package/dist/shell/telemetry-ledger-io.js +132 -0
  127. package/dist/{term.js → shell/term.js} +36 -46
  128. package/dist/shell/topology-io.js +361 -0
  129. package/dist/shell/trust-audit.js +471 -0
  130. package/dist/{multi-agent-trust.js → shell/trust-policy-io.js} +67 -205
  131. package/dist/shell/verifier.js +48 -0
  132. package/dist/shell/workbench-host.js +250 -0
  133. package/dist/shell/workbench-text.js +18 -0
  134. package/dist/shell/workbench.js +175 -0
  135. package/dist/shell/worker-cli.js +124 -0
  136. package/dist/shell/worker-isolation.js +852 -0
  137. package/dist/shell/workflow-app-loader.js +650 -0
  138. package/docs/agent-delegation-drive.7.md +2 -0
  139. package/docs/cli-mcp-parity.7.md +280 -219
  140. package/docs/contract-migration-tooling.7.md +2 -0
  141. package/docs/control-plane-scheduling.7.md +2 -0
  142. package/docs/durable-state-and-locking.7.md +2 -0
  143. package/docs/evidence-adoption-reasoning-chain.7.md +2 -0
  144. package/docs/execution-backends.7.md +2 -0
  145. package/docs/multi-agent-cli-mcp-surface.7.md +2 -0
  146. package/docs/multi-agent-eval-replay-harness.7.md +2 -0
  147. package/docs/multi-agent-operator-ux.7.md +2 -0
  148. package/docs/node-snapshot-diff-replay.7.md +2 -0
  149. package/docs/observability-cost-accounting.7.md +2 -0
  150. package/docs/project-index.md +132 -71
  151. package/docs/real-execution-backends.7.md +2 -0
  152. package/docs/release-and-migration.7.md +2 -0
  153. package/docs/release-tooling.7.md +2 -0
  154. package/docs/run-registry-control-plane.7.md +2 -0
  155. package/docs/run-retention-reclamation.7.md +23 -0
  156. package/docs/state-explosion-management.7.md +2 -0
  157. package/docs/team-collaboration.7.md +2 -0
  158. package/docs/web-desktop-workbench.7.md +2 -0
  159. package/manifest/plugin.manifest.json +1 -1
  160. package/manifest/source-context-profiles.json +9 -13
  161. package/package.json +1 -1
  162. package/scripts/agents/cw-attest-wrap.js +2 -2
  163. package/scripts/bump-version.js +4 -3
  164. package/scripts/canonical-apps.js +4 -4
  165. package/scripts/dogfood-architecture-review.js +2 -3
  166. package/scripts/dogfood-release.js +3 -3
  167. package/scripts/gen-parity-doc.js +15 -2
  168. package/scripts/golden-path.js +4 -4
  169. package/scripts/onramp-check.js +1 -1
  170. package/scripts/parity-check.js +38 -21
  171. package/scripts/release-flow.js +2 -2
  172. package/scripts/sync-project-index.js +51 -27
  173. package/scripts/validate-run-state-schema.js +2 -2
  174. package/scripts/version-sync-check.js +30 -30
  175. package/dist/candidate-scoring.js +0 -729
  176. package/dist/capability-core.js +0 -1189
  177. package/dist/capability-registry.js +0 -885
  178. package/dist/cli/command-surface.js +0 -494
  179. package/dist/cli/format.js +0 -56
  180. package/dist/cli/handlers/audit.js +0 -82
  181. package/dist/cli/handlers/blackboard.js +0 -81
  182. package/dist/cli/handlers/candidate.js +0 -40
  183. package/dist/cli/handlers/clones.js +0 -34
  184. package/dist/cli/handlers/collaboration.js +0 -61
  185. package/dist/cli/handlers/eval.js +0 -40
  186. package/dist/cli/handlers/ledger.js +0 -169
  187. package/dist/cli/handlers/maintenance.js +0 -107
  188. package/dist/cli/handlers/multi-agent.js +0 -165
  189. package/dist/cli/handlers/node.js +0 -41
  190. package/dist/cli/handlers/operational.js +0 -155
  191. package/dist/cli/handlers/operator.js +0 -146
  192. package/dist/cli/handlers/registry.js +0 -68
  193. package/dist/cli/handlers/run.js +0 -153
  194. package/dist/cli/handlers/scheduling.js +0 -132
  195. package/dist/cli/handlers/workbench.js +0 -41
  196. package/dist/cli/handlers/worker.js +0 -45
  197. package/dist/cli/run-summary.js +0 -45
  198. package/dist/clones.js +0 -162
  199. package/dist/collaboration.js +0 -726
  200. package/dist/commit.js +0 -592
  201. package/dist/compare.js +0 -18
  202. package/dist/coordinator/classify.js +0 -45
  203. package/dist/coordinator/paths.js +0 -42
  204. package/dist/coordinator/util.js +0 -126
  205. package/dist/coordinator.js +0 -990
  206. package/dist/daemon.js +0 -44
  207. package/dist/dispatch.js +0 -250
  208. package/dist/drive.js +0 -864
  209. package/dist/error-feedback.js +0 -419
  210. package/dist/evidence-grounding.js +0 -184
  211. package/dist/execution-backend/agent.js +0 -354
  212. package/dist/execution-backend/probes.js +0 -112
  213. package/dist/execution-backend/util.js +0 -47
  214. package/dist/execution-backend.js +0 -961
  215. package/dist/gates.js +0 -48
  216. package/dist/harness.js +0 -61
  217. package/dist/ledger.js +0 -313
  218. package/dist/loop-expansion.js +0 -60
  219. package/dist/mcp/tool-call.js +0 -470
  220. package/dist/mcp/tool-definitions.js +0 -1066
  221. package/dist/mcp-surface.js +0 -30
  222. package/dist/multi-agent/graph.js +0 -84
  223. package/dist/multi-agent/helpers.js +0 -141
  224. package/dist/multi-agent/ids.js +0 -20
  225. package/dist/multi-agent/paths.js +0 -22
  226. package/dist/multi-agent-eval/normalize.js +0 -51
  227. package/dist/multi-agent-eval.js +0 -678
  228. package/dist/multi-agent-host.js +0 -777
  229. package/dist/multi-agent.js +0 -984
  230. package/dist/node-projection.js +0 -59
  231. package/dist/node-snapshot.js +0 -260
  232. package/dist/operator-ux.js +0 -631
  233. package/dist/orchestrator/app-operations.js +0 -211
  234. package/dist/orchestrator/audit-operations.js +0 -182
  235. package/dist/orchestrator/candidate-operations.js +0 -117
  236. package/dist/orchestrator/cli-options.js +0 -294
  237. package/dist/orchestrator/collaboration-operations.js +0 -86
  238. package/dist/orchestrator/feedback-operations.js +0 -81
  239. package/dist/orchestrator/host-operations.js +0 -78
  240. package/dist/orchestrator/lifecycle-operations.js +0 -650
  241. package/dist/orchestrator/migration-operations.js +0 -44
  242. package/dist/orchestrator/multi-agent-operations.js +0 -362
  243. package/dist/orchestrator/topology-operations.js +0 -84
  244. package/dist/orchestrator.js +0 -925
  245. package/dist/pipeline-runner.js +0 -285
  246. package/dist/reclamation/hash.js +0 -72
  247. package/dist/reclamation.js +0 -812
  248. package/dist/reporter.js +0 -67
  249. package/dist/run-export.js +0 -815
  250. package/dist/run-registry/derive.js +0 -175
  251. package/dist/run-registry/format.js +0 -124
  252. package/dist/run-registry/gc.js +0 -251
  253. package/dist/run-registry/policy.js +0 -16
  254. package/dist/run-registry/queue.js +0 -115
  255. package/dist/run-registry.js +0 -850
  256. package/dist/run-state-schema.js +0 -68
  257. package/dist/scheduling.js +0 -184
  258. package/dist/state-explosion.js +0 -1014
  259. package/dist/state.js +0 -367
  260. package/dist/telemetry-ledger.js +0 -196
  261. package/dist/topology.js +0 -565
  262. package/dist/triggers.js +0 -184
  263. package/dist/trust-audit.js +0 -644
  264. package/dist/types/blackboard.js +0 -2
  265. package/dist/types/candidate.js +0 -2
  266. package/dist/types/collaboration.js +0 -2
  267. package/dist/types/core.js +0 -2
  268. package/dist/types/drive.js +0 -10
  269. package/dist/types/error-feedback.js +0 -2
  270. package/dist/types/evidence-reasoning.js +0 -2
  271. package/dist/types/execution-backend.js +0 -2
  272. package/dist/types/multi-agent.js +0 -2
  273. package/dist/types/observability.js +0 -2
  274. package/dist/types/pipeline.js +0 -2
  275. package/dist/types/reclamation.js +0 -8
  276. package/dist/types/report-bundle.js +0 -6
  277. package/dist/types/result.js +0 -2
  278. package/dist/types/run-registry.js +0 -2
  279. package/dist/types/run.js +0 -2
  280. package/dist/types/sandbox.js +0 -2
  281. package/dist/types/schedule.js +0 -2
  282. package/dist/types/state-node.js +0 -2
  283. package/dist/types/topology.js +0 -2
  284. package/dist/types/trust.js +0 -2
  285. package/dist/types/workbench.js +0 -2
  286. package/dist/types/worker.js +0 -2
  287. package/dist/types/workflow-app.js +0 -2
  288. package/dist/types.js +0 -44
  289. package/dist/util/fingerprint.js +0 -19
  290. package/dist/util/fingerprint.test.js +0 -27
  291. package/dist/verifier.js +0 -78
  292. package/dist/version.js +0 -8
  293. package/dist/workbench-host.js +0 -192
  294. package/dist/workbench.js +0 -192
  295. package/dist/worker-accept/acceptance.js +0 -114
  296. package/dist/worker-accept/blackboard-fanout.js +0 -80
  297. package/dist/worker-accept/blackboard-linkage.js +0 -19
  298. package/dist/worker-accept/context.js +0 -2
  299. package/dist/worker-accept/telemetry-ledger.js +0 -126
  300. package/dist/worker-accept/validation.js +0 -77
  301. package/dist/worker-accept/verifier-completion.js +0 -73
  302. package/dist/worker-isolation/helpers.js +0 -51
  303. package/dist/worker-isolation/paths.js +0 -46
  304. package/dist/worker-isolation.js +0 -656
  305. package/dist/workflow-api.js +0 -131
  306. /package/dist/{types → core/types}/boundary.js +0 -0
@@ -1,850 +0,0 @@
1
- "use strict";
2
- // Run Registry / Control Plane (v0.1.28) — a DERIVED, rebuildable index over the
3
- // runs that live under each repo's `.cw/runs/<id>/`, plus a home-level cross-repo
4
- // registry. It manages many runs across repos: search, resume, archive, queue,
5
- // cross-repo history, and failed-run rerun.
6
- //
7
- // BSD / Unix discipline (each non-trivial choice cites its tenet):
8
- //
9
- // - SEPARATE MECHANISM FROM POLICY. The per-run `.cw/runs/<id>/state.json` is the
10
- // SINGLE SOURCE OF TRUTH (loadRunFromCwd) and is never owned or mutated here.
11
- // The registry is MECHANISM: a derived cache, rebuilt from source on demand.
12
- // Retention windows, queue ordering, and archive thresholds are POLICY and live
13
- // in RunRegistryPolicy / explicit flags, never baked into the index.
14
- //
15
- // - DERIVED, NOT AUTHORITATIVE (fail closed). Every record carries a
16
- // `sourceFingerprint`; every read reports `valid|stale|absent` freshness, just
17
- // like the v0.1.25 state-explosion summaries. We ALWAYS re-derive a record from
18
- // source state when source is present, and surface `missing` (never a fabricated
19
- // status) when it is gone. An unreadable run is never treated as success.
20
- //
21
- // - APPEND-ONLY HISTORY; NEVER MUTATE THE PAST. Resume continues an existing run
22
- // from durable state (read-only over source). Rerun creates a NEW run that
23
- // records a provenance link to the original; the failed run is preserved.
24
- // Archive is an overlay mark, not a delete — source truth stays in place and
25
- // stays searchable.
26
- //
27
- // - EXPLICIT, INSPECTABLE STATE. Cross-repo discovery and the queue are plain
28
- // files under a home registry ($CW_HOME / XDG), readable and diffable. No hidden
29
- // database, no daemon required to read state.
30
- //
31
- // - STABLE INTERFACES. Pre-v0.1.28 single-repo runs keep working with an empty /
32
- // rebuildable registry; nothing about `.cw/runs/` layout changes.
33
- var __importDefault = (this && this.__importDefault) || function (mod) {
34
- return (mod && mod.__esModule) ? mod : { "default": mod };
35
- };
36
- Object.defineProperty(exports, "__esModule", { value: true });
37
- exports.formatQueueList = exports.formatHistory = exports.formatResume = exports.formatGcVerify = exports.formatGcRun = exports.formatGcPlan = exports.formatRunShow = exports.formatRunSearch = exports.formatRegistryReport = exports.RunRegistry = exports.RUN_REGISTRY_SCHEMA_VERSION = exports.DEFAULT_RUN_REGISTRY_POLICY = exports.isRunLifecycleState = exports.compareQueue = void 0;
38
- exports.resolveCwHome = resolveCwHome;
39
- exports.deriveLifecycle = deriveLifecycle;
40
- const node_crypto_1 = __importDefault(require("node:crypto"));
41
- const node_fs_1 = __importDefault(require("node:fs"));
42
- const node_os_1 = __importDefault(require("node:os"));
43
- const node_path_1 = __importDefault(require("node:path"));
44
- const state_1 = require("./state");
45
- // planReclamation/runReclamation/verifyReclamation/ReclamationError moved with the
46
- // GC cluster into ./run-registry/gc (FreeBSD-audit R2 deep).
47
- const compare_1 = require("./compare");
48
- const derive_1 = require("./run-registry/derive");
49
- Object.defineProperty(exports, "compareQueue", { enumerable: true, get: function () { return derive_1.compareQueue; } });
50
- Object.defineProperty(exports, "isRunLifecycleState", { enumerable: true, get: function () { return derive_1.isRunLifecycleState; } });
51
- const gc_1 = require("./run-registry/gc");
52
- const queue_1 = require("./run-registry/queue");
53
- const policy_1 = require("./run-registry/policy");
54
- Object.defineProperty(exports, "DEFAULT_RUN_REGISTRY_POLICY", { enumerable: true, get: function () { return policy_1.DEFAULT_RUN_REGISTRY_POLICY; } });
55
- Object.defineProperty(exports, "RUN_REGISTRY_SCHEMA_VERSION", { enumerable: true, get: function () { return policy_1.RUN_REGISTRY_SCHEMA_VERSION; } });
56
- // ---------------------------------------------------------------------------
57
- // Home registry location (EXPLICIT, INSPECTABLE STATE)
58
- // ---------------------------------------------------------------------------
59
- /** Resolve the home registry root: CW_HOME, then XDG_STATE_HOME/cool-workflow,
60
- * then ~/.local/state/cool-workflow. Always a plain directory of plain files. */
61
- function resolveCwHome(env = process.env) {
62
- if (env.CW_HOME && String(env.CW_HOME).trim())
63
- return node_path_1.default.resolve(String(env.CW_HOME));
64
- if (env.XDG_STATE_HOME && String(env.XDG_STATE_HOME).trim()) {
65
- return node_path_1.default.join(node_path_1.default.resolve(String(env.XDG_STATE_HOME)), "cool-workflow");
66
- }
67
- return node_path_1.default.join(node_os_1.default.homedir(), ".local", "state", "cool-workflow");
68
- }
69
- // ---------------------------------------------------------------------------
70
- // Fingerprints (same shape/strength as state-explosion's)
71
- // ---------------------------------------------------------------------------
72
- function fingerprintStrings(values) {
73
- const hash = node_crypto_1.default.createHash("sha256");
74
- hash.update(JSON.stringify([...values].sort()));
75
- return `sha256:${hash.digest("hex").slice(0, 32)}`;
76
- }
77
- /** Content fingerprint of a run's source state.json. Structural, not just mtime,
78
- * so a tampered task status trips `stale` even if updatedAt is unchanged. */
79
- function fingerprintRun(run) {
80
- const parts = [
81
- `id:${run.id}`,
82
- `updatedAt:${run.updatedAt}`,
83
- `loopStage:${run.loopStage}`,
84
- `schema:${run.schemaVersion}`
85
- ];
86
- for (const task of [...run.tasks].sort((a, b) => (0, compare_1.compareBytes)(a.id, b.id))) {
87
- parts.push(`task:${task.id}:${task.status}`);
88
- }
89
- for (const commit of [...run.commits].sort((a, b) => (0, compare_1.compareBytes)(a.id, b.id))) {
90
- parts.push(`commit:${commit.id}:${commit.verifierGated ? "gated" : "checkpoint"}`);
91
- }
92
- for (const phase of [...run.phases].sort((a, b) => (0, compare_1.compareBytes)(a.id, b.id))) {
93
- parts.push(`phase:${phase.id}:${phase.status}`);
94
- }
95
- for (const fb of [...(run.feedback || [])].sort((a, b) => (0, compare_1.compareBytes)(a.id, b.id))) {
96
- parts.push(`feedback:${fb.id}:${fb.status}`);
97
- }
98
- return fingerprintStrings(parts);
99
- }
100
- /**
101
- * Classify a run's lifecycle purely from its source state. First match wins:
102
- * 1. running > 0 -> running
103
- * 2. openFeedback > 0 -> blocked (failures under correction)
104
- * 3. failed > 0 -> failed
105
- * 4. total > 0 && completed === total -> completed
106
- * 5. verifierGatedCommits > 0 && pending === 0 -> completed (commit-only runs)
107
- * 6. completed > 0 -> running (mid-flight)
108
- * 7. otherwise -> queued
109
- * The classifier never invents status; `archived` is applied as an overlay on
110
- * top of this by deriveRecord, which keeps `derivedLifecycle` for search.
111
- */
112
- function deriveLifecycle(input) {
113
- if (input.running > 0)
114
- return "running";
115
- if (input.openFeedback > 0)
116
- return "blocked";
117
- if (input.failed > 0)
118
- return "failed";
119
- if (input.total > 0 && input.completed === input.total)
120
- return "completed";
121
- if (input.verifierGatedCommits > 0 && input.pending === 0)
122
- return "completed";
123
- if (input.completed > 0)
124
- return "running";
125
- return "queued";
126
- }
127
- function lifecycleInputs(run) {
128
- const tasks = run.tasks || [];
129
- return {
130
- total: tasks.length,
131
- pending: tasks.filter((t) => t.status === "pending").length,
132
- running: tasks.filter((t) => t.status === "running").length,
133
- failed: tasks.filter((t) => t.status === "failed").length,
134
- completed: tasks.filter((t) => t.status === "completed").length,
135
- verifierGatedCommits: (run.commits || []).filter((c) => c.verifierGated).length,
136
- openFeedback: (run.feedback || []).filter((f) => f.status === "open" || f.status === "tasked").length,
137
- loopStage: run.loopStage
138
- };
139
- }
140
- // ---------------------------------------------------------------------------
141
- // The registry
142
- // ---------------------------------------------------------------------------
143
- class RunRegistry {
144
- repoRoot;
145
- homeRoot;
146
- planner;
147
- constructor(cwd = process.cwd(), planner, env = process.env) {
148
- this.repoRoot = node_path_1.default.resolve(cwd);
149
- this.homeRoot = resolveCwHome(env);
150
- this.planner = planner;
151
- }
152
- // ---- path helpers -------------------------------------------------------
153
- repoRunsDir(repo) {
154
- return node_path_1.default.join(repo, ".cw", "runs");
155
- }
156
- repoRegistryDir(repo) {
157
- return node_path_1.default.join(repo, ".cw", "registry");
158
- }
159
- // Public so the carved queue cluster (run-registry/queue.ts) can resolve the
160
- // home-registry dir without reaching into private state (QueueHost).
161
- homeRegistryDir() {
162
- return node_path_1.default.join(this.homeRoot, "registry");
163
- }
164
- // ---- per-repo overlays (plain files) ------------------------------------
165
- // Overlay reads distinguish ABSENT (clean default) from PRESENT-but-corrupt
166
- // (fail closed). readJson throws `Invalid JSON in <file>` on a present file
167
- // that won't parse; we let that propagate instead of swallowing it. Swallowing
168
- // is the absent-vs-corrupt conflation telemetry-ledger.ts flags as the bug that
169
- // "let a corrupt overlay verify green" — here it would silently un-archive every
170
- // archived run / drop every provenance link. This is authoritative durable state.
171
- // A present overlay must parse to a JSON OBJECT. readJson already fails closed
172
- // on unparseable bytes; this catches the next shape over: valid JSON that is
173
- // `null`, an array, or a scalar. Without it `parsed.archived` throws a cryptic
174
- // TypeError (null) or silently reads `undefined` (array) and the whole registry
175
- // scan breaks. Fail closed with a clear message instead.
176
- requireOverlayObject(parsed, file) {
177
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
178
- throw new Error(`Corrupt overlay ${file}: expected a JSON object, got ${Array.isArray(parsed) ? "array" : parsed === null ? "null" : typeof parsed}`);
179
- }
180
- return parsed;
181
- }
182
- loadArchiveOverlay(repo) {
183
- const file = node_path_1.default.join(this.repoRegistryDir(repo), "archive.json");
184
- if (!node_fs_1.default.existsSync(file))
185
- return { schemaVersion: 1, archived: {} };
186
- const parsed = this.requireOverlayObject((0, state_1.readJson)(file), file);
187
- return { schemaVersion: 1, archived: parsed.archived || {} };
188
- }
189
- loadProvenanceOverlay(repo) {
190
- const file = node_path_1.default.join(this.repoRegistryDir(repo), "provenance.json");
191
- if (!node_fs_1.default.existsSync(file))
192
- return { schemaVersion: 1, links: {} };
193
- const parsed = this.requireOverlayObject((0, state_1.readJson)(file), file);
194
- return { schemaVersion: 1, links: parsed.links || {} };
195
- }
196
- loadRepoOverlays(repo) {
197
- return {
198
- archive: this.loadArchiveOverlay(repo),
199
- provenance: this.loadProvenanceOverlay(repo)
200
- };
201
- }
202
- /** Default queue priority from POLICY (QueueHost). Exposed so the carved queue
203
- * cluster never re-derives policy. */
204
- get defaultQueuePriority() {
205
- return policy_1.DEFAULT_RUN_REGISTRY_POLICY.defaultQueuePriority;
206
- }
207
- // ---- home registry files ------------------------------------------------
208
- reposFilePath() {
209
- return node_path_1.default.join(this.homeRegistryDir(), "repos.json");
210
- }
211
- loadRepos() {
212
- const file = this.reposFilePath();
213
- // Absent => no registered repos. Present-but-corrupt must fail closed: a
214
- // swallowed parse error here silently drops every cross-repo root the
215
- // operator registered, shrinking the home index to the current repo with no
216
- // signal. Let readJson's `Invalid JSON` throw surface the corruption.
217
- if (!node_fs_1.default.existsSync(file))
218
- return { schemaVersion: 1, repos: [] };
219
- const parsed = this.requireOverlayObject((0, state_1.readJson)(file), file);
220
- return { schemaVersion: 1, repos: Array.isArray(parsed.repos) ? parsed.repos : [] };
221
- }
222
- /** Persisted union of registered repo roots and the current repo, deduped and
223
- * sorted. Read-only: does NOT write repos.json (reads stay pure). */
224
- knownRepos() {
225
- const roots = new Set([this.repoRoot]);
226
- for (const entry of this.loadRepos().repos)
227
- roots.add(node_path_1.default.resolve(entry.root));
228
- return [...roots].sort();
229
- }
230
- /** Register a repo root into the home repos.json (idempotent). Only mutating
231
- * operations call this; reads never do. */
232
- registerRepo(repo = this.repoRoot) {
233
- const resolved = node_path_1.default.resolve(repo);
234
- const file = this.reposFilePath();
235
- // Cross-process read-modify-write: lock so a concurrent register can't drop a
236
- // repo (v0.1.40, P1-D), and persist durably.
237
- return (0, state_1.withFileLock)(file, () => {
238
- const current = this.loadRepos();
239
- const already = current.repos.some((entry) => node_path_1.default.resolve(entry.root) === resolved);
240
- if (!already)
241
- current.repos.push({ root: resolved, addedAt: new Date().toISOString() });
242
- current.repos.sort((a, b) => (0, compare_1.compareBytes)(a.root, b.root));
243
- (0, state_1.writeJson)(file, current, { durable: true });
244
- return { registered: !already, repos: current.repos.map((entry) => entry.root) };
245
- });
246
- }
247
- // Queue file helpers + queueAdd/List/Show/Drain now live in ./run-registry/queue
248
- // (FreeBSD-audit R2 deep). These remain as thin delegators; `this` satisfies the
249
- // QueueHost contract structurally (repoRoot, defaultQueuePriority,
250
- // homeRegistryDir, registerRepo).
251
- loadQueue() {
252
- return (0, queue_1.loadQueue)(this);
253
- }
254
- // Public queue accessors for the v0.1.37 control-plane scheduler (it operates ON
255
- // this queue store via pure functions in scheduling.ts; the queue file is never
256
- // duplicated). The scheduling-policy file lives beside the queue in the home
257
- // registry, plain and diffable.
258
- loadQueueEntries() {
259
- return this.loadQueue();
260
- }
261
- saveQueueEntries(entries) {
262
- (0, queue_1.saveQueue)(this, entries);
263
- }
264
- schedulingPolicyPath() {
265
- return node_path_1.default.join(this.homeRegistryDir(), "scheduling-policy.json");
266
- }
267
- // ---- record derivation (always from source) -----------------------------
268
- /** Derive a RunRecord from a run directory's source state.json. Returns the
269
- * record, or null when source is unreadable/unsupported (caller decides how to
270
- * surface `missing` — we never fabricate a status). */
271
- deriveRecord(repo, runDir, overlays = this.loadRepoOverlays(repo)) {
272
- const statePath = node_path_1.default.join(runDir, "state.json");
273
- if (!node_fs_1.default.existsSync(statePath))
274
- return null;
275
- let run;
276
- try {
277
- const result = (0, state_1.loadRunStateFile)(statePath, { dryRun: true });
278
- if (result.report.status === "unsupported")
279
- return null;
280
- run = result.run;
281
- }
282
- catch {
283
- return null;
284
- }
285
- const li = lifecycleInputs(run);
286
- const derived = deriveLifecycle(li);
287
- const archive = overlays.archive.archived[run.id];
288
- const provenance = overlays.provenance.links[run.id];
289
- // Run Retention & Provable Reclamation (v0.1.39): the per-run reclaimed.json
290
- // overlay (if any) raises the disk-tier above `archived` and downgrades the
291
- // capability. Derived from source, never invented.
292
- const reclaim = (0, derive_1.loadReclaimedFromDir)(runDir);
293
- const lastTombstone = reclaim.tombstones[reclaim.tombstones.length - 1];
294
- const tier = lastTombstone ? "reclaimed" : archive ? "archived" : "live";
295
- const capability = lastTombstone ? lastTombstone.capability : "re-runnable";
296
- const capabilityReason = lastTombstone
297
- ? lastTombstone.capabilityReason
298
- : archive
299
- ? "archived-full"
300
- : "live-full";
301
- return {
302
- schemaVersion: 1,
303
- runId: run.id,
304
- appId: run.workflow.app?.id,
305
- appVersion: run.workflow.app?.version,
306
- workflowId: run.workflow.id,
307
- title: run.workflow.title,
308
- repo,
309
- runDir,
310
- statePath,
311
- createdAt: run.createdAt,
312
- updatedAt: run.updatedAt,
313
- loopStage: run.loopStage,
314
- lifecycle: lastTombstone ? "reclaimed" : archive ? "archived" : derived,
315
- derivedLifecycle: derived,
316
- archived: Boolean(archive),
317
- archivedAt: archive?.archivedAt,
318
- archiveReason: archive?.reason,
319
- tier,
320
- capability,
321
- capabilityReason,
322
- reclaimedAt: lastTombstone?.reclaimedAt,
323
- reclaimedBytes: reclaim.tombstones.reduce((sum, t) => sum + (t.bytesFreed || 0), 0) || undefined,
324
- tombstoneHash: lastTombstone?.tombstoneHash,
325
- tasks: {
326
- total: li.total,
327
- pending: li.pending,
328
- running: li.running,
329
- failed: li.failed,
330
- completed: li.completed
331
- },
332
- commitCount: (run.commits || []).length,
333
- verifierGatedCommitCount: li.verifierGatedCommits,
334
- openFeedbackCount: li.openFeedback,
335
- backends: (0, derive_1.distinctBackends)(run),
336
- inputsDigest: (0, derive_1.digestInputs)(run.inputs),
337
- sourceFingerprint: fingerprintRun(run),
338
- freshness: "valid",
339
- provenance
340
- };
341
- }
342
- /** Scan one repo's `.cw/runs/` and derive a record per run, deterministically
343
- * ordered (createdAt asc, then runId). Unreadable runs are skipped here; the
344
- * freshness layer is responsible for reporting persisted-but-missing runs. */
345
- scanRepo(repo) {
346
- const runsDir = this.repoRunsDir(repo);
347
- if (!node_fs_1.default.existsSync(runsDir))
348
- return [];
349
- const overlays = this.loadRepoOverlays(repo);
350
- const records = [];
351
- for (const entry of node_fs_1.default.readdirSync(runsDir, { withFileTypes: true })) {
352
- if (!entry.isDirectory())
353
- continue;
354
- const record = this.deriveRecord(repo, node_path_1.default.join(runsDir, entry.name), overlays);
355
- if (record)
356
- records.push(record);
357
- }
358
- return records.sort(derive_1.compareRecords);
359
- }
360
- // ---- index construction (current truth) ---------------------------------
361
- /** Build the CURRENT index fresh from source for the requested scope. This is
362
- * the authoritative-from-source view; persistence/freshness is layered on top. */
363
- buildIndex(scope) {
364
- const repos = scope === "home" ? this.knownRepos() : [this.repoRoot];
365
- const records = [];
366
- for (const repo of repos)
367
- records.push(...this.scanRepo(repo));
368
- records.sort(derive_1.compareRecords);
369
- const queue = scope === "home" ? this.loadQueue() : this.loadQueue().filter((q) => node_path_1.default.resolve(q.repo) === this.repoRoot);
370
- const sourceFingerprint = fingerprintStrings([
371
- ...repos.map((r) => `repo:${r}`),
372
- ...records.map((r) => `${r.runId}:${r.sourceFingerprint}:${r.lifecycle}`)
373
- ]);
374
- return {
375
- schemaVersion: 1,
376
- scope,
377
- root: scope === "home" ? this.homeRoot : this.repoRoot,
378
- generatedAt: new Date().toISOString(),
379
- sourceFingerprint,
380
- repos,
381
- records,
382
- queue,
383
- counts: (0, derive_1.countRecords)(records)
384
- };
385
- }
386
- persistedIndexPath(scope) {
387
- return scope === "home"
388
- ? node_path_1.default.join(this.homeRegistryDir(), "index.json")
389
- : node_path_1.default.join(this.repoRegistryDir(this.repoRoot), "index.json");
390
- }
391
- loadPersistedIndex(scope) {
392
- const file = this.persistedIndexPath(scope);
393
- if (!node_fs_1.default.existsSync(file))
394
- return undefined;
395
- try {
396
- const parsed = (0, state_1.readJson)(file);
397
- if (!parsed || parsed.schemaVersion !== 1)
398
- return undefined;
399
- return parsed;
400
- }
401
- catch {
402
- return undefined;
403
- }
404
- }
405
- /** Refresh (recompute and persist) the index. registers the current repo into
406
- * the home registry so cross-repo discovery finds it later. MECHANISM only:
407
- * never touches source state.json. */
408
- refresh(options = {}) {
409
- const scope = options.scope || "repo";
410
- // Registering the current repo is what makes a single-repo run discoverable
411
- // cross-repo. Always safe (idempotent) and never mutates run source.
412
- this.registerRepo(this.repoRoot);
413
- const index = this.buildIndex(scope);
414
- (0, state_1.writeJson)(this.persistedIndexPath(scope), index);
415
- if (scope === "repo") {
416
- // A repo refresh also keeps the home aggregate fresh enough to discover this
417
- // repo's runs, without forcing a full cross-repo rebuild.
418
- const homeIndex = this.buildIndex("home");
419
- (0, state_1.writeJson)(this.persistedIndexPath("home"), homeIndex);
420
- }
421
- return this.report(scope, index);
422
- }
423
- /** Read the index with explicit freshness against current source. Re-derives
424
- * every record from source (never fabricates); compares to the persisted cache
425
- * to report valid|stale|absent + staleRuns/missingRuns. */
426
- show(options = {}) {
427
- const scope = options.scope || "repo";
428
- return this.report(scope, this.buildIndex(scope));
429
- }
430
- report(scope, current) {
431
- const persisted = this.loadPersistedIndex(scope);
432
- const currentById = new Map(current.records.map((r) => [r.runId, r]));
433
- let status = persisted ? "valid" : "absent";
434
- const staleRuns = [];
435
- const missingRuns = [];
436
- if (persisted) {
437
- if (persisted.sourceFingerprint !== current.sourceFingerprint)
438
- status = "stale";
439
- for (const prior of persisted.records) {
440
- const now = currentById.get(prior.runId);
441
- if (!now) {
442
- missingRuns.push(prior.runId);
443
- }
444
- else if (now.sourceFingerprint !== prior.sourceFingerprint) {
445
- staleRuns.push(prior.runId);
446
- }
447
- }
448
- if (staleRuns.length || missingRuns.length)
449
- status = "stale";
450
- }
451
- const refreshCmd = scope === "home" ? "node scripts/cw.js registry refresh --scope home" : "node scripts/cw.js registry refresh";
452
- return {
453
- schemaVersion: 1,
454
- scope,
455
- root: current.root,
456
- generatedAt: current.generatedAt,
457
- freshness: {
458
- status,
459
- persistedFingerprint: persisted?.sourceFingerprint,
460
- currentFingerprint: current.sourceFingerprint,
461
- staleRuns: staleRuns.sort(),
462
- missingRuns: missingRuns.sort()
463
- },
464
- index: current,
465
- counts: current.counts,
466
- nextAction: status === "valid" ? "node scripts/cw.js run search" : refreshCmd
467
- };
468
- }
469
- // ---- search (deterministic, paginated) ----------------------------------
470
- search(raw = {}) {
471
- const scope = raw.scope || "home";
472
- const index = this.buildIndex(scope);
473
- const report = this.report(scope, index);
474
- const query = {
475
- text: (0, derive_1.optionalLower)(raw.text),
476
- app: (0, derive_1.optionalLower)(raw.app),
477
- status: raw.status,
478
- repo: raw.repo ? node_path_1.default.resolve(raw.repo) : undefined,
479
- since: raw.since,
480
- until: raw.until,
481
- includeArchived: raw.includeArchived ?? true,
482
- offset: (0, derive_1.clampInt)(raw.offset, 0, 0),
483
- limit: (0, derive_1.clampInt)(raw.limit, 50, 1)
484
- };
485
- let records = index.records.filter((record) => (0, derive_1.matchesQuery)(record, query));
486
- if (!query.includeArchived)
487
- records = records.filter((record) => !record.archived);
488
- records.sort(derive_1.compareRecords);
489
- const total = records.length;
490
- const page = records.slice(query.offset, query.offset + query.limit);
491
- return {
492
- schemaVersion: 1,
493
- scope,
494
- query,
495
- freshness: report.freshness.status,
496
- total,
497
- offset: query.offset,
498
- limit: query.limit,
499
- records: page,
500
- nextAction: report.freshness.status === "valid"
501
- ? "node scripts/cw.js run show <run-id>"
502
- : "node scripts/cw.js registry refresh"
503
- };
504
- }
505
- list(options = {}) {
506
- return this.search({
507
- scope: options.scope || "home",
508
- includeArchived: options.includeArchived ?? true,
509
- limit: options.limit,
510
- offset: options.offset
511
- });
512
- }
513
- // ---- resolve one run by id (cross-repo, fail-closed) --------------------
514
- /** Resolve a run by id, preferring the current repo, then any registered repo.
515
- * Returns found=false with freshness `missing` (and the last-known persisted
516
- * record, clearly flagged) when source is gone. */
517
- showRun(runId, options = {}) {
518
- const scope = options.scope || "home";
519
- const located = this.locate(runId, scope);
520
- if (located) {
521
- return {
522
- schemaVersion: 1,
523
- runId,
524
- found: true,
525
- freshness: "valid",
526
- resolvedFrom: located.from,
527
- repo: located.record.repo,
528
- record: located.record,
529
- nextAction: located.record.archived
530
- ? "node scripts/cw.js run resume " + runId
531
- : "node scripts/cw.js run show " + runId
532
- };
533
- }
534
- // Not present in source. Surface the last-known persisted record (if any),
535
- // flagged `missing` — never as a live status.
536
- const persisted = this.findPersisted(runId, scope);
537
- return {
538
- schemaVersion: 1,
539
- runId,
540
- found: false,
541
- freshness: "missing",
542
- repo: persisted?.repo,
543
- persisted,
544
- nextAction: "node scripts/cw.js registry refresh" + (scope === "home" ? " --scope home" : "")
545
- };
546
- }
547
- // Public so the carved gc cluster (run-registry/gc.ts) can resolve a run
548
- // repo-first without reaching into private state (GcHost).
549
- locate(runId, scope) {
550
- // Current repo first (least astonishment: cwd wins).
551
- const here = this.deriveRecordForRun(this.repoRoot, runId);
552
- if (here)
553
- return { record: here, from: "repo" };
554
- if (scope === "repo")
555
- return undefined;
556
- for (const repo of this.knownRepos()) {
557
- if (node_path_1.default.resolve(repo) === this.repoRoot)
558
- continue;
559
- const record = this.deriveRecordForRun(repo, runId);
560
- if (record)
561
- return { record, from: "home" };
562
- }
563
- return undefined;
564
- }
565
- deriveRecordForRun(repo, runId) {
566
- const runDir = node_path_1.default.join(this.repoRunsDir(repo), runId);
567
- if (!node_fs_1.default.existsSync(node_path_1.default.join(runDir, "state.json")))
568
- return null;
569
- return this.deriveRecord(repo, runDir);
570
- }
571
- findPersisted(runId, scope) {
572
- for (const s of scope === "home" ? ["home", "repo"] : ["repo"]) {
573
- const persisted = this.loadPersistedIndex(s);
574
- const hit = persisted?.records.find((r) => r.runId === runId);
575
- if (hit)
576
- return hit;
577
- }
578
- return undefined;
579
- }
580
- // Public so the carved gc cluster (run-registry/gc.ts) can load source state
581
- // for a resolved run without reaching into private state (GcHost).
582
- loadRun(repo, runId) {
583
- const statePath = node_path_1.default.join(this.repoRunsDir(repo), runId, "state.json");
584
- if (!node_fs_1.default.existsSync(statePath))
585
- throw new Error(`Run not found: ${runId}`);
586
- const result = (0, state_1.loadRunStateFile)(statePath, { dryRun: true });
587
- if (result.report.status === "unsupported") {
588
- throw new Error(`Unsupported run state for ${runId}: ${result.report.errors.join("; ")}`);
589
- }
590
- return result.run;
591
- }
592
- // ---- resume (continue from durable state; read-only over source) --------
593
- resume(runId, options = {}) {
594
- const scope = options.scope || "home";
595
- const located = this.locate(runId, scope);
596
- if (!located) {
597
- throw new Error(`Cannot resume: run ${runId} not found in source state (fail closed; try registry refresh).`);
598
- }
599
- const record = located.record;
600
- const run = this.loadRun(record.repo, runId);
601
- const limit = (0, derive_1.clampInt)(options.limit, 5, 1);
602
- const nextTasks = (run.tasks || [])
603
- .filter((t) => t.status === "pending" || t.status === "running")
604
- .slice(0, limit)
605
- .map((t) => ({ id: t.id, phase: t.phase, status: t.status, taskPath: t.taskPath }));
606
- const terminal = record.derivedLifecycle === "completed" || record.derivedLifecycle === "failed";
607
- const resumable = nextTasks.length > 0 || (!terminal && record.derivedLifecycle !== "completed");
608
- const nextActions = [];
609
- if (nextTasks.length) {
610
- nextActions.push({
611
- command: `node scripts/cw.js dispatch ${runId} --cwd ${record.repo}`,
612
- reason: `Continue ${nextTasks.length} pending/running task(s) from durable state.`
613
- });
614
- nextActions.push({
615
- command: `node scripts/cw.js multi-agent step ${runId} --cwd ${record.repo}`,
616
- reason: "Take one deterministic host step without spawning agents."
617
- });
618
- }
619
- else if (record.derivedLifecycle === "failed") {
620
- nextActions.push({
621
- command: `node scripts/cw.js run rerun ${runId}`,
622
- reason: "Run terminated as failed with no runnable tasks; rerun as a new linked run."
623
- });
624
- }
625
- else {
626
- nextActions.push({
627
- command: `node scripts/cw.js status ${runId} --cwd ${record.repo} --json`,
628
- reason: "No runnable tasks remain; inspect status.",
629
- });
630
- }
631
- return {
632
- schemaVersion: 1,
633
- runId,
634
- repo: record.repo,
635
- runDir: record.runDir,
636
- statePath: record.statePath,
637
- resolvedFrom: located.from,
638
- lifecycle: record.lifecycle,
639
- derivedLifecycle: record.derivedLifecycle,
640
- loopStage: record.loopStage,
641
- freshness: "valid",
642
- resumable,
643
- reason: record.archived ? "Run is archived; resuming reads durable state without un-archiving." : undefined,
644
- record,
645
- nextTasks,
646
- nextActions
647
- };
648
- }
649
- // ---- archive (overlay mark; never deletes source) -----------------------
650
- archive(runId, options = {}) {
651
- const scope = options.scope || "home";
652
- const located = this.locate(runId, scope);
653
- if (!located)
654
- throw new Error(`Cannot archive: run ${runId} not found in source state (fail closed).`);
655
- const repo = located.record.repo;
656
- const file = node_path_1.default.join(this.repoRegistryDir(repo), "archive.json");
657
- // Lock the archive-overlay read-modify-write (v0.1.40, P1-D) + durable write.
658
- (0, state_1.withFileLock)(file, () => {
659
- const overlay = this.loadArchiveOverlay(repo);
660
- if (options.unarchive) {
661
- delete overlay.archived[runId];
662
- }
663
- else {
664
- overlay.archived[runId] = { archivedAt: new Date().toISOString(), reason: options.reason };
665
- }
666
- (0, state_1.writeJson)(file, overlay, { durable: true });
667
- });
668
- const record = this.deriveRecord(repo, located.record.runDir);
669
- return {
670
- runId,
671
- repo,
672
- archived: record.archived,
673
- archivedAt: record.archivedAt,
674
- reason: record.archiveReason,
675
- record,
676
- overlayPath: file
677
- };
678
- }
679
- /** Apply a retention POLICY: archive eligible runs older than the window. The
680
- * window/states are policy inputs, never baked into the index. Returns the set
681
- * archived; archives are overlay marks, so nothing is destroyed. */
682
- archiveByPolicy(policy = policy_1.DEFAULT_RUN_REGISTRY_POLICY, options = {}) {
683
- const scope = options.scope || "home";
684
- if (!policy.archiveOlderThanDays || policy.archiveOlderThanDays <= 0) {
685
- return { policy, archived: [], eligible: 0 };
686
- }
687
- const nowMs = options.now ? Date.parse(options.now) : Date.now();
688
- const cutoff = nowMs - policy.archiveOlderThanDays * 24 * 60 * 60 * 1000;
689
- const index = this.buildIndex(scope);
690
- const eligible = index.records.filter((r) => !r.archived && policy.archiveStates.includes(r.derivedLifecycle) && Date.parse(r.updatedAt) < cutoff);
691
- const archived = [];
692
- for (const record of eligible) {
693
- this.archive(record.runId, { reason: `retention:${policy.archiveOlderThanDays}d`, scope });
694
- archived.push(record.runId);
695
- }
696
- return { policy, archived: archived.sort(), eligible: eligible.length };
697
- }
698
- // ---- Run Retention & Provable Reclamation (v0.1.39) ----------------------
699
- // A small, verifiable GC built on the archive overlay. `gc plan` is a pure
700
- // dry-run (frees nothing); `gc run` executes the write-ahead reclamation
701
- // transaction (skeleton → tombstone → fsync → free); `gc verify` re-proves a
702
- // reclaimed run independently. Eligibility is explicit and fail-closed.
703
- // Implementations live in ./run-registry/gc (FreeBSD-audit R2 deep); these are
704
- // thin delegators preserving the public surface. `this` satisfies GcHost
705
- // (buildIndex, locate, loadRun).
706
- /** Resolve the effective reclamation policy (defaults reclaim NOTHING). */
707
- reclamationPolicy(overrides = {}) {
708
- return (0, gc_1.reclamationPolicy)(overrides);
709
- }
710
- /** Dry-run: compute eligible runs, per-kind bytes that WOULD be freed, and the
711
- * capability downgrade. Frees NOTHING. */
712
- gcPlan(options = {}) {
713
- return (0, gc_1.gcPlan)(this, options);
714
- }
715
- /** Execute the write-ahead reclamation transaction for eligible runs. Bounded
716
- * (`maxReclaimRuns` / `maxReclaimBytes`), fail-closed on any incomplete
717
- * skeleton. Produces a tombstone and frees the bulk. */
718
- gcRun(options = {}) {
719
- return (0, gc_1.gcRun)(this, options);
720
- }
721
- /** Re-prove a reclaimed run: skeleton schema-complete, tombstone chain
722
- * recomputed-and-untampered, each reconstructable artifact re-derived from its
723
- * RETAINED inputs to its expectDigest, and eligible-when-reclaimed. */
724
- gcVerify(runId, options = {}) {
725
- return (0, gc_1.gcVerify)(this, runId, options);
726
- }
727
- // ---- rerun (NEW run linked to the original; original preserved) ---------
728
- rerun(runId, options = {}) {
729
- if (!this.planner)
730
- throw new Error("rerun requires a run planner (CoolWorkflowRunner)");
731
- const scope = options.scope || "home";
732
- const located = this.locate(runId, scope);
733
- if (!located)
734
- throw new Error(`Cannot rerun: run ${runId} not found in source state (fail closed).`);
735
- const original = located.record;
736
- const originalRun = this.loadRun(original.repo, runId);
737
- const appId = originalRun.workflow.app?.id || originalRun.workflow.id;
738
- // Reuse the original inputs verbatim, pinned to the original repo so the new
739
- // run lands beside it. We never fork run creation — this is runner.plan.
740
- const inputs = { ...(originalRun.inputs || {}), cwd: original.repo, repo: original.repo };
741
- const newRun = this.planner.plan(appId, inputs);
742
- const priorProv = original.provenance;
743
- const provenance = {
744
- rerunOf: runId,
745
- rerunOfRepo: original.repo,
746
- originRunId: priorProv?.originRunId || runId,
747
- generation: (priorProv?.generation || 0) + 1,
748
- reason: options.reason || "rerun of failed run",
749
- createdAt: new Date().toISOString()
750
- };
751
- // Record provenance in the per-repo overlay (derived metadata), NOT in the
752
- // original run's source state — the past is never mutated.
753
- const provFile = node_path_1.default.join(this.repoRegistryDir(original.repo), "provenance.json");
754
- // Lock the read-modify-write: a concurrent rerun/archive on the same repo
755
- // overlay would otherwise last-writer-wins and drop a provenance link. The
756
- // sibling writers (registerRepo, archive) already serialize via withFileLock.
757
- (0, state_1.withFileLock)(provFile, () => {
758
- const provOverlay = this.loadProvenanceOverlay(original.repo);
759
- provOverlay.links[newRun.id] = provenance;
760
- (0, state_1.writeJson)(provFile, provOverlay, { durable: true });
761
- });
762
- return {
763
- schemaVersion: 1,
764
- originalRunId: runId,
765
- originalRepo: original.repo,
766
- originalLifecycle: original.lifecycle,
767
- newRunId: newRun.id,
768
- repo: original.repo,
769
- appId: newRun.workflow.app?.id || appId,
770
- workflowId: newRun.workflow.id,
771
- statePath: newRun.paths.state,
772
- reportPath: newRun.paths.report,
773
- pendingTasks: newRun.tasks.filter((t) => t.status === "pending").length,
774
- provenance,
775
- nextActions: [
776
- { command: `node scripts/cw.js run resume ${newRun.id}`, reason: "Continue the new linked run." },
777
- { command: `node scripts/cw.js run show ${runId}`, reason: "The original failed run is preserved for audit." }
778
- ]
779
- };
780
- }
781
- // ---- queue (durable, ordered; drained by the host) ----------------------
782
- // Implementations live in ./run-registry/queue (FreeBSD-audit R2 deep); these
783
- // are thin delegators preserving the public surface. `this` satisfies QueueHost.
784
- queueAdd(options = {}) {
785
- return (0, queue_1.queueAdd)(this, options);
786
- }
787
- queueList(options = {}) {
788
- return (0, queue_1.queueList)(this, options);
789
- }
790
- queueShow(id) {
791
- return (0, queue_1.queueShow)(this, id);
792
- }
793
- queueDrain(options = {}) {
794
- return (0, queue_1.queueDrain)(this, options);
795
- }
796
- // ---- cross-repo history (unified timeline) ------------------------------
797
- history(options = {}) {
798
- const scope = options.scope || "home";
799
- const index = this.buildIndex(scope);
800
- const report = this.report(scope, index);
801
- const app = (0, derive_1.optionalLower)(options.app);
802
- const limit = (0, derive_1.clampInt)(options.limit, 50, 1);
803
- const offset = (0, derive_1.clampInt)(options.offset, 0, 0);
804
- let records = index.records;
805
- if (app)
806
- records = records.filter((r) => (r.appId || r.workflowId || "").toLowerCase().includes(app));
807
- if (options.status)
808
- records = records.filter((r) => r.lifecycle === options.status || r.derivedLifecycle === options.status);
809
- const ordered = [...records].sort(derive_1.compareHistory);
810
- const total = ordered.length;
811
- const page = ordered.slice(offset, offset + limit);
812
- const entries = page.map((r) => ({
813
- runId: r.runId,
814
- repo: r.repo,
815
- appId: r.appId,
816
- workflowId: r.workflowId,
817
- lifecycle: r.lifecycle,
818
- loopStage: r.loopStage,
819
- createdAt: r.createdAt,
820
- updatedAt: r.updatedAt,
821
- freshness: r.freshness,
822
- provenance: r.provenance
823
- }));
824
- return {
825
- schemaVersion: 1,
826
- scope,
827
- freshness: report.freshness.status,
828
- total,
829
- offset,
830
- limit,
831
- repos: index.repos,
832
- entries,
833
- nextAction: report.freshness.status === "valid" ? "node scripts/cw.js run show <run-id>" : "node scripts/cw.js registry refresh --scope home"
834
- };
835
- }
836
- }
837
- exports.RunRegistry = RunRegistry;
838
- // Human formatting (CLI-only) now lives in ./run-registry/format.ts (FreeBSD-
839
- // audit R2: rendering carved out of the registry class). Re-exported so that
840
- // importers of "./run-registry" see an unchanged surface.
841
- var format_1 = require("./run-registry/format");
842
- Object.defineProperty(exports, "formatRegistryReport", { enumerable: true, get: function () { return format_1.formatRegistryReport; } });
843
- Object.defineProperty(exports, "formatRunSearch", { enumerable: true, get: function () { return format_1.formatRunSearch; } });
844
- Object.defineProperty(exports, "formatRunShow", { enumerable: true, get: function () { return format_1.formatRunShow; } });
845
- Object.defineProperty(exports, "formatGcPlan", { enumerable: true, get: function () { return format_1.formatGcPlan; } });
846
- Object.defineProperty(exports, "formatGcRun", { enumerable: true, get: function () { return format_1.formatGcRun; } });
847
- Object.defineProperty(exports, "formatGcVerify", { enumerable: true, get: function () { return format_1.formatGcVerify; } });
848
- Object.defineProperty(exports, "formatResume", { enumerable: true, get: function () { return format_1.formatResume; } });
849
- Object.defineProperty(exports, "formatHistory", { enumerable: true, get: function () { return format_1.formatHistory; } });
850
- Object.defineProperty(exports, "formatQueueList", { enumerable: true, get: function () { return format_1.formatQueueList; } });