cool-workflow 0.1.81 → 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 (66) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/README.md +1 -1
  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/candidate-scoring.js +20 -26
  11. package/dist/capability-core.js +64 -85
  12. package/dist/capability-registry.js +22 -3
  13. package/dist/commit.js +212 -203
  14. package/dist/coordinator/util.js +6 -9
  15. package/dist/dispatch.js +11 -3
  16. package/dist/evidence-reasoning.js +4 -1
  17. package/dist/execution-backend/agent.js +11 -48
  18. package/dist/execution-backend.js +11 -31
  19. package/dist/gates.js +48 -0
  20. package/dist/multi-agent/helpers.js +6 -10
  21. package/dist/multi-agent/ids.js +20 -0
  22. package/dist/multi-agent-eval.js +27 -1
  23. package/dist/multi-agent-host.js +53 -21
  24. package/dist/multi-agent-operator-ux.js +2 -1
  25. package/dist/multi-agent-trust.js +5 -5
  26. package/dist/node-projection.js +59 -0
  27. package/dist/node-snapshot.js +8 -18
  28. package/dist/orchestrator/lifecycle-operations.js +22 -1
  29. package/dist/orchestrator.js +16 -2
  30. package/dist/reclamation.js +8 -36
  31. package/dist/scheduler.js +34 -4
  32. package/dist/topology.js +25 -4
  33. package/dist/trust-audit.js +70 -38
  34. package/dist/validation.js +328 -0
  35. package/dist/version.js +1 -1
  36. package/dist/worker-isolation.js +143 -58
  37. package/docs/agent-delegation-drive.7.md +1 -0
  38. package/docs/cli-mcp-parity.7.md +1 -0
  39. package/docs/contract-migration-tooling.7.md +1 -0
  40. package/docs/control-plane-scheduling.7.md +1 -0
  41. package/docs/durable-state-and-locking.7.md +1 -0
  42. package/docs/evidence-adoption-reasoning-chain.7.md +1 -0
  43. package/docs/execution-backends.7.md +1 -0
  44. package/docs/multi-agent-cli-mcp-surface.7.md +1 -0
  45. package/docs/multi-agent-eval-replay-harness.7.md +1 -0
  46. package/docs/multi-agent-operator-ux.7.md +1 -0
  47. package/docs/node-snapshot-diff-replay.7.md +1 -0
  48. package/docs/observability-cost-accounting.7.md +1 -0
  49. package/docs/project-index.md +8 -4
  50. package/docs/real-execution-backends.7.md +1 -0
  51. package/docs/release-and-migration.7.md +1 -0
  52. package/docs/release-tooling.7.md +33 -2
  53. package/docs/run-registry-control-plane.7.md +1 -0
  54. package/docs/run-retention-reclamation.7.md +1 -0
  55. package/docs/state-explosion-management.7.md +1 -0
  56. package/docs/team-collaboration.7.md +1 -0
  57. package/docs/web-desktop-workbench.7.md +1 -0
  58. package/manifest/plugin.manifest.json +1 -1
  59. package/manifest/source-context-profiles.json +1 -1
  60. package/package.json +1 -1
  61. package/scripts/canonical-apps.js +4 -4
  62. package/scripts/children/batch-delegate-child.js +58 -0
  63. package/scripts/children/http-delegate-child.js +39 -0
  64. package/scripts/dogfood-release.js +1 -1
  65. package/scripts/golden-path.js +4 -4
  66. package/scripts/release-flow.js +181 -5
package/dist/commit.js CHANGED
@@ -18,6 +18,7 @@ const collaboration_1 = require("./collaboration");
18
18
  const evidence_grounding_1 = require("./evidence-grounding");
19
19
  const verifier_1 = require("./verifier");
20
20
  const compare_1 = require("./compare");
21
+ const gates_1 = require("./gates");
21
22
  class CommitGateError extends Error {
22
23
  structured;
23
24
  feedbackId;
@@ -119,26 +120,6 @@ function verifierNodeRequiresEvidence(run, verifierNode) {
119
120
  return (0, verifier_1.taskRequiresEvidence)(task);
120
121
  return true; // candidate/selection verifier with no 1:1 task — enforce grounding.
121
122
  }
122
- /** The HARD no-false-green gate (DIRECTION.md "ambiguity is a visible state").
123
- * A verifier node is built FROM a result node; when that result captured no
124
- * structured signal at all the result node carries an `metadata.captureWarning`
125
- * marker (set in worker-isolation / lifecycle ingest via isEmptyCapture). The
126
- * worker output is still ACCEPTED (a recorded warning, never a silent pass), but
127
- * a verifier-GATED commit must NOT be able to present that zero-evidence result
128
- * as clean/green. We detect it here, reading ONLY persisted state (the source
129
- * result node's metadata) — purely functional, no clock/ordering — so snapshot
130
- * replay reaches the same gate decision. Returns the marker string, or undefined.
131
- *
132
- * Resolution trail: verifier node -> its input/parent result node. We look at
133
- * `inputs.inputNodeId` (set by runPipelineStage) first, then fall back to the
134
- * first parent, so it works regardless of which ingest path produced the node. */
135
- function emptyCaptureWarning(run, verifierNode) {
136
- const resultNodeId = (typeof verifierNode.inputs?.inputNodeId === "string" ? verifierNode.inputs.inputNodeId : undefined) ||
137
- verifierNode.parents[0];
138
- const resultNode = resultNodeId ? findNode(run, resultNodeId) : undefined;
139
- const warning = resultNode?.metadata?.captureWarning;
140
- return typeof warning === "string" && warning ? warning : undefined;
141
- }
142
123
  function evidenceLocatorString(entry) {
143
124
  const ref = entry.locator || entry.path || entry.summary || entry.id;
144
125
  return ref ? String(ref) : undefined;
@@ -165,10 +146,6 @@ function resolveCommitGate(run, options) {
165
146
  const taskVerifierNodeId = taskVerifierFromReason(run, options.reason);
166
147
  const explicitGate = Boolean(options.verifierNodeId || options.candidateId || options.selectionId || options.verifierGated);
167
148
  const verifierGated = explicitGate || Boolean(taskVerifierNodeId);
168
- let verifierNodeId = options.verifierNodeId || taskVerifierNodeId;
169
- let candidateId = options.candidateId;
170
- let selectionId = options.selectionId;
171
- let selectionNodeId;
172
149
  if (!verifierGated) {
173
150
  return {
174
151
  verifierGated: false,
@@ -179,177 +156,234 @@ function resolveCommitGate(run, options) {
179
156
  }
180
157
  metadata.verifierGated = true;
181
158
  metadata.checkpoint = false;
182
- if (selectionId) {
183
- const selection = findSelection(run, selectionId);
184
- if (!selection) {
185
- errors.push(error("commit-selection-not-found", `Commit selection not found: ${selectionId}`, { details: { selectionId } }));
186
- }
187
- else {
188
- candidateId = candidateId || selection.candidateId;
189
- verifierNodeId = resolveLinkedVerifier(verifierNodeId, selection.verifierNodeId, errors, "selection", selection.id);
190
- const selectionNode = findSelectionNode(run, selection.id);
191
- selectionNodeId = selectionNode?.id;
192
- if (!selectionNode) {
193
- errors.push(error("commit-selection-node-missing", `Selection ${selection.id} has no state node`, {
194
- details: { selectionId: selection.id, candidateId: selection.candidateId }
195
- }));
196
- }
197
- else if (selectionNode.kind !== "candidate" || selectionNode.status !== "verified") {
198
- errors.push(error("commit-selection-not-verified", `Selection ${selection.id} is not a verified candidate selection`, {
199
- nodeId: selectionNode.id,
200
- details: { selectionId: selection.id, status: selectionNode.status, kind: selectionNode.kind }
201
- }));
202
- }
203
- if (!selection.scoreId) {
204
- errors.push(error("commit-candidate-unscored", `Selection ${selection.id} has no score evidence`, {
205
- details: { selectionId: selection.id, candidateId: selection.candidateId }
206
- }));
207
- }
208
- }
209
- }
210
- if (candidateId) {
211
- const candidate = findCandidate(run, candidateId);
212
- if (!candidate) {
213
- errors.push(error("commit-candidate-not-found", `Commit candidate not found: ${candidateId}`, { details: { candidateId } }));
214
- }
215
- else {
216
- if (candidate.status === "rejected" || candidate.status === "failed") {
217
- errors.push(error("commit-candidate-not-selectable", `Candidate ${candidateId} is ${candidate.status}`, {
218
- details: { candidateId, status: candidate.status }
219
- }));
220
- }
221
- if (!candidate.scores.length) {
222
- errors.push(error("commit-candidate-unscored", `Candidate ${candidateId} has no score evidence`, {
223
- details: { candidateId }
224
- }));
225
- }
226
- if (candidate.status !== "verified") {
227
- errors.push(error("commit-candidate-not-verified", `Candidate ${candidateId} is not verifier-gated`, {
228
- details: { candidateId, status: candidate.status }
229
- }));
230
- }
231
- const selection = selectionId ? findSelection(run, selectionId) : latestSelectionForCandidate(run, candidateId);
232
- if (!selection) {
233
- errors.push(error("commit-candidate-selection-missing", `Candidate ${candidateId} has no verified selection`, {
234
- details: { candidateId }
235
- }));
236
- }
237
- else {
238
- selectionId = selection.id;
239
- verifierNodeId = resolveLinkedVerifier(verifierNodeId, selection.verifierNodeId || candidate.verifierNodeId, errors, "candidate", candidateId);
240
- const selectionNode = findSelectionNode(run, selection.id);
241
- selectionNodeId = selectionNode?.id;
242
- if (!selectionNode || selectionNode.status !== "verified") {
243
- errors.push(error("commit-selection-not-verified", `Candidate ${candidateId} selection ${selection.id} is not verified`, {
244
- nodeId: selectionNode?.id,
245
- details: { candidateId, selectionId: selection.id, status: selectionNode?.status || "missing" }
246
- }));
247
- }
248
- if (!selection.scoreId) {
249
- errors.push(error("commit-candidate-unscored", `Candidate ${candidateId} selection ${selection.id} has no score evidence`, {
250
- details: { candidateId, selectionId: selection.id }
251
- }));
252
- }
253
- }
254
- }
255
- }
256
- 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) {
257
169
  errors.push(error("commit-verifier-required", "Verifier-gated commit requires --verifier, --candidate, or --selection", {
258
170
  details: {
259
171
  hint: "Use --allow-unverified-checkpoint to write a non-gated checkpoint."
260
172
  }
261
173
  }));
262
174
  }
263
- const verifierNode = verifierNodeId ? findNode(run, verifierNodeId) : undefined;
264
- if (verifierNodeId && !verifierNode) {
265
- 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
+ }));
266
180
  }
267
181
  if (verifierNode) {
268
- if (verifierNode.kind !== "verifier") {
269
- errors.push(error("commit-verifier-wrong-kind", `Node ${verifierNode.id} is not a verifier node`, {
270
- nodeId: verifierNode.id,
271
- details: { verifierNodeId: verifierNode.id, kind: verifierNode.kind }
272
- }));
273
- }
274
- if (verifierNode.status !== "verified") {
275
- errors.push(error("commit-verifier-not-verified", `Verifier node ${verifierNode.id} is ${verifierNode.status}`, {
276
- nodeId: verifierNode.id,
277
- details: { verifierNodeId: verifierNode.id, status: verifierNode.status }
278
- }));
279
- }
280
- // HARD no-false-green gate (v0.1.43): if the backing result was an
281
- // empty-capture (no findings AND no evidence even after robust
282
- // normalization), the verifier node only carries a non-grounded summary
283
- // fallback. That can otherwise pass the length-only path for
284
- // optional-evidence tasks and present a 0-real-evidence review as
285
- // clean/green. Block it BEFORE the rationale is built so the commit fails
286
- // visibly (commit-gate-failed node + feedback) instead of silently passing.
287
- const captureWarning = emptyCaptureWarning(run, verifierNode);
288
- if (captureWarning) {
289
- errors.push(error("commit-rationale-empty-capture", `Verifier node ${verifierNode.id} cannot back a commit: ${captureWarning}`, {
290
- nodeId: verifierNode.id,
291
- details: { verifierNodeId: verifierNode.id, reason: captureWarning }
292
- }));
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
293
202
  }
294
- if (!verifierNode.evidence.length) {
295
- 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)`, {
296
338
  nodeId: verifierNode.id,
297
- details: { verifierNodeId: verifierNode.id }
339
+ details: { verifierNodeId: verifierNode.id, evidence: locators }
298
340
  }));
299
341
  }
300
- else if (verifierNodeRequiresEvidence(run, verifierNode)) {
301
- // Evidence grounding (v0.1.40 self-audit P1/P2): for verifier nodes whose
302
- // task REQUIRES evidence (verify/verdict/requiresEvidence, and explicit
303
- // candidate/selection commits), the gate must not accept unverifiable free
304
- // text. Require at least one GROUNDED locator (path-like / URL /
305
- // namespace:value), and — when the operator opts in via
306
- // CW_REQUIRE_RESOLVABLE_EVIDENCE — that file-style locators actually resolve
307
- // on disk. Closes the "presence != existence" gap. Optional-evidence tasks
308
- // (e.g. map/assess) keep the length-only check so the gate is never stricter
309
- // than result acceptance was.
310
- const locators = verifierNode.evidence.map(evidenceLocatorString).filter(Boolean);
311
- if (!(0, evidence_grounding_1.hasGroundedEvidence)(locators)) {
312
- 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(", ")}`, {
313
346
  nodeId: verifierNode.id,
314
- details: { verifierNodeId: verifierNode.id, evidence: locators }
347
+ details: { verifierNodeId: verifierNode.id, unresolved }
315
348
  }));
316
349
  }
317
- if ((0, evidence_grounding_1.requireResolvableEvidence)()) {
318
- const unresolved = (0, evidence_grounding_1.unresolvedFileEvidence)(locators, commitEvidenceBaseDirs(run));
319
- if (unresolved.length) {
320
- errors.push(error("commit-verifier-evidence-unresolvable", `Verifier node ${verifierNode.id} cites file evidence that does not resolve on disk: ${unresolved.join(", ")}`, {
321
- nodeId: verifierNode.id,
322
- details: { verifierNodeId: verifierNode.id, unresolved }
323
- }));
324
- }
325
- }
326
350
  }
327
351
  }
328
- let rationale;
329
- if (verifierGated && candidateId && selectionId) {
330
- const candidate = findCandidate(run, candidateId);
331
- const selection = findSelection(run, selectionId);
332
- const score = selection?.scoreId ? findScore(run, candidateId, selection.scoreId) : undefined;
333
- rationale = selection?.acceptanceRationale || (0, trust_audit_1.buildAcceptanceRationale)({
334
- selectedCandidateId: candidateId,
335
- scoreId: selection?.scoreId,
336
- scoreCriteria: score?.criteria,
337
- verifierNodeId,
338
- evidenceCount: verifierNode?.evidence.length || 0,
339
- sandboxProfileId: sandboxProfileForCandidate(run, candidate),
340
- workerId: candidate?.workerId,
341
- commitGateResult: "passed"
342
- });
343
- for (const failure of (0, trust_audit_1.validateAcceptanceRationale)(rationale)) {
344
- errors.push(error("commit-rationale-incomplete", `Verifier-gated commit cannot explain acceptance: ${failure}`, {
345
- details: { candidateId, selectionId, verifierNodeId }
346
- }));
347
- }
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
+ }));
348
377
  }
349
- // REVIEW GATE — POLICY layered ON TOP of the verifier MECHANISM. These errors
350
- // can only ADD constraints (required approvals from authorized roles); they
351
- // never relax verifier acceptance. Empty unless a policy applies to commits.
352
- // 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;
353
387
  const reviewErrors = (0, collaboration_1.reviewGateErrors)(run, {
354
388
  targetKind: "commit",
355
389
  candidateId,
@@ -357,7 +391,7 @@ function resolveCommitGate(run, options) {
357
391
  selfActorIds: (0, collaboration_1.selfActorIdsForCandidate)(run, candidateId, selectionId)
358
392
  });
359
393
  errors.push(...reviewErrors);
360
- const review = errors.length
394
+ return errors.length
361
395
  ? undefined
362
396
  : (0, collaboration_1.commitReviewProvenance)(run, {
363
397
  targetKind: "commit",
@@ -365,24 +399,6 @@ function resolveCommitGate(run, options) {
365
399
  selectionId,
366
400
  selfActorIds: (0, collaboration_1.selfActorIdsForCandidate)(run, candidateId, selectionId)
367
401
  });
368
- return {
369
- verifierGated: true,
370
- verifierNodeId,
371
- candidateId,
372
- selectionId,
373
- selectionNodeId,
374
- evidence: verifierNode?.evidence || [],
375
- errors,
376
- rationale,
377
- review,
378
- metadata: {
379
- ...metadata,
380
- verifierNodeId,
381
- candidateId,
382
- selectionId,
383
- selectionNodeId
384
- }
385
- };
386
402
  }
387
403
  function recordCommitNode(run, commit, options, gate) {
388
404
  const contract = (0, state_node_1.upsertRunContract)(run, (0, pipeline_contract_1.createDefaultPipelineContract)());
@@ -534,13 +550,6 @@ function findScore(run, candidateId, scoreId) {
534
550
  return undefined;
535
551
  }
536
552
  }
537
- function sandboxProfileForCandidate(run, candidate) {
538
- const worker = candidate?.workerId ? (run.workers || []).find((entry) => entry.id === candidate.workerId) : undefined;
539
- if (worker?.sandboxProfileId)
540
- return worker.sandboxProfileId;
541
- const task = candidate?.taskId ? (run.tasks || []).find((entry) => entry.id === candidate.taskId) : undefined;
542
- return task?.sandboxProfileId;
543
- }
544
553
  function findNode(run, nodeId) {
545
554
  return (run.nodes || []).find((node) => node.id === nodeId);
546
555
  }
@@ -3,13 +3,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createId = void 0;
6
7
  exports.checksumFile = checksumFile;
7
8
  exports.assertUnique = assertUnique;
8
9
  exports.assertNoRecordPathCollisions = assertNoRecordPathCollisions;
9
10
  exports.indexRow = indexRow;
10
11
  exports.compareRecords = compareRecords;
11
12
  exports.uniqueEdges = uniqueEdges;
12
- exports.createId = createId;
13
13
  exports.touch = touch;
14
14
  exports.timestamp = timestamp;
15
15
  exports.unique = unique;
@@ -68,14 +68,11 @@ function uniqueEdges(edges) {
68
68
  }
69
69
  return result;
70
70
  }
71
- // Deterministic record id (FreeBSD-audit L12/L13): the record's POSITION in its
72
- // per-run blackboard collection, threaded from the call site. No wall-clock stamp,
73
- // no PRNG suffix replaying the same coordination mints byte-identical ids, so
74
- // snapshot/replay digests match. Each call site already asserts the minted id is
75
- // unique within its collection, and these collections only ever append.
76
- function createId(prefix, seq) {
77
- return `${prefix}-${String(seq).padStart(4, "0")}`;
78
- }
71
+ // Deterministic record id single source of truth in ../multi-agent/ids.
72
+ // Re-exported here so coordinator.ts importers stay byte-unchanged (F10 dedup:
73
+ // the multi-agent kernel shares the exact same helper).
74
+ var ids_1 = require("../multi-agent/ids");
75
+ Object.defineProperty(exports, "createId", { enumerable: true, get: function () { return ids_1.createId; } });
79
76
  function touch(record) {
80
77
  record.updatedAt = timestamp();
81
78
  return record;
package/dist/dispatch.js CHANGED
@@ -55,7 +55,7 @@ function createDispatchManifest(run, limit, options = {}) {
55
55
  backendSelection
56
56
  };
57
57
  }
58
- const dispatchId = createDispatchId();
58
+ const dispatchId = createDispatchId(run);
59
59
  const manifestPath = node_path_1.default.join(run.paths.dispatchesDir, `${dispatchId}.json`);
60
60
  node_fs_1.default.mkdirSync(run.paths.dispatchesDir, { recursive: true });
61
61
  const taskIds = new Set(tasks.map((task) => task.id));
@@ -201,9 +201,17 @@ function formatDispatchTask(task) {
201
201
  multiAgent: task.multiAgent
202
202
  };
203
203
  }
204
- function createDispatchId() {
204
+ // Deterministic dispatch id (replay-determinism self-audit): the wall-clock stamp
205
+ // is an edge timestamp (stripped on replay), but the former Math.random() suffix
206
+ // made every dispatch mint a non-reproducible id. The suffix is now a per-run
207
+ // sequence — the count of dispatches already allocated on this run — so re-running
208
+ // the same workflow yields byte-identical dispatch ids while each dispatch within a
209
+ // run still gets a distinct, monotonically increasing id. Mirrors the de-clock done
210
+ // for worker ids in src/worker-isolation/paths.ts.
211
+ function createDispatchId(run) {
205
212
  const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\..+/, "Z");
206
- return `dispatch-${stamp}-${Math.random().toString(36).slice(2, 8)}`;
213
+ const seq = (run.dispatches?.length || 0) + 1;
214
+ return `dispatch-${stamp}-${String(seq).padStart(4, "0")}`;
207
215
  }
208
216
  // H7: persist a CUSTOM sandbox profile DEFINITION (loaded from a FILE at dispatch)
209
217
  // onto run.customSandboxProfiles, keyed by the definition's logical id. Only fires
@@ -19,6 +19,7 @@ const state_1 = require("./state");
19
19
  const multi_agent_operator_ux_1 = require("./multi-agent-operator-ux");
20
20
  const trust_audit_1 = require("./trust-audit");
21
21
  const multi_agent_trust_1 = require("./multi-agent-trust");
22
+ const validation_1 = require("./validation");
22
23
  // ---------------------------------------------------------------------------
23
24
  // Evidence Adoption Reasoning Chain (v0.1.26)
24
25
  //
@@ -616,7 +617,9 @@ function readAllScores(run) {
616
617
  continue;
617
618
  for (const file of node_fs_1.default.readdirSync(dir).filter((entry) => entry.endsWith(".json")).sort()) {
618
619
  try {
619
- const score = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(dir, file), "utf8"));
620
+ const score = (0, validation_1.tryValidateCandidateScore)(JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(dir, file), "utf8")));
621
+ if (!score)
622
+ continue; // Malformed/forged score shape: skip; the score gate fails closed.
620
623
  scores.set(score.id, score);
621
624
  }
622
625
  catch {
@@ -255,53 +255,16 @@ function prepareAgentSpawn(request) {
255
255
  timeoutMs: resolved.timeoutMs || 600000
256
256
  };
257
257
  }
258
- // Reads jobs JSON on stdin, spawns ALL concurrently (shell:false, inherited env —
259
- // the agent's own credentials resolve; CW never reads them), per-job SIGTERM at
260
- // timeoutMs + SIGKILL at +5s, caps each captured stdout at 32MB, and prints the
261
- // outcome array when every job has settled. stderr is drained (a full pipe must
262
- // never wedge a child). A kill yields exitCode null the no-exit-code refusal.
263
- const BATCH_DELEGATE_CHILD = `
264
- const { spawn } = require("node:child_process");
265
- let raw = "";
266
- process.stdin.setEncoding("utf8");
267
- process.stdin.on("data", (d) => (raw += d));
268
- process.stdin.on("end", () => {
269
- const jobs = JSON.parse(raw);
270
- if (!jobs.length) { process.stdout.write("[]"); return; }
271
- const out = new Array(jobs.length);
272
- let pending = jobs.length;
273
- const CAP = 32 * 1024 * 1024;
274
- jobs.forEach((job, i) => {
275
- let stdout = "";
276
- let settled = false;
277
- const settle = (o) => {
278
- if (settled) return;
279
- settled = true;
280
- out[i] = o;
281
- if (--pending === 0) process.stdout.write(JSON.stringify(out));
282
- };
283
- let child;
284
- try {
285
- child = spawn(job.binary, job.args, { cwd: job.cwd, env: process.env, shell: false });
286
- } catch (error) {
287
- settle({ spawnError: String((error && error.message) || error), exitCode: null, stdout: "" });
288
- return;
289
- }
290
- const term = setTimeout(() => { try { child.kill("SIGTERM"); } catch {} }, job.timeoutMs);
291
- const kill = setTimeout(() => { try { child.kill("SIGKILL"); } catch {} }, job.timeoutMs + 5000);
292
- child.stdout.on("data", (d) => { if (stdout.length < CAP) stdout += d; });
293
- child.stderr.on("data", () => {});
294
- child.on("error", (error) => {
295
- clearTimeout(term); clearTimeout(kill);
296
- settle({ spawnError: String((error && error.message) || error), exitCode: null, stdout });
297
- });
298
- child.on("close", (code) => {
299
- clearTimeout(term); clearTimeout(kill);
300
- settle({ exitCode: typeof code === "number" ? code : null, stdout });
301
- });
302
- });
303
- });
304
- `;
258
+ // The batch delegate child is a real, packaged Node script (not an embedded
259
+ // `node -e` string F11). It reads jobs JSON on stdin, spawns ALL concurrently
260
+ // (shell:false, inherited env the agent's own credentials resolve; CW never
261
+ // reads them), per-job SIGTERM at timeoutMs + SIGKILL at +5s, caps each captured
262
+ // stdout at 32MB, and prints the outcome array when every job has settled. stderr
263
+ // is drained (a full pipe must never wedge a child). A kill yields exitCode null —
264
+ // the no-exit-code refusal. We spawn it BY PATH (shell:false); the path is
265
+ // resolved from this compiled module (dist/execution-backend/agent.js) up to the
266
+ // package's `scripts/children/` dir, which package.json ships in "files".
267
+ const BATCH_DELEGATE_CHILD_SCRIPT = node_path_1.default.resolve(__dirname, "..", "..", "scripts", "children", "batch-delegate-child.js");
305
268
  /** Run a batch of agent spawns concurrently; outcomes index-align with jobs. The
306
269
  * parent backstop timeout (max job timeout + 30s) means even a wedged delegate
307
270
  * child cannot deadlock the drive: on any batch-level failure EVERY job settles
@@ -310,7 +273,7 @@ function runAgentBatchOutcomes(jobs) {
310
273
  if (!jobs.length)
311
274
  return [];
312
275
  const maxTimeout = Math.max(...jobs.map((job) => job.timeoutMs));
313
- const child = (0, node_child_process_1.spawnSync)(process.execPath, ["-e", BATCH_DELEGATE_CHILD], {
276
+ const child = (0, node_child_process_1.spawnSync)(process.execPath, [BATCH_DELEGATE_CHILD_SCRIPT], {
314
277
  input: JSON.stringify(jobs),
315
278
  encoding: "utf8",
316
279
  maxBuffer: 33 * 1024 * 1024 * jobs.length,