cool-workflow 0.1.81 → 0.1.83

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 (102) hide show
  1. package/.claude-plugin/plugin.json +2 -2
  2. package/.codex-plugin/plugin.json +4 -4
  3. package/README.md +125 -121
  4. package/apps/architecture-review/app.json +1 -1
  5. package/apps/architecture-review-fast/app.json +1 -1
  6. package/apps/end-to-end-golden-path/app.json +1 -1
  7. package/apps/pr-review-fix-ci/app.json +1 -1
  8. package/apps/release-cut/app.json +1 -1
  9. package/apps/research-synthesis/app.json +1 -1
  10. package/dist/candidate-scoring.js +20 -26
  11. package/dist/capability-core.js +80 -93
  12. package/dist/capability-registry.js +30 -3
  13. package/dist/cli.js +12 -1
  14. package/dist/commit.js +217 -204
  15. package/dist/coordinator/util.js +6 -9
  16. package/dist/dispatch.js +11 -3
  17. package/dist/doctor.js +153 -0
  18. package/dist/evidence-reasoning.js +4 -1
  19. package/dist/execution-backend/agent.js +11 -48
  20. package/dist/execution-backend.js +11 -31
  21. package/dist/gates.js +48 -0
  22. package/dist/mcp-server.js +11 -0
  23. package/dist/multi-agent/helpers.js +6 -10
  24. package/dist/multi-agent/ids.js +20 -0
  25. package/dist/multi-agent-eval.js +27 -1
  26. package/dist/multi-agent-host.js +53 -21
  27. package/dist/multi-agent-operator-ux.js +2 -1
  28. package/dist/multi-agent-trust.js +5 -5
  29. package/dist/node-projection.js +59 -0
  30. package/dist/node-snapshot.js +8 -18
  31. package/dist/orchestrator/lifecycle-operations.js +22 -1
  32. package/dist/orchestrator.js +29 -2
  33. package/dist/reclamation/hash.js +72 -0
  34. package/dist/reclamation.js +31 -112
  35. package/dist/run-registry/queue.js +6 -7
  36. package/dist/run-registry.js +35 -24
  37. package/dist/scheduler.js +112 -57
  38. package/dist/topology.js +25 -4
  39. package/dist/trust-audit.js +70 -38
  40. package/dist/validation.js +328 -0
  41. package/dist/version.js +1 -1
  42. package/dist/worker-isolation.js +163 -58
  43. package/docs/agent-delegation-drive.7.md +90 -85
  44. package/docs/agent-framework.md +33 -32
  45. package/docs/candidate-scoring.7.md +26 -24
  46. package/docs/canonical-workflow-apps.7.md +40 -40
  47. package/docs/capability-topology-registry.7.md +24 -24
  48. package/docs/cli-mcp-parity.7.md +227 -154
  49. package/docs/contract-migration-tooling.7.md +49 -41
  50. package/docs/control-plane-scheduling.7.md +45 -40
  51. package/docs/coordinator-blackboard.7.md +30 -30
  52. package/docs/dogfood-one-real-repo.7.md +44 -44
  53. package/docs/durable-state-and-locking.7.md +35 -30
  54. package/docs/end-to-end-golden-path.7.md +29 -29
  55. package/docs/error-feedback.7.md +27 -27
  56. package/docs/evidence-adoption-reasoning-chain.7.md +63 -58
  57. package/docs/execution-backends.7.md +84 -79
  58. package/docs/getting-started.md +35 -18
  59. package/docs/index.md +3 -3
  60. package/docs/mcp-app-surface.7.md +64 -64
  61. package/docs/multi-agent-cli-mcp-surface.7.md +83 -77
  62. package/docs/multi-agent-eval-replay-harness.7.md +59 -54
  63. package/docs/multi-agent-operator-ux.7.md +70 -65
  64. package/docs/multi-agent-runtime-core.7.md +39 -39
  65. package/docs/multi-agent-topologies.7.md +24 -24
  66. package/docs/multi-agent-trust-policy-audit.7.md +38 -38
  67. package/docs/node-snapshot-diff-replay.7.md +26 -21
  68. package/docs/observability-cost-accounting.7.md +50 -45
  69. package/docs/operator-ux.7.md +30 -30
  70. package/docs/pipeline-runner.7.md +31 -31
  71. package/docs/project-index.md +15 -6
  72. package/docs/real-execution-backends.7.md +47 -42
  73. package/docs/release-and-migration.7.md +43 -38
  74. package/docs/release-tooling.7.md +74 -39
  75. package/docs/routines.md +16 -16
  76. package/docs/run-registry-control-plane.7.md +120 -115
  77. package/docs/run-retention-reclamation.7.md +46 -41
  78. package/docs/sandbox-profiles.7.md +32 -32
  79. package/docs/scheduled-tasks.md +14 -14
  80. package/docs/security-trust-hardening.7.md +29 -29
  81. package/docs/source-context-profiles.7.md +28 -28
  82. package/docs/state-explosion-management.7.md +64 -59
  83. package/docs/state-node.7.md +8 -8
  84. package/docs/team-collaboration.7.md +63 -58
  85. package/docs/trust-model.md +126 -126
  86. package/docs/unix-principles.md +80 -80
  87. package/docs/vendor-manifest-loadability.7.md +20 -20
  88. package/docs/verifier-gated-commit.7.md +16 -16
  89. package/docs/web-desktop-workbench.7.md +70 -65
  90. package/docs/worker-isolation.7.md +34 -37
  91. package/docs/workflow-app-framework.7.md +38 -38
  92. package/manifest/plugin.manifest.json +4 -4
  93. package/manifest/source-context-profiles.json +1 -1
  94. package/package.json +3 -2
  95. package/scripts/canonical-apps.js +4 -4
  96. package/scripts/children/batch-delegate-child.js +58 -0
  97. package/scripts/children/http-delegate-child.js +39 -0
  98. package/scripts/dogfood-release.js +1 -1
  99. package/scripts/gen-parity-doc.js +106 -0
  100. package/scripts/golden-path.js +4 -4
  101. package/scripts/release-flow.js +181 -5
  102. package/dist/verifier-registry.js +0 -46
@@ -162,29 +162,36 @@ class RunRegistry {
162
162
  return node_path_1.default.join(this.homeRoot, "registry");
163
163
  }
164
164
  // ---- per-repo overlays (plain files) ------------------------------------
165
+ // Overlay reads distinguish ABSENT (clean default) from PRESENT-but-corrupt
166
+ // (fail closed). readJson throws `Invalid JSON in <file>` on a present file
167
+ // that won't parse; we let that propagate instead of swallowing it. Swallowing
168
+ // is the absent-vs-corrupt conflation telemetry-ledger.ts flags as the bug that
169
+ // "let a corrupt overlay verify green" — here it would silently un-archive every
170
+ // archived run / drop every provenance link. This is authoritative durable state.
171
+ // A present overlay must parse to a JSON OBJECT. readJson already fails closed
172
+ // on unparseable bytes; this catches the next shape over: valid JSON that is
173
+ // `null`, an array, or a scalar. Without it `parsed.archived` throws a cryptic
174
+ // TypeError (null) or silently reads `undefined` (array) and the whole registry
175
+ // scan breaks. Fail closed with a clear message instead.
176
+ requireOverlayObject(parsed, file) {
177
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
178
+ throw new Error(`Corrupt overlay ${file}: expected a JSON object, got ${Array.isArray(parsed) ? "array" : parsed === null ? "null" : typeof parsed}`);
179
+ }
180
+ return parsed;
181
+ }
165
182
  loadArchiveOverlay(repo) {
166
183
  const file = node_path_1.default.join(this.repoRegistryDir(repo), "archive.json");
167
184
  if (!node_fs_1.default.existsSync(file))
168
185
  return { schemaVersion: 1, archived: {} };
169
- try {
170
- const parsed = (0, state_1.readJson)(file);
171
- return { schemaVersion: 1, archived: parsed.archived || {} };
172
- }
173
- catch {
174
- return { schemaVersion: 1, archived: {} };
175
- }
186
+ const parsed = this.requireOverlayObject((0, state_1.readJson)(file), file);
187
+ return { schemaVersion: 1, archived: parsed.archived || {} };
176
188
  }
177
189
  loadProvenanceOverlay(repo) {
178
190
  const file = node_path_1.default.join(this.repoRegistryDir(repo), "provenance.json");
179
191
  if (!node_fs_1.default.existsSync(file))
180
192
  return { schemaVersion: 1, links: {} };
181
- try {
182
- const parsed = (0, state_1.readJson)(file);
183
- return { schemaVersion: 1, links: parsed.links || {} };
184
- }
185
- catch {
186
- return { schemaVersion: 1, links: {} };
187
- }
193
+ const parsed = this.requireOverlayObject((0, state_1.readJson)(file), file);
194
+ return { schemaVersion: 1, links: parsed.links || {} };
188
195
  }
189
196
  loadRepoOverlays(repo) {
190
197
  return {
@@ -203,15 +210,14 @@ class RunRegistry {
203
210
  }
204
211
  loadRepos() {
205
212
  const file = this.reposFilePath();
213
+ // Absent => no registered repos. Present-but-corrupt must fail closed: a
214
+ // swallowed parse error here silently drops every cross-repo root the
215
+ // operator registered, shrinking the home index to the current repo with no
216
+ // signal. Let readJson's `Invalid JSON` throw surface the corruption.
206
217
  if (!node_fs_1.default.existsSync(file))
207
218
  return { schemaVersion: 1, repos: [] };
208
- try {
209
- const parsed = (0, state_1.readJson)(file);
210
- return { schemaVersion: 1, repos: Array.isArray(parsed.repos) ? parsed.repos : [] };
211
- }
212
- catch {
213
- return { schemaVersion: 1, repos: [] };
214
- }
219
+ const parsed = this.requireOverlayObject((0, state_1.readJson)(file), file);
220
+ return { schemaVersion: 1, repos: Array.isArray(parsed.repos) ? parsed.repos : [] };
215
221
  }
216
222
  /** Persisted union of registered repo roots and the current repo, deduped and
217
223
  * sorted. Read-only: does NOT write repos.json (reads stay pure). */
@@ -745,9 +751,14 @@ class RunRegistry {
745
751
  // Record provenance in the per-repo overlay (derived metadata), NOT in the
746
752
  // original run's source state — the past is never mutated.
747
753
  const provFile = node_path_1.default.join(this.repoRegistryDir(original.repo), "provenance.json");
748
- const provOverlay = this.loadProvenanceOverlay(original.repo);
749
- provOverlay.links[newRun.id] = provenance;
750
- (0, state_1.writeJson)(provFile, provOverlay, { durable: true });
754
+ // Lock the read-modify-write: a concurrent rerun/archive on the same repo
755
+ // overlay would otherwise last-writer-wins and drop a provenance link. The
756
+ // sibling writers (registerRepo, archive) already serialize via withFileLock.
757
+ (0, state_1.withFileLock)(provFile, () => {
758
+ const provOverlay = this.loadProvenanceOverlay(original.repo);
759
+ provOverlay.links[newRun.id] = provenance;
760
+ (0, state_1.writeJson)(provFile, provOverlay, { durable: true });
761
+ });
751
762
  return {
752
763
  schemaVersion: 1,
753
764
  originalRunId: runId,
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");
@@ -15,6 +16,16 @@ class Scheduler {
15
16
  this.cwd = node_path_1.default.resolve(cwd);
16
17
  this.storePath = node_path_1.default.join(this.cwd, ".cw", "schedules", "tasks.json");
17
18
  }
19
+ // Every mutation is a cross-process read-modify-write of the one store file
20
+ // (the daemon polls `due` while CLI calls create/complete/delete concurrently).
21
+ // Without serialization two writers both load, both atomically rename their
22
+ // copy back, and the second silently clobbers the first's new task / status /
23
+ // history record. `locked` holds the same advisory lock the queue and
24
+ // reclamation stores already use. Reads (list/history) need no lock: the atomic
25
+ // rename means a reader always sees a whole old-or-new store.
26
+ locked(fn) {
27
+ return (0, state_1.withFileLock)(this.storePath, fn);
28
+ }
18
29
  create(options) {
19
30
  const kind = normalizeKind(options.kind);
20
31
  const now = new Date();
@@ -42,23 +53,30 @@ class Scheduler {
42
53
  maxRuns: numberOption(options.maxRuns),
43
54
  runCount: 0
44
55
  };
45
- const store = this.load();
46
- store.tasks.push(task);
47
- this.save(store);
48
- return task;
56
+ return this.locked(() => {
57
+ const store = this.load();
58
+ store.tasks.push(task);
59
+ this.save(store);
60
+ return task;
61
+ });
49
62
  }
50
63
  list(status) {
51
64
  const store = this.load();
52
65
  return status ? store.tasks.filter((task) => task.status === status) : store.tasks;
53
66
  }
54
67
  delete(id) {
55
- const store = this.load();
56
- const before = store.tasks.length;
57
- store.tasks = store.tasks.filter((task) => task.id !== id);
58
- this.save(store);
59
- return { deleted: store.tasks.length !== before, id };
68
+ return this.locked(() => {
69
+ const store = this.load();
70
+ const before = store.tasks.length;
71
+ store.tasks = store.tasks.filter((task) => task.id !== id);
72
+ this.save(store);
73
+ return { deleted: store.tasks.length !== before, id };
74
+ });
60
75
  }
61
76
  due(now = new Date()) {
77
+ return this.locked(() => this.dueLocked(now));
78
+ }
79
+ dueLocked(now) {
62
80
  const store = this.load();
63
81
  let changed = false;
64
82
  for (const task of store.tasks) {
@@ -86,63 +104,71 @@ class Scheduler {
86
104
  return dueTasks;
87
105
  }
88
106
  complete(id, options = {}) {
89
- const store = this.load();
90
- const task = store.tasks.find((candidate) => candidate.id === id);
91
- if (!task)
92
- throw new Error(`Scheduled task not found: ${id}`);
93
- const now = new Date();
94
- task.runCount += 1;
95
- task.lastRunAt = now.toISOString();
96
- task.updatedAt = now.toISOString();
97
- const maxRuns = numberOption(options.maxRuns) ?? task.maxRuns;
98
- if (maxRuns !== undefined)
99
- task.maxRuns = maxRuns;
100
- if (task.kind === "reminder" || (task.maxRuns !== undefined && task.runCount >= task.maxRuns)) {
101
- task.status = "completed";
102
- }
103
- else {
104
- task.nextRunAt = computeNextRunAt(task, now).toISOString();
105
- }
106
- this.save(store);
107
- return task;
107
+ return this.locked(() => {
108
+ const store = this.load();
109
+ const task = store.tasks.find((candidate) => candidate.id === id);
110
+ if (!task)
111
+ throw new Error(`Scheduled task not found: ${id}`);
112
+ const now = new Date();
113
+ task.runCount += 1;
114
+ task.lastRunAt = now.toISOString();
115
+ task.updatedAt = now.toISOString();
116
+ const maxRuns = numberOption(options.maxRuns) ?? task.maxRuns;
117
+ if (maxRuns !== undefined)
118
+ task.maxRuns = maxRuns;
119
+ if (task.kind === "reminder" || (task.maxRuns !== undefined && task.runCount >= task.maxRuns)) {
120
+ task.status = "completed";
121
+ }
122
+ else {
123
+ task.nextRunAt = computeNextRunAt(task, now).toISOString();
124
+ }
125
+ this.save(store);
126
+ return task;
127
+ });
108
128
  }
109
129
  pause(id) {
110
130
  return this.setStatus(id, "paused");
111
131
  }
112
132
  resume(id) {
113
- const store = this.load();
114
- const task = findTask(store, id);
115
- const now = new Date();
116
- task.status = "active";
117
- task.updatedAt = now.toISOString();
118
- if (new Date(task.nextRunAt).getTime() <= now.getTime()) {
119
- task.nextRunAt = computeNextRunAt(task, now).toISOString();
120
- }
121
- this.save(store);
122
- return task;
133
+ return this.locked(() => {
134
+ const store = this.load();
135
+ const task = findTask(store, id);
136
+ const now = new Date();
137
+ task.status = "active";
138
+ task.updatedAt = now.toISOString();
139
+ if (new Date(task.nextRunAt).getTime() <= now.getTime()) {
140
+ task.nextRunAt = computeNextRunAt(task, now).toISOString();
141
+ }
142
+ this.save(store);
143
+ return task;
144
+ });
123
145
  }
124
146
  runNow(id) {
125
- const store = this.load();
126
- const task = findTask(store, id);
127
- const now = new Date();
128
- task.lastDueAt = now.toISOString();
129
- task.updatedAt = now.toISOString();
130
- const record = createHistoryRecord(task, "started", this.cwd, now);
131
- store.history.push(record);
132
- this.save(store);
133
- return record;
147
+ return this.locked(() => {
148
+ const store = this.load();
149
+ const task = findTask(store, id);
150
+ const now = new Date();
151
+ task.lastDueAt = now.toISOString();
152
+ task.updatedAt = now.toISOString();
153
+ const record = createHistoryRecord(task, "started", this.cwd, now);
154
+ store.history.push(record);
155
+ this.save(store);
156
+ return record;
157
+ });
134
158
  }
135
159
  history(id) {
136
160
  const store = this.load();
137
161
  return id ? store.history.filter((record) => record.scheduleId === id) : store.history;
138
162
  }
139
163
  setStatus(id, status) {
140
- const store = this.load();
141
- const task = findTask(store, id);
142
- task.status = status;
143
- task.updatedAt = new Date().toISOString();
144
- this.save(store);
145
- return task;
164
+ return this.locked(() => {
165
+ const store = this.load();
166
+ const task = findTask(store, id);
167
+ task.status = status;
168
+ task.updatedAt = new Date().toISOString();
169
+ this.save(store);
170
+ return task;
171
+ });
146
172
  }
147
173
  load() {
148
174
  if (!node_fs_1.default.existsSync(this.storePath))
@@ -233,19 +259,48 @@ function matchesCron(value, expr, min, max) {
233
259
  return Number.isInteger(parsed) && parsed >= min && parsed <= max && parsed === value;
234
260
  });
235
261
  }
262
+ // Deterministic jitter (replay-determinism self-audit): the jittered Date is NOT a
263
+ // runtime-only sleep — it lands in persisted/replayed state (task.nextRunAt and,
264
+ // transitively, the schedule store), so a Math.random() offset broke replay
265
+ // determinism. The spread (0..jitterSeconds) is now derived from a content hash of
266
+ // the base instant, so the same base time + jitter window always yields the same
267
+ // offset; distinct base times still spread out across the window. The base Date is
268
+ // itself an edge timestamp that is recorded once.
236
269
  function addJitter(date, jitterSeconds) {
237
270
  if (!jitterSeconds)
238
271
  return date;
239
- const offset = Math.floor(Math.random() * (jitterSeconds + 1)) * 1000;
240
- return new Date(date.getTime() + offset);
272
+ const digest = node_crypto_1.default.createHash("sha256").update(`${date.getTime()}`).digest();
273
+ const seconds = digest.readUInt32BE(0) % (jitterSeconds + 1);
274
+ return new Date(date.getTime() + seconds * 1000);
241
275
  }
276
+ // Deterministic schedule id (replay-determinism self-audit): the stamp is an edge
277
+ // timestamp (recorded once), but the former Math.random() suffix made each
278
+ // persisted schedule id non-reproducible. The suffix is now a content hash of the
279
+ // schedule's deterministic identity (kind + the recorded stamp), so re-deriving the
280
+ // id for a recorded schedule yields the byte-identical value while schedules created
281
+ // at distinct instants still get distinct ids. Mirrors src/worker-isolation/paths.ts.
282
+ let scheduleIdSequence = 0;
242
283
  function createScheduleId(kind) {
243
284
  const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\..+/, "Z");
244
- return `${kind}-${stamp}-${Math.random().toString(36).slice(2, 8)}`;
285
+ // Second-resolution stamp: two schedules of the same kind created within one
286
+ // second would otherwise collide on an identical id. process.pid + a monotonic
287
+ // counter break the tie across concurrent processes and within one process,
288
+ // deterministically (not a PRNG).
289
+ scheduleIdSequence += 1;
290
+ const suffix = node_crypto_1.default.createHash("sha256").update(`${kind}:${stamp}:${process.pid}:${scheduleIdSequence}`).digest("hex").slice(0, 6);
291
+ return `${kind}-${stamp}-${suffix}`;
245
292
  }
293
+ // Deterministic schedule-run (history) id — same rationale as createScheduleId. The
294
+ // history record stamp differs from the owning schedule's, so the hashed identity
295
+ // (kind + run stamp) stays distinct from the schedule id while remaining a pure
296
+ // function of already-recorded state.
297
+ let scheduleRunIdSequence = 0;
246
298
  function createScheduleRunId(kind) {
247
299
  const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\..+/, "Z");
248
- return `run-${kind}-${stamp}-${Math.random().toString(36).slice(2, 8)}`;
300
+ // pid + counter break same-kind/same-second collisions (see createScheduleId).
301
+ scheduleRunIdSequence += 1;
302
+ const suffix = node_crypto_1.default.createHash("sha256").update(`run:${kind}:${stamp}:${process.pid}:${scheduleRunIdSequence}`).digest("hex").slice(0, 6);
303
+ return `run-${kind}-${stamp}-${suffix}`;
249
304
  }
250
305
  function requiredString(value, name) {
251
306
  const text = stringOption(value);
package/dist/topology.js CHANGED
@@ -17,6 +17,8 @@ exports.showTopologyRun = showTopologyRun;
17
17
  const node_fs_1 = __importDefault(require("node:fs"));
18
18
  const node_path_1 = __importDefault(require("node:path"));
19
19
  const state_1 = require("./state");
20
+ const execution_backend_1 = require("./execution-backend");
21
+ const telemetry_attestation_1 = require("./telemetry-attestation");
20
22
  const pipeline_contract_1 = require("./pipeline-contract");
21
23
  const state_node_1 = require("./state-node");
22
24
  const trust_audit_1 = require("./trust-audit");
@@ -186,10 +188,16 @@ function applyTopology(run, topologyId, input = {}) {
186
188
  const definition = validation.definition;
187
189
  const state = ensureTopologyState(run);
188
190
  (0, multi_agent_1.ensureMultiAgentState)(run);
189
- const id = input.id || `${definition.id}-${timestampId()}`;
191
+ const taskIds = selectedTaskIds(run, input.taskIds);
192
+ // Default id is a DETERMINISTIC content-hash (replay determinism): two `topology
193
+ // apply` invocations WITHOUT --id over the same definition/tasks/run produce a
194
+ // byte-identical id. Same sha256/stableStringify the kernel uses for node ids
195
+ // (state-node/createNodeId) and ledger record ids. `state.runs.length` is the
196
+ // stable sequence so repeated applies on the same run get distinct ids without a
197
+ // wall-clock stamp. input.id stays an explicit override.
198
+ const id = input.id || topologyRunId(definition, taskIds, run.id, state.runs.length);
190
199
  if (state.runs.some((record) => record.id === id))
191
200
  throw new Error(`Duplicate MultiAgentTopologyRun id: ${id}`);
192
- const taskIds = selectedTaskIds(run, input.taskIds);
193
201
  const board = (0, coordinator_1.resolveBlackboard)(run, {
194
202
  id: input.blackboardId || `${id}-blackboard`,
195
203
  title: `${definition.title} Blackboard`,
@@ -514,8 +522,21 @@ function statusToNodeStatus(status) {
514
522
  function issue(code, message, path) {
515
523
  return { code, message, path };
516
524
  }
517
- function timestampId() {
518
- return new Date().toISOString().replace(/[-:.]/g, "").slice(0, 15).toLowerCase();
525
+ // Deterministic default topology-run id (F2, replay determinism): no wall-clock.
526
+ // Bound to a short sha256 of canonical content — definition id, the SORTED role and
527
+ // selected-task ids, the workflow run id, and a stable sequence (the count of
528
+ // topology runs already on this run). Same sha256/stableStringify the kernel uses
529
+ // for node ids and ledger record hashes, so two replays without --id reach a
530
+ // byte-identical fingerprint, while distinct applies on one run stay distinct.
531
+ function topologyRunId(definition, taskIds, runId, sequence) {
532
+ const digest = (0, execution_backend_1.sha256)((0, telemetry_attestation_1.stableStringify)({
533
+ definitionId: definition.id,
534
+ roleIds: [...definition.roles.map((role) => role.id)].sort(),
535
+ taskIds: [...taskIds].sort(),
536
+ runId,
537
+ sequence
538
+ }));
539
+ return `${definition.id}-${digest.replace("sha256:", "").slice(0, 16)}`;
519
540
  }
520
541
  function countBy(items, pick) {
521
542
  const counts = {};
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.TRUST_AUDIT_SCHEMA_VERSION = void 0;
6
+ exports.CORRELATION_ID_FIELDS = exports.TRUST_AUDIT_SCHEMA_VERSION = void 0;
7
7
  exports.trustAuditGenesis = trustAuditGenesis;
8
8
  exports.verifyTrustAudit = verifyTrustAudit;
9
9
  exports.ensureTrustAudit = ensureTrustAudit;
@@ -152,6 +152,65 @@ function verifyTrustAudit(run) {
152
152
  }
153
153
  return { present: events.length > 0, verified, eventCount: events.length, chained, unchained, corruptLines, checks };
154
154
  }
155
+ // ---- Correlation-id fields (single source of truth, F12) -------------------
156
+ // These id fields are plain pass-throughs that flow input -> persisted event ->
157
+ // summary index unchanged. They were previously re-listed by hand in three
158
+ // places (recordTrustAuditEvent, the index writer, and partially in
159
+ // multi-agent-trust), so adding a new correlation id meant editing every list and
160
+ // silently dropping it from the index when one was missed. They now live in ONE
161
+ // array; both spread sites pick from it via pickCorrelationIds, so a new id is
162
+ // added in exactly one place.
163
+ //
164
+ // SERIALIZATION-PRESERVING by design: this is a plain key-copy with NO transform,
165
+ // so the persisted JSON for these keys is byte-identical to the old hand-spread
166
+ // (compact() still drops the undefined ones). The hash chain that binds these
167
+ // fields is therefore unaffected. Fields needing derivation/normalization
168
+ // (feedbackIds, sandboxProfileId, policyRef, multiAgentPolicyRef, normalizedPath,
169
+ // envVars, …) are intentionally NOT here — they keep their bespoke handling at the
170
+ // call site.
171
+ exports.CORRELATION_ID_FIELDS = [
172
+ "candidateId",
173
+ "scoreId",
174
+ "selectionId",
175
+ "commitId",
176
+ "multiAgentRunId",
177
+ "agentRoleId",
178
+ "agentGroupId",
179
+ "agentMembershipId",
180
+ "agentFanoutId",
181
+ "agentFaninId",
182
+ "blackboardId",
183
+ "blackboardTopicId",
184
+ "blackboardMessageId",
185
+ "blackboardContextId",
186
+ "blackboardArtifactRefId",
187
+ "blackboardSnapshotId",
188
+ "coordinatorDecisionId",
189
+ "topologyId",
190
+ "topologyRunId"
191
+ ];
192
+ /** Copy exactly the correlation-id keys (and no others) from `source`, preserving
193
+ * each value verbatim (including `undefined`, which compact()/JSON.stringify drop
194
+ * identically to the prior per-key spread). Spread the result; do not transform. */
195
+ function pickCorrelationIds(source) {
196
+ const picked = {};
197
+ for (const field of exports.CORRELATION_ID_FIELDS)
198
+ picked[field] = source[field];
199
+ return picked;
200
+ }
201
+ // The summary index has always carried every correlation id EXCEPT scoreId (the
202
+ // per-candidate score lives in the candidates[] rows, not the flat event index).
203
+ // Pin that exception in ONE place so the index stays byte-identical while still
204
+ // inheriting any NEW id from CORRELATION_ID_FIELDS automatically.
205
+ const INDEX_OMITTED_CORRELATION_IDS = new Set(["scoreId"]);
206
+ /** Correlation ids for the summary index: the full pick minus the keys the index
207
+ * has historically omitted (see INDEX_OMITTED_CORRELATION_IDS). */
208
+ function indexCorrelationIds(event) {
209
+ const picked = pickCorrelationIds(event);
210
+ for (const field of INDEX_OMITTED_CORRELATION_IDS)
211
+ delete picked[field];
212
+ return picked;
213
+ }
155
214
  function ensureTrustAudit(run) {
156
215
  const auditDir = auditRoot(run);
157
216
  node_fs_1.default.mkdirSync(auditDir, { recursive: true });
@@ -177,25 +236,11 @@ function recordTrustAuditEvent(run, input) {
177
236
  taskId: input.taskId,
178
237
  nodeId: input.nodeId,
179
238
  feedbackIds: input.feedbackIds?.filter(Boolean).sort(),
180
- candidateId: input.candidateId,
181
- scoreId: input.scoreId,
182
- selectionId: input.selectionId,
183
- commitId: input.commitId,
184
- multiAgentRunId: input.multiAgentRunId,
185
- agentRoleId: input.agentRoleId,
186
- agentGroupId: input.agentGroupId,
187
- agentMembershipId: input.agentMembershipId,
188
- agentFanoutId: input.agentFanoutId,
189
- agentFaninId: input.agentFaninId,
190
- blackboardId: input.blackboardId,
191
- blackboardTopicId: input.blackboardTopicId,
192
- blackboardMessageId: input.blackboardMessageId,
193
- blackboardContextId: input.blackboardContextId,
194
- blackboardArtifactRefId: input.blackboardArtifactRefId,
195
- blackboardSnapshotId: input.blackboardSnapshotId,
196
- coordinatorDecisionId: input.coordinatorDecisionId,
197
- topologyId: input.topologyId,
198
- topologyRunId: input.topologyRunId,
239
+ // Plain correlation-id pass-throughs (candidateId … topologyRunId) come from the
240
+ // single CORRELATION_ID_FIELDS list. Key order is preserved (the list is in the
241
+ // same order the keys used to be hand-written), so the persisted JSON — and thus
242
+ // the eventHash chain — is byte-identical.
243
+ ...pickCorrelationIds(input),
199
244
  sandboxProfileId: input.sandboxProfileId || input.policySnapshot?.id,
200
245
  policyRef: input.policyRef || (input.policySnapshot?.id ? `run.sandboxProfiles.${input.policySnapshot.id}` : undefined),
201
246
  multiAgentPolicyRef: input.policyRef,
@@ -351,24 +396,11 @@ function summarizeTrustAudit(run) {
351
396
  source: event.source,
352
397
  workerId: event.workerId,
353
398
  taskId: event.taskId,
354
- candidateId: event.candidateId,
355
- selectionId: event.selectionId,
356
- commitId: event.commitId,
357
- multiAgentRunId: event.multiAgentRunId,
358
- agentRoleId: event.agentRoleId,
359
- agentGroupId: event.agentGroupId,
360
- agentMembershipId: event.agentMembershipId,
361
- agentFanoutId: event.agentFanoutId,
362
- agentFaninId: event.agentFaninId,
363
- blackboardId: event.blackboardId,
364
- blackboardTopicId: event.blackboardTopicId,
365
- blackboardMessageId: event.blackboardMessageId,
366
- blackboardContextId: event.blackboardContextId,
367
- blackboardArtifactRefId: event.blackboardArtifactRefId,
368
- blackboardSnapshotId: event.blackboardSnapshotId,
369
- coordinatorDecisionId: event.coordinatorDecisionId,
370
- topologyId: event.topologyId,
371
- topologyRunId: event.topologyRunId,
399
+ // Same single CORRELATION_ID_FIELDS list the record path uses, so the index
400
+ // can never silently omit a correlation id that the event carries. (The index
401
+ // historically omitted scoreId; that is preserved below by deleting it after
402
+ // the pick, keeping this writer's output byte-identical.)
403
+ ...indexCorrelationIds(event),
372
404
  sandboxProfileId: event.sandboxProfileId,
373
405
  policyRef: event.policyRef,
374
406
  multiAgentPolicyRef: event.multiAgentPolicyRef