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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +1 -1
- package/apps/architecture-review/app.json +1 -1
- package/apps/architecture-review-fast/app.json +1 -1
- package/apps/end-to-end-golden-path/app.json +1 -1
- package/apps/pr-review-fix-ci/app.json +1 -1
- package/apps/release-cut/app.json +1 -1
- package/apps/research-synthesis/app.json +1 -1
- package/dist/candidate-scoring.js +20 -26
- package/dist/capability-core.js +64 -85
- package/dist/capability-registry.js +22 -3
- package/dist/commit.js +212 -203
- package/dist/coordinator/util.js +6 -9
- package/dist/dispatch.js +11 -3
- package/dist/evidence-reasoning.js +4 -1
- package/dist/execution-backend/agent.js +11 -48
- package/dist/execution-backend.js +11 -31
- package/dist/gates.js +48 -0
- package/dist/multi-agent/helpers.js +6 -10
- package/dist/multi-agent/ids.js +20 -0
- package/dist/multi-agent-eval.js +27 -1
- package/dist/multi-agent-host.js +53 -21
- package/dist/multi-agent-operator-ux.js +2 -1
- package/dist/multi-agent-trust.js +5 -5
- package/dist/node-projection.js +59 -0
- package/dist/node-snapshot.js +8 -18
- package/dist/orchestrator/lifecycle-operations.js +22 -1
- package/dist/orchestrator.js +16 -2
- package/dist/reclamation.js +8 -36
- package/dist/scheduler.js +34 -4
- package/dist/topology.js +25 -4
- package/dist/trust-audit.js +70 -38
- package/dist/validation.js +328 -0
- package/dist/version.js +1 -1
- package/dist/worker-isolation.js +143 -58
- package/docs/agent-delegation-drive.7.md +1 -0
- package/docs/cli-mcp-parity.7.md +1 -0
- package/docs/contract-migration-tooling.7.md +1 -0
- package/docs/control-plane-scheduling.7.md +1 -0
- package/docs/durable-state-and-locking.7.md +1 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +1 -0
- package/docs/execution-backends.7.md +1 -0
- package/docs/multi-agent-cli-mcp-surface.7.md +1 -0
- package/docs/multi-agent-eval-replay-harness.7.md +1 -0
- package/docs/multi-agent-operator-ux.7.md +1 -0
- package/docs/node-snapshot-diff-replay.7.md +1 -0
- package/docs/observability-cost-accounting.7.md +1 -0
- package/docs/project-index.md +8 -4
- package/docs/real-execution-backends.7.md +1 -0
- package/docs/release-and-migration.7.md +1 -0
- package/docs/release-tooling.7.md +33 -2
- package/docs/run-registry-control-plane.7.md +1 -0
- package/docs/run-retention-reclamation.7.md +1 -0
- package/docs/state-explosion-management.7.md +1 -0
- package/docs/team-collaboration.7.md +1 -0
- package/docs/web-desktop-workbench.7.md +1 -0
- package/manifest/plugin.manifest.json +1 -1
- package/manifest/source-context-profiles.json +1 -1
- package/package.json +1 -1
- package/scripts/canonical-apps.js +4 -4
- package/scripts/children/batch-delegate-child.js +58 -0
- package/scripts/children/http-delegate-child.js +39 -0
- package/scripts/dogfood-release.js +1 -1
- package/scripts/golden-path.js +4 -4
- 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
|
|
240
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
518
|
-
|
|
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 = {};
|
package/dist/trust-audit.js
CHANGED
|
@@ -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
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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.
|
|
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;
|