cool-workflow 0.2.0 → 0.2.2

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 (141) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/README.md +7 -5
  4. package/apps/architecture-review/app.json +2 -2
  5. package/apps/architecture-review/workflow.js +12 -12
  6. package/apps/architecture-review-fast/app.json +1 -1
  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/cli/dispatch.js +20 -88
  12. package/dist/cli/parseargv.js +6 -3
  13. package/dist/core/capability-data.js +270 -0
  14. package/dist/core/capability-table.js +15 -3519
  15. package/dist/core/format/help.js +30 -7
  16. package/dist/core/hash.js +1 -1
  17. package/dist/core/multi-agent/candidate-scoring.js +1 -1
  18. package/dist/core/multi-agent/collaboration.js +3 -2
  19. package/dist/core/multi-agent/coordinator.js +4 -3
  20. package/dist/core/multi-agent/eval-replay.js +1 -1
  21. package/dist/core/multi-agent/runtime.js +2 -2
  22. package/dist/core/multi-agent/trust-policy.js +1 -1
  23. package/dist/core/pipeline/commit-gate.js +2 -1
  24. package/dist/core/pipeline/contract.js +1 -1
  25. package/dist/core/pipeline/drive-decide.js +1 -1
  26. package/dist/core/state/contract-migration.js +1 -1
  27. package/dist/core/state/migrations.js +2 -2
  28. package/dist/core/state/node-snapshot.js +1 -1
  29. package/dist/core/state/state-explosion/digest.js +3 -2
  30. package/dist/core/state/state-explosion/graph.js +13 -12
  31. package/dist/core/state/state-explosion/helpers.js +5 -4
  32. package/dist/core/state/state-explosion/report.js +1 -1
  33. package/dist/core/state/state-explosion/size.js +1 -1
  34. package/dist/core/state/state-node.js +2 -2
  35. package/dist/core/state/types.js +1 -1
  36. package/dist/core/trust/ledger.js +3 -2
  37. package/dist/core/trust/telemetry-attestation.js +3 -3
  38. package/dist/core/trust/telemetry-ledger.js +2 -2
  39. package/dist/core/types/execution-backend.js +18 -0
  40. package/dist/core/types/observability.js +7 -0
  41. package/dist/core/util/collate.js +23 -0
  42. package/dist/core/version.js +1 -1
  43. package/dist/core/workflow-apps/app-schema.js +1 -1
  44. package/dist/mcp/dispatch.js +1 -1
  45. package/dist/mcp/server.js +1 -1
  46. package/dist/shell/agent-config.js +1 -1
  47. package/dist/shell/audit-cli.js +46 -2
  48. package/dist/shell/commit-summary.js +2 -1
  49. package/dist/shell/doctor.js +17 -4
  50. package/dist/shell/drive.js +47 -19
  51. package/dist/shell/evidence-reasoning.js +4 -3
  52. package/dist/shell/execution-backend/agent.js +42 -10
  53. package/dist/shell/execution-backend/ci.js +1 -1
  54. package/dist/shell/execution-backend/container.js +1 -1
  55. package/dist/shell/execution-backend/envelopes.js +1 -1
  56. package/dist/shell/execution-backend/local.js +1 -1
  57. package/dist/shell/execution-backend/probes.js +1 -1
  58. package/dist/shell/execution-backend/registry.js +3 -2
  59. package/dist/shell/execution-backend/remote.js +1 -1
  60. package/dist/shell/execution-backend/types.js +0 -9
  61. package/dist/shell/fs-atomic.js +15 -2
  62. package/dist/shell/ledger-io.js +1 -1
  63. package/dist/shell/metrics-cli.js +4 -2
  64. package/dist/shell/multi-agent-host.js +1 -1
  65. package/dist/shell/multi-agent-operator-ux.js +4 -3
  66. package/dist/shell/observability.js +12 -11
  67. package/dist/shell/onramp.js +17 -1
  68. package/dist/shell/operator-ux-text.js +2 -1
  69. package/dist/shell/operator-ux.js +7 -6
  70. package/dist/shell/pipeline-cli.js +82 -72
  71. package/dist/shell/reclamation-io.js +3 -2
  72. package/dist/shell/registry-cli.js +15 -0
  73. package/dist/shell/report.js +2 -1
  74. package/dist/shell/run-registry-io.js +1 -1
  75. package/dist/shell/run-store.js +17 -0
  76. package/dist/shell/sandbox-profile.js +1 -1
  77. package/dist/shell/scheduler-io.js +46 -37
  78. package/dist/shell/scheduling-io.js +32 -22
  79. package/dist/shell/state-explosion-cli.js +2 -1
  80. package/dist/shell/telemetry-ledger-io.js +1 -1
  81. package/dist/shell/term.js +1 -1
  82. package/dist/shell/topology-io.js +2 -1
  83. package/dist/shell/trust-audit.js +45 -5
  84. package/dist/shell/workbench-host.js +9 -1
  85. package/dist/shell/workbench.js +6 -11
  86. package/dist/shell/worker-cli.js +9 -1
  87. package/dist/shell/worker-isolation.js +34 -9
  88. package/dist/shell/workflow-app-loader.js +3 -2
  89. package/dist/wiring/capability-table/basics.js +66 -0
  90. package/dist/wiring/capability-table/exec-backend.js +162 -0
  91. package/dist/wiring/capability-table/index.js +42 -0
  92. package/dist/wiring/capability-table/multi-agent.js +623 -0
  93. package/dist/wiring/capability-table/parity.js +466 -0
  94. package/dist/wiring/capability-table/pipeline.js +280 -0
  95. package/dist/wiring/capability-table/registry-core.js +189 -0
  96. package/dist/wiring/capability-table/reporting.js +394 -0
  97. package/dist/wiring/capability-table/scheduling-registry.js +558 -0
  98. package/dist/wiring/capability-table/state.js +173 -0
  99. package/dist/wiring/capability-table/trust-ledger.js +134 -0
  100. package/dist/wiring/capability-table/workflow-apps.js +366 -0
  101. package/docs/agent-delegation-drive.7.md +4 -0
  102. package/docs/cli-mcp-parity.7.md +9 -2
  103. package/docs/contract-migration-tooling.7.md +4 -0
  104. package/docs/control-plane-scheduling.7.md +4 -0
  105. package/docs/durable-state-and-locking.7.md +24 -0
  106. package/docs/evidence-adoption-reasoning-chain.7.md +4 -0
  107. package/docs/execution-backends.7.md +4 -0
  108. package/docs/index.md +1 -0
  109. package/docs/multi-agent-cli-mcp-surface.7.md +4 -0
  110. package/docs/multi-agent-eval-replay-harness.7.md +4 -0
  111. package/docs/multi-agent-operator-ux.7.md +4 -0
  112. package/docs/node-snapshot-diff-replay.7.md +4 -0
  113. package/docs/observability-cost-accounting.7.md +6 -1
  114. package/docs/project-index.md +30 -5
  115. package/docs/real-execution-backends.7.md +4 -0
  116. package/docs/release-and-migration.7.md +4 -0
  117. package/docs/release-tooling.7.md +4 -0
  118. package/docs/remote-source-review.7.md +4 -4
  119. package/docs/report-verifiable-bundle.7.md +1 -1
  120. package/docs/run-registry-control-plane.7.md +4 -0
  121. package/docs/run-retention-reclamation.7.md +4 -0
  122. package/docs/security-trust-hardening.7.md +33 -1
  123. package/docs/state-explosion-management.7.md +4 -0
  124. package/docs/team-collaboration.7.md +4 -0
  125. package/docs/trust-audit-anchor.7.md +69 -0
  126. package/docs/web-desktop-workbench.7.md +15 -1
  127. package/manifest/plugin.manifest.json +1 -1
  128. package/package.json +3 -1
  129. package/scripts/agents/agent-adapter-core.js +22 -2
  130. package/scripts/agents/claude-p-agent.js +8 -3
  131. package/scripts/agents/gemini-agent.js +5 -1
  132. package/scripts/agents/opencode-agent.js +5 -1
  133. package/scripts/bump-version.js +24 -2
  134. package/scripts/canonical-apps.js +4 -4
  135. package/scripts/dogfood-release.js +1 -1
  136. package/scripts/golden-path.js +4 -4
  137. package/scripts/purity-baseline.json +68 -0
  138. package/scripts/purity-gate.js +239 -0
  139. package/scripts/release-check.js +8 -1
  140. package/scripts/version-sync-check.js +33 -12
  141. package/workflows/README.md +19 -0
@@ -3,7 +3,7 @@
3
3
  // lease-plan mechanism over the run-registry-io.ts queue store, plus the
4
4
  // policy file IO.
5
5
  //
6
- // MILESTONE 10 (v2/PLAN.md build order, step 10). Byte-exact port of the
6
+ // MILESTONE 10 (docs/rebuild/PLAN.md build order, step 10). Byte-exact port of the
7
7
  // old build's src/scheduling.ts + the sched-specific slice of
8
8
  // src/capability-core.ts. `sched` is a DIFFERENT system from `schedule`
9
9
  // (see scheduler-io.ts's file header) — this operates on
@@ -251,43 +251,53 @@ function schedLeaseCli(options = {}) {
251
251
  const now = nowIso(options);
252
252
  const policy = loadSchedulingPolicy(registry).policy;
253
253
  const limit = options.limit === undefined ? undefined : Number(options.limit);
254
- const { entries, leases } = applyLease(registry.loadQueueEntries(), policy, now, limit);
255
- registry.saveQueueEntries(entries);
256
- return { schemaVersion: 1, now, granted: leases.length, leases };
254
+ return (0, fs_atomic_1.withFileLock)(registry.queueFilePath(), () => {
255
+ const { entries, leases } = applyLease(registry.loadQueueEntries(), policy, now, limit);
256
+ registry.saveQueueEntries(entries);
257
+ return { schemaVersion: 1, now, granted: leases.length, leases };
258
+ });
257
259
  }
258
260
  function schedReleaseCli(leaseId, options = {}) {
259
261
  const registry = new run_registry_io_1.RunRegistry(resolveCwd(options));
260
262
  const now = nowIso(options);
261
263
  const failed = isTrue(options.failed);
262
264
  const reason = typeof options.reason === "string" ? options.reason : undefined;
263
- const { entries, matched } = leaseRelease(registry.loadQueueEntries(), leaseId, loadSchedulingPolicy(registry).policy, now, { failed, reason });
264
- if (!matched)
265
- throw new Error(`No active lease to release: ${leaseId}`);
266
- registry.saveQueueEntries(entries);
267
- return { schemaVersion: 1, released: leaseId, failed };
265
+ return (0, fs_atomic_1.withFileLock)(registry.queueFilePath(), () => {
266
+ const { entries, matched } = leaseRelease(registry.loadQueueEntries(), leaseId, loadSchedulingPolicy(registry).policy, now, { failed, reason });
267
+ if (!matched)
268
+ throw new Error(`No active lease to release: ${leaseId}`);
269
+ registry.saveQueueEntries(entries);
270
+ return { schemaVersion: 1, released: leaseId, failed };
271
+ });
268
272
  }
269
273
  function schedCompleteCli(leaseId, options = {}) {
270
274
  const registry = new run_registry_io_1.RunRegistry(resolveCwd(options));
271
- const { entries, matched } = leaseComplete(registry.loadQueueEntries(), leaseId, nowIso(options));
272
- if (!matched)
273
- throw new Error(`No active lease to complete: ${leaseId}`);
274
- registry.saveQueueEntries(entries);
275
- return { schemaVersion: 1, completed: leaseId };
275
+ return (0, fs_atomic_1.withFileLock)(registry.queueFilePath(), () => {
276
+ const { entries, matched } = leaseComplete(registry.loadQueueEntries(), leaseId, nowIso(options));
277
+ if (!matched)
278
+ throw new Error(`No active lease to complete: ${leaseId}`);
279
+ registry.saveQueueEntries(entries);
280
+ return { schemaVersion: 1, completed: leaseId };
281
+ });
276
282
  }
277
283
  function schedReclaimCli(options = {}) {
278
284
  const registry = new run_registry_io_1.RunRegistry(resolveCwd(options));
279
285
  const now = nowIso(options);
280
- const { entries, reclaimed } = reclaimExpired(registry.loadQueueEntries(), loadSchedulingPolicy(registry).policy, now);
281
- registry.saveQueueEntries(entries);
282
- return { schemaVersion: 1, now, reclaimed };
286
+ return (0, fs_atomic_1.withFileLock)(registry.queueFilePath(), () => {
287
+ const { entries, reclaimed } = reclaimExpired(registry.loadQueueEntries(), loadSchedulingPolicy(registry).policy, now);
288
+ registry.saveQueueEntries(entries);
289
+ return { schemaVersion: 1, now, reclaimed };
290
+ });
283
291
  }
284
292
  function schedResetCli(id, options = {}) {
285
293
  const registry = new run_registry_io_1.RunRegistry(resolveCwd(options));
286
- const { entries, matched } = resetEntry(registry.loadQueueEntries(), id);
287
- if (!matched)
288
- throw new Error(`No parked entry to reset: ${id}`);
289
- registry.saveQueueEntries(entries);
290
- return { schemaVersion: 1, reset: id };
294
+ return (0, fs_atomic_1.withFileLock)(registry.queueFilePath(), () => {
295
+ const { entries, matched } = resetEntry(registry.loadQueueEntries(), id);
296
+ if (!matched)
297
+ throw new Error(`No parked entry to reset: ${id}`);
298
+ registry.saveQueueEntries(entries);
299
+ return { schemaVersion: 1, reset: id };
300
+ });
291
301
  }
292
302
  function schedPolicyShowCli(options = {}) {
293
303
  const registry = new run_registry_io_1.RunRegistry(resolveCwd(options));
@@ -66,6 +66,7 @@ const report_1 = require("../core/state/state-explosion/report");
66
66
  Object.defineProperty(exports, "buildStateExplosionReport", { enumerable: true, get: function () { return report_1.buildStateExplosionReport; } });
67
67
  const size_2 = require("../core/state/state-explosion/size");
68
68
  const multi_agent_operator_ux_1 = require("./multi-agent-operator-ux");
69
+ const collate_1 = require("../core/util/collate");
69
70
  function summariesDir(run) {
70
71
  return path.join(run.paths.runDir, "summaries");
71
72
  }
@@ -134,7 +135,7 @@ function refreshStateExplosionSummaries(run, options = {}) {
134
135
  status: "valid",
135
136
  deterministic: true,
136
137
  nextAction: `node scripts/cw.js summary show ${run.id}`,
137
- entries: entries.sort((a, b) => a.id.localeCompare(b.id)),
138
+ entries: entries.sort((a, b) => (0, collate_1.stableCompare)(a.id, b.id)),
138
139
  views,
139
140
  paths: { summariesDir: dir, indexPath: path.join(dir, "index.json"), reportPath },
140
141
  };
@@ -4,7 +4,7 @@
4
4
  // math lives in core/trust/telemetry-ledger.ts.
5
5
  //
6
6
  // MILESTONE 8. Byte-exact port of the old build's src/telemetry-ledger.ts
7
- // IO-touching half. CRITICAL invariant (v2/PLAN.md byte-compat item 12):
7
+ // IO-touching half. CRITICAL invariant (docs/rebuild/PLAN.md byte-compat item 12):
8
8
  // an ABSENT telemetry.json is an empty, clean-verifying chain
9
9
  // (`present:false`); a PRESENT but unparseable one is CORRUPT — reads
10
10
  // report `telemetry-ledger-corrupt`, and append THROWS
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  // shell/term.ts — zero-dependency terminal styling.
3
3
  //
4
- // MILESTONE 5 (v2/PLAN.md build order, step 5, doctor/fix): TTY-gated
4
+ // MILESTONE 5 (docs/rebuild/PLAN.md build order, step 5, doctor/fix): TTY-gated
5
5
  // ANSI formatting, so a piped run (every conformance case pipes with
6
6
  // NO_COLOR=1) prints plain text. EXTENDED at MILESTONE 11 (reporting/
7
7
  // observability) with the rest of the old build's src/term.ts: indent,
@@ -63,6 +63,7 @@ const hash_2 = require("../core/hash");
63
63
  const topo = __importStar(require("../core/multi-agent/topology"));
64
64
  const multi_agent_io_1 = require("./multi-agent-io");
65
65
  const coordinator_io_1 = require("./coordinator-io");
66
+ const collate_1 = require("../core/util/collate");
66
67
  function topologyRoot(run) {
67
68
  return run.paths.topologiesDir || path.join(run.paths.runDir, "topologies");
68
69
  }
@@ -307,7 +308,7 @@ function buildTopologyGraph(run) {
307
308
  return topo.buildTopologyGraphFromRuns(run.id, state.runs, (id) => topologyRunPath(run, id));
308
309
  }
309
310
  function formatTopologyCounts(counts) {
310
- const entries = Object.entries(counts).sort(([a], [b]) => a.localeCompare(b));
311
+ const entries = Object.entries(counts).sort(([a], [b]) => (0, collate_1.stableCompare)(a, b));
311
312
  if (!entries.length)
312
313
  return "none";
313
314
  return entries.map(([k, v]) => `${k}=${v}`).join(", ");
@@ -13,7 +13,7 @@
13
13
  // the era-rule check + trustAuditGenesis on top, and switches
14
14
  // computeEventHash to use core/hash.ts's named `eventHashInput` export
15
15
  // (byte-identical behavior to the pre-existing inline JSON-round-trip,
16
- // now shared with the rest of the hash-dedup story per v2/PLAN.md byte-
16
+ // now shared with the rest of the hash-dedup story per docs/rebuild/PLAN.md byte-
17
17
  // compat item 2).
18
18
  //
19
19
  // Evidence: SPEC/ledger-trust.md "Trust-audit chain", invariant 10 (era
@@ -58,6 +58,7 @@ exports.TRUST_AUDIT_SCHEMA_VERSION = void 0;
58
58
  exports.ensureTrustAudit = ensureTrustAudit;
59
59
  exports.trustAuditGenesis = trustAuditGenesis;
60
60
  exports.listTrustAuditEvents = listTrustAuditEvents;
61
+ exports.trustAuditHead = trustAuditHead;
61
62
  exports.verifyTrustAudit = verifyTrustAudit;
62
63
  exports.recordTrustAuditEvent = recordTrustAuditEvent;
63
64
  exports.recordSandboxPathDecision = recordSandboxPathDecision;
@@ -97,7 +98,7 @@ function trustAuditGenesis(runId) {
97
98
  * itself, via core/hash.ts's `eventHashInput` (the JSON round-trip
98
99
  * pre-pass that drops nested `undefined`-valued keys BEFORE the
99
100
  * sort-and-stringify step — not a formatting flag on the same shape,
100
- * see v2/PLAN.md byte-compat item 2). Hashing the PERSISTED form this
101
+ * see docs/rebuild/PLAN.md byte-compat item 2). Hashing the PERSISTED form this
101
102
  * way makes record-time hashes equal verify-time (parsed-from-disk)
102
103
  * hashes. */
103
104
  function computeEventHash(event) {
@@ -136,19 +137,40 @@ function readEventsRaw(eventLogPath) {
136
137
  function listTrustAuditEvents(run) {
137
138
  return readEventsRaw(ensureTrustAudit(run).eventLogPath);
138
139
  }
140
+ /** The current head of a run's trust-audit chain: the hash the NEXT
141
+ * appended event will link from (genesis when the log is empty), plus
142
+ * the event count. Read-only projection over existing data. Capture it
143
+ * (e.g. right after a run, or at export time) and later hand it to
144
+ * `verifyTrustAudit`'s anchor / `cw audit verify --expect-head` to
145
+ * re-prove the log was not shortened since the capture. */
146
+ function trustAuditHead(run) {
147
+ const audit = ensureTrustAudit(run);
148
+ const events = readEventsRaw(audit.eventLogPath);
149
+ let head = trustAuditGenesis(run.id);
150
+ for (const event of events) {
151
+ head = event.eventHash !== undefined ? event.eventHash : computeEventHash(event);
152
+ }
153
+ return { eventCount: events.length, headHash: head };
154
+ }
139
155
  /** Re-prove the run's trust-audit chain: prevEventHash linkage (append
140
156
  * order) + per-event hash recompute. A corrupt line, an edited event,
141
157
  * or a removed event flips verified=false. Legacy events without a
142
158
  * hash are reported as `unchained` (skipped), NOT treated as a forgery
143
159
  * — they predate the chain.
144
160
  *
145
- * ERA RULE (v2/PLAN.md, SPEC/ledger-trust.md invariant 10): a single
161
+ * ERA RULE (docs/rebuild/PLAN.md, SPEC/ledger-trust.md invariant 10): a single
146
162
  * run is written by one code version, so a log is all-chained (chain
147
163
  * era) or all-legacy (pre-chain). An unchained (eventHash-less) line
148
164
  * mixed into an otherwise-chained log is a forgery attempt — dropping
149
165
  * the hash to be waved through as "legacy" — so it fails with
150
- * `trust-audit-unchained-event`, never silently accepted. */
151
- function verifyTrustAudit(run) {
166
+ * `trust-audit-unchained-event`, never silently accepted.
167
+ *
168
+ * ANCHOR (optional): the walk alone cannot see tail truncation — see
169
+ * TrustAuditAnchor. With an anchor, the head-hash trail (genesis plus
170
+ * the hash after each event) must contain `expectHead`, and the log
171
+ * must reach `expectCount` events; a shortfall fails closed with
172
+ * `trust-audit-truncated`. Without an anchor, behavior is unchanged. */
173
+ function verifyTrustAudit(run, anchor) {
152
174
  const audit = ensureTrustAudit(run);
153
175
  const { events, corruptLines } = readEventsRawCounted(audit.eventLogPath);
154
176
  const checks = [];
@@ -158,12 +180,14 @@ function verifyTrustAudit(run) {
158
180
  let chained = 0;
159
181
  let unchained = 0;
160
182
  let expectedPrev = trustAuditGenesis(run.id);
183
+ const headTrail = new Set([expectedPrev]);
161
184
  for (let i = 0; i < events.length; i++) {
162
185
  const event = events[i];
163
186
  const recomputed = computeEventHash(event);
164
187
  if (event.eventHash === undefined) {
165
188
  unchained += 1;
166
189
  expectedPrev = recomputed; // advance the chain over legacy events
190
+ headTrail.add(expectedPrev);
167
191
  continue;
168
192
  }
169
193
  chained += 1;
@@ -176,12 +200,27 @@ function verifyTrustAudit(run) {
176
200
  checks.push({ name: `chain-link[${i}]`, pass: false, code: "trust-audit-chain-broken" });
177
201
  }
178
202
  expectedPrev = event.eventHash;
203
+ headTrail.add(expectedPrev);
179
204
  }
180
205
  // Era rule: a log with ANY chained event must have EVERY event chained.
181
206
  if (chained > 0 && unchained > 0) {
182
207
  verified = false;
183
208
  checks.push({ name: "unchained-events", pass: false, code: "trust-audit-unchained-event" });
184
209
  }
210
+ // Anchor rule: the captured head must still be ON the chain, and the log
211
+ // must be at least as long as it was at capture time. A truncated-then-
212
+ // appended log fails the head check (new events link from an earlier
213
+ // point, so the old head is no longer in the trail).
214
+ if (anchor) {
215
+ if (anchor.expectCount !== undefined && events.length < anchor.expectCount) {
216
+ verified = false;
217
+ checks.push({ name: "anchor-count", pass: false, code: "trust-audit-truncated" });
218
+ }
219
+ if (anchor.expectHead !== undefined && !headTrail.has(anchor.expectHead)) {
220
+ verified = false;
221
+ checks.push({ name: "anchor-head", pass: false, code: "trust-audit-truncated" });
222
+ }
223
+ }
185
224
  return { present: events.length > 0, verified, eventCount: events.length, chained, unchained, corruptLines, checks };
186
225
  }
187
226
  function unique(values) {
@@ -277,6 +316,7 @@ function recordTrustAuditEvent(run, input) {
277
316
  normalizedPath: input.normalizedPath ? path.resolve(input.normalizedPath) : undefined,
278
317
  command: input.command,
279
318
  networkTarget: input.networkTarget,
319
+ envVars: input.envVars?.filter(Boolean).sort(),
280
320
  evidence: normalizeEvidence(run, input.evidence || [], { source: input.source, workerId: input.workerId, taskId: input.taskId, resultNodeId: input.nodeId }),
281
321
  evidenceRefs: unique(input.evidenceRefs || []).sort(),
282
322
  parentEventIds: unique(input.parentEventIds || []).sort(),
@@ -233,8 +233,16 @@ class WorkbenchHost {
233
233
  /** `cw workbench serve` entry point: `--once`/`--json` prints ONLY the
234
234
  * descriptor (compact JSON) and starts nothing; the default binds and
235
235
  * blocks, printing one compact line (`{...descriptor, boundPort}`)
236
- * once listening. */
236
+ * once listening. `--require-token` is a strict opt-in: default behavior
237
+ * (unauthenticated local reads when no token is configured) is
238
+ * unchanged unless the caller explicitly asks to fail closed. */
237
239
  async run() {
240
+ const requireToken = Boolean(this.args.requireToken || this.args["require-token"]);
241
+ if (requireToken && !this.token) {
242
+ process.stderr.write("workbench serve --require-token: CW_WORKBENCH_TOKEN is not set; refusing to start.\n");
243
+ process.exitCode = 1;
244
+ return;
245
+ }
238
246
  const once = Boolean(this.args.once || this.args.json);
239
247
  if (once) {
240
248
  process.stdout.write(`${JSON.stringify(this.descriptor(true))}\n`);
@@ -116,18 +116,13 @@ function buildWorkbenchRunView(runId, args = {}) {
116
116
  }
117
117
  return { schemaVersion: 1, surface: "workbench", runId, resolved, ...(resolveError ? { error: resolveError } : {}), panels };
118
118
  }
119
+ /** Package-relative resolution only — never falls back to the invocation
120
+ * cwd. `ui/` ships as a sibling of `dist/` in the published package
121
+ * (package.json's `files`), so from `dist/shell/workbench.js` the fixed
122
+ * path is two levels up. Matches the existing precedent in
123
+ * execution-backend/agent.ts's BATCH_DELEGATE_CHILD_SCRIPT resolution. */
119
124
  function workbenchUiRoot() {
120
- let dir = __dirname;
121
- for (let i = 0; i < 8; i++) {
122
- const candidate = path.join(dir, "plugins", "cool-workflow", exports.WORKBENCH_UI_RELATIVE);
123
- if (require("node:fs").existsSync(candidate))
124
- return candidate;
125
- const parent = path.dirname(dir);
126
- if (parent === dir)
127
- break;
128
- dir = parent;
129
- }
130
- return path.join(process.cwd(), exports.WORKBENCH_UI_RELATIVE);
125
+ return path.resolve(__dirname, "..", "..", exports.WORKBENCH_UI_RELATIVE);
131
126
  }
132
127
  const WORKBENCH_ROUTES = [
133
128
  { method: "GET", path: "/", description: "Index page (or the UI's index.html, when installed)." },
@@ -51,6 +51,7 @@ const drive_1 = require("./drive");
51
51
  const commit_1 = require("./commit");
52
52
  const report_1 = require("./report");
53
53
  const operator_ux_1 = require("./operator-ux");
54
+ const agent_config_1 = require("./agent-config");
54
55
  function cwdFor(args) {
55
56
  return typeof args.cwd === "string" && args.cwd.trim() ? path.resolve(args.cwd) : process.cwd();
56
57
  }
@@ -60,6 +61,10 @@ function req(value, label) {
60
61
  throw new Error(`Missing ${label}`);
61
62
  return s;
62
63
  }
64
+ /** `--allow-unattested` (CLI: dashed key; MCP: allowUnattested). */
65
+ function allowUnattestedOption(args) {
66
+ return Boolean(args.allowUnattested ?? args["allow-unattested"]);
67
+ }
63
68
  function workerListCli(args) {
64
69
  const run = (0, run_store_1.loadRunFromCwd)(req(args.runId, "run id"), cwdFor(args));
65
70
  return (0, worker_isolation_1.listWorkerScopes)(run, { status: typeof args.status === "string" ? args.status : undefined });
@@ -92,7 +97,10 @@ function workerManifestCli(args) {
92
97
  * same steps itself around the bare accept, so it never routes through here. */
93
98
  function workerOutputCli(args) {
94
99
  const run = (0, run_store_1.loadRunFromCwd)(req(args.runId, "run id"), cwdFor(args));
95
- (0, worker_isolation_1.recordWorkerOutput)(run, req(args.workerId, "worker id"), req(args.resultPath, "result file"), {});
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),
103
+ });
96
104
  run.loopStage = "observe";
97
105
  (0, dispatch_1.updatePhaseStatuses)(run);
98
106
  (0, drive_1.maybeExpandLoop)(run);
@@ -106,6 +106,7 @@ function workerBlackboardManifest(run, task) {
106
106
  const verifier_1 = require("./verifier");
107
107
  const runner_1 = require("../core/pipeline/runner");
108
108
  const hash_1 = require("../core/hash");
109
+ const collate_1 = require("../core/util/collate");
109
110
  const telemetry_attestation_1 = require("../core/trust/telemetry-attestation");
110
111
  const telemetry_ledger_io_1 = require("./telemetry-ledger-io");
111
112
  exports.WORKER_ISOLATION_SCHEMA_VERSION = 1;
@@ -531,13 +532,37 @@ function recordWorkerOutput(run, workerId, resultPath, options = {}) {
531
532
  })
532
533
  : undefined;
533
534
  // Opt-in fail-closed gate (default off): when the operator requires
534
- // attested telemetry, a delegated hop whose verdict is not `attested`
535
- // is REJECTED here — BEFORE any accept-side state mutation — so the
536
- // drive parks it instead of recording unverifiable usage.
537
- if (options.requireAttestedTelemetry && telemetry && telemetry.status !== "attested") {
538
- const message = `Worker ${workerId} telemetry is ${telemetry.status} (${telemetry.reason || "unverified"}) and require-attested-telemetry is enabled refusing to accept a hop whose usage cannot be cryptographically verified`;
539
- recordWorkerFailure(run, workerId, message, { code: "telemetry-unattested-blocked", path: absoluteResultPath, retryable: false });
540
- throw new Error(message);
535
+ // attested telemetry, an accept whose usage cannot be verified is
536
+ // REJECTED here — BEFORE any accept-side state mutation — so the drive
537
+ // parks it instead of recording unverifiable usage. This fires on BOTH
538
+ // shapes: a delegation present but not attested (telemetry.status !==
539
+ // "attested"), and NO delegation metadata at all. The second shape is
540
+ // the gap a manual `cw worker output` / `cw result` accept used to slip
541
+ // through silently: options.agentDelegation was simply absent, so
542
+ // `telemetry` was undefined and the old `telemetry &&` condition
543
+ // short-circuited false — an unattested result could be laundered
544
+ // through the manual accept path even with the require flag on.
545
+ // --allow-unattested is the operator's explicit way past this: it never
546
+ // skips the gate silently, it records a telemetry.gate-override event.
547
+ if (options.requireAttestedTelemetry && (!telemetry || telemetry.status !== "attested")) {
548
+ if (options.allowUnattested) {
549
+ (0, trust_audit_1.recordTrustAuditEvent)(run, {
550
+ kind: "telemetry.gate-override",
551
+ decision: "allowed",
552
+ source: "operator",
553
+ workerId,
554
+ taskId: task.id,
555
+ metadata: { reason: "--allow-unattested", telemetryStatus: telemetry ? telemetry.status : "absent" },
556
+ });
557
+ }
558
+ else {
559
+ const code = telemetry ? "telemetry-unattested-blocked" : "telemetry-missing-blocked";
560
+ const message = telemetry
561
+ ? `Worker ${workerId} telemetry is ${telemetry.status} (${telemetry.reason || "unverified"}) and require-attested-telemetry is enabled — refusing to accept a hop whose usage cannot be cryptographically verified`
562
+ : `Worker ${workerId} carries no agent-delegation telemetry at all and require-attested-telemetry is enabled — refusing to accept an unattested manual result (pass --allow-unattested to record an audited override)`;
563
+ recordWorkerFailure(run, workerId, message, { code, path: absoluteResultPath, retryable: false });
564
+ throw new Error(message);
565
+ }
541
566
  }
542
567
  const agentDelegationMeta = delegation
543
568
  ? {
@@ -797,7 +822,7 @@ function listWorkerScopes(run, options = {}) {
797
822
  // silently drops workers whenever run.workers was reset.
798
823
  const merged = mergeScopes(run.workers || [], loadWorkerScopesFromDisk(run));
799
824
  run.workers = merged;
800
- const workers = merged.slice().sort((a, b) => a.id.localeCompare(b.id));
825
+ const workers = merged.slice().sort((a, b) => (0, collate_1.stableCompare)(a.id, b.id));
801
826
  return options.status ? workers.filter((w) => w.status === options.status) : workers;
802
827
  }
803
828
  function countByStatus(workers) {
@@ -824,7 +849,7 @@ function countBucket(values) {
824
849
  return counts;
825
850
  }
826
851
  function formatCountBucket(counts) {
827
- const entries = Object.entries(counts).sort(([a], [b]) => a.localeCompare(b));
852
+ const entries = Object.entries(counts).sort(([a], [b]) => (0, collate_1.stableCompare)(a, b));
828
853
  if (!entries.length)
829
854
  return "none";
830
855
  return entries.map(([k, v]) => `${k}=${v}`).join(", ");
@@ -75,6 +75,7 @@ const path = __importStar(require("node:path"));
75
75
  const app_schema_1 = require("../core/workflow-apps/app-schema");
76
76
  const sandbox_profile_1 = require("./sandbox-profile");
77
77
  const version_1 = require("../core/version");
78
+ const collate_1 = require("../core/util/collate");
78
79
  class WorkflowAppNotFoundError extends Error {
79
80
  constructor(appId) {
80
81
  super(`Workflow app not found: ${appId}`);
@@ -421,10 +422,10 @@ function listWorkflowAppRecords() {
421
422
  ...loadWorkflowFiles(workflowsDir).map((file) => loadWorkflowAppFromEntrypoint(file)),
422
423
  ...loadAppManifestFiles(appsDir).map((file) => loadWorkflowAppFromManifest(file)),
423
424
  ].sort((left, right) => {
424
- const byId = left.app.id.localeCompare(right.app.id);
425
+ const byId = (0, collate_1.stableCompare)(left.app.id, right.app.id);
425
426
  if (byId)
426
427
  return byId;
427
- return sourcePathOf(left).localeCompare(sourcePathOf(right));
428
+ return (0, collate_1.stableCompare)(sourcePathOf(left), sourcePathOf(right));
428
429
  });
429
430
  const seen = new Map();
430
431
  for (const record of records) {
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ // wiring/capability-table/basics.ts — MILESTONE 2's CLI bindings (version,
3
+ // list, status, sandbox.list). Split out of core/capability-table.ts,
4
+ // byte-for-byte (extracted with sed, not retyped).
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const registry_core_1 = require("./registry-core");
7
+ // ---------------------------------------------------------------------
8
+ // CLI bindings wired at THIS milestone (version, list, status,
9
+ // sandbox.list). `version` is cli-only per SPEC/mcp.md's declared
10
+ // one-surface list (`help` is handled directly by cli/entry.ts's
11
+ // top-level flag redirect, same as milestone 1 — it is not itself a
12
+ // dispatchable command row); `list`/`status`/`sandbox.list` reuse the mcp
13
+ // row's capability id and get a cli binding layered on top. Every handler
14
+ // below returns a `CliHandlerResult`; core/ never touches process.stdout
15
+ // or process.exitCode directly (see docs/rebuild/PLAN.md's core/shell split) —
16
+ // cli/dispatch.ts's generic executor performs the actual write.
17
+ // ---------------------------------------------------------------------
18
+ const version_1 = require("../../core/version");
19
+ const workflow_app_loader_1 = require("../../shell/workflow-app-loader");
20
+ const help_1 = require("../../core/format/help");
21
+ (0, registry_core_1.addCliOnlyCapability)("version", "Print the current cool-workflow version.", {
22
+ path: ["version"],
23
+ jsonMode: "default",
24
+ handler: () => ({ text: `${version_1.CURRENT_COOL_WORKFLOW_VERSION}\n` }),
25
+ }, "version is a local, no-run-state print; the old build never gave it an MCP peer.");
26
+ /** `cw search <keyword>` — filters the SAME real app discovery `cw list`
27
+ * shows, by id/title/summary (byte-behavior port of cli/dispatch.ts's
28
+ * 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. */
34
+ (0, registry_core_1.addCliOnlyCapability)("search", "Search bundled workflows by id/title/summary keyword.", {
35
+ path: ["search"],
36
+ jsonMode: "flag",
37
+ hiddenFromHelp: true,
38
+ handler: (args) => {
39
+ const keyword = args.positionals.join(" ");
40
+ if (!keyword.trim()) {
41
+ throw new Error('Missing search keyword.\n Tip: cw search architecture to find workflows about architecture.');
42
+ }
43
+ const lower = keyword.toLowerCase();
44
+ const results = (0, workflow_app_loader_1.listWorkflowApps)()
45
+ .filter((a) => String(a.title).toLowerCase().includes(lower) ||
46
+ String(a.summary).toLowerCase().includes(lower) ||
47
+ String(a.id).toLowerCase().includes(lower))
48
+ .map((a) => ({ id: String(a.id), title: String(a.title), summary: String(a.summary) }));
49
+ return { json: results, text: (0, help_1.formatSearchResults)(keyword, results) };
50
+ },
51
+ }, "CLI-only discovery helper over the same real app data cw list shows; no MCP client needs a free-text search tool alongside cw_list's structured output.");
52
+ (0, registry_core_1.attachCliBinding)("list", {
53
+ path: ["list"],
54
+ jsonMode: "default",
55
+ handler: () => ({ json: (0, registry_core_1.listBundledWorkflows)() }),
56
+ });
57
+ (0, registry_core_1.attachCliBinding)("status", {
58
+ path: ["status"],
59
+ jsonMode: "flag",
60
+ handler: (args) => ({ json: (0, registry_core_1.statusPayload)(args.positionals[0]) }),
61
+ });
62
+ (0, registry_core_1.attachCliBinding)("sandbox.list", {
63
+ path: ["sandbox", "list"],
64
+ jsonMode: "default",
65
+ handler: () => ({ json: (0, registry_core_1.listBundledSandboxProfiles)() }),
66
+ });