cool-workflow 0.2.2 → 0.2.3

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 (136) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/apps/architecture-review/app.json +1 -1
  4. package/apps/architecture-review-fast/app.json +1 -1
  5. package/apps/end-to-end-golden-path/app.json +1 -1
  6. package/apps/pr-review-fix-ci/app.json +1 -1
  7. package/apps/release-cut/app.json +1 -1
  8. package/apps/research-synthesis/app.json +1 -1
  9. package/dist/cli/dispatch.js +11 -5
  10. package/dist/cli/entry.js +41 -1
  11. package/dist/cli/io.js +6 -2
  12. package/dist/cli/parseargv.js +1 -0
  13. package/dist/core/capability-data.js +1 -0
  14. package/dist/core/format/completion.js +68 -0
  15. package/dist/core/format/help.js +25 -6
  16. package/dist/core/format/safe-json.js +73 -0
  17. package/dist/core/format/state-explosion-text.js +1 -1
  18. package/dist/core/multi-agent/candidate-scoring.js +5 -1
  19. package/dist/core/multi-agent/collaboration.js +3 -3
  20. package/dist/core/multi-agent/coordinator.js +5 -5
  21. package/dist/core/multi-agent/runtime.js +4 -4
  22. package/dist/core/multi-agent/topology.js +3 -3
  23. package/dist/core/pipeline/dispatch.js +18 -4
  24. package/dist/core/pipeline/drive-decide.js +2 -1
  25. package/dist/core/state/migrations.js +16 -1
  26. package/dist/core/state/state-explosion/digest.js +13 -13
  27. package/dist/core/state/state-explosion/graph.js +17 -11
  28. package/dist/core/state/state-explosion/report.js +6 -6
  29. package/dist/core/util/numeric-flag.js +40 -0
  30. package/dist/core/version.js +1 -1
  31. package/dist/mcp/server.js +99 -11
  32. package/dist/shell/audit-cli.js +57 -25
  33. package/dist/shell/coordinator-io.js +73 -13
  34. package/dist/shell/dispatch.js +1 -1
  35. package/dist/shell/doctor.js +80 -1
  36. package/dist/shell/drive.js +243 -49
  37. package/dist/shell/eval-text.js +2 -2
  38. package/dist/shell/evidence-reasoning.js +4 -4
  39. package/dist/shell/execution-backend/agent.js +20 -1
  40. package/dist/shell/execution-backend/container.js +4 -1
  41. package/dist/shell/execution-backend/local.js +4 -1
  42. package/dist/shell/feedback-cli.js +6 -6
  43. package/dist/shell/fs-atomic.js +218 -29
  44. package/dist/shell/man-cli.js +6 -0
  45. package/dist/shell/metrics-cli.js +2 -1
  46. package/dist/shell/multi-agent-cli.js +367 -323
  47. package/dist/shell/multi-agent-host.js +9 -9
  48. package/dist/shell/multi-agent-operator-ux.js +80 -38
  49. package/dist/shell/node-store.js +10 -4
  50. package/dist/shell/observability.js +1 -1
  51. package/dist/shell/operator-ux-text.js +22 -22
  52. package/dist/shell/operator-ux.js +15 -14
  53. package/dist/shell/orchestrator.js +49 -38
  54. package/dist/shell/pipeline-cli.js +87 -40
  55. package/dist/shell/reclamation-io.js +14 -4
  56. package/dist/shell/registry-cli.js +19 -17
  57. package/dist/shell/remote-source.js +13 -8
  58. package/dist/shell/report-cli.js +45 -0
  59. package/dist/shell/report.js +2 -1
  60. package/dist/shell/run-registry-io.js +77 -19
  61. package/dist/shell/run-store.js +74 -2
  62. package/dist/shell/scheduling-io.js +12 -13
  63. package/dist/shell/state-cli.js +2 -7
  64. package/dist/shell/state-explosion-cli.js +17 -9
  65. package/dist/shell/topology-io.js +36 -5
  66. package/dist/shell/trust-audit.js +224 -22
  67. package/dist/shell/trust-policy-io.js +1 -1
  68. package/dist/shell/worker-cli.js +35 -31
  69. package/dist/wiring/capability-table/basics.js +33 -8
  70. package/dist/wiring/capability-table/exec-backend.js +20 -11
  71. package/dist/wiring/capability-table/multi-agent.js +172 -165
  72. package/dist/wiring/capability-table/parity.js +1 -0
  73. package/dist/wiring/capability-table/pipeline.js +54 -42
  74. package/dist/wiring/capability-table/registry-core.js +29 -10
  75. package/dist/wiring/capability-table/reporting.js +137 -96
  76. package/dist/wiring/capability-table/scheduling-registry.js +195 -161
  77. package/dist/wiring/capability-table/state.js +36 -28
  78. package/dist/wiring/capability-table/trust-ledger.js +46 -22
  79. package/dist/wiring/capability-table/workflow-apps.js +83 -68
  80. package/docs/agent-delegation-drive.7.md +2 -0
  81. package/docs/candidate-scoring.7.md +1 -1
  82. package/docs/canonical-workflow-apps.7.md +7 -7
  83. package/docs/cli-mcp-parity.7.md +8 -3
  84. package/docs/contract-migration-tooling.7.md +2 -0
  85. package/docs/control-plane-scheduling.7.md +2 -0
  86. package/docs/coordinator-blackboard.7.md +17 -17
  87. package/docs/dogfood-one-real-repo.7.md +11 -11
  88. package/docs/durable-state-and-locking.7.md +2 -0
  89. package/docs/end-to-end-golden-path.7.md +14 -14
  90. package/docs/evidence-adoption-reasoning-chain.7.md +2 -0
  91. package/docs/execution-backends.7.md +2 -0
  92. package/docs/getting-started.md +37 -37
  93. package/docs/multi-agent-cli-mcp-surface.7.md +19 -17
  94. package/docs/multi-agent-eval-replay-harness.7.md +15 -13
  95. package/docs/multi-agent-operator-ux.7.md +21 -19
  96. package/docs/multi-agent-runtime-core.7.md +22 -22
  97. package/docs/multi-agent-topologies.7.md +6 -6
  98. package/docs/multi-agent-trust-policy-audit.7.md +11 -11
  99. package/docs/node-snapshot-diff-replay.7.md +2 -0
  100. package/docs/observability-cost-accounting.7.md +2 -0
  101. package/docs/operator-ux.7.md +34 -34
  102. package/docs/pipeline-runner.7.md +4 -4
  103. package/docs/project-index.md +27 -4
  104. package/docs/real-execution-backends.7.md +2 -0
  105. package/docs/release-and-migration.7.md +3 -1
  106. package/docs/release-tooling.7.md +2 -0
  107. package/docs/routines.md +4 -4
  108. package/docs/run-registry-control-plane.7.md +21 -19
  109. package/docs/run-retention-reclamation.7.md +2 -0
  110. package/docs/scheduled-tasks.md +14 -14
  111. package/docs/security-trust-hardening.7.md +15 -15
  112. package/docs/state-explosion-management.7.md +12 -10
  113. package/docs/team-collaboration.7.md +2 -0
  114. package/docs/trust-audit-anchor.7.md +2 -0
  115. package/docs/unix-principles.md +3 -1
  116. package/docs/verifier-gated-commit.7.md +1 -1
  117. package/docs/web-desktop-workbench.7.md +2 -0
  118. package/docs/workflow-app-framework.7.md +13 -13
  119. package/manifest/plugin.manifest.json +1 -1
  120. package/package.json +2 -2
  121. package/scripts/agents/claude-p-agent.js +2 -2
  122. package/scripts/block-unapproved-tag.sh +23 -2
  123. package/scripts/canonical-apps.js +4 -4
  124. package/scripts/children/batch-delegate-child.js +52 -2
  125. package/scripts/dogfood-release.js +1 -1
  126. package/scripts/fake-date-for-reproduction.js +44 -0
  127. package/scripts/golden-path.js +4 -4
  128. package/scripts/purity-baseline.json +3 -0
  129. package/scripts/release-flow.js +57 -1
  130. package/scripts/verdict-keygen.js +83 -0
  131. package/scripts/verify-bump-reproduction.sh +148 -0
  132. package/scripts/verify-verdict-signature.js +61 -0
  133. package/skills/cool-workflow/SKILL.md +9 -9
  134. package/skills/cool-workflow/references/commands.md +89 -88
  135. package/ui/workbench/app.css +37 -1
  136. package/ui/workbench/app.js +124 -6
@@ -60,6 +60,7 @@ exports.trustAuditGenesis = trustAuditGenesis;
60
60
  exports.listTrustAuditEvents = listTrustAuditEvents;
61
61
  exports.trustAuditHead = trustAuditHead;
62
62
  exports.verifyTrustAudit = verifyTrustAudit;
63
+ exports.repairTrustAuditTornTail = repairTrustAuditTornTail;
63
64
  exports.recordTrustAuditEvent = recordTrustAuditEvent;
64
65
  exports.recordSandboxPathDecision = recordSandboxPathDecision;
65
66
  exports.normalizeEvidence = normalizeEvidence;
@@ -106,6 +107,43 @@ function computeEventHash(event) {
106
107
  void eventHash;
107
108
  return (0, hash_1.sha256)((0, hash_1.eventHashInput)(rest));
108
109
  }
110
+ function tailCachePathFor(eventLogPath) {
111
+ return path.join(path.dirname(eventLogPath), "tail-cache.json");
112
+ }
113
+ function readAuditTailCache(tailCachePath) {
114
+ if (!fs.existsSync(tailCachePath))
115
+ return undefined;
116
+ try {
117
+ const parsed = JSON.parse(fs.readFileSync(tailCachePath, "utf8"));
118
+ if (parsed && parsed.schemaVersion === 1 && typeof parsed.logBytes === "number" && typeof parsed.count === "number" && typeof parsed.lastHash === "string") {
119
+ return parsed;
120
+ }
121
+ }
122
+ catch {
123
+ // Corrupt/unreadable cache -- fall back to the full parse below.
124
+ }
125
+ return undefined;
126
+ }
127
+ function writeAuditTailCache(tailCachePath, cache) {
128
+ try {
129
+ (0, fs_atomic_1.writeJson)(tailCachePath, cache);
130
+ }
131
+ catch {
132
+ // Best-effort: a failed cache write must never break the real append.
133
+ }
134
+ }
135
+ /** Deletes the tail cache so the NEXT append always re-derives ground
136
+ * truth from the real log, rather than trusting a byte-size coincidence.
137
+ * Called whenever something OTHER than a plain append changes the log's
138
+ * content (currently: repair). */
139
+ function invalidateAuditTailCache(eventLogPath) {
140
+ try {
141
+ fs.unlinkSync(tailCachePathFor(eventLogPath));
142
+ }
143
+ catch {
144
+ // Nothing to invalidate (no cache existed yet) -- fine.
145
+ }
146
+ }
109
147
  /** Read events in FILE (append) order, tolerating corrupt lines — one
110
148
  * bad line must not brick the whole audit read surface (it is counted,
111
149
  * not thrown). The chain links append order, so this is the order
@@ -170,16 +208,17 @@ function trustAuditHead(run) {
170
208
  * the hash after each event) must contain `expectHead`, and the log
171
209
  * must reach `expectCount` events; a shortfall fails closed with
172
210
  * `trust-audit-truncated`. Without an anchor, behavior is unchanged. */
173
- function verifyTrustAudit(run, anchor) {
174
- const audit = ensureTrustAudit(run);
175
- const { events, corruptLines } = readEventsRawCounted(audit.eventLogPath);
211
+ /** Chain-walk core shared by `verifyTrustAudit` (reads from disk) and
212
+ * `repairTrustAuditTornTail` (re-checks an in-memory candidate result
213
+ * BEFORE ever writing it to disk). Pure — no fs. */
214
+ function verifyEventsChain(runId, events, corruptLines, anchor) {
176
215
  const checks = [];
177
216
  let verified = corruptLines === 0;
178
217
  if (corruptLines > 0)
179
218
  checks.push({ name: "parse", pass: false, code: "trust-audit-corrupt-line" });
180
219
  let chained = 0;
181
220
  let unchained = 0;
182
- let expectedPrev = trustAuditGenesis(run.id);
221
+ let expectedPrev = trustAuditGenesis(runId);
183
222
  const headTrail = new Set([expectedPrev]);
184
223
  for (let i = 0; i < events.length; i++) {
185
224
  const event = events[i];
@@ -195,7 +234,11 @@ function verifyTrustAudit(run, anchor) {
195
234
  verified = false;
196
235
  checks.push({ name: `event-hash[${i}]`, pass: false, code: "trust-audit-digest-mismatch" });
197
236
  }
198
- if (event.prevEventHash !== undefined && event.prevEventHash !== expectedPrev) {
237
+ // No `undefined` skip here: the writer ALWAYS sets prevEventHash (the
238
+ // first event gets the genesis hash), so a chained event without it is
239
+ // a forgery — dropping the field and re-making eventHash must NOT let a
240
+ // cut or re-ordered chain verify green. Fail closed on the mismatch.
241
+ if (event.prevEventHash !== expectedPrev) {
199
242
  verified = false;
200
243
  checks.push({ name: `chain-link[${i}]`, pass: false, code: "trust-audit-chain-broken" });
201
244
  }
@@ -223,6 +266,94 @@ function verifyTrustAudit(run, anchor) {
223
266
  }
224
267
  return { present: events.length > 0, verified, eventCount: events.length, chained, unchained, corruptLines, checks };
225
268
  }
269
+ function verifyTrustAudit(run, anchor) {
270
+ const audit = ensureTrustAudit(run);
271
+ const { events, corruptLines } = readEventsRawCounted(audit.eventLogPath);
272
+ return verifyEventsChain(run.id, events, corruptLines, anchor);
273
+ }
274
+ /** Repairs a torn TRAILING write in the audit event log — the ONE
275
+ * corruption shape a crash mid-append can produce (`durableAppendFileSync`
276
+ * only ever adds bytes at the current end of file, so an interruption can
277
+ * only ever leave the LAST append incomplete; it can never touch earlier,
278
+ * already-flushed lines). Every line is always actually parsed — the
279
+ * file's trailing-newline byte says only whether the LAST WRITE
280
+ * completed, nothing about whether any line parses: if a run is RESUMED
281
+ * after a torn write (another event appended right after the garbled
282
+ * remnant, with no separating newline of its own), the two merge into one
283
+ * unparseable line and the file ends in a newline again, even though
284
+ * content is still corrupt. Refuses (`outcome: "refused"`) rather than
285
+ * touching anything when:
286
+ * - more than one line is unparseable, or the sole bad line is NOT the
287
+ * last one (not a shape a crash can produce — treated as possible
288
+ * tampering, not auto-repaired);
289
+ * - removing the bad trailing line still leaves an unverifiable chain,
290
+ * OR (when `anchor` is given) the repaired chain doesn't reach
291
+ * `anchor.expectCount`/contain `anchor.expectHead` — an anchor
292
+ * captured before the corruption is the ONLY way to catch an attacker
293
+ * deleting real historical events and leaving a torn-looking fragment
294
+ * behind (an empty/short chain otherwise "verifies" trivially — the
295
+ * same documented blind spot `verifyTrustAudit` itself has without an
296
+ * anchor; this function must never launder that shape into a
297
+ * confidently-"repaired" empty log).
298
+ * `write: false` (default) reports what WOULD happen without touching
299
+ * disk, matching this codebase's `cw state check [--write]` convention.
300
+ * Held under the SAME `withFileLock` as `recordTrustAuditEvent` (below),
301
+ * so a repair can never interleave with a live append. */
302
+ function repairTrustAuditTornTail(run, options = {}) {
303
+ const audit = ensureTrustAudit(run);
304
+ return (0, fs_atomic_1.withFileLock)(audit.eventLogPath, () => {
305
+ const raw = fs.readFileSync(audit.eventLogPath, "utf8");
306
+ const lines = raw.split("\n").filter((line) => line.trim() !== "");
307
+ const badIndexes = [];
308
+ const events = [];
309
+ lines.forEach((line, i) => {
310
+ try {
311
+ events.push(JSON.parse(line));
312
+ }
313
+ catch {
314
+ badIndexes.push(i);
315
+ }
316
+ });
317
+ if (badIndexes.length === 0) {
318
+ return { outcome: "clean", reason: "every line parses — no torn trailing write to repair" };
319
+ }
320
+ if (badIndexes.length > 1 || badIndexes[0] !== lines.length - 1) {
321
+ return {
322
+ outcome: "refused",
323
+ reason: "corruption is not confined to exactly the trailing line — this is not a shape a crash mid-append can produce and will not be auto-repaired (looks like tampering)",
324
+ };
325
+ }
326
+ // `events` already holds every line EXCEPT the one bad trailing line
327
+ // (JSON.parse threw for it, so nothing was pushed) — exactly the "good"
328
+ // set, in file order.
329
+ const recheck = verifyEventsChain(run.id, events, 0, options.anchor);
330
+ if (!recheck.verified) {
331
+ return {
332
+ outcome: "refused",
333
+ reason: options.anchor
334
+ ? "removing the torn trailing write still doesn't reach the given --expect-head/--expect-count anchor — refusing to repair (this looks like deleted history, not a crash)"
335
+ : "removing the torn trailing write still leaves an unverifiable chain — refusing to repair (this looks like tampering, not a crash)",
336
+ };
337
+ }
338
+ const removedBytes = Buffer.byteLength(lines[lines.length - 1], "utf8");
339
+ const repairedContent = events.length > 0 ? `${lines.slice(0, -1).join("\n")}\n` : "";
340
+ if (options.write) {
341
+ (0, fs_atomic_1.writeTextDurable)(audit.eventLogPath, repairedContent, { durable: true });
342
+ // The log's bytes just changed out from under the append tail cache
343
+ // (perf cycle P1-2) -- invalidate rather than rely on the size check
344
+ // alone catching every case.
345
+ invalidateAuditTailCache(audit.eventLogPath);
346
+ }
347
+ return {
348
+ outcome: "repaired",
349
+ reason: options.write
350
+ ? `removed a torn trailing write (${removedBytes} bytes) and restored a verified chain of ${events.length} event(s)`
351
+ : `would remove a torn trailing write (${removedBytes} bytes) and restore a verified chain of ${events.length} event(s) — pass --write to apply`,
352
+ removedLines: 1,
353
+ removedBytes,
354
+ };
355
+ });
356
+ }
226
357
  function unique(values) {
227
358
  return Array.from(new Set(values.filter(Boolean)));
228
359
  }
@@ -249,9 +380,14 @@ function scrubMetadata(value) {
249
380
  function compact(value) {
250
381
  return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
251
382
  }
252
- function createEventId(run, kind) {
253
- const count = readEventsRaw(trustAuditPaths(run).eventLogPath).length + 1;
254
- return `audit-${(0, fs_atomic_1.safeFileName)(kind)}-${String(count).padStart(4, "0")}`;
383
+ /** `count` must be the number of PRIOR events, read under the SAME lock
384
+ * the append itself happens under (see `recordTrustAuditEvent`) an
385
+ * earlier version read this count separately, outside any lock, so two
386
+ * concurrent writers could mint the SAME id for two different events
387
+ * (the hash chain still forked-proof, but the id — referenced elsewhere
388
+ * as `auditEventIds`/`parentEventIds` for provenance — was not unique). */
389
+ function createEventId(kind, count) {
390
+ return `audit-${(0, fs_atomic_1.safeFileName)(kind)}-${String(count + 1).padStart(4, "0")}`;
255
391
  }
256
392
  /** Correlation-id keys copied verbatim (and no others) — byte-exact list/
257
393
  * order to the old build's CORRELATION_ID_FIELDS. */
@@ -294,11 +430,20 @@ function indexCorrelationIds(event) {
294
430
  }
295
431
  return picked;
296
432
  }
433
+ /** Read-modify-append: computes `prevEventHash` from the CURRENT last event
434
+ * and appends. Held under `withFileLock` (like every other read-modify-
435
+ * write in this codebase) so two processes recording events for the same
436
+ * run at once can never both read the same tail and compute the same
437
+ * `prevEventHash` — that would fork the hash chain, and a forked chain
438
+ * fails `verifyTrustAudit` for good, with no repair for THAT shape (unlike
439
+ * a torn trailing write, a fork is not confined to the last line). */
297
440
  function recordTrustAuditEvent(run, input) {
298
441
  const audit = ensureTrustAudit(run);
442
+ // `id` is NOT set here — it depends on the prior event count, which (like
443
+ // prevEventHash) must be read under the lock below, or two concurrent
444
+ // writers could mint the same id for two different events.
299
445
  const event = compact({
300
446
  schemaVersion: exports.TRUST_AUDIT_SCHEMA_VERSION,
301
- id: createEventId(run, input.kind),
302
447
  createdAt: new Date().toISOString(),
303
448
  runId: run.id,
304
449
  kind: input.kind,
@@ -322,11 +467,35 @@ function recordTrustAuditEvent(run, input) {
322
467
  parentEventIds: unique(input.parentEventIds || []).sort(),
323
468
  metadata: scrubMetadata(input.metadata || {}),
324
469
  });
325
- const prior = readEventsRaw(audit.eventLogPath);
326
- event.prevEventHash = prior.length ? prior[prior.length - 1].eventHash || computeEventHash(prior[prior.length - 1]) : trustAuditGenesis(run.id);
327
- event.eventHash = computeEventHash(event);
328
- (0, fs_atomic_1.durableAppendFileSync)(audit.eventLogPath, `${JSON.stringify(event)}\n`);
329
- return event;
470
+ return (0, fs_atomic_1.withFileLock)(audit.eventLogPath, () => {
471
+ // The prior event count and last-event hash are the ONLY two things
472
+ // this append needs from the existing log. A tail cache (keyed on the
473
+ // log's own byte size) serves both without a full parse when nothing
474
+ // else has touched the log since it was written; any size mismatch
475
+ // (a repair, a torn write, this being the very first append) falls
476
+ // back to the full parse, same as before this cache existed.
477
+ const tailCachePath = tailCachePathFor(audit.eventLogPath);
478
+ const currentBytes = fs.existsSync(audit.eventLogPath) ? fs.statSync(audit.eventLogPath).size : 0;
479
+ const cached = readAuditTailCache(tailCachePath);
480
+ let count;
481
+ let prevHash;
482
+ if (cached && cached.logBytes === currentBytes) {
483
+ count = cached.count;
484
+ prevHash = cached.lastHash;
485
+ }
486
+ else {
487
+ const prior = readEventsRaw(audit.eventLogPath);
488
+ count = prior.length;
489
+ prevHash = prior.length ? prior[prior.length - 1].eventHash || computeEventHash(prior[prior.length - 1]) : trustAuditGenesis(run.id);
490
+ }
491
+ event.id = createEventId(input.kind, count);
492
+ event.prevEventHash = prevHash;
493
+ event.eventHash = computeEventHash(event);
494
+ const line = `${JSON.stringify(event)}\n`;
495
+ (0, fs_atomic_1.durableAppendFileSync)(audit.eventLogPath, line);
496
+ writeAuditTailCache(tailCachePath, { schemaVersion: 1, logBytes: currentBytes + Buffer.byteLength(line, "utf8"), count: count + 1, lastHash: event.eventHash });
497
+ return event;
498
+ });
330
499
  }
331
500
  function recordSandboxPathDecision(run, input) {
332
501
  return recordTrustAuditEvent(run, {
@@ -387,11 +556,32 @@ function countBy(values, key) {
387
556
  * here — the old build's extra workers/candidates/commits/multiAgent/
388
557
  * blackboard rollups are milestone 9's own summarizeMultiAgent/
389
558
  * candidate-scoring-io/coordinator-io surfaces, not duplicated here. */
559
+ /** Groups events by a key field once, so each row below is an O(1) Map
560
+ * lookup instead of an O(events) `.filter()` re-scan per id -- found
561
+ * alongside the id.find() version of this same shape while pinning perf
562
+ * cycle P1-1's review-fix regression test (O(ids x events) otherwise). */
563
+ function groupEventsBy(events, key) {
564
+ const groups = new Map();
565
+ for (const event of events) {
566
+ const k = key(event);
567
+ if (!k)
568
+ continue;
569
+ const list = groups.get(k);
570
+ if (list)
571
+ list.push(event);
572
+ else
573
+ groups.set(k, [event]);
574
+ }
575
+ return groups;
576
+ }
390
577
  function workerRows(events, run) {
391
- const workerIds = unique([...(run.workers || []).map((w) => w.id), ...events.map((e) => e.workerId || "")]).sort();
578
+ const workers = run.workers || [];
579
+ const workersById = new Map(workers.map((w) => [w.id, w]));
580
+ const eventsByWorkerId = groupEventsBy(events, (e) => e.workerId);
581
+ const workerIds = unique([...workers.map((w) => w.id), ...events.map((e) => e.workerId || "")]).sort();
392
582
  return workerIds.filter(Boolean).map((workerId) => {
393
- const worker = (run.workers || []).find((w) => w.id === workerId);
394
- const scoped = events.filter((e) => e.workerId === workerId);
583
+ const worker = workersById.get(workerId);
584
+ const scoped = eventsByWorkerId.get(workerId) || [];
395
585
  return {
396
586
  workerId,
397
587
  taskId: worker?.taskId || scoped.find((e) => e.taskId)?.taskId,
@@ -405,11 +595,21 @@ function workerRows(events, run) {
405
595
  function candidateRows(events, run) {
406
596
  const cands = run.candidates || [];
407
597
  const selectionsAll = run.candidateSelections || [];
598
+ const candsById = new Map(cands.map((c) => [c.id, c]));
599
+ const selectionsByCandidateId = new Map();
600
+ for (const selection of selectionsAll) {
601
+ const list = selectionsByCandidateId.get(selection.candidateId);
602
+ if (list)
603
+ list.push(selection);
604
+ else
605
+ selectionsByCandidateId.set(selection.candidateId, [selection]);
606
+ }
607
+ const eventsByCandidateId = groupEventsBy(events, (e) => e.candidateId);
408
608
  const ids = unique([...cands.map((c) => c.id), ...events.map((e) => e.candidateId || "")]).sort();
409
609
  return ids.filter(Boolean).map((candidateId) => {
410
- const candidate = cands.find((c) => c.id === candidateId);
411
- const selections = selectionsAll.filter((s) => s.candidateId === candidateId);
412
- const scoped = events.filter((e) => e.candidateId === candidateId);
610
+ const candidate = candsById.get(candidateId);
611
+ const selections = selectionsByCandidateId.get(candidateId) || [];
612
+ const scoped = eventsByCandidateId.get(candidateId) || [];
413
613
  return {
414
614
  candidateId,
415
615
  scoreIds: unique([...(candidate?.scores || []), ...scoped.map((e) => e.scoreId || "")]).filter(Boolean).sort(),
@@ -419,9 +619,11 @@ function candidateRows(events, run) {
419
619
  });
420
620
  }
421
621
  function commitRows(events, run) {
422
- const ids = unique([...(run.commits || []).map((c) => c.id), ...events.map((e) => e.commitId || "")]).sort();
622
+ const commits = run.commits || [];
623
+ const commitsById = new Map(commits.map((c) => [c.id, c]));
624
+ const ids = unique([...commits.map((c) => c.id), ...events.map((e) => e.commitId || "")]).sort();
423
625
  return ids.filter(Boolean).map((commitId) => {
424
- const commit = (run.commits || []).find((c) => c.id === commitId);
626
+ const commit = commitsById.get(commitId);
425
627
  return {
426
628
  commitId,
427
629
  verifierGated: Boolean(commit?.verifierGated),
@@ -209,7 +209,7 @@ function summarizeMultiAgentTrust(run) {
209
209
  judgeRationales: byKind("judge.rationale"),
210
210
  panelDecisions: byKind("judge.panel-decision"),
211
211
  policyViolations,
212
- nextAction: policyViolations.length ? `node scripts/cw.js audit policy ${run.id}` : `node scripts/cw.js audit multi-agent ${run.id} --json`,
212
+ nextAction: policyViolations.length ? `cw audit policy ${run.id}` : `cw audit multi-agent ${run.id} --json`,
213
213
  };
214
214
  }
215
215
  function hasAcceptedJudgeRationale(run, input = {}) {
@@ -77,13 +77,14 @@ function workerShowCli(args) {
77
77
  return scope;
78
78
  }
79
79
  function workerManifestCli(args) {
80
- const run = (0, run_store_1.loadRunFromCwd)(req(args.runId, "run id"), cwdFor(args));
81
- const scope = (0, worker_isolation_1.getWorkerScope)(run, req(args.workerId, "worker id"));
82
- if (!scope)
83
- throw new Error(`Unknown worker for run ${run.id}: ${args.workerId}`);
84
- const manifest = (0, worker_isolation_1.writeWorkerManifest)(run, scope);
85
- (0, run_store_1.saveCheckpoint)(run);
86
- return manifest;
80
+ return (0, run_store_1.withRunStateLock)(req(args.runId, "run id"), cwdFor(args), (run) => {
81
+ const scope = (0, worker_isolation_1.getWorkerScope)(run, req(args.workerId, "worker id"));
82
+ if (!scope)
83
+ throw new Error(`Unknown worker for run ${run.id}: ${args.workerId}`);
84
+ const manifest = (0, worker_isolation_1.writeWorkerManifest)(run, scope);
85
+ (0, run_store_1.saveCheckpoint)(run);
86
+ return manifest;
87
+ });
87
88
  }
88
89
  /** `cw worker output <run> <worker> <result>` — records the worker's result
89
90
  * and returns the full RunSummary, a byte-behavior port of the old build's
@@ -96,37 +97,40 @@ function workerManifestCli(args) {
96
97
  * tasks.completed, workers.byStatus, and loopStage. The drive loop does these
97
98
  * same steps itself around the bare accept, so it never routes through here. */
98
99
  function workerOutputCli(args) {
99
- const run = (0, run_store_1.loadRunFromCwd)(req(args.runId, "run id"), cwdFor(args));
100
- (0, worker_isolation_1.recordWorkerOutput)(run, req(args.workerId, "worker id"), req(args.resultPath, "result file"), {
101
- requireAttestedTelemetry: (0, agent_config_1.resolveAgentConfig)(args).requireAttestedTelemetry,
102
- allowUnattested: allowUnattestedOption(args),
100
+ return (0, run_store_1.withRunStateLock)(req(args.runId, "run id"), cwdFor(args), (run) => {
101
+ (0, worker_isolation_1.recordWorkerOutput)(run, req(args.workerId, "worker id"), req(args.resultPath, "result file"), {
102
+ requireAttestedTelemetry: (0, agent_config_1.resolveAgentConfig)(args).requireAttestedTelemetry,
103
+ allowUnattested: allowUnattestedOption(args),
104
+ });
105
+ run.loopStage = "observe";
106
+ (0, dispatch_1.updatePhaseStatuses)(run);
107
+ (0, drive_1.maybeExpandLoop)(run);
108
+ (0, commit_1.commitState)(run, `worker:${req(args.workerId, "worker id")}:result`);
109
+ (0, report_1.writeReport)(run);
110
+ (0, run_store_1.saveCheckpoint)(run);
111
+ return (0, operator_ux_1.summarizeRun)(run);
103
112
  });
104
- run.loopStage = "observe";
105
- (0, dispatch_1.updatePhaseStatuses)(run);
106
- (0, drive_1.maybeExpandLoop)(run);
107
- (0, commit_1.commitState)(run, `worker:${req(args.workerId, "worker id")}:result`);
108
- (0, report_1.writeReport)(run);
109
- (0, run_store_1.saveCheckpoint)(run);
110
- return (0, operator_ux_1.summarizeRun)(run);
111
113
  }
112
114
  function workerFailCli(args) {
113
- const run = (0, run_store_1.loadRunFromCwd)(req(args.runId, "run id"), cwdFor(args));
114
- const message = String(args.message || req(args.resultPath, "failure message"));
115
- const scope = (0, worker_isolation_1.recordWorkerFailure)(run, req(args.workerId, "worker id"), message, {
116
- code: typeof args.code === "string" ? args.code : undefined,
117
- path: typeof args.path === "string" ? args.path : undefined,
118
- retryable: args.retryable !== undefined ? Boolean(args.retryable) : undefined,
115
+ return (0, run_store_1.withRunStateLock)(req(args.runId, "run id"), cwdFor(args), (run) => {
116
+ const message = String(args.message || req(args.resultPath, "failure message"));
117
+ const scope = (0, worker_isolation_1.recordWorkerFailure)(run, req(args.workerId, "worker id"), message, {
118
+ code: typeof args.code === "string" ? args.code : undefined,
119
+ path: typeof args.path === "string" ? args.path : undefined,
120
+ retryable: args.retryable !== undefined ? Boolean(args.retryable) : undefined,
121
+ });
122
+ (0, run_store_1.saveCheckpoint)(run);
123
+ return scope;
119
124
  });
120
- (0, run_store_1.saveCheckpoint)(run);
121
- return scope;
122
125
  }
123
126
  /** validate returns the boundary violation (null when the write path is
124
127
  * allowed) and signals a violation through a non-zero exit code, not just
125
128
  * stdout — a validate verb must report an invalid verdict via its exit code. */
126
129
  function workerValidateCli(args) {
127
- const run = (0, run_store_1.loadRunFromCwd)(req(args.runId, "run id"), cwdFor(args));
128
- const target = args.path || args.resultPath;
129
- const violation = (0, worker_isolation_1.validateWorkerBoundary)(run, req(args.workerId, "worker id"), target ? { path: String(target) } : {});
130
- (0, run_store_1.saveCheckpoint)(run);
131
- return { violation, exitCode: violation ? 1 : undefined };
130
+ return (0, run_store_1.withRunStateLock)(req(args.runId, "run id"), cwdFor(args), (run) => {
131
+ const target = args.path || args.resultPath;
132
+ const violation = (0, worker_isolation_1.validateWorkerBoundary)(run, req(args.workerId, "worker id"), target ? { path: String(target) } : {});
133
+ (0, run_store_1.saveCheckpoint)(run);
134
+ return { violation, exitCode: violation ? 1 : undefined };
135
+ });
132
136
  }
@@ -16,32 +16,57 @@ const registry_core_1 = require("./registry-core");
16
16
  // cli/dispatch.ts's generic executor performs the actual write.
17
17
  // ---------------------------------------------------------------------
18
18
  const version_1 = require("../../core/version");
19
- const workflow_app_loader_1 = require("../../shell/workflow-app-loader");
20
19
  const help_1 = require("../../core/format/help");
20
+ const completion_1 = require("../../core/format/completion");
21
+ const io_1 = require("../../cli/io");
22
+ // This whole module is required unconditionally at startup for EVERY
23
+ // command (see wiring/capability-table/index.ts) — a top-level import of
24
+ // `shell/workflow-app-loader` here means even `cw --version` pays its
25
+ // load cost, though only `search`'s handler ever calls it.
26
+ function loadWorkflowAppLoader() {
27
+ return require("../../shell/workflow-app-loader");
28
+ }
21
29
  (0, registry_core_1.addCliOnlyCapability)("version", "Print the current cool-workflow version.", {
22
30
  path: ["version"],
23
31
  jsonMode: "default",
24
32
  handler: () => ({ text: `${version_1.CURRENT_COOL_WORKFLOW_VERSION}\n` }),
25
33
  }, "version is a local, no-run-state print; the old build never gave it an MCP peer.");
34
+ /** `cw completion <bash|zsh|fish>` — a static shell-completion script for
35
+ * the top-level command word list (core/format/completion.ts). CLI-only:
36
+ * a shell-integration script is meaningless as an MCP tool result. */
37
+ (0, registry_core_1.addCliOnlyCapability)("completion", "Print a shell-completion script for the top-level cw commands (bash, zsh, or fish).", {
38
+ path: ["completion"],
39
+ jsonMode: "human",
40
+ handler: (args) => {
41
+ const shell = (0, io_1.optionalArg)(args.positionals[0]);
42
+ if (!shell)
43
+ throw new Error('Missing shell name.\n Try: cw completion bash|zsh|fish');
44
+ return { text: (0, completion_1.formatCompletionScript)(shell) };
45
+ },
46
+ }, "shell-completion output is a script for the user's OWN shell to source, not data an MCP client could use — CLI-only, same reasoning as doctor/fix.");
26
47
  /** `cw search <keyword>` — filters the SAME real app discovery `cw list`
27
48
  * shows, by id/title/summary (byte-behavior port of cli/dispatch.ts's
28
49
  * milestone-1 carry-over `search` arm, moved here so the dispatchLegacy
29
- * switch shrinks per its file header's rule). `hiddenFromHelp` keeps it
30
- * out of the per-verb help listing exactly as before (it never had one —
31
- * `search` only ever appeared in formatHelp's hard-coded "More commands"
32
- * index line, which this row does not touch), so `cw help search` keeps
33
- * its existing "Unknown command: search" text. */
50
+ * switch shrinks per its file header's rule). The old v0.1.98 CLI DID
51
+ * have its own `cw help search` row (`docs/rebuild/SPEC/cli-help/
52
+ * search.txt`: "cw search Search workflow apps by keyword (title,
53
+ * description, id)."); `hiddenFromHelp` here was a rebuild regression
54
+ * that hid it, not a preserved old-build quirk — removed so the row
55
+ * shows again. The summary text below is intentionally NOT byte-matched
56
+ * to that old capture: the old wording says "description," but the
57
+ * filter below matches `summary` (there is no `description` field) — so
58
+ * the summary text stays accurate to current fields rather than
59
+ * reproducing the old capture's stale terminology. */
34
60
  (0, registry_core_1.addCliOnlyCapability)("search", "Search bundled workflows by id/title/summary keyword.", {
35
61
  path: ["search"],
36
62
  jsonMode: "flag",
37
- hiddenFromHelp: true,
38
63
  handler: (args) => {
39
64
  const keyword = args.positionals.join(" ");
40
65
  if (!keyword.trim()) {
41
66
  throw new Error('Missing search keyword.\n Tip: cw search architecture to find workflows about architecture.');
42
67
  }
43
68
  const lower = keyword.toLowerCase();
44
- const results = (0, workflow_app_loader_1.listWorkflowApps)()
69
+ const results = loadWorkflowAppLoader().listWorkflowApps()
45
70
  .filter((a) => String(a.title).toLowerCase().includes(lower) ||
46
71
  String(a.summary).toLowerCase().includes(lower) ||
47
72
  String(a.id).toLowerCase().includes(lower))
@@ -17,9 +17,16 @@ const registry_core_1 = require("./registry-core");
17
17
  // as milestones 3/4 did for their own rows.
18
18
  // ---------------------------------------------------------------------
19
19
  const exec_backend_cli_1 = require("../../shell/exec-backend-cli");
20
- const doctor_1 = require("../../shell/doctor");
21
20
  const io_1 = require("../../cli/io");
22
- const app_run_cli_1 = require("../../shell/app-run-cli");
21
+ // This slice is required unconditionally at startup for every command;
22
+ // load doctor/app-run-cli lazily so only doctor/fix/sandbox.choose/
23
+ // sandbox.resolve/app.run handlers pay their require cost.
24
+ function loadDoctor() {
25
+ return require("../../shell/doctor");
26
+ }
27
+ function loadAppRunCli() {
28
+ return require("../../shell/app-run-cli");
29
+ }
23
30
  (0, registry_core_1.attachCliBinding)("sandbox.list", {
24
31
  path: ["sandbox", "list"],
25
32
  jsonMode: "default",
@@ -29,9 +36,9 @@ registry_core_1.REGISTRY_BY_CAPABILITY.get("sandbox.list").mcp.handler = (args)
29
36
  // GAP #24: cw_sandbox_choose / cw_sandbox_resolve + cw_app_run were declared
30
37
  // MCP-only rows with the notYetImplemented placeholder handler. Wire them to
31
38
  // the ported shell bodies (both are MCP-only in the old build — no CLI path).
32
- registry_core_1.REGISTRY_BY_CAPABILITY.get("sandbox.choose").mcp.handler = (args) => (0, app_run_cli_1.sandboxChooseCli)(args);
33
- registry_core_1.REGISTRY_BY_CAPABILITY.get("sandbox.resolve").mcp.handler = (args) => (0, app_run_cli_1.sandboxChooseCli)(args);
34
- registry_core_1.REGISTRY_BY_CAPABILITY.get("app.run").mcp.handler = (args) => (0, app_run_cli_1.appRunCli)(args);
39
+ registry_core_1.REGISTRY_BY_CAPABILITY.get("sandbox.choose").mcp.handler = (args) => loadAppRunCli().sandboxChooseCli(args);
40
+ registry_core_1.REGISTRY_BY_CAPABILITY.get("sandbox.resolve").mcp.handler = (args) => loadAppRunCli().sandboxChooseCli(args);
41
+ registry_core_1.REGISTRY_BY_CAPABILITY.get("app.run").mcp.handler = (args) => loadAppRunCli().appRunCli(args);
35
42
  (0, registry_core_1.attachCliBinding)("sandbox.show", {
36
43
  path: ["sandbox", "show"],
37
44
  jsonMode: "default",
@@ -57,12 +64,12 @@ registry_core_1.REGISTRY_BY_CAPABILITY.get("sandbox.validate").mcp.handler = (ar
57
64
  (0, registry_core_1.attachCliBinding)("sandbox.choose", {
58
65
  path: ["sandbox", "choose"],
59
66
  jsonMode: "default",
60
- handler: (args) => ({ json: (0, app_run_cli_1.sandboxChooseCli)(args.options) }),
67
+ handler: (args) => ({ json: loadAppRunCli().sandboxChooseCli(args.options) }),
61
68
  });
62
69
  (0, registry_core_1.attachCliBinding)("sandbox.resolve", {
63
70
  path: ["sandbox", "resolve"],
64
71
  jsonMode: "default",
65
- handler: (args) => ({ json: (0, app_run_cli_1.sandboxChooseCli)(args.options) }),
72
+ handler: (args) => ({ json: loadAppRunCli().sandboxChooseCli(args.options) }),
66
73
  });
67
74
  (0, registry_core_1.attachCliBinding)("backend.list", {
68
75
  path: ["backend", "list"],
@@ -134,7 +141,8 @@ registry_core_1.REGISTRY_BY_CAPABILITY.get("backend.agent.config.set").reason =
134
141
  path: ["doctor"],
135
142
  jsonMode: "flag",
136
143
  handler: (args) => {
137
- const report = (0, doctor_1.runDoctor)(args.options, process.env, String(args.options.cwd || process.cwd()));
144
+ const doctor = loadDoctor();
145
+ const report = doctor.runDoctor(args.options, process.env, String(args.options.cwd || process.cwd()));
138
146
  // Byte-exact port of src/cli/command-surface.ts:170-176: both text
139
147
  // branches are written as `${formatX(report)}\n` UNCONDITIONALLY —
140
148
  // formatDoctorFixes already ends in its own "\n" (its last joined
@@ -143,7 +151,7 @@ registry_core_1.REGISTRY_BY_CAPABILITY.get("backend.agent.config.set").reason =
143
151
  // renderer only appends "\n" when the text does NOT already end in
144
152
  // one, so a bare `formatDoctorFixes(report)` here would silently
145
153
  // drop the old build's trailing blank line.
146
- const text = (0, io_1.wantsJson)(args.options) ? undefined : args.options.fix ? `${(0, doctor_1.formatDoctorFixes)(report)}\n` : (0, doctor_1.formatDoctorReport)(report);
154
+ const text = (0, io_1.wantsJson)(args.options) ? undefined : args.options.fix ? `${doctor.formatDoctorFixes(report)}\n` : doctor.formatDoctorReport(report);
147
155
  return { json: report, text, exitCode: report.ok ? undefined : 1 };
148
156
  },
149
157
  }, "Environment diagnostics are inherently local to the CLI host — Node version, $PATH, $CW_HOME/cwd writability. An MCP client diagnosing the server process's environment is not meaningful; agents already receive the same readiness facts in their typed results (e.g. status: blocked, agentConfigured). Inspired by `brew doctor`.");
@@ -151,12 +159,13 @@ registry_core_1.REGISTRY_BY_CAPABILITY.get("backend.agent.config.set").reason =
151
159
  path: ["fix"],
152
160
  jsonMode: "human",
153
161
  handler: (args) => {
154
- const report = (0, doctor_1.runDoctor)(args.options, process.env, String(args.options.cwd || process.cwd()));
162
+ const doctor = loadDoctor();
163
+ const report = doctor.runDoctor(args.options, process.env, String(args.options.cwd || process.cwd()));
155
164
  // See the "doctor" handler's comment above: formatDoctorFixes
156
165
  // already ends in "\n", so one more explicit "\n" here reproduces
157
166
  // src/cli/command-surface.ts:126-130's unconditional
158
167
  // `${formatDoctorFixes(report)}\n` write.
159
- return { text: `${(0, doctor_1.formatDoctorFixes)(report)}\n`, exitCode: report.ok ? undefined : 1 };
168
+ return { text: `${doctor.formatDoctorFixes(report)}\n`, exitCode: report.ok ? undefined : 1 };
160
169
  },
161
170
  }, "Environment fix commands are local diagnostics, same reasoning as doctor.");
162
171
  // ---------------------------------------------------------------------