cool-workflow 0.1.80 → 0.1.82

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 (122) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/README.md +42 -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/agent-config.js +21 -7
  11. package/dist/candidate-scoring.js +62 -48
  12. package/dist/capability-core.js +152 -96
  13. package/dist/capability-registry.js +160 -174
  14. package/dist/cli.js +90 -100
  15. package/dist/collaboration.js +5 -6
  16. package/dist/commit.js +232 -209
  17. package/dist/compare.js +18 -0
  18. package/dist/coordinator/classify.js +45 -0
  19. package/dist/coordinator/paths.js +42 -0
  20. package/dist/coordinator/util.js +126 -0
  21. package/dist/coordinator.js +127 -300
  22. package/dist/dispatch.js +46 -3
  23. package/dist/drive.js +7 -7
  24. package/dist/error-feedback.js +8 -4
  25. package/dist/evidence-reasoning.js +5 -2
  26. package/dist/execution-backend/agent.js +294 -0
  27. package/dist/execution-backend/probes.js +96 -0
  28. package/dist/execution-backend/util.js +47 -0
  29. package/dist/execution-backend.js +77 -450
  30. package/dist/gates.js +48 -0
  31. package/dist/mcp-server.js +34 -173
  32. package/dist/multi-agent/graph.js +84 -0
  33. package/dist/multi-agent/helpers.js +141 -0
  34. package/dist/multi-agent/ids.js +20 -0
  35. package/dist/multi-agent/paths.js +22 -0
  36. package/dist/multi-agent-eval/format.js +194 -0
  37. package/dist/multi-agent-eval/normalize.js +51 -0
  38. package/dist/multi-agent-eval.js +63 -242
  39. package/dist/multi-agent-host.js +53 -40
  40. package/dist/multi-agent-operator-ux.js +2 -1
  41. package/dist/multi-agent-trust.js +5 -5
  42. package/dist/multi-agent.js +125 -314
  43. package/dist/node-projection.js +59 -0
  44. package/dist/node-snapshot.js +11 -21
  45. package/dist/observability/format.js +61 -0
  46. package/dist/observability/intake.js +98 -0
  47. package/dist/observability.js +14 -160
  48. package/dist/operator-ux/format.js +364 -0
  49. package/dist/operator-ux.js +22 -363
  50. package/dist/orchestrator/lifecycle-operations.js +22 -1
  51. package/dist/orchestrator/report.js +8 -0
  52. package/dist/orchestrator.js +41 -10
  53. package/dist/reclamation.js +32 -55
  54. package/dist/run-export.js +138 -14
  55. package/dist/run-registry/derive.js +172 -0
  56. package/dist/run-registry/format.js +124 -0
  57. package/dist/run-registry/gc.js +251 -0
  58. package/dist/run-registry/policy.js +16 -0
  59. package/dist/run-registry/queue.js +116 -0
  60. package/dist/run-registry.js +78 -593
  61. package/dist/run-state-schema.js +1 -0
  62. package/dist/sandbox-profile.js +43 -2
  63. package/dist/scheduler.js +34 -4
  64. package/dist/state-explosion/format.js +159 -0
  65. package/dist/state-explosion/helpers.js +82 -0
  66. package/dist/state-explosion.js +65 -283
  67. package/dist/state-node.js +19 -4
  68. package/dist/telemetry-attestation.js +55 -0
  69. package/dist/telemetry-demo.js +15 -3
  70. package/dist/telemetry-ledger.js +60 -15
  71. package/dist/topology.js +50 -12
  72. package/dist/triggers.js +33 -14
  73. package/dist/trust-audit.js +215 -71
  74. package/dist/validation.js +328 -0
  75. package/dist/version.js +1 -1
  76. package/dist/worker-isolation/helpers.js +51 -0
  77. package/dist/worker-isolation/paths.js +46 -0
  78. package/dist/worker-isolation.js +182 -173
  79. package/docs/agent-delegation-drive.7.md +14 -0
  80. package/docs/cli-mcp-parity.7.md +5 -0
  81. package/docs/contract-migration-tooling.7.md +3 -0
  82. package/docs/control-plane-scheduling.7.md +3 -0
  83. package/docs/dogfood/resume-drive-real-agent-2026-06-14.md +40 -0
  84. package/docs/durable-state-and-locking.7.md +5 -0
  85. package/docs/evidence-adoption-reasoning-chain.7.md +3 -0
  86. package/docs/execution-backends.7.md +3 -0
  87. package/docs/index.md +1 -0
  88. package/docs/launch/launch-kit.md +46 -23
  89. package/docs/launch/pre-launch-checklist.md +14 -14
  90. package/docs/multi-agent-cli-mcp-surface.7.md +5 -0
  91. package/docs/multi-agent-eval-replay-harness.7.md +3 -0
  92. package/docs/multi-agent-operator-ux.7.md +3 -0
  93. package/docs/multi-agent-trust-policy-audit.7.md +27 -0
  94. package/docs/node-snapshot-diff-replay.7.md +3 -0
  95. package/docs/observability-cost-accounting.7.md +3 -0
  96. package/docs/project-index.md +23 -6
  97. package/docs/real-execution-backends.7.md +3 -0
  98. package/docs/release-and-migration.7.md +5 -0
  99. package/docs/release-tooling.7.md +35 -2
  100. package/docs/run-registry-control-plane.7.md +55 -8
  101. package/docs/run-retention-reclamation.7.md +5 -0
  102. package/docs/state-explosion-management.7.md +3 -0
  103. package/docs/team-collaboration.7.md +3 -0
  104. package/docs/trust-model.md +267 -0
  105. package/docs/vendor-manifest-loadability.7.md +43 -0
  106. package/docs/web-desktop-workbench.7.md +3 -0
  107. package/manifest/plugin.manifest.json +1 -1
  108. package/manifest/source-context-profiles.json +1 -1
  109. package/package.json +4 -2
  110. package/scripts/agents/builtin-templates.json +7 -0
  111. package/scripts/bump-version.js +5 -11
  112. package/scripts/canonical-apps-list.js +64 -0
  113. package/scripts/canonical-apps.js +19 -4
  114. package/scripts/children/batch-delegate-child.js +58 -0
  115. package/scripts/children/http-delegate-child.js +39 -0
  116. package/scripts/dogfood-release.js +1 -1
  117. package/scripts/golden-path.js +4 -4
  118. package/scripts/parity-check.js +5 -0
  119. package/scripts/release-check.js +5 -1
  120. package/scripts/release-flow.js +181 -5
  121. package/scripts/version-sync-check.js +5 -8
  122. package/dist/capability-dispatcher.js +0 -86
package/dist/commit.js CHANGED
@@ -17,6 +17,8 @@ const trust_audit_1 = require("./trust-audit");
17
17
  const collaboration_1 = require("./collaboration");
18
18
  const evidence_grounding_1 = require("./evidence-grounding");
19
19
  const verifier_1 = require("./verifier");
20
+ const compare_1 = require("./compare");
21
+ const gates_1 = require("./gates");
20
22
  class CommitGateError extends Error {
21
23
  structured;
22
24
  feedbackId;
@@ -37,7 +39,7 @@ function commitState(run, input) {
37
39
  throw recordCommitGateFailure(run, options, gate);
38
40
  }
39
41
  node_fs_1.default.mkdirSync(run.paths.commitsDir, { recursive: true });
40
- const id = createCommitId();
42
+ const id = createCommitId(run);
41
43
  const snapshotPath = node_path_1.default.join(run.paths.commitsDir, `${id}.json`);
42
44
  const audit = gate.verifierGated
43
45
  ? (0, trust_audit_1.recordTrustAuditEvent)(run, {
@@ -118,26 +120,6 @@ function verifierNodeRequiresEvidence(run, verifierNode) {
118
120
  return (0, verifier_1.taskRequiresEvidence)(task);
119
121
  return true; // candidate/selection verifier with no 1:1 task — enforce grounding.
120
122
  }
121
- /** The HARD no-false-green gate (DIRECTION.md "ambiguity is a visible state").
122
- * A verifier node is built FROM a result node; when that result captured no
123
- * structured signal at all the result node carries an `metadata.captureWarning`
124
- * marker (set in worker-isolation / lifecycle ingest via isEmptyCapture). The
125
- * worker output is still ACCEPTED (a recorded warning, never a silent pass), but
126
- * a verifier-GATED commit must NOT be able to present that zero-evidence result
127
- * as clean/green. We detect it here, reading ONLY persisted state (the source
128
- * result node's metadata) — purely functional, no clock/ordering — so snapshot
129
- * replay reaches the same gate decision. Returns the marker string, or undefined.
130
- *
131
- * Resolution trail: verifier node -> its input/parent result node. We look at
132
- * `inputs.inputNodeId` (set by runPipelineStage) first, then fall back to the
133
- * first parent, so it works regardless of which ingest path produced the node. */
134
- function emptyCaptureWarning(run, verifierNode) {
135
- const resultNodeId = (typeof verifierNode.inputs?.inputNodeId === "string" ? verifierNode.inputs.inputNodeId : undefined) ||
136
- verifierNode.parents[0];
137
- const resultNode = resultNodeId ? findNode(run, resultNodeId) : undefined;
138
- const warning = resultNode?.metadata?.captureWarning;
139
- return typeof warning === "string" && warning ? warning : undefined;
140
- }
141
123
  function evidenceLocatorString(entry) {
142
124
  const ref = entry.locator || entry.path || entry.summary || entry.id;
143
125
  return ref ? String(ref) : undefined;
@@ -164,10 +146,6 @@ function resolveCommitGate(run, options) {
164
146
  const taskVerifierNodeId = taskVerifierFromReason(run, options.reason);
165
147
  const explicitGate = Boolean(options.verifierNodeId || options.candidateId || options.selectionId || options.verifierGated);
166
148
  const verifierGated = explicitGate || Boolean(taskVerifierNodeId);
167
- let verifierNodeId = options.verifierNodeId || taskVerifierNodeId;
168
- let candidateId = options.candidateId;
169
- let selectionId = options.selectionId;
170
- let selectionNodeId;
171
149
  if (!verifierGated) {
172
150
  return {
173
151
  verifierGated: false,
@@ -178,177 +156,234 @@ function resolveCommitGate(run, options) {
178
156
  }
179
157
  metadata.verifierGated = true;
180
158
  metadata.checkpoint = false;
181
- if (selectionId) {
182
- const selection = findSelection(run, selectionId);
183
- if (!selection) {
184
- errors.push(error("commit-selection-not-found", `Commit selection not found: ${selectionId}`, { details: { selectionId } }));
185
- }
186
- else {
187
- candidateId = candidateId || selection.candidateId;
188
- verifierNodeId = resolveLinkedVerifier(verifierNodeId, selection.verifierNodeId, errors, "selection", selection.id);
189
- const selectionNode = findSelectionNode(run, selection.id);
190
- selectionNodeId = selectionNode?.id;
191
- if (!selectionNode) {
192
- errors.push(error("commit-selection-node-missing", `Selection ${selection.id} has no state node`, {
193
- details: { selectionId: selection.id, candidateId: selection.candidateId }
194
- }));
195
- }
196
- else if (selectionNode.kind !== "candidate" || selectionNode.status !== "verified") {
197
- errors.push(error("commit-selection-not-verified", `Selection ${selection.id} is not a verified candidate selection`, {
198
- nodeId: selectionNode.id,
199
- details: { selectionId: selection.id, status: selectionNode.status, kind: selectionNode.kind }
200
- }));
201
- }
202
- if (!selection.scoreId) {
203
- errors.push(error("commit-candidate-unscored", `Selection ${selection.id} has no score evidence`, {
204
- details: { selectionId: selection.id, candidateId: selection.candidateId }
205
- }));
206
- }
207
- }
208
- }
209
- if (candidateId) {
210
- const candidate = findCandidate(run, candidateId);
211
- if (!candidate) {
212
- errors.push(error("commit-candidate-not-found", `Commit candidate not found: ${candidateId}`, { details: { candidateId } }));
213
- }
214
- else {
215
- if (candidate.status === "rejected" || candidate.status === "failed") {
216
- errors.push(error("commit-candidate-not-selectable", `Candidate ${candidateId} is ${candidate.status}`, {
217
- details: { candidateId, status: candidate.status }
218
- }));
219
- }
220
- if (!candidate.scores.length) {
221
- errors.push(error("commit-candidate-unscored", `Candidate ${candidateId} has no score evidence`, {
222
- details: { candidateId }
223
- }));
224
- }
225
- if (candidate.status !== "verified") {
226
- errors.push(error("commit-candidate-not-verified", `Candidate ${candidateId} is not verifier-gated`, {
227
- details: { candidateId, status: candidate.status }
228
- }));
229
- }
230
- const selection = selectionId ? findSelection(run, selectionId) : latestSelectionForCandidate(run, candidateId);
231
- if (!selection) {
232
- errors.push(error("commit-candidate-selection-missing", `Candidate ${candidateId} has no verified selection`, {
233
- details: { candidateId }
234
- }));
235
- }
236
- else {
237
- selectionId = selection.id;
238
- verifierNodeId = resolveLinkedVerifier(verifierNodeId, selection.verifierNodeId || candidate.verifierNodeId, errors, "candidate", candidateId);
239
- const selectionNode = findSelectionNode(run, selection.id);
240
- selectionNodeId = selectionNode?.id;
241
- if (!selectionNode || selectionNode.status !== "verified") {
242
- errors.push(error("commit-selection-not-verified", `Candidate ${candidateId} selection ${selection.id} is not verified`, {
243
- nodeId: selectionNode?.id,
244
- details: { candidateId, selectionId: selection.id, status: selectionNode?.status || "missing" }
245
- }));
246
- }
247
- if (!selection.scoreId) {
248
- errors.push(error("commit-candidate-unscored", `Candidate ${candidateId} selection ${selection.id} has no score evidence`, {
249
- details: { candidateId, selectionId: selection.id }
250
- }));
251
- }
252
- }
253
- }
254
- }
255
- if (!verifierNodeId) {
159
+ const resolution = {
160
+ verifierNodeId: options.verifierNodeId || taskVerifierNodeId,
161
+ candidateId: options.candidateId,
162
+ selectionId: options.selectionId,
163
+ selectionNodeId: undefined,
164
+ errors
165
+ };
166
+ resolveSelectionForCommit(run, resolution);
167
+ resolveCandidateForCommit(run, resolution);
168
+ if (!resolution.verifierNodeId) {
256
169
  errors.push(error("commit-verifier-required", "Verifier-gated commit requires --verifier, --candidate, or --selection", {
257
170
  details: {
258
171
  hint: "Use --allow-unverified-checkpoint to write a non-gated checkpoint."
259
172
  }
260
173
  }));
261
174
  }
262
- const verifierNode = verifierNodeId ? findNode(run, verifierNodeId) : undefined;
263
- if (verifierNodeId && !verifierNode) {
264
- errors.push(error("commit-verifier-not-found", `Verifier node not found: ${verifierNodeId}`, { details: { verifierNodeId } }));
175
+ const verifierNode = resolution.verifierNodeId ? findNode(run, resolution.verifierNodeId) : undefined;
176
+ if (resolution.verifierNodeId && !verifierNode) {
177
+ errors.push(error("commit-verifier-not-found", `Verifier node not found: ${resolution.verifierNodeId}`, {
178
+ details: { verifierNodeId: resolution.verifierNodeId }
179
+ }));
265
180
  }
266
181
  if (verifierNode) {
267
- if (verifierNode.kind !== "verifier") {
268
- errors.push(error("commit-verifier-wrong-kind", `Node ${verifierNode.id} is not a verifier node`, {
269
- nodeId: verifierNode.id,
270
- details: { verifierNodeId: verifierNode.id, kind: verifierNode.kind }
271
- }));
272
- }
273
- if (verifierNode.status !== "verified") {
274
- errors.push(error("commit-verifier-not-verified", `Verifier node ${verifierNode.id} is ${verifierNode.status}`, {
275
- nodeId: verifierNode.id,
276
- details: { verifierNodeId: verifierNode.id, status: verifierNode.status }
277
- }));
278
- }
279
- // HARD no-false-green gate (v0.1.43): if the backing result was an
280
- // empty-capture (no findings AND no evidence even after robust
281
- // normalization), the verifier node only carries a non-grounded summary
282
- // fallback. That can otherwise pass the length-only path for
283
- // optional-evidence tasks and present a 0-real-evidence review as
284
- // clean/green. Block it BEFORE the rationale is built so the commit fails
285
- // visibly (commit-gate-failed node + feedback) instead of silently passing.
286
- const captureWarning = emptyCaptureWarning(run, verifierNode);
287
- if (captureWarning) {
288
- errors.push(error("commit-rationale-empty-capture", `Verifier node ${verifierNode.id} cannot back a commit: ${captureWarning}`, {
289
- nodeId: verifierNode.id,
290
- details: { verifierNodeId: verifierNode.id, reason: captureWarning }
291
- }));
182
+ groundVerifierEvidence(run, verifierNode, errors);
183
+ }
184
+ const rationale = buildCommitRationale(run, resolution, verifierNode);
185
+ const review = layerCommitReviewGate(run, resolution, errors);
186
+ return {
187
+ verifierGated: true,
188
+ verifierNodeId: resolution.verifierNodeId,
189
+ candidateId: resolution.candidateId,
190
+ selectionId: resolution.selectionId,
191
+ selectionNodeId: resolution.selectionNodeId,
192
+ evidence: verifierNode?.evidence || [],
193
+ errors,
194
+ rationale,
195
+ review,
196
+ metadata: {
197
+ ...metadata,
198
+ verifierNodeId: resolution.verifierNodeId,
199
+ candidateId: resolution.candidateId,
200
+ selectionId: resolution.selectionId,
201
+ selectionNodeId: resolution.selectionNodeId
292
202
  }
293
- if (!verifierNode.evidence.length) {
294
- errors.push(error("commit-verifier-missing-evidence", `Verifier node ${verifierNode.id} has no evidence`, {
203
+ };
204
+ }
205
+ /** Selection pass: when a selectionId is supplied, resolve the candidate it
206
+ * carries, the linked verifier, and its state node — pushing the SAME errors as
207
+ * the inline block did, in the same order. No-op when no selectionId is set. */
208
+ function resolveSelectionForCommit(run, resolution) {
209
+ const { selectionId, errors } = resolution;
210
+ if (!selectionId)
211
+ return;
212
+ const selection = findSelection(run, selectionId);
213
+ if (!selection) {
214
+ errors.push(error("commit-selection-not-found", `Commit selection not found: ${selectionId}`, { details: { selectionId } }));
215
+ return;
216
+ }
217
+ resolution.candidateId = resolution.candidateId || selection.candidateId;
218
+ resolution.verifierNodeId = resolveLinkedVerifier(resolution.verifierNodeId, selection.verifierNodeId, errors, "selection", selection.id);
219
+ const selectionNode = findSelectionNode(run, selection.id);
220
+ resolution.selectionNodeId = selectionNode?.id;
221
+ if (!selectionNode) {
222
+ errors.push(error("commit-selection-node-missing", `Selection ${selection.id} has no state node`, {
223
+ details: { selectionId: selection.id, candidateId: selection.candidateId }
224
+ }));
225
+ }
226
+ else if (selectionNode.kind !== "candidate" || selectionNode.status !== "verified") {
227
+ errors.push(error("commit-selection-not-verified", `Selection ${selection.id} is not a verified candidate selection`, {
228
+ nodeId: selectionNode.id,
229
+ details: { selectionId: selection.id, status: selectionNode.status, kind: selectionNode.kind }
230
+ }));
231
+ }
232
+ if (!selection.scoreId) {
233
+ errors.push(error("commit-candidate-unscored", `Selection ${selection.id} has no score evidence`, {
234
+ details: { selectionId: selection.id, candidateId: selection.candidateId }
235
+ }));
236
+ }
237
+ }
238
+ /** Candidate pass: when a candidateId is resolved, enforce its status/score bar
239
+ * and bind it to a verified selection (the supplied one, else the latest). Same
240
+ * errors, same order, same mutations as the inline block. No-op without one. */
241
+ function resolveCandidateForCommit(run, resolution) {
242
+ const { candidateId, errors } = resolution;
243
+ if (!candidateId)
244
+ return;
245
+ const candidate = findCandidate(run, candidateId);
246
+ if (!candidate) {
247
+ errors.push(error("commit-candidate-not-found", `Commit candidate not found: ${candidateId}`, { details: { candidateId } }));
248
+ return;
249
+ }
250
+ if (candidate.status === "rejected" || candidate.status === "failed") {
251
+ errors.push(error("commit-candidate-not-selectable", `Candidate ${candidateId} is ${candidate.status}`, {
252
+ details: { candidateId, status: candidate.status }
253
+ }));
254
+ }
255
+ if (!candidate.scores.length) {
256
+ errors.push(error("commit-candidate-unscored", `Candidate ${candidateId} has no score evidence`, {
257
+ details: { candidateId }
258
+ }));
259
+ }
260
+ if (candidate.status !== "verified") {
261
+ errors.push(error("commit-candidate-not-verified", `Candidate ${candidateId} is not verifier-gated`, {
262
+ details: { candidateId, status: candidate.status }
263
+ }));
264
+ }
265
+ const selection = resolution.selectionId ? findSelection(run, resolution.selectionId) : latestSelectionForCandidate(run, candidateId);
266
+ if (!selection) {
267
+ errors.push(error("commit-candidate-selection-missing", `Candidate ${candidateId} has no verified selection`, {
268
+ details: { candidateId }
269
+ }));
270
+ return;
271
+ }
272
+ resolution.selectionId = selection.id;
273
+ resolution.verifierNodeId = resolveLinkedVerifier(resolution.verifierNodeId, selection.verifierNodeId || candidate.verifierNodeId, errors, "candidate", candidateId);
274
+ const selectionNode = findSelectionNode(run, selection.id);
275
+ resolution.selectionNodeId = selectionNode?.id;
276
+ if (!selectionNode || selectionNode.status !== "verified") {
277
+ errors.push(error("commit-selection-not-verified", `Candidate ${candidateId} selection ${selection.id} is not verified`, {
278
+ nodeId: selectionNode?.id,
279
+ details: { candidateId, selectionId: selection.id, status: selectionNode?.status || "missing" }
280
+ }));
281
+ }
282
+ if (!selection.scoreId) {
283
+ errors.push(error("commit-candidate-unscored", `Candidate ${candidateId} selection ${selection.id} has no score evidence`, {
284
+ details: { candidateId, selectionId: selection.id }
285
+ }));
286
+ }
287
+ }
288
+ /** Verifier-node grounding pass: kind/status checks plus the HARD no-false-green
289
+ * gate (empty-capture) and the grounded-evidence gate. These remain EXACTLY as
290
+ * strict — same predicates, same error codes, same order — only lifted out of
291
+ * the monolith. */
292
+ function groundVerifierEvidence(run, verifierNode, errors) {
293
+ if (verifierNode.kind !== "verifier") {
294
+ errors.push(error("commit-verifier-wrong-kind", `Node ${verifierNode.id} is not a verifier node`, {
295
+ nodeId: verifierNode.id,
296
+ details: { verifierNodeId: verifierNode.id, kind: verifierNode.kind }
297
+ }));
298
+ }
299
+ if (verifierNode.status !== "verified") {
300
+ errors.push(error("commit-verifier-not-verified", `Verifier node ${verifierNode.id} is ${verifierNode.status}`, {
301
+ nodeId: verifierNode.id,
302
+ details: { verifierNodeId: verifierNode.id, status: verifierNode.status }
303
+ }));
304
+ }
305
+ // HARD no-false-green gate (v0.1.43): if the backing result was an
306
+ // empty-capture (no findings AND no evidence even after robust
307
+ // normalization), the verifier node only carries a non-grounded summary
308
+ // fallback. That can otherwise pass the length-only path for
309
+ // optional-evidence tasks and present a 0-real-evidence review as
310
+ // clean/green. Block it BEFORE the rationale is built so the commit fails
311
+ // visibly (commit-gate-failed node + feedback) instead of silently passing.
312
+ const captureWarning = (0, gates_1.emptyCaptureWarning)(run, verifierNode);
313
+ if (captureWarning) {
314
+ errors.push(error("commit-rationale-empty-capture", `Verifier node ${verifierNode.id} cannot back a commit: ${captureWarning}`, {
315
+ nodeId: verifierNode.id,
316
+ details: { verifierNodeId: verifierNode.id, reason: captureWarning }
317
+ }));
318
+ }
319
+ if (!verifierNode.evidence.length) {
320
+ errors.push(error("commit-verifier-missing-evidence", `Verifier node ${verifierNode.id} has no evidence`, {
321
+ nodeId: verifierNode.id,
322
+ details: { verifierNodeId: verifierNode.id }
323
+ }));
324
+ }
325
+ else if (verifierNodeRequiresEvidence(run, verifierNode)) {
326
+ // Evidence grounding (v0.1.40 self-audit P1/P2): for verifier nodes whose
327
+ // task REQUIRES evidence (verify/verdict/requiresEvidence, and explicit
328
+ // candidate/selection commits), the gate must not accept unverifiable free
329
+ // text. Require at least one GROUNDED locator (path-like / URL /
330
+ // namespace:value), and — when the operator opts in via
331
+ // CW_REQUIRE_RESOLVABLE_EVIDENCE — that file-style locators actually resolve
332
+ // on disk. Closes the "presence != existence" gap. Optional-evidence tasks
333
+ // (e.g. map/assess) keep the length-only check so the gate is never stricter
334
+ // than result acceptance was.
335
+ const locators = verifierNode.evidence.map(evidenceLocatorString).filter(Boolean);
336
+ if (!(0, evidence_grounding_1.hasGroundedEvidence)(locators)) {
337
+ errors.push(error("commit-verifier-evidence-ungrounded", `Verifier node ${verifierNode.id} evidence is not grounded (needs a path-like locator, URL, or namespace:value token)`, {
295
338
  nodeId: verifierNode.id,
296
- details: { verifierNodeId: verifierNode.id }
339
+ details: { verifierNodeId: verifierNode.id, evidence: locators }
297
340
  }));
298
341
  }
299
- else if (verifierNodeRequiresEvidence(run, verifierNode)) {
300
- // Evidence grounding (v0.1.40 self-audit P1/P2): for verifier nodes whose
301
- // task REQUIRES evidence (verify/verdict/requiresEvidence, and explicit
302
- // candidate/selection commits), the gate must not accept unverifiable free
303
- // text. Require at least one GROUNDED locator (path-like / URL /
304
- // namespace:value), and — when the operator opts in via
305
- // CW_REQUIRE_RESOLVABLE_EVIDENCE — that file-style locators actually resolve
306
- // on disk. Closes the "presence != existence" gap. Optional-evidence tasks
307
- // (e.g. map/assess) keep the length-only check so the gate is never stricter
308
- // than result acceptance was.
309
- const locators = verifierNode.evidence.map(evidenceLocatorString).filter(Boolean);
310
- if (!(0, evidence_grounding_1.hasGroundedEvidence)(locators)) {
311
- errors.push(error("commit-verifier-evidence-ungrounded", `Verifier node ${verifierNode.id} evidence is not grounded (needs a path-like locator, URL, or namespace:value token)`, {
342
+ if ((0, evidence_grounding_1.requireResolvableEvidence)()) {
343
+ const unresolved = (0, evidence_grounding_1.unresolvedFileEvidence)(locators, commitEvidenceBaseDirs(run));
344
+ if (unresolved.length) {
345
+ errors.push(error("commit-verifier-evidence-unresolvable", `Verifier node ${verifierNode.id} cites file evidence that does not resolve on disk: ${unresolved.join(", ")}`, {
312
346
  nodeId: verifierNode.id,
313
- details: { verifierNodeId: verifierNode.id, evidence: locators }
347
+ details: { verifierNodeId: verifierNode.id, unresolved }
314
348
  }));
315
349
  }
316
- if ((0, evidence_grounding_1.requireResolvableEvidence)()) {
317
- const unresolved = (0, evidence_grounding_1.unresolvedFileEvidence)(locators, commitEvidenceBaseDirs(run));
318
- if (unresolved.length) {
319
- errors.push(error("commit-verifier-evidence-unresolvable", `Verifier node ${verifierNode.id} cites file evidence that does not resolve on disk: ${unresolved.join(", ")}`, {
320
- nodeId: verifierNode.id,
321
- details: { verifierNodeId: verifierNode.id, unresolved }
322
- }));
323
- }
324
- }
325
350
  }
326
351
  }
327
- let rationale;
328
- if (verifierGated && candidateId && selectionId) {
329
- const candidate = findCandidate(run, candidateId);
330
- const selection = findSelection(run, selectionId);
331
- const score = selection?.scoreId ? findScore(run, candidateId, selection.scoreId) : undefined;
332
- rationale = selection?.acceptanceRationale || (0, trust_audit_1.buildAcceptanceRationale)({
333
- selectedCandidateId: candidateId,
334
- scoreId: selection?.scoreId,
335
- scoreCriteria: score?.criteria,
336
- verifierNodeId,
337
- evidenceCount: verifierNode?.evidence.length || 0,
338
- sandboxProfileId: sandboxProfileForCandidate(run, candidate),
339
- workerId: candidate?.workerId,
340
- commitGateResult: "passed"
341
- });
342
- for (const failure of (0, trust_audit_1.validateAcceptanceRationale)(rationale)) {
343
- errors.push(error("commit-rationale-incomplete", `Verifier-gated commit cannot explain acceptance: ${failure}`, {
344
- details: { candidateId, selectionId, verifierNodeId }
345
- }));
346
- }
352
+ }
353
+ /** Rationale pass: when candidate + selection are both resolved, reuse the
354
+ * selection's acceptance rationale or rebuild it, then push completeness errors.
355
+ * Returns the rationale (or undefined). Identical to the inline block. */
356
+ function buildCommitRationale(run, resolution, verifierNode) {
357
+ const { candidateId, selectionId, verifierNodeId, errors } = resolution;
358
+ if (!candidateId || !selectionId)
359
+ return undefined;
360
+ const candidate = findCandidate(run, candidateId);
361
+ const selection = findSelection(run, selectionId);
362
+ const score = selection?.scoreId ? findScore(run, candidateId, selection.scoreId) : undefined;
363
+ const rationale = selection?.acceptanceRationale || (0, trust_audit_1.buildAcceptanceRationale)({
364
+ selectedCandidateId: candidateId,
365
+ scoreId: selection?.scoreId,
366
+ scoreCriteria: score?.criteria,
367
+ verifierNodeId,
368
+ evidenceCount: verifierNode?.evidence.length || 0,
369
+ sandboxProfileId: (0, gates_1.sandboxProfileForCandidate)(run, candidate),
370
+ workerId: candidate?.workerId,
371
+ commitGateResult: "passed"
372
+ });
373
+ for (const failure of (0, trust_audit_1.validateAcceptanceRationale)(rationale)) {
374
+ errors.push(error("commit-rationale-incomplete", `Verifier-gated commit cannot explain acceptance: ${failure}`, {
375
+ details: { candidateId, selectionId, verifierNodeId }
376
+ }));
347
377
  }
348
- // REVIEW GATE — POLICY layered ON TOP of the verifier MECHANISM. These errors
349
- // can only ADD constraints (required approvals from authorized roles); they
350
- // never relax verifier acceptance. Empty unless a policy applies to commits.
351
- // Fail closed: a commit lacking its required approvals is BLOCKED here.
378
+ return rationale;
379
+ }
380
+ /** Review-gate pass POLICY layered ON TOP of the verifier MECHANISM. These
381
+ * errors can only ADD constraints (required approvals from authorized roles);
382
+ * they never relax verifier acceptance. Empty unless a policy applies to
383
+ * commits. Fail closed: a commit lacking its required approvals is BLOCKED here,
384
+ * and provenance is only emitted when NO errors remain. */
385
+ function layerCommitReviewGate(run, resolution, errors) {
386
+ const { candidateId, selectionId } = resolution;
352
387
  const reviewErrors = (0, collaboration_1.reviewGateErrors)(run, {
353
388
  targetKind: "commit",
354
389
  candidateId,
@@ -356,7 +391,7 @@ function resolveCommitGate(run, options) {
356
391
  selfActorIds: (0, collaboration_1.selfActorIdsForCandidate)(run, candidateId, selectionId)
357
392
  });
358
393
  errors.push(...reviewErrors);
359
- const review = errors.length
394
+ return errors.length
360
395
  ? undefined
361
396
  : (0, collaboration_1.commitReviewProvenance)(run, {
362
397
  targetKind: "commit",
@@ -364,24 +399,6 @@ function resolveCommitGate(run, options) {
364
399
  selectionId,
365
400
  selfActorIds: (0, collaboration_1.selfActorIdsForCandidate)(run, candidateId, selectionId)
366
401
  });
367
- return {
368
- verifierGated: true,
369
- verifierNodeId,
370
- candidateId,
371
- selectionId,
372
- selectionNodeId,
373
- evidence: verifierNode?.evidence || [],
374
- errors,
375
- rationale,
376
- review,
377
- metadata: {
378
- ...metadata,
379
- verifierNodeId,
380
- candidateId,
381
- selectionId,
382
- selectionNodeId
383
- }
384
- };
385
402
  }
386
403
  function recordCommitNode(run, commit, options, gate) {
387
404
  const contract = (0, state_node_1.upsertRunContract)(run, (0, pipeline_contract_1.createDefaultPipelineContract)());
@@ -440,7 +457,7 @@ function recordCommitNode(run, commit, options, gate) {
440
457
  function recordCommitGateFailure(run, options, gate) {
441
458
  const first = gate.errors[0] || error("commit-gate-blocked", "Verifier-gated commit blocked");
442
459
  const node = (0, state_node_1.recordNodeError)((0, state_node_1.createStateNode)({
443
- id: `${run.id}:commit-gate-failed:${createCommitId()}`,
460
+ id: `${run.id}:commit-gate-failed:${gateFailureSeq(run)}`,
444
461
  kind: "error",
445
462
  status: "pending",
446
463
  loopStage: "checkpoint",
@@ -511,7 +528,7 @@ function resolveLinkedVerifier(requested, linked, errors, ownerKind, ownerId) {
511
528
  function latestSelectionForCandidate(run, candidateId) {
512
529
  return [...(run.candidateSelections || [])]
513
530
  .filter((selection) => selection.candidateId === candidateId)
514
- .sort((left, right) => right.selectedAt.localeCompare(left.selectedAt))[0];
531
+ .sort((left, right) => (0, compare_1.compareBytes)(right.selectedAt, left.selectedAt))[0];
515
532
  }
516
533
  function findSelection(run, selectionId) {
517
534
  return (run.candidateSelections || []).find((selection) => selection.id === selectionId);
@@ -533,13 +550,6 @@ function findScore(run, candidateId, scoreId) {
533
550
  return undefined;
534
551
  }
535
552
  }
536
- function sandboxProfileForCandidate(run, candidate) {
537
- const worker = candidate?.workerId ? (run.workers || []).find((entry) => entry.id === candidate.workerId) : undefined;
538
- if (worker?.sandboxProfileId)
539
- return worker.sandboxProfileId;
540
- const task = candidate?.taskId ? (run.tasks || []).find((entry) => entry.id === candidate.taskId) : undefined;
541
- return task?.sandboxProfileId;
542
- }
543
553
  function findNode(run, nodeId) {
544
554
  return (run.nodes || []).find((node) => node.id === nodeId);
545
555
  }
@@ -552,9 +562,22 @@ function error(code, message, options = {}) {
552
562
  ...options
553
563
  };
554
564
  }
555
- function createCommitId() {
556
- const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\..+/, "Z");
557
- return `state-${stamp}-${(0, state_1.safeFileName)(Math.random().toString(36).slice(2, 8))}`;
565
+ // Deterministic commit id (FreeBSD-audit L12/L13): the commit's POSITION in the
566
+ // run's append-only commit log, not a wall-clock stamp + PRNG suffix. Re-running
567
+ // the same workflow mints byte-identical commit ids, so snapshot/replay digests
568
+ // match. The sequence is unique within a run (commits only ever append), and the
569
+ // commitState caller writes the snapshot under this id before pushing the commit.
570
+ function createCommitId(run) {
571
+ const seq = (run.commits || []).length + 1;
572
+ return `state-${String(seq).padStart(4, "0")}`;
573
+ }
574
+ // Deterministic suffix for a blocked-commit node id. Counts the commit-gate-failed
575
+ // nodes already recorded on the run and returns the next position, so repeated
576
+ // gate failures in one run stay collision-free and replay-stable.
577
+ function gateFailureSeq(run) {
578
+ const marker = ":commit-gate-failed:";
579
+ const seq = (run.nodes || []).filter((node) => node.id.includes(marker)).length + 1;
580
+ return String(seq).padStart(4, "0");
558
581
  }
559
582
  function readGitHead(cwd) {
560
583
  try {
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.compareBytes = compareBytes;
4
+ // Locale-INDEPENDENT total order for strings (FreeBSD-audit L12 determinism).
5
+ //
6
+ // `String.localeCompare` is host/locale-sensitive: two hosts can order the same
7
+ // byte-identical strings differently. That is fine for human-facing display, but
8
+ // FATAL when the sorted order feeds a sha256 digest, a tombstone/hash chain, or a
9
+ // stable-persisted projection (index.json, messages.jsonl, an export manifest):
10
+ // the same content would then serialize/hash differently across hosts, breaking
11
+ // CW's cross-host reproducibility and replay-determinism guarantees.
12
+ //
13
+ // Use compareBytes() for any ordering that flows into a hash, an export, a
14
+ // persisted projection, or a commit/replay-bearing decision. Pure display sorts
15
+ // may keep localeCompare.
16
+ function compareBytes(a, b) {
17
+ return a < b ? -1 : a > b ? 1 : 0;
18
+ }
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.statusToNodeStatus = statusToNodeStatus;
4
+ exports.decisionStatus = decisionStatus;
5
+ exports.auditDecision = auditDecision;
6
+ exports.sourceForAuthor = sourceForAuthor;
7
+ function statusToNodeStatus(status) {
8
+ switch (status) {
9
+ case "active":
10
+ case "open":
11
+ return "running";
12
+ case "resolved":
13
+ case "superseded":
14
+ return "completed";
15
+ case "conflicting":
16
+ return "blocked";
17
+ case "rejected":
18
+ return "rejected";
19
+ default:
20
+ return "completed";
21
+ }
22
+ }
23
+ function decisionStatus(outcome) {
24
+ if (outcome === "conflicting" || outcome === "blocked")
25
+ return "conflicting";
26
+ if (outcome === "rejected")
27
+ return "rejected";
28
+ if (outcome === "superseded")
29
+ return "superseded";
30
+ return "active";
31
+ }
32
+ function auditDecision(outcome) {
33
+ if (outcome === "rejected")
34
+ return "rejected";
35
+ if (outcome === "blocked" || outcome === "conflicting")
36
+ return "failed";
37
+ return "accepted";
38
+ }
39
+ function sourceForAuthor(author) {
40
+ if (author.kind === "runtime" || author.kind === "coordinator")
41
+ return "runtime-derived";
42
+ if (author.kind === "worker" || author.kind === "verifier")
43
+ return "cw-validated";
44
+ return "operator-recorded";
45
+ }
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.boardPaths = boardPaths;
7
+ exports.blackboardRoot = blackboardRoot;
8
+ exports.messagesPath = messagesPath;
9
+ exports.recordPath = recordPath;
10
+ // Filesystem path derivation for the coordinator/blackboard layer
11
+ // (FreeBSD-audit R-carve). Carved out of coordinator.ts so the module no longer
12
+ // bundles the per-run path computation alongside the stateful blackboard
13
+ // operations. Re-exported from coordinator.ts to keep the public surface
14
+ // byte-identical.
15
+ //
16
+ // BEHAVIOR-PRESERVING — pure code movement, zero logic change. Each function is a
17
+ // function of a WorkflowRun's paths only: it reads run.paths and joins names; it
18
+ // never mutates run, never touches the blackboard state, never writes the disk.
19
+ const node_path_1 = __importDefault(require("node:path"));
20
+ const state_1 = require("../state");
21
+ function boardPaths(run) {
22
+ const root = blackboardRoot(run);
23
+ return {
24
+ root,
25
+ index: node_path_1.default.join(root, "index.json"),
26
+ messages: messagesPath(run),
27
+ topicsDir: node_path_1.default.join(root, "topics"),
28
+ contextsDir: node_path_1.default.join(root, "contexts"),
29
+ artifactsDir: node_path_1.default.join(root, "artifacts"),
30
+ snapshotsDir: node_path_1.default.join(root, "snapshots"),
31
+ decisionsDir: node_path_1.default.join(root, "decisions")
32
+ };
33
+ }
34
+ function blackboardRoot(run) {
35
+ return run.paths.blackboardDir || node_path_1.default.join(run.paths.runDir, "blackboard");
36
+ }
37
+ function messagesPath(run) {
38
+ return node_path_1.default.join(blackboardRoot(run), "messages.jsonl");
39
+ }
40
+ function recordPath(run, kind, id) {
41
+ return node_path_1.default.join(blackboardRoot(run), kind, `${(0, state_1.safeFileName)(id)}.json`);
42
+ }