cool-workflow 0.1.80 → 0.1.82

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/README.md +42 -2
  4. package/apps/architecture-review/app.json +1 -1
  5. package/apps/architecture-review-fast/app.json +1 -1
  6. package/apps/end-to-end-golden-path/app.json +1 -1
  7. package/apps/pr-review-fix-ci/app.json +1 -1
  8. package/apps/release-cut/app.json +1 -1
  9. package/apps/research-synthesis/app.json +1 -1
  10. package/dist/agent-config.js +21 -7
  11. package/dist/candidate-scoring.js +62 -48
  12. package/dist/capability-core.js +152 -96
  13. package/dist/capability-registry.js +160 -174
  14. package/dist/cli.js +90 -100
  15. package/dist/collaboration.js +5 -6
  16. package/dist/commit.js +232 -209
  17. package/dist/compare.js +18 -0
  18. package/dist/coordinator/classify.js +45 -0
  19. package/dist/coordinator/paths.js +42 -0
  20. package/dist/coordinator/util.js +126 -0
  21. package/dist/coordinator.js +127 -300
  22. package/dist/dispatch.js +46 -3
  23. package/dist/drive.js +7 -7
  24. package/dist/error-feedback.js +8 -4
  25. package/dist/evidence-reasoning.js +5 -2
  26. package/dist/execution-backend/agent.js +294 -0
  27. package/dist/execution-backend/probes.js +96 -0
  28. package/dist/execution-backend/util.js +47 -0
  29. package/dist/execution-backend.js +77 -450
  30. package/dist/gates.js +48 -0
  31. package/dist/mcp-server.js +34 -173
  32. package/dist/multi-agent/graph.js +84 -0
  33. package/dist/multi-agent/helpers.js +141 -0
  34. package/dist/multi-agent/ids.js +20 -0
  35. package/dist/multi-agent/paths.js +22 -0
  36. package/dist/multi-agent-eval/format.js +194 -0
  37. package/dist/multi-agent-eval/normalize.js +51 -0
  38. package/dist/multi-agent-eval.js +63 -242
  39. package/dist/multi-agent-host.js +53 -40
  40. package/dist/multi-agent-operator-ux.js +2 -1
  41. package/dist/multi-agent-trust.js +5 -5
  42. package/dist/multi-agent.js +125 -314
  43. package/dist/node-projection.js +59 -0
  44. package/dist/node-snapshot.js +11 -21
  45. package/dist/observability/format.js +61 -0
  46. package/dist/observability/intake.js +98 -0
  47. package/dist/observability.js +14 -160
  48. package/dist/operator-ux/format.js +364 -0
  49. package/dist/operator-ux.js +22 -363
  50. package/dist/orchestrator/lifecycle-operations.js +22 -1
  51. package/dist/orchestrator/report.js +8 -0
  52. package/dist/orchestrator.js +41 -10
  53. package/dist/reclamation.js +32 -55
  54. package/dist/run-export.js +138 -14
  55. package/dist/run-registry/derive.js +172 -0
  56. package/dist/run-registry/format.js +124 -0
  57. package/dist/run-registry/gc.js +251 -0
  58. package/dist/run-registry/policy.js +16 -0
  59. package/dist/run-registry/queue.js +116 -0
  60. package/dist/run-registry.js +78 -593
  61. package/dist/run-state-schema.js +1 -0
  62. package/dist/sandbox-profile.js +43 -2
  63. package/dist/scheduler.js +34 -4
  64. package/dist/state-explosion/format.js +159 -0
  65. package/dist/state-explosion/helpers.js +82 -0
  66. package/dist/state-explosion.js +65 -283
  67. package/dist/state-node.js +19 -4
  68. package/dist/telemetry-attestation.js +55 -0
  69. package/dist/telemetry-demo.js +15 -3
  70. package/dist/telemetry-ledger.js +60 -15
  71. package/dist/topology.js +50 -12
  72. package/dist/triggers.js +33 -14
  73. package/dist/trust-audit.js +215 -71
  74. package/dist/validation.js +328 -0
  75. package/dist/version.js +1 -1
  76. package/dist/worker-isolation/helpers.js +51 -0
  77. package/dist/worker-isolation/paths.js +46 -0
  78. package/dist/worker-isolation.js +182 -173
  79. package/docs/agent-delegation-drive.7.md +14 -0
  80. package/docs/cli-mcp-parity.7.md +5 -0
  81. package/docs/contract-migration-tooling.7.md +3 -0
  82. package/docs/control-plane-scheduling.7.md +3 -0
  83. package/docs/dogfood/resume-drive-real-agent-2026-06-14.md +40 -0
  84. package/docs/durable-state-and-locking.7.md +5 -0
  85. package/docs/evidence-adoption-reasoning-chain.7.md +3 -0
  86. package/docs/execution-backends.7.md +3 -0
  87. package/docs/index.md +1 -0
  88. package/docs/launch/launch-kit.md +46 -23
  89. package/docs/launch/pre-launch-checklist.md +14 -14
  90. package/docs/multi-agent-cli-mcp-surface.7.md +5 -0
  91. package/docs/multi-agent-eval-replay-harness.7.md +3 -0
  92. package/docs/multi-agent-operator-ux.7.md +3 -0
  93. package/docs/multi-agent-trust-policy-audit.7.md +27 -0
  94. package/docs/node-snapshot-diff-replay.7.md +3 -0
  95. package/docs/observability-cost-accounting.7.md +3 -0
  96. package/docs/project-index.md +23 -6
  97. package/docs/real-execution-backends.7.md +3 -0
  98. package/docs/release-and-migration.7.md +5 -0
  99. package/docs/release-tooling.7.md +35 -2
  100. package/docs/run-registry-control-plane.7.md +55 -8
  101. package/docs/run-retention-reclamation.7.md +5 -0
  102. package/docs/state-explosion-management.7.md +3 -0
  103. package/docs/team-collaboration.7.md +3 -0
  104. package/docs/trust-model.md +267 -0
  105. package/docs/vendor-manifest-loadability.7.md +43 -0
  106. package/docs/web-desktop-workbench.7.md +3 -0
  107. package/manifest/plugin.manifest.json +1 -1
  108. package/manifest/source-context-profiles.json +1 -1
  109. package/package.json +4 -2
  110. package/scripts/agents/builtin-templates.json +7 -0
  111. package/scripts/bump-version.js +5 -11
  112. package/scripts/canonical-apps-list.js +64 -0
  113. package/scripts/canonical-apps.js +19 -4
  114. package/scripts/children/batch-delegate-child.js +58 -0
  115. package/scripts/children/http-delegate-child.js +39 -0
  116. package/scripts/dogfood-release.js +1 -1
  117. package/scripts/golden-path.js +4 -4
  118. package/scripts/parity-check.js +5 -0
  119. package/scripts/release-check.js +5 -1
  120. package/scripts/release-flow.js +181 -5
  121. package/scripts/version-sync-check.js +5 -8
  122. package/dist/capability-dispatcher.js +0 -86
@@ -17,6 +17,8 @@ exports.artifactExists = artifactExists;
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
  exports.STATE_NODE_SCHEMA_VERSION = 1;
21
23
  exports.PIPELINE_CONTRACT_SCHEMA_VERSION = 1;
22
24
  class PipelineContractError extends Error {
@@ -35,7 +37,7 @@ function createStateNode(input) {
35
37
  const now = new Date().toISOString();
36
38
  return {
37
39
  schemaVersion: exports.STATE_NODE_SCHEMA_VERSION,
38
- id: input.id || createNodeId(input.kind),
40
+ id: input.id || createNodeId(input),
39
41
  kind: input.kind,
40
42
  status: input.status || "pending",
41
43
  loopStage: input.loopStage,
@@ -281,9 +283,22 @@ function contractError(code, message, options = {}) {
281
283
  ...options
282
284
  });
283
285
  }
284
- function createNodeId(kind) {
285
- const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\..+/, "Z");
286
- return `${kind}-${stamp}-${Math.random().toString(36).slice(2, 8)}`;
286
+ // Deterministic id (FreeBSD-audit L12/L13): no wall-clock stamp, no PRNG suffix.
287
+ // Almost every node is created WITH an explicit, already-deterministic id
288
+ // (e.g. `${run.id}:result:${task.id}`); this fallback only fires for ad-hoc nodes
289
+ // minted without an id. We bind the id to a short sha256 of the node's stable
290
+ // content (kind + loopStage + canonical inputs/outputs/contract), so the same
291
+ // logical node yields a byte-identical id across runs and replay reaches the same
292
+ // fingerprint. Two nodes with identical content collapse to the same id by design.
293
+ function createNodeId(input) {
294
+ const digest = (0, execution_backend_1.sha256)((0, telemetry_attestation_1.stableStringify)({
295
+ kind: input.kind,
296
+ loopStage: input.loopStage,
297
+ contractId: input.contractId ?? null,
298
+ inputs: input.inputs ?? null,
299
+ outputs: input.outputs ?? null
300
+ }));
301
+ return `${input.kind}-${digest.replace("sha256:", "").slice(0, 16)}`;
287
302
  }
288
303
  function mergeById(existing, next) {
289
304
  const values = [...existing];
@@ -33,6 +33,7 @@ exports.normalizeReportedUsage = normalizeReportedUsage;
33
33
  exports.verifyTelemetryAttestation = verifyTelemetryAttestation;
34
34
  exports.resolveTrustPublicKey = resolveTrustPublicKey;
35
35
  exports.signTelemetry = signTelemetry;
36
+ exports.verifyTelemetrySignatures = verifyTelemetrySignatures;
36
37
  const node_crypto_1 = __importDefault(require("node:crypto"));
37
38
  /** Deterministic, key-sorted JSON so signer and verifier hash byte-identical
38
39
  * input regardless of object key order. */
@@ -154,3 +155,57 @@ function signTelemetry(usage, privateKeyPem, ctx) {
154
155
  const key = node_crypto_1.default.createPrivateKey(privateKeyPem);
155
156
  return node_crypto_1.default.sign(null, payload, key).toString("base64");
156
157
  }
158
+ function stableDigest(value) {
159
+ return `sha256:${node_crypto_1.default.createHash("sha256").update(stableStringify(value), "utf8").digest("hex")}`;
160
+ }
161
+ function verifyTelemetrySignatures(records, trustPublicKeyPem) {
162
+ const checks = [];
163
+ let checked = 0;
164
+ let reverified = 0;
165
+ let failed = 0;
166
+ for (let i = 0; i < records.length; i++) {
167
+ const record = records[i];
168
+ if (record.attestation !== "attested")
169
+ continue;
170
+ checked += 1;
171
+ if (!trustPublicKeyPem) {
172
+ checks.push({ name: `signature[${i}]`, pass: true, code: "signature-unchecked-no-key" });
173
+ continue;
174
+ }
175
+ if (!record.reportedUsage) {
176
+ // A claimed-`attested` record with no re-verifiable raw usage cannot be
177
+ // independently checked — fail closed rather than trust the stored verdict.
178
+ failed += 1;
179
+ checks.push({ name: `signature[${i}]`, pass: false, code: "telemetry-usage-unavailable" });
180
+ continue;
181
+ }
182
+ if (record.reportedUsageDigest !== stableDigest(record.reportedUsage)) {
183
+ // The raw usage is stored so a public-key verifier can re-run ed25519.
184
+ // The hash-chained record binds its digest; verify the two still match
185
+ // before trusting the raw payload for signature re-verification.
186
+ failed += 1;
187
+ checks.push({ name: `signature[${i}]`, pass: false, code: "telemetry-usage-digest-mismatch" });
188
+ continue;
189
+ }
190
+ const result = verifyTelemetryAttestation(record.reportedUsage, record.usageSignature, trustPublicKeyPem, {
191
+ runId: record.runId,
192
+ taskId: record.taskId,
193
+ promptDigest: record.promptDigest
194
+ });
195
+ if (result.status === "attested") {
196
+ reverified += 1;
197
+ checks.push({ name: `signature[${i}]`, pass: true });
198
+ }
199
+ else {
200
+ failed += 1;
201
+ checks.push({
202
+ name: `signature[${i}]`,
203
+ pass: false,
204
+ code: result.reason && result.reason.startsWith("trust key unreadable")
205
+ ? "telemetry-pubkey-unreadable"
206
+ : "telemetry-signature-mismatch"
207
+ });
208
+ }
209
+ }
210
+ return { keyProvided: Boolean(trustPublicKeyPem), checked, reverified, failed, checks };
211
+ }
@@ -31,12 +31,24 @@ const telemetry_attestation_1 = require("./telemetry-attestation");
31
31
  const execution_backend_1 = require("./execution-backend");
32
32
  /** Human-facing render of `telemetry verify <run>`. */
33
33
  function formatTelemetryVerify(r) {
34
- if (!r.present)
34
+ const keyUnreadable = r.failedChecks.some((c) => c.code === "telemetry-pubkey-unreadable");
35
+ if (!r.present && !keyUnreadable)
35
36
  return `telemetry: run ${r.runId} has no attestation ledger (nothing to verify)`;
36
- const head = r.verified ? `✓ VERIFIED — ${r.records} record(s), chain intact, every hash recomputed independently` : `✗ TAMPERING DETECTED — ${r.failedChecks.length} check(s) failed`;
37
+ const head = r.verified
38
+ ? `✓ VERIFIED — ${r.records} record(s), chain intact, every hash recomputed independently`
39
+ : keyUnreadable
40
+ ? `✗ VERIFICATION REFUSED — supplied public key was unreadable`
41
+ : `✗ TAMPERING DETECTED — ${r.failedChecks.length} check(s) failed`;
37
42
  const tally = ` attested ${r.attested} · unattested ${r.unattested} · absent ${r.absent}`;
43
+ const sig = keyUnreadable
44
+ ? `\n signatures: public key unreadable; ed25519 re-check refused`
45
+ : r.signatureKeyProvided
46
+ ? `\n signatures: ${r.signaturesReverified}/${r.signaturesChecked} re-verified against the supplied public key${r.signaturesFailed ? ` · ${r.signaturesFailed} FAILED` : ""}`
47
+ : r.signaturesChecked
48
+ ? `\n signatures: ${r.signaturesChecked} attested record(s) — chain-proven only; pass --pubkey to re-verify ed25519 offline`
49
+ : "";
38
50
  const fails = r.failedChecks.length ? "\n" + r.failedChecks.map((c) => ` ✗ ${c.name} ${c.code || ""}`).join("\n") : "";
39
- return `telemetry verify ${r.runId}\n${head}\n${tally}${fails}`;
51
+ return `telemetry verify ${r.runId}\n${head}\n${tally}${sig}${fails}`;
40
52
  }
41
53
  /** Human-facing render of `demo tamper` — the visible tamper-evidence proof. */
42
54
  function formatTamperDemo(r) {
@@ -17,7 +17,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
17
17
  return (mod && mod.__esModule) ? mod : { "default": mod };
18
18
  };
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
- exports.TELEMETRY_LEDGER_SCHEMA_VERSION = void 0;
20
+ exports.TelemetryLedgerCorruptError = exports.TELEMETRY_LEDGER_SCHEMA_VERSION = void 0;
21
21
  exports.telemetryLedgerPath = telemetryLedgerPath;
22
22
  exports.loadTelemetryLedger = loadTelemetryLedger;
23
23
  exports.genesisPrevHash = genesisPrevHash;
@@ -34,19 +34,47 @@ exports.TELEMETRY_LEDGER_SCHEMA_VERSION = 1;
34
34
  function telemetryLedgerPath(run) {
35
35
  return node_path_1.default.join(run.paths.runDir, "telemetry.json");
36
36
  }
37
- /** Load the ledger; fail closed to an empty chain on a malformed overlay (a
38
- * corrupt file must never brick the run and an empty chain verifies as such). */
39
- function loadTelemetryLedger(run) {
37
+ /** A telemetry ledger that EXISTS on disk but cannot be parsed (or whose shape is
38
+ * not a record array). This is exactly the corruption/truncation case the hash
39
+ * chain exists to catch — it must fail closed, never be silently treated as the
40
+ * "empty/absent" chain (which verifies as clean). */
41
+ class TelemetryLedgerCorruptError extends Error {
42
+ file;
43
+ constructor(file) {
44
+ super(`Telemetry ledger exists but is corrupt (unparseable): ${file}`);
45
+ this.name = "TelemetryLedgerCorruptError";
46
+ this.file = file;
47
+ }
48
+ }
49
+ exports.TelemetryLedgerCorruptError = TelemetryLedgerCorruptError;
50
+ /** Read the ledger, DISTINGUISHING absent (never written -> empty chain, fine)
51
+ * from corrupt (exists but unparseable/wrong shape -> fail closed). Conflating
52
+ * the two was the bug that let a corrupt overlay verify green and let an append
53
+ * silently re-genesis on top of it, discarding history. */
54
+ function readTelemetryLedgerState(run) {
40
55
  const file = telemetryLedgerPath(run);
41
56
  if (!node_fs_1.default.existsSync(file))
42
- return { schemaVersion: 1, runId: run.id, records: [] };
57
+ return { status: "absent", ledger: { schemaVersion: 1, runId: run.id, records: [] } };
58
+ let parsed;
43
59
  try {
44
- const parsed = JSON.parse(node_fs_1.default.readFileSync(file, "utf8"));
45
- return { schemaVersion: 1, runId: run.id, records: Array.isArray(parsed.records) ? parsed.records : [] };
60
+ parsed = JSON.parse(node_fs_1.default.readFileSync(file, "utf8"));
46
61
  }
47
62
  catch {
48
- return { schemaVersion: 1, runId: run.id, records: [] };
63
+ return { status: "corrupt", file };
64
+ }
65
+ if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.records)) {
66
+ return { status: "corrupt", file };
49
67
  }
68
+ return { status: "ok", ledger: { schemaVersion: 1, runId: run.id, records: parsed.records } };
69
+ }
70
+ /** Load the ledger for read/append. Absent -> empty chain. Corrupt -> THROWS, so
71
+ * an append can never silently re-genesis on a poisoned/edited file, and a read
72
+ * surfaces the corruption rather than swallowing it. */
73
+ function loadTelemetryLedger(run) {
74
+ const state = readTelemetryLedgerState(run);
75
+ if (state.status === "corrupt")
76
+ throw new TelemetryLedgerCorruptError(state.file);
77
+ return state.ledger;
50
78
  }
51
79
  /** genesis prevHash for a run's chain (no prior record). */
52
80
  function genesisPrevHash(runId) {
@@ -64,6 +92,7 @@ function recordHashInput(record) {
64
92
  taskId: record.taskId,
65
93
  promptDigest: record.promptDigest,
66
94
  reportedUsageDigest: record.reportedUsageDigest,
95
+ ...(record.reportedUsage !== undefined ? { reportedUsage: record.reportedUsage } : {}),
67
96
  usageSignature: record.usageSignature || null,
68
97
  attestation: record.attestation,
69
98
  attestationReason: record.attestationReason || null,
@@ -78,11 +107,10 @@ function computeRecordHash(record) {
78
107
  function reportedUsageDigest(usage) {
79
108
  return (0, execution_backend_1.sha256)((0, telemetry_attestation_1.stableStringify)(usage ?? null));
80
109
  }
81
- let recordCounter = 0;
82
- function recordId(now) {
83
- recordCounter += 1;
84
- const stamp = now.replace(/[-:.TZ]/g, "").slice(0, 14);
85
- return `tel-${stamp}-${String(recordCounter).padStart(3, "0")}`;
110
+ function recordId(seq) {
111
+ // Deterministic (FreeBSD-audit L13): the chain POSITION, not a process-global
112
+ // counter or wall-clock stamp — recordId is bound into the recordHash chain.
113
+ return `tel-${String(seq).padStart(3, "0")}`;
86
114
  }
87
115
  /** Append one attestation record DURABLY to the append-only chain, linking it to
88
116
  * the prior record (or genesis). Returns the committed record. */
@@ -93,12 +121,15 @@ function appendTelemetryAttestation(run, input) {
93
121
  const base = {
94
122
  schemaVersion: 1,
95
123
  runId: run.id,
96
- recordId: recordId(now),
124
+ recordId: recordId(ledger.records.length + 1),
97
125
  recordedAt: now,
98
126
  workerId: input.workerId,
99
127
  taskId: input.taskId,
100
128
  promptDigest: input.promptDigest,
101
129
  reportedUsageDigest: reportedUsageDigest(input.reportedUsage),
130
+ // Store the raw usage verbatim, digest-bound, and hash-chained so the
131
+ // signature can be independently re-verified offline at `telemetry verify`.
132
+ ...(input.reportedUsage ? { reportedUsage: input.reportedUsage } : {}),
102
133
  usageSignature: input.usageSignature,
103
134
  attestation: input.attestation,
104
135
  attestationReason: input.attestationReason,
@@ -114,7 +145,21 @@ function appendTelemetryAttestation(run, input) {
114
145
  * value — so an edited record/verdict is detected. An empty ledger verifies as
115
146
  * present:false (nothing to prove), NOT a failure. */
116
147
  function verifyTelemetryLedger(run) {
117
- const records = loadTelemetryLedger(run).records;
148
+ const state = readTelemetryLedgerState(run);
149
+ if (state.status === "corrupt") {
150
+ // Fail closed: a ledger that exists but cannot be parsed is indistinguishable
151
+ // from a truncated/forged one — report it, never green it.
152
+ return {
153
+ present: true,
154
+ verified: false,
155
+ records: [],
156
+ checks: [{ name: "ledger-load", pass: false, code: "telemetry-ledger-corrupt" }],
157
+ attested: 0,
158
+ unattested: 0,
159
+ absent: 0
160
+ };
161
+ }
162
+ const records = state.ledger.records;
118
163
  const checks = [];
119
164
  const tally = { attested: 0, unattested: 0, absent: 0 };
120
165
  for (const record of records)
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");
@@ -30,7 +32,7 @@ exports.OFFICIAL_TOPOLOGIES = [
30
32
  title: "Map-Reduce",
31
33
  summary: "Fan out mapper roles, index mapper evidence on the blackboard, then reduce only after required evidence is present.",
32
34
  roles: [
33
- roleSpec("mapper", "Mapper", ["Produce an independent shard result and cite evidence."], ["mapper output artifact"], ["indexed mapper artifact"]),
35
+ roleSpec("mapper", "Mapper", ["Produce an independent shard result and cite evidence."], ["mapper output artifact"], ["indexed mapper artifact"], 2),
34
36
  roleSpec("reducer", "Reducer", ["Synthesize mapper outputs only after fanin is verifier-ready."], ["reducer synthesis"], ["all mapper evidence"])
35
37
  ],
36
38
  groups: [{ id: "map-reduce", title: "Map-Reduce Group", roleIds: ["mapper", "reducer"] }],
@@ -83,7 +85,7 @@ exports.OFFICIAL_TOPOLOGIES = [
83
85
  title: "Judge Panel",
84
86
  summary: "Collect independent judge outputs, aggregate scores, and select a panel decision with linked evidence.",
85
87
  roles: [
86
- roleSpec("judge", "Judge", ["Score candidates independently and cite evidence."], ["judge score artifact"], ["judge verdict"]),
88
+ roleSpec("judge", "Judge", ["Score candidates independently and cite evidence."], ["judge score artifact"], ["judge verdict"], 3),
87
89
  roleSpec("panel-chair", "Panel Chair", ["Aggregate scores and write a panel decision."], ["panel decision"], ["judge evidence"])
88
90
  ],
89
91
  groups: [{ id: "judge-panel", title: "Judge Panel Group", roleIds: ["judge", "panel-chair"] }],
@@ -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`,
@@ -211,7 +219,7 @@ function applyTopology(run, topologyId, input = {}) {
211
219
  metadata: { topologyId: definition.id, topologyRunId: id }
212
220
  });
213
221
  const roleIds = [];
214
- for (const role of materializedRoles(definition, input)) {
222
+ for (const role of materializedRoles(definition, withLegacyRoleCounts(input))) {
215
223
  const record = (0, multi_agent_1.createAgentRole)(run, {
216
224
  id: `${id}-${role.id}`,
217
225
  multiAgentRunId: multiAgentRun.id,
@@ -387,7 +395,7 @@ function summarizeTopologies(run) {
387
395
  runId: run.id,
388
396
  totalRuns: state.runs.length,
389
397
  runsByStatus: countBy(active, (record) => record.status),
390
- officialTopologies: exports.OFFICIAL_TOPOLOGIES.map((definition) => definition.id),
398
+ officialTopologies: listTopologyDefinitions().map((definition) => definition.id),
391
399
  active,
392
400
  nextAction: active.find((record) => record.nextActions.length)?.nextActions[0] || `node scripts/cw.js topology apply ${run.id} map-reduce --task <task-id>`
393
401
  };
@@ -422,11 +430,28 @@ function showTopologyRun(run, topologyRunId) {
422
430
  throw new Error(`Unknown topology run id: ${topologyRunId}`);
423
431
  return record;
424
432
  }
433
+ /** Boundary adapter: fold the legacy id-keyed mapperCount/judgeCount flags into
434
+ * the uniform roleCounts map so materializedRoles can stay purely data-driven.
435
+ * An explicit roleCounts entry always wins; the judge floor (>= 2) preserves the
436
+ * prior clamp so a panel never collapses to a single judge. */
437
+ function withLegacyRoleCounts(input) {
438
+ const legacy = {
439
+ mapper: input.mapperCount === undefined ? undefined : Math.max(1, input.mapperCount),
440
+ judge: input.judgeCount === undefined ? undefined : Math.max(2, input.judgeCount)
441
+ };
442
+ const roleCounts = { ...input.roleCounts };
443
+ for (const [roleId, value] of Object.entries(legacy)) {
444
+ if (value !== undefined && roleCounts[roleId] === undefined)
445
+ roleCounts[roleId] = value;
446
+ }
447
+ return Object.keys(roleCounts).length ? { ...input, roleCounts } : input;
448
+ }
425
449
  function materializedRoles(definition, input) {
426
- const count = definition.id === "map-reduce" ? Math.max(1, input.mapperCount || 2) : definition.id === "judge-panel" ? Math.max(2, input.judgeCount || 3) : 1;
427
450
  const roles = [];
428
451
  for (const role of definition.roles) {
429
- const roleCount = role.count ?? (role.id === "mapper" || role.id === "judge" ? count : 1);
452
+ // Width is data-driven: a uniform per-role override, else the role's
453
+ // declared count, else a single instance. No topology-id/role-id branching.
454
+ const roleCount = Math.max(1, input.roleCounts?.[role.id] ?? role.count ?? 1);
430
455
  if (roleCount > 1) {
431
456
  for (let index = 1; index <= roleCount; index += 1)
432
457
  roles.push(expandRole(role, `${role.id}-${index}`, `${role.title} ${index}`));
@@ -458,8 +483,8 @@ function appendTopologyNode(run, record, status) {
458
483
  metadata: { topologyId: record.topologyId, topologyRunId: record.id }
459
484
  }));
460
485
  }
461
- function roleSpec(id, title, responsibilities, expectedArtifacts, faninObligations) {
462
- return { id, title, responsibilities, requiredEvidence: expectedArtifacts, expectedArtifacts, faninObligations };
486
+ function roleSpec(id, title, responsibilities, expectedArtifacts, faninObligations, count) {
487
+ return { id, title, responsibilities, requiredEvidence: expectedArtifacts, expectedArtifacts, faninObligations, ...(count !== undefined ? { count } : {}) };
463
488
  }
464
489
  function topicSpec(id, title, description) {
465
490
  return { id, title, description };
@@ -497,8 +522,21 @@ function statusToNodeStatus(status) {
497
522
  function issue(code, message, path) {
498
523
  return { code, message, path };
499
524
  }
500
- function timestampId() {
501
- 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)}`;
502
540
  }
503
541
  function countBy(items, pick) {
504
542
  const counts = {};
package/dist/triggers.js CHANGED
@@ -18,8 +18,14 @@ class RoutineTriggerBridge {
18
18
  }
19
19
  create(options) {
20
20
  const now = new Date().toISOString();
21
+ const store = this.load();
22
+ // Monotonic id, NOT triggers.length: delete shrinks the collection, so a
23
+ // length-based seq would reuse a live id after delete+create (corrupting the
24
+ // append-only event/audit log). nextTriggerSeq only ever increments.
25
+ const seq = (store.nextTriggerSeq || 0) + 1;
26
+ store.nextTriggerSeq = seq;
21
27
  const trigger = {
22
- id: createTriggerId(normalizeKind(options.kind)),
28
+ id: createTriggerId(normalizeKind(options.kind), seq),
23
29
  kind: normalizeKind(options.kind),
24
30
  createdAt: now,
25
31
  updatedAt: now,
@@ -30,7 +36,6 @@ class RoutineTriggerBridge {
30
36
  match: parseJsonObject(options.match),
31
37
  metadata: parseJsonObject(options.metadata)
32
38
  };
33
- const store = this.load();
34
39
  store.triggers.push(trigger);
35
40
  this.save(store);
36
41
  return trigger;
@@ -50,9 +55,10 @@ class RoutineTriggerBridge {
50
55
  const normalizedKind = normalizeKind(kind);
51
56
  const store = this.load();
52
57
  const now = new Date().toISOString();
58
+ const base = store.events.length;
53
59
  const events = store.triggers
54
60
  .filter((trigger) => trigger.kind === normalizedKind)
55
- .map((trigger) => this.createEvent(trigger, payload, now));
61
+ .map((trigger, index) => this.createEvent(trigger, payload, now, base + index + 1));
56
62
  store.events.push(...events);
57
63
  this.save(store);
58
64
  return events;
@@ -61,9 +67,9 @@ class RoutineTriggerBridge {
61
67
  const store = this.load();
62
68
  return triggerId ? store.events.filter((event) => event.triggerId === triggerId) : store.events;
63
69
  }
64
- createEvent(trigger, payload, receivedAt) {
70
+ createEvent(trigger, payload, receivedAt, seq) {
65
71
  const matched = matches(trigger.match, payload);
66
- const eventId = createEventId(trigger.kind);
72
+ const eventId = createEventId(trigger.kind, seq);
67
73
  const payloadPath = node_path_1.default.join(this.payloadsDir, `${(0, state_1.safeFileName)(eventId)}.json`);
68
74
  (0, state_1.writeJson)(payloadPath, {
69
75
  schemaVersion: 1,
@@ -84,12 +90,21 @@ class RoutineTriggerBridge {
84
90
  }
85
91
  load() {
86
92
  if (!node_fs_1.default.existsSync(this.storePath))
87
- return { schemaVersion: 1, triggers: [], events: [] };
93
+ return { schemaVersion: 1, triggers: [], events: [], nextTriggerSeq: 0 };
88
94
  const value = (0, state_1.readJson)(this.storePath);
95
+ const triggers = Array.isArray(value.triggers) ? value.triggers : [];
96
+ // Recover the monotonic sequence: max(persisted, highest existing id seq). The
97
+ // second term protects legacy stores (no nextTriggerSeq) and any store written
98
+ // before this field existed — a post-delete create can never reuse a live id.
99
+ const maxExisting = triggers.reduce((max, trigger) => {
100
+ const n = Number((String(trigger.id).match(/(\d+)$/) || [])[1] || 0);
101
+ return Number.isFinite(n) && n > max ? n : max;
102
+ }, 0);
89
103
  return {
90
104
  schemaVersion: 1,
91
- triggers: Array.isArray(value.triggers) ? value.triggers : [],
92
- events: Array.isArray(value.events) ? value.events : []
105
+ triggers,
106
+ events: Array.isArray(value.events) ? value.events : [],
107
+ nextTriggerSeq: Math.max(typeof value.nextTriggerSeq === "number" ? value.nextTriggerSeq : 0, maxExisting)
93
108
  };
94
109
  }
95
110
  save(store) {
@@ -149,11 +164,15 @@ function stringOption(value) {
149
164
  return undefined;
150
165
  return String(value);
151
166
  }
152
- function createTriggerId(kind) {
153
- const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\..+/, "Z");
154
- return `${kind}-${stamp}-${Math.random().toString(36).slice(2, 8)}`;
167
+ // Deterministic trigger id (FreeBSD-audit L12/L13): the trigger's POSITION in the
168
+ // append-only trigger store, qualified by kind. No wall-clock stamp, no PRNG suffix
169
+ // — registering the same triggers in the same order mints byte-identical ids.
170
+ function createTriggerId(kind, seq) {
171
+ return `${kind}-${String(seq).padStart(4, "0")}`;
155
172
  }
156
- function createEventId(kind) {
157
- const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\..+/, "Z");
158
- return `event-${kind}-${stamp}-${Math.random().toString(36).slice(2, 8)}`;
173
+ // Deterministic event id (FreeBSD-audit L12/L13): the event's POSITION in the
174
+ // append-only event log (firing many triggers at once still yields a distinct,
175
+ // stable id per trigger). No clock, no PRNG.
176
+ function createEventId(kind, seq) {
177
+ return `event-${kind}-${String(seq).padStart(4, "0")}`;
159
178
  }