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/scheduler.js CHANGED
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.Scheduler = void 0;
7
+ const node_crypto_1 = __importDefault(require("node:crypto"));
7
8
  const node_fs_1 = __importDefault(require("node:fs"));
8
9
  const node_path_1 = __importDefault(require("node:path"));
9
10
  const state_1 = require("./state");
@@ -233,19 +234,48 @@ function matchesCron(value, expr, min, max) {
233
234
  return Number.isInteger(parsed) && parsed >= min && parsed <= max && parsed === value;
234
235
  });
235
236
  }
237
+ // Deterministic jitter (replay-determinism self-audit): the jittered Date is NOT a
238
+ // runtime-only sleep — it lands in persisted/replayed state (task.nextRunAt and,
239
+ // transitively, the schedule store), so a Math.random() offset broke replay
240
+ // determinism. The spread (0..jitterSeconds) is now derived from a content hash of
241
+ // the base instant, so the same base time + jitter window always yields the same
242
+ // offset; distinct base times still spread out across the window. The base Date is
243
+ // itself an edge timestamp that is recorded once.
236
244
  function addJitter(date, jitterSeconds) {
237
245
  if (!jitterSeconds)
238
246
  return date;
239
- const offset = Math.floor(Math.random() * (jitterSeconds + 1)) * 1000;
240
- return new Date(date.getTime() + offset);
247
+ const digest = node_crypto_1.default.createHash("sha256").update(`${date.getTime()}`).digest();
248
+ const seconds = digest.readUInt32BE(0) % (jitterSeconds + 1);
249
+ return new Date(date.getTime() + seconds * 1000);
241
250
  }
251
+ // Deterministic schedule id (replay-determinism self-audit): the stamp is an edge
252
+ // timestamp (recorded once), but the former Math.random() suffix made each
253
+ // persisted schedule id non-reproducible. The suffix is now a content hash of the
254
+ // schedule's deterministic identity (kind + the recorded stamp), so re-deriving the
255
+ // id for a recorded schedule yields the byte-identical value while schedules created
256
+ // at distinct instants still get distinct ids. Mirrors src/worker-isolation/paths.ts.
257
+ let scheduleIdSequence = 0;
242
258
  function createScheduleId(kind) {
243
259
  const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\..+/, "Z");
244
- return `${kind}-${stamp}-${Math.random().toString(36).slice(2, 8)}`;
260
+ // Second-resolution stamp: two schedules of the same kind created within one
261
+ // second would otherwise collide on an identical id. process.pid + a monotonic
262
+ // counter break the tie across concurrent processes and within one process,
263
+ // deterministically (not a PRNG).
264
+ scheduleIdSequence += 1;
265
+ const suffix = node_crypto_1.default.createHash("sha256").update(`${kind}:${stamp}:${process.pid}:${scheduleIdSequence}`).digest("hex").slice(0, 6);
266
+ return `${kind}-${stamp}-${suffix}`;
245
267
  }
268
+ // Deterministic schedule-run (history) id — same rationale as createScheduleId. The
269
+ // history record stamp differs from the owning schedule's, so the hashed identity
270
+ // (kind + run stamp) stays distinct from the schedule id while remaining a pure
271
+ // function of already-recorded state.
272
+ let scheduleRunIdSequence = 0;
246
273
  function createScheduleRunId(kind) {
247
274
  const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\..+/, "Z");
248
- return `run-${kind}-${stamp}-${Math.random().toString(36).slice(2, 8)}`;
275
+ // pid + counter break same-kind/same-second collisions (see createScheduleId).
276
+ scheduleRunIdSequence += 1;
277
+ const suffix = node_crypto_1.default.createHash("sha256").update(`run:${kind}:${stamp}:${process.pid}:${scheduleRunIdSequence}`).digest("hex").slice(0, 6);
278
+ return `run-${kind}-${stamp}-${suffix}`;
249
279
  }
250
280
  function requiredString(value, name) {
251
281
  const text = stringOption(value);
package/dist/topology.js CHANGED
@@ -17,6 +17,8 @@ exports.showTopologyRun = showTopologyRun;
17
17
  const node_fs_1 = __importDefault(require("node:fs"));
18
18
  const node_path_1 = __importDefault(require("node:path"));
19
19
  const state_1 = require("./state");
20
+ const execution_backend_1 = require("./execution-backend");
21
+ const telemetry_attestation_1 = require("./telemetry-attestation");
20
22
  const pipeline_contract_1 = require("./pipeline-contract");
21
23
  const state_node_1 = require("./state-node");
22
24
  const trust_audit_1 = require("./trust-audit");
@@ -186,10 +188,16 @@ function applyTopology(run, topologyId, input = {}) {
186
188
  const definition = validation.definition;
187
189
  const state = ensureTopologyState(run);
188
190
  (0, multi_agent_1.ensureMultiAgentState)(run);
189
- const id = input.id || `${definition.id}-${timestampId()}`;
191
+ const taskIds = selectedTaskIds(run, input.taskIds);
192
+ // Default id is a DETERMINISTIC content-hash (replay determinism): two `topology
193
+ // apply` invocations WITHOUT --id over the same definition/tasks/run produce a
194
+ // byte-identical id. Same sha256/stableStringify the kernel uses for node ids
195
+ // (state-node/createNodeId) and ledger record ids. `state.runs.length` is the
196
+ // stable sequence so repeated applies on the same run get distinct ids without a
197
+ // wall-clock stamp. input.id stays an explicit override.
198
+ const id = input.id || topologyRunId(definition, taskIds, run.id, state.runs.length);
190
199
  if (state.runs.some((record) => record.id === id))
191
200
  throw new Error(`Duplicate MultiAgentTopologyRun id: ${id}`);
192
- const taskIds = selectedTaskIds(run, input.taskIds);
193
201
  const board = (0, coordinator_1.resolveBlackboard)(run, {
194
202
  id: input.blackboardId || `${id}-blackboard`,
195
203
  title: `${definition.title} Blackboard`,
@@ -514,8 +522,21 @@ function statusToNodeStatus(status) {
514
522
  function issue(code, message, path) {
515
523
  return { code, message, path };
516
524
  }
517
- function timestampId() {
518
- return new Date().toISOString().replace(/[-:.]/g, "").slice(0, 15).toLowerCase();
525
+ // Deterministic default topology-run id (F2, replay determinism): no wall-clock.
526
+ // Bound to a short sha256 of canonical content — definition id, the SORTED role and
527
+ // selected-task ids, the workflow run id, and a stable sequence (the count of
528
+ // topology runs already on this run). Same sha256/stableStringify the kernel uses
529
+ // for node ids and ledger record hashes, so two replays without --id reach a
530
+ // byte-identical fingerprint, while distinct applies on one run stay distinct.
531
+ function topologyRunId(definition, taskIds, runId, sequence) {
532
+ const digest = (0, execution_backend_1.sha256)((0, telemetry_attestation_1.stableStringify)({
533
+ definitionId: definition.id,
534
+ roleIds: [...definition.roles.map((role) => role.id)].sort(),
535
+ taskIds: [...taskIds].sort(),
536
+ runId,
537
+ sequence
538
+ }));
539
+ return `${definition.id}-${digest.replace("sha256:", "").slice(0, 16)}`;
519
540
  }
520
541
  function countBy(items, pick) {
521
542
  const counts = {};
@@ -3,7 +3,7 @@ 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.TRUST_AUDIT_SCHEMA_VERSION = void 0;
6
+ exports.CORRELATION_ID_FIELDS = exports.TRUST_AUDIT_SCHEMA_VERSION = void 0;
7
7
  exports.trustAuditGenesis = trustAuditGenesis;
8
8
  exports.verifyTrustAudit = verifyTrustAudit;
9
9
  exports.ensureTrustAudit = ensureTrustAudit;
@@ -152,6 +152,65 @@ function verifyTrustAudit(run) {
152
152
  }
153
153
  return { present: events.length > 0, verified, eventCount: events.length, chained, unchained, corruptLines, checks };
154
154
  }
155
+ // ---- Correlation-id fields (single source of truth, F12) -------------------
156
+ // These id fields are plain pass-throughs that flow input -> persisted event ->
157
+ // summary index unchanged. They were previously re-listed by hand in three
158
+ // places (recordTrustAuditEvent, the index writer, and partially in
159
+ // multi-agent-trust), so adding a new correlation id meant editing every list and
160
+ // silently dropping it from the index when one was missed. They now live in ONE
161
+ // array; both spread sites pick from it via pickCorrelationIds, so a new id is
162
+ // added in exactly one place.
163
+ //
164
+ // SERIALIZATION-PRESERVING by design: this is a plain key-copy with NO transform,
165
+ // so the persisted JSON for these keys is byte-identical to the old hand-spread
166
+ // (compact() still drops the undefined ones). The hash chain that binds these
167
+ // fields is therefore unaffected. Fields needing derivation/normalization
168
+ // (feedbackIds, sandboxProfileId, policyRef, multiAgentPolicyRef, normalizedPath,
169
+ // envVars, …) are intentionally NOT here — they keep their bespoke handling at the
170
+ // call site.
171
+ exports.CORRELATION_ID_FIELDS = [
172
+ "candidateId",
173
+ "scoreId",
174
+ "selectionId",
175
+ "commitId",
176
+ "multiAgentRunId",
177
+ "agentRoleId",
178
+ "agentGroupId",
179
+ "agentMembershipId",
180
+ "agentFanoutId",
181
+ "agentFaninId",
182
+ "blackboardId",
183
+ "blackboardTopicId",
184
+ "blackboardMessageId",
185
+ "blackboardContextId",
186
+ "blackboardArtifactRefId",
187
+ "blackboardSnapshotId",
188
+ "coordinatorDecisionId",
189
+ "topologyId",
190
+ "topologyRunId"
191
+ ];
192
+ /** Copy exactly the correlation-id keys (and no others) from `source`, preserving
193
+ * each value verbatim (including `undefined`, which compact()/JSON.stringify drop
194
+ * identically to the prior per-key spread). Spread the result; do not transform. */
195
+ function pickCorrelationIds(source) {
196
+ const picked = {};
197
+ for (const field of exports.CORRELATION_ID_FIELDS)
198
+ picked[field] = source[field];
199
+ return picked;
200
+ }
201
+ // The summary index has always carried every correlation id EXCEPT scoreId (the
202
+ // per-candidate score lives in the candidates[] rows, not the flat event index).
203
+ // Pin that exception in ONE place so the index stays byte-identical while still
204
+ // inheriting any NEW id from CORRELATION_ID_FIELDS automatically.
205
+ const INDEX_OMITTED_CORRELATION_IDS = new Set(["scoreId"]);
206
+ /** Correlation ids for the summary index: the full pick minus the keys the index
207
+ * has historically omitted (see INDEX_OMITTED_CORRELATION_IDS). */
208
+ function indexCorrelationIds(event) {
209
+ const picked = pickCorrelationIds(event);
210
+ for (const field of INDEX_OMITTED_CORRELATION_IDS)
211
+ delete picked[field];
212
+ return picked;
213
+ }
155
214
  function ensureTrustAudit(run) {
156
215
  const auditDir = auditRoot(run);
157
216
  node_fs_1.default.mkdirSync(auditDir, { recursive: true });
@@ -177,25 +236,11 @@ function recordTrustAuditEvent(run, input) {
177
236
  taskId: input.taskId,
178
237
  nodeId: input.nodeId,
179
238
  feedbackIds: input.feedbackIds?.filter(Boolean).sort(),
180
- candidateId: input.candidateId,
181
- scoreId: input.scoreId,
182
- selectionId: input.selectionId,
183
- commitId: input.commitId,
184
- multiAgentRunId: input.multiAgentRunId,
185
- agentRoleId: input.agentRoleId,
186
- agentGroupId: input.agentGroupId,
187
- agentMembershipId: input.agentMembershipId,
188
- agentFanoutId: input.agentFanoutId,
189
- agentFaninId: input.agentFaninId,
190
- blackboardId: input.blackboardId,
191
- blackboardTopicId: input.blackboardTopicId,
192
- blackboardMessageId: input.blackboardMessageId,
193
- blackboardContextId: input.blackboardContextId,
194
- blackboardArtifactRefId: input.blackboardArtifactRefId,
195
- blackboardSnapshotId: input.blackboardSnapshotId,
196
- coordinatorDecisionId: input.coordinatorDecisionId,
197
- topologyId: input.topologyId,
198
- topologyRunId: input.topologyRunId,
239
+ // Plain correlation-id pass-throughs (candidateId … topologyRunId) come from the
240
+ // single CORRELATION_ID_FIELDS list. Key order is preserved (the list is in the
241
+ // same order the keys used to be hand-written), so the persisted JSON — and thus
242
+ // the eventHash chain — is byte-identical.
243
+ ...pickCorrelationIds(input),
199
244
  sandboxProfileId: input.sandboxProfileId || input.policySnapshot?.id,
200
245
  policyRef: input.policyRef || (input.policySnapshot?.id ? `run.sandboxProfiles.${input.policySnapshot.id}` : undefined),
201
246
  multiAgentPolicyRef: input.policyRef,
@@ -351,24 +396,11 @@ function summarizeTrustAudit(run) {
351
396
  source: event.source,
352
397
  workerId: event.workerId,
353
398
  taskId: event.taskId,
354
- candidateId: event.candidateId,
355
- selectionId: event.selectionId,
356
- commitId: event.commitId,
357
- multiAgentRunId: event.multiAgentRunId,
358
- agentRoleId: event.agentRoleId,
359
- agentGroupId: event.agentGroupId,
360
- agentMembershipId: event.agentMembershipId,
361
- agentFanoutId: event.agentFanoutId,
362
- agentFaninId: event.agentFaninId,
363
- blackboardId: event.blackboardId,
364
- blackboardTopicId: event.blackboardTopicId,
365
- blackboardMessageId: event.blackboardMessageId,
366
- blackboardContextId: event.blackboardContextId,
367
- blackboardArtifactRefId: event.blackboardArtifactRefId,
368
- blackboardSnapshotId: event.blackboardSnapshotId,
369
- coordinatorDecisionId: event.coordinatorDecisionId,
370
- topologyId: event.topologyId,
371
- topologyRunId: event.topologyRunId,
399
+ // Same single CORRELATION_ID_FIELDS list the record path uses, so the index
400
+ // can never silently omit a correlation id that the event carries. (The index
401
+ // historically omitted scoreId; that is preserved below by deleting it after
402
+ // the pick, keeping this writer's output byte-identical.)
403
+ ...indexCorrelationIds(event),
372
404
  sandboxProfileId: event.sandboxProfileId,
373
405
  policyRef: event.policyRef,
374
406
  multiAgentPolicyRef: event.multiAgentPolicyRef
@@ -0,0 +1,328 @@
1
+ "use strict";
2
+ // Fail-closed shape guards for persisted per-record types (F5, integrity boundary).
3
+ //
4
+ // `JSON.parse(...) as T` is a LIE: the cast asserts a shape TypeScript never
5
+ // checked at runtime, so a corrupt/forged/old-schema record flows in as if it
6
+ // were a valid T and is used/upserted unvalidated. These guards re-establish the
7
+ // integrity boundary at the read edge: after parse, the raw value is structurally
8
+ // validated against the type def in src/types/* BEFORE it is trusted.
9
+ //
10
+ // Two callers, two error semantics — both fail closed, neither fabricates:
11
+ // - validate*() throw a descriptive Error on mismatch (for readers that
12
+ // already let parse errors propagate / require the record).
13
+ // - tryValidate*() return null on mismatch (for best-effort readers that
14
+ // swallow parse errors and SKIP the record — the downstream
15
+ // gate then fails closed on the absence). Never throws.
16
+ //
17
+ // Dependency-light by construction: imports ONLY from ./types. No fs, no clock,
18
+ // no randomness — pure structural checks, safe in replay/core paths.
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.RecordValidationError = void 0;
21
+ exports.validateWorkerScope = validateWorkerScope;
22
+ exports.tryValidateWorkerScope = tryValidateWorkerScope;
23
+ exports.validateNodeSnapshot = validateNodeSnapshot;
24
+ exports.tryValidateNodeSnapshot = tryValidateNodeSnapshot;
25
+ exports.validateNodeReplayRun = validateNodeReplayRun;
26
+ exports.tryValidateNodeReplayRun = tryValidateNodeReplayRun;
27
+ exports.validateCandidateScore = validateCandidateScore;
28
+ exports.tryValidateCandidateScore = tryValidateCandidateScore;
29
+ exports.validateCandidateRecord = validateCandidateRecord;
30
+ exports.tryValidateCandidateRecord = tryValidateCandidateRecord;
31
+ // ---------------------------------------------------------------------------
32
+ // Primitive predicates — small, total, never throw.
33
+ // ---------------------------------------------------------------------------
34
+ function isRecord(value) {
35
+ return value !== null && typeof value === "object" && !Array.isArray(value);
36
+ }
37
+ function isString(value) {
38
+ return typeof value === "string";
39
+ }
40
+ function isFiniteNumber(value) {
41
+ return typeof value === "number" && Number.isFinite(value);
42
+ }
43
+ function isStringArray(value) {
44
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
45
+ }
46
+ function isObjectArray(value) {
47
+ return Array.isArray(value) && value.every((entry) => isRecord(entry));
48
+ }
49
+ const WORKER_STATUSES = new Set([
50
+ "allocated",
51
+ "running",
52
+ "completed",
53
+ "failed",
54
+ "rejected",
55
+ "verified",
56
+ "orphaned"
57
+ ]);
58
+ const SCORE_VERDICTS = new Set(["pass", "warn", "fail"]);
59
+ const CANDIDATE_STATUSES = new Set([
60
+ "registered",
61
+ "scored",
62
+ "selected",
63
+ "rejected",
64
+ "verified",
65
+ "failed"
66
+ ]);
67
+ const CANDIDATE_KINDS = new Set(["worker-output", "result", "artifact", "manual", "release"]);
68
+ const SNAPSHOT_FRESHNESS = new Set(["valid", "stale", "absent"]);
69
+ /** Descriptive integrity error — the message names the type and the field that
70
+ * broke, so a corrupt record is diagnosable from logs alone. */
71
+ class RecordValidationError extends Error {
72
+ code = "record-shape-invalid";
73
+ typeName;
74
+ field;
75
+ constructor(typeName, reason, field) {
76
+ super(`Invalid persisted ${typeName}: ${reason}`);
77
+ this.name = "RecordValidationError";
78
+ this.typeName = typeName;
79
+ this.field = field;
80
+ }
81
+ }
82
+ exports.RecordValidationError = RecordValidationError;
83
+ // ---------------------------------------------------------------------------
84
+ // WorkerScope — worker-isolation.ts:309 / :905
85
+ // Required (per src/types/worker.ts WorkerScope): schemaVersion===1, id, runId,
86
+ // taskId, createdAt, updatedAt, status (enum), workerDir, inputPath, resultPath,
87
+ // artifactsDir, logsDir are strings; allowedPaths string[]; feedbackIds string[];
88
+ // errors object[]. Optional fields are not enforced (additive, may be absent).
89
+ // ---------------------------------------------------------------------------
90
+ function workerScopeReason(value) {
91
+ if (!isRecord(value))
92
+ return { reason: "not an object" };
93
+ if (value.schemaVersion !== 1)
94
+ return { field: "schemaVersion", reason: "must equal 1" };
95
+ const requiredStrings = [
96
+ "id",
97
+ "runId",
98
+ "taskId",
99
+ "createdAt",
100
+ "updatedAt",
101
+ "workerDir",
102
+ "inputPath",
103
+ "resultPath",
104
+ "artifactsDir",
105
+ "logsDir"
106
+ ];
107
+ for (const field of requiredStrings) {
108
+ if (!isString(value[field]))
109
+ return { field: field, reason: "must be a string" };
110
+ }
111
+ if (!isString(value.status) || !WORKER_STATUSES.has(value.status)) {
112
+ return { field: "status", reason: "must be a valid WorkerIsolationStatus" };
113
+ }
114
+ if (!isStringArray(value.allowedPaths))
115
+ return { field: "allowedPaths", reason: "must be a string[]" };
116
+ if (!isStringArray(value.feedbackIds))
117
+ return { field: "feedbackIds", reason: "must be a string[]" };
118
+ if (!isObjectArray(value.errors))
119
+ return { field: "errors", reason: "must be a StateNodeError[]" };
120
+ return undefined;
121
+ }
122
+ /** Throw-on-mismatch guard for WorkerScope (callers that require the record). */
123
+ function validateWorkerScope(value) {
124
+ const problem = workerScopeReason(value);
125
+ if (problem)
126
+ throw new RecordValidationError("WorkerScope", problem.reason, problem.field);
127
+ return value;
128
+ }
129
+ /** Best-effort variant: returns null on mismatch (caller skips the record). */
130
+ function tryValidateWorkerScope(value) {
131
+ return workerScopeReason(value) ? null : value;
132
+ }
133
+ // ---------------------------------------------------------------------------
134
+ // NodeSnapshotBody — shared by NodeSnapshot.body and NodeReplayRun.body.
135
+ // Required (per src/types/state-node.ts): id, kind, status, loopStage strings;
136
+ // inputs/outputs records; artifacts/evidence/errors object arrays;
137
+ // parents/children string arrays.
138
+ // ---------------------------------------------------------------------------
139
+ function nodeSnapshotBodyReason(value, prefix) {
140
+ if (!isRecord(value))
141
+ return { field: prefix, reason: "must be a NodeSnapshotBody object" };
142
+ const requiredStrings = ["id", "kind", "status", "loopStage"];
143
+ for (const field of requiredStrings) {
144
+ if (!isString(value[field]))
145
+ return { field: `${prefix}.${String(field)}`, reason: "must be a string" };
146
+ }
147
+ if (!isRecord(value.inputs))
148
+ return { field: `${prefix}.inputs`, reason: "must be an object" };
149
+ if (!isRecord(value.outputs))
150
+ return { field: `${prefix}.outputs`, reason: "must be an object" };
151
+ if (!isObjectArray(value.artifacts))
152
+ return { field: `${prefix}.artifacts`, reason: "must be a StateArtifact[]" };
153
+ if (!isObjectArray(value.evidence))
154
+ return { field: `${prefix}.evidence`, reason: "must be a StateEvidence[]" };
155
+ if (!isObjectArray(value.errors))
156
+ return { field: `${prefix}.errors`, reason: "must be a StateNodeError[]" };
157
+ if (!isStringArray(value.parents))
158
+ return { field: `${prefix}.parents`, reason: "must be a string[]" };
159
+ if (!isStringArray(value.children))
160
+ return { field: `${prefix}.children`, reason: "must be a string[]" };
161
+ return undefined;
162
+ }
163
+ // ---------------------------------------------------------------------------
164
+ // NodeSnapshot — node-snapshot.ts:121
165
+ // Required: schemaVersion===1, snapshotId, runId, nodeId, capturedAt,
166
+ // sourceFingerprint strings; body a valid NodeSnapshotBody.
167
+ // ---------------------------------------------------------------------------
168
+ function nodeSnapshotReason(value) {
169
+ if (!isRecord(value))
170
+ return { reason: "not an object" };
171
+ if (value.schemaVersion !== 1)
172
+ return { field: "schemaVersion", reason: "must equal 1" };
173
+ const requiredStrings = ["snapshotId", "runId", "nodeId", "capturedAt", "sourceFingerprint"];
174
+ for (const field of requiredStrings) {
175
+ if (!isString(value[field]))
176
+ return { field: field, reason: "must be a string" };
177
+ }
178
+ return nodeSnapshotBodyReason(value.body, "body");
179
+ }
180
+ /** Throw-on-mismatch guard for NodeSnapshot (read edge requires the record). */
181
+ function validateNodeSnapshot(value) {
182
+ const problem = nodeSnapshotReason(value);
183
+ if (problem)
184
+ throw new RecordValidationError("NodeSnapshot", problem.reason, problem.field);
185
+ return value;
186
+ }
187
+ /** Best-effort variant: returns null on mismatch. */
188
+ function tryValidateNodeSnapshot(value) {
189
+ return nodeSnapshotReason(value) ? null : value;
190
+ }
191
+ // ---------------------------------------------------------------------------
192
+ // NodeReplayRun — node-snapshot.ts:133
193
+ // Required: schemaVersion===1, replayId, runId, nodeId, snapshotId, replayedAt,
194
+ // outputFingerprint strings; freshness enum; contractValidated boolean; body a
195
+ // valid NodeSnapshotBody.
196
+ // ---------------------------------------------------------------------------
197
+ function nodeReplayRunReason(value) {
198
+ if (!isRecord(value))
199
+ return { reason: "not an object" };
200
+ if (value.schemaVersion !== 1)
201
+ return { field: "schemaVersion", reason: "must equal 1" };
202
+ const requiredStrings = [
203
+ "replayId",
204
+ "runId",
205
+ "nodeId",
206
+ "snapshotId",
207
+ "replayedAt",
208
+ "outputFingerprint"
209
+ ];
210
+ for (const field of requiredStrings) {
211
+ if (!isString(value[field]))
212
+ return { field: field, reason: "must be a string" };
213
+ }
214
+ if (!isString(value.freshness) || !SNAPSHOT_FRESHNESS.has(value.freshness)) {
215
+ return { field: "freshness", reason: "must be a valid NodeSnapshotFreshness" };
216
+ }
217
+ if (typeof value.contractValidated !== "boolean") {
218
+ return { field: "contractValidated", reason: "must be a boolean" };
219
+ }
220
+ return nodeSnapshotBodyReason(value.body, "body");
221
+ }
222
+ /** Throw-on-mismatch guard for NodeReplayRun (read edge requires the record). */
223
+ function validateNodeReplayRun(value) {
224
+ const problem = nodeReplayRunReason(value);
225
+ if (problem)
226
+ throw new RecordValidationError("NodeReplayRun", problem.reason, problem.field);
227
+ return value;
228
+ }
229
+ /** Best-effort variant: returns null on mismatch. */
230
+ function tryValidateNodeReplayRun(value) {
231
+ return nodeReplayRunReason(value) ? null : value;
232
+ }
233
+ // ---------------------------------------------------------------------------
234
+ // CandidateScore — multi-agent-operator-ux.ts:502 / evidence-reasoning.ts:750
235
+ // Required (per src/types/candidate.ts): schemaVersion===1, id, candidateId,
236
+ // runId, createdAt, scorer strings; criteria record of numbers; total/maxTotal/
237
+ // normalized finite numbers; verdict enum; evidence/artifacts object arrays.
238
+ // ---------------------------------------------------------------------------
239
+ function isNumberRecord(value) {
240
+ return isRecord(value) && Object.values(value).every((entry) => isFiniteNumber(entry));
241
+ }
242
+ function candidateScoreReason(value) {
243
+ if (!isRecord(value))
244
+ return { reason: "not an object" };
245
+ if (value.schemaVersion !== 1)
246
+ return { field: "schemaVersion", reason: "must equal 1" };
247
+ const requiredStrings = ["id", "candidateId", "runId", "createdAt", "scorer"];
248
+ for (const field of requiredStrings) {
249
+ if (!isString(value[field]))
250
+ return { field: field, reason: "must be a string" };
251
+ }
252
+ if (!isNumberRecord(value.criteria))
253
+ return { field: "criteria", reason: "must be a Record<string, number>" };
254
+ if (!isFiniteNumber(value.total))
255
+ return { field: "total", reason: "must be a finite number" };
256
+ if (!isFiniteNumber(value.maxTotal))
257
+ return { field: "maxTotal", reason: "must be a finite number" };
258
+ if (!isFiniteNumber(value.normalized))
259
+ return { field: "normalized", reason: "must be a finite number" };
260
+ if (!isString(value.verdict) || !SCORE_VERDICTS.has(value.verdict)) {
261
+ return { field: "verdict", reason: "must be a valid CandidateScoreVerdict" };
262
+ }
263
+ if (!isObjectArray(value.evidence))
264
+ return { field: "evidence", reason: "must be a StateEvidence[]" };
265
+ if (!isObjectArray(value.artifacts))
266
+ return { field: "artifacts", reason: "must be a StateArtifact[]" };
267
+ return undefined;
268
+ }
269
+ /** Throw-on-mismatch guard for CandidateScore (callers that require the record). */
270
+ function validateCandidateScore(value) {
271
+ const problem = candidateScoreReason(value);
272
+ if (problem)
273
+ throw new RecordValidationError("CandidateScore", problem.reason, problem.field);
274
+ return value;
275
+ }
276
+ /** Best-effort variant: returns null on mismatch (caller skips the record so the
277
+ * downstream score gate fails closed on its absence). */
278
+ function tryValidateCandidateScore(value) {
279
+ return candidateScoreReason(value) ? null : value;
280
+ }
281
+ // ---------------------------------------------------------------------------
282
+ // CandidateRecord — candidate-scoring.ts getCandidate / loadCandidatesFromDisk.
283
+ // Required (per src/types/candidate.ts): schemaVersion===1, id, runId, createdAt,
284
+ // updatedAt strings; kind/status enums; artifacts/evidence object arrays;
285
+ // scores/feedbackIds string arrays. Optional ids (workerId/taskId/…) not enforced.
286
+ // A corrupt/forged candidate record must NOT flow into the run as a trusted T —
287
+ // the gate that backs commit selection reads these fields, so we fail closed at
288
+ // the read edge instead of upserting an unvalidated cast.
289
+ // ---------------------------------------------------------------------------
290
+ function candidateRecordReason(value) {
291
+ if (!isRecord(value))
292
+ return { reason: "not an object" };
293
+ if (value.schemaVersion !== 1)
294
+ return { field: "schemaVersion", reason: "must equal 1" };
295
+ const requiredStrings = ["id", "runId", "createdAt", "updatedAt"];
296
+ for (const field of requiredStrings) {
297
+ if (!isString(value[field]))
298
+ return { field: field, reason: "must be a string" };
299
+ }
300
+ if (!isString(value.kind) || !CANDIDATE_KINDS.has(value.kind)) {
301
+ return { field: "kind", reason: "must be a valid CandidateKind" };
302
+ }
303
+ if (!isString(value.status) || !CANDIDATE_STATUSES.has(value.status)) {
304
+ return { field: "status", reason: "must be a valid CandidateStatus" };
305
+ }
306
+ if (!isObjectArray(value.artifacts))
307
+ return { field: "artifacts", reason: "must be a StateArtifact[]" };
308
+ if (!isObjectArray(value.evidence))
309
+ return { field: "evidence", reason: "must be a StateEvidence[]" };
310
+ if (!isStringArray(value.scores))
311
+ return { field: "scores", reason: "must be a string[]" };
312
+ if (!isStringArray(value.feedbackIds))
313
+ return { field: "feedbackIds", reason: "must be a string[]" };
314
+ return undefined;
315
+ }
316
+ /** Throw-on-mismatch guard for CandidateRecord (callers that require the record
317
+ * at the disk read edge — getCandidate / loadCandidatesFromDisk). */
318
+ function validateCandidateRecord(value) {
319
+ const problem = candidateRecordReason(value);
320
+ if (problem)
321
+ throw new RecordValidationError("CandidateRecord", problem.reason, problem.field);
322
+ return value;
323
+ }
324
+ /** Best-effort variant: returns null on mismatch (caller skips the record so the
325
+ * downstream selection gate fails closed on its absence). */
326
+ function tryValidateCandidateRecord(value) {
327
+ return candidateRecordReason(value) ? null : value;
328
+ }
package/dist/version.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MIN_SUPPORTED_RUN_STATE_SCHEMA_VERSION = exports.LEGACY_RUN_STATE_SCHEMA_VERSION = exports.CURRENT_RUN_STATE_SCHEMA_VERSION = exports.WORKFLOW_APP_SCHEMA_VERSION = exports.CURRENT_COOL_WORKFLOW_VERSION = void 0;
4
- exports.CURRENT_COOL_WORKFLOW_VERSION = "0.1.81";
4
+ exports.CURRENT_COOL_WORKFLOW_VERSION = "0.1.82";
5
5
  exports.WORKFLOW_APP_SCHEMA_VERSION = 1;
6
6
  exports.CURRENT_RUN_STATE_SCHEMA_VERSION = 1;
7
7
  exports.LEGACY_RUN_STATE_SCHEMA_VERSION = 0;