cool-workflow 0.1.79 → 0.1.81

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 (131) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/README.md +51 -3
  4. package/apps/architecture-review/app.json +1 -1
  5. package/apps/architecture-review-fast/app.json +64 -0
  6. package/apps/architecture-review-fast/workflow.js +153 -0
  7. package/apps/end-to-end-golden-path/app.json +1 -1
  8. package/apps/pr-review-fix-ci/app.json +1 -1
  9. package/apps/release-cut/app.json +1 -1
  10. package/apps/research-synthesis/app.json +1 -1
  11. package/dist/agent-config.js +21 -7
  12. package/dist/candidate-scoring.js +42 -22
  13. package/dist/capability-core.js +132 -17
  14. package/dist/capability-registry.js +138 -168
  15. package/dist/cli.js +97 -98
  16. package/dist/collaboration.js +5 -6
  17. package/dist/commit.js +20 -6
  18. package/dist/compare.js +18 -0
  19. package/dist/coordinator/classify.js +45 -0
  20. package/dist/coordinator/paths.js +42 -0
  21. package/dist/coordinator/util.js +129 -0
  22. package/dist/coordinator.js +127 -300
  23. package/dist/dispatch.js +35 -0
  24. package/dist/drive.js +79 -6
  25. package/dist/error-feedback.js +8 -4
  26. package/dist/evidence-reasoning.js +3 -3
  27. package/dist/execution-backend/agent.js +331 -0
  28. package/dist/execution-backend/probes.js +96 -0
  29. package/dist/execution-backend/util.js +47 -0
  30. package/dist/execution-backend.js +73 -421
  31. package/dist/mcp-server.js +79 -183
  32. package/dist/multi-agent/graph.js +84 -0
  33. package/dist/multi-agent/helpers.js +145 -0
  34. package/dist/multi-agent/paths.js +22 -0
  35. package/dist/multi-agent-eval/format.js +194 -0
  36. package/dist/multi-agent-eval/normalize.js +51 -0
  37. package/dist/multi-agent-eval.js +39 -244
  38. package/dist/multi-agent-host.js +0 -19
  39. package/dist/multi-agent.js +125 -314
  40. package/dist/node-snapshot.js +3 -3
  41. package/dist/observability/format.js +61 -0
  42. package/dist/observability/intake.js +98 -0
  43. package/dist/observability.js +14 -160
  44. package/dist/operator-ux/format.js +364 -0
  45. package/dist/operator-ux.js +22 -363
  46. package/dist/orchestrator/lifecycle-operations.js +2 -1
  47. package/dist/orchestrator/report.js +8 -0
  48. package/dist/orchestrator.js +26 -9
  49. package/dist/reclamation.js +26 -21
  50. package/dist/run-export.js +494 -25
  51. package/dist/run-registry/derive.js +172 -0
  52. package/dist/run-registry/format.js +124 -0
  53. package/dist/run-registry/gc.js +251 -0
  54. package/dist/run-registry/policy.js +16 -0
  55. package/dist/run-registry/queue.js +116 -0
  56. package/dist/run-registry.js +89 -597
  57. package/dist/run-state-schema.js +1 -0
  58. package/dist/sandbox-profile.js +43 -2
  59. package/dist/state-explosion/format.js +159 -0
  60. package/dist/state-explosion/helpers.js +82 -0
  61. package/dist/state-explosion.js +165 -304
  62. package/dist/state-node.js +19 -4
  63. package/dist/telemetry-attestation.js +55 -0
  64. package/dist/telemetry-demo.js +15 -3
  65. package/dist/telemetry-ledger.js +60 -15
  66. package/dist/topology.js +25 -8
  67. package/dist/triggers.js +33 -14
  68. package/dist/trust-audit.js +145 -33
  69. package/dist/version.js +1 -1
  70. package/dist/worker-isolation/helpers.js +51 -0
  71. package/dist/worker-isolation/paths.js +46 -0
  72. package/dist/worker-isolation.js +39 -115
  73. package/docs/agent-delegation-drive.7.md +71 -0
  74. package/docs/canonical-workflow-apps.7.md +37 -0
  75. package/docs/cli-mcp-parity.7.md +16 -0
  76. package/docs/contract-migration-tooling.7.md +6 -0
  77. package/docs/control-plane-scheduling.7.md +6 -0
  78. package/docs/dogfood/resume-drive-real-agent-2026-06-14.md +40 -0
  79. package/docs/durable-state-and-locking.7.md +8 -0
  80. package/docs/evidence-adoption-reasoning-chain.7.md +6 -0
  81. package/docs/execution-backends.7.md +6 -0
  82. package/docs/index.md +2 -0
  83. package/docs/launch/demo.tape +28 -0
  84. package/docs/launch/launch-kit.md +96 -17
  85. package/docs/launch/pre-launch-checklist.md +53 -0
  86. package/docs/multi-agent-cli-mcp-surface.7.md +8 -0
  87. package/docs/multi-agent-eval-replay-harness.7.md +6 -0
  88. package/docs/multi-agent-operator-ux.7.md +6 -0
  89. package/docs/multi-agent-trust-policy-audit.7.md +27 -0
  90. package/docs/node-snapshot-diff-replay.7.md +6 -0
  91. package/docs/observability-cost-accounting.7.md +6 -0
  92. package/docs/project-index.md +27 -6
  93. package/docs/real-execution-backends.7.md +6 -0
  94. package/docs/release-and-migration.7.md +8 -0
  95. package/docs/release-tooling.7.md +6 -0
  96. package/docs/routines.md +23 -0
  97. package/docs/run-registry-control-plane.7.md +89 -2
  98. package/docs/run-retention-reclamation.7.md +8 -0
  99. package/docs/source-context-profiles.7.md +119 -0
  100. package/docs/state-explosion-management.7.md +13 -0
  101. package/docs/team-collaboration.7.md +6 -0
  102. package/docs/trust-model.md +267 -0
  103. package/docs/unix-principles.md +49 -1
  104. package/docs/vendor-manifest-loadability.7.md +43 -0
  105. package/docs/web-desktop-workbench.7.md +6 -0
  106. package/manifest/plugin.manifest.json +1 -1
  107. package/manifest/source-context-profiles.json +142 -0
  108. package/package.json +4 -1
  109. package/scripts/agents/builtin-templates.json +7 -0
  110. package/scripts/agents/claude-p-agent.js +129 -43
  111. package/scripts/architecture-review-fast.js +362 -0
  112. package/scripts/bump-version.js +5 -10
  113. package/scripts/canonical-apps-list.js +64 -0
  114. package/scripts/canonical-apps.js +36 -4
  115. package/scripts/coverage-gate.js +211 -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/source-context.js +291 -0
  121. package/scripts/version-sync-check.js +5 -7
  122. package/skills/ci-triage/SKILL.md +50 -0
  123. package/skills/ci-triage/agents/openai.yaml +4 -0
  124. package/skills/cool-workflow/SKILL.md +4 -1
  125. package/skills/deploy-check/SKILL.md +55 -0
  126. package/skills/deploy-check/agents/openai.yaml +4 -0
  127. package/skills/design-qa/SKILL.md +49 -0
  128. package/skills/design-qa/agents/openai.yaml +4 -0
  129. package/skills/pr-review/SKILL.md +45 -0
  130. package/skills/pr-review/agents/openai.yaml +4 -0
  131. 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
@@ -30,7 +30,7 @@ exports.OFFICIAL_TOPOLOGIES = [
30
30
  title: "Map-Reduce",
31
31
  summary: "Fan out mapper roles, index mapper evidence on the blackboard, then reduce only after required evidence is present.",
32
32
  roles: [
33
- roleSpec("mapper", "Mapper", ["Produce an independent shard result and cite evidence."], ["mapper output artifact"], ["indexed mapper artifact"]),
33
+ roleSpec("mapper", "Mapper", ["Produce an independent shard result and cite evidence."], ["mapper output artifact"], ["indexed mapper artifact"], 2),
34
34
  roleSpec("reducer", "Reducer", ["Synthesize mapper outputs only after fanin is verifier-ready."], ["reducer synthesis"], ["all mapper evidence"])
35
35
  ],
36
36
  groups: [{ id: "map-reduce", title: "Map-Reduce Group", roleIds: ["mapper", "reducer"] }],
@@ -83,7 +83,7 @@ exports.OFFICIAL_TOPOLOGIES = [
83
83
  title: "Judge Panel",
84
84
  summary: "Collect independent judge outputs, aggregate scores, and select a panel decision with linked evidence.",
85
85
  roles: [
86
- roleSpec("judge", "Judge", ["Score candidates independently and cite evidence."], ["judge score artifact"], ["judge verdict"]),
86
+ roleSpec("judge", "Judge", ["Score candidates independently and cite evidence."], ["judge score artifact"], ["judge verdict"], 3),
87
87
  roleSpec("panel-chair", "Panel Chair", ["Aggregate scores and write a panel decision."], ["panel decision"], ["judge evidence"])
88
88
  ],
89
89
  groups: [{ id: "judge-panel", title: "Judge Panel Group", roleIds: ["judge", "panel-chair"] }],
@@ -211,7 +211,7 @@ function applyTopology(run, topologyId, input = {}) {
211
211
  metadata: { topologyId: definition.id, topologyRunId: id }
212
212
  });
213
213
  const roleIds = [];
214
- for (const role of materializedRoles(definition, input)) {
214
+ for (const role of materializedRoles(definition, withLegacyRoleCounts(input))) {
215
215
  const record = (0, multi_agent_1.createAgentRole)(run, {
216
216
  id: `${id}-${role.id}`,
217
217
  multiAgentRunId: multiAgentRun.id,
@@ -387,7 +387,7 @@ function summarizeTopologies(run) {
387
387
  runId: run.id,
388
388
  totalRuns: state.runs.length,
389
389
  runsByStatus: countBy(active, (record) => record.status),
390
- officialTopologies: exports.OFFICIAL_TOPOLOGIES.map((definition) => definition.id),
390
+ officialTopologies: listTopologyDefinitions().map((definition) => definition.id),
391
391
  active,
392
392
  nextAction: active.find((record) => record.nextActions.length)?.nextActions[0] || `node scripts/cw.js topology apply ${run.id} map-reduce --task <task-id>`
393
393
  };
@@ -422,11 +422,28 @@ function showTopologyRun(run, topologyRunId) {
422
422
  throw new Error(`Unknown topology run id: ${topologyRunId}`);
423
423
  return record;
424
424
  }
425
+ /** Boundary adapter: fold the legacy id-keyed mapperCount/judgeCount flags into
426
+ * the uniform roleCounts map so materializedRoles can stay purely data-driven.
427
+ * An explicit roleCounts entry always wins; the judge floor (>= 2) preserves the
428
+ * prior clamp so a panel never collapses to a single judge. */
429
+ function withLegacyRoleCounts(input) {
430
+ const legacy = {
431
+ mapper: input.mapperCount === undefined ? undefined : Math.max(1, input.mapperCount),
432
+ judge: input.judgeCount === undefined ? undefined : Math.max(2, input.judgeCount)
433
+ };
434
+ const roleCounts = { ...input.roleCounts };
435
+ for (const [roleId, value] of Object.entries(legacy)) {
436
+ if (value !== undefined && roleCounts[roleId] === undefined)
437
+ roleCounts[roleId] = value;
438
+ }
439
+ return Object.keys(roleCounts).length ? { ...input, roleCounts } : input;
440
+ }
425
441
  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
442
  const roles = [];
428
443
  for (const role of definition.roles) {
429
- const roleCount = role.count ?? (role.id === "mapper" || role.id === "judge" ? count : 1);
444
+ // Width is data-driven: a uniform per-role override, else the role's
445
+ // declared count, else a single instance. No topology-id/role-id branching.
446
+ const roleCount = Math.max(1, input.roleCounts?.[role.id] ?? role.count ?? 1);
430
447
  if (roleCount > 1) {
431
448
  for (let index = 1; index <= roleCount; index += 1)
432
449
  roles.push(expandRole(role, `${role.id}-${index}`, `${role.title} ${index}`));
@@ -458,8 +475,8 @@ function appendTopologyNode(run, record, status) {
458
475
  metadata: { topologyId: record.topologyId, topologyRunId: record.id }
459
476
  }));
460
477
  }
461
- function roleSpec(id, title, responsibilities, expectedArtifacts, faninObligations) {
462
- return { id, title, responsibilities, requiredEvidence: expectedArtifacts, expectedArtifacts, faninObligations };
478
+ function roleSpec(id, title, responsibilities, expectedArtifacts, faninObligations, count) {
479
+ return { id, title, responsibilities, requiredEvidence: expectedArtifacts, expectedArtifacts, faninObligations, ...(count !== undefined ? { count } : {}) };
463
480
  }
464
481
  function topicSpec(id, title, description) {
465
482
  return { id, title, description };
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
  }