cool-workflow 0.2.1 → 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 (93) 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 +18 -86
  10. package/dist/cli/parseargv.js +5 -2
  11. package/dist/core/capability-data.js +270 -0
  12. package/dist/core/capability-table.js +16 -3504
  13. package/dist/core/format/help.js +28 -5
  14. package/dist/core/multi-agent/collaboration.js +3 -2
  15. package/dist/core/multi-agent/coordinator.js +4 -3
  16. package/dist/core/pipeline/commit-gate.js +2 -1
  17. package/dist/core/state/state-explosion/digest.js +3 -2
  18. package/dist/core/state/state-explosion/graph.js +9 -8
  19. package/dist/core/state/state-explosion/helpers.js +2 -1
  20. package/dist/core/trust/ledger.js +2 -1
  21. package/dist/core/types/execution-backend.js +18 -0
  22. package/dist/core/types/observability.js +7 -0
  23. package/dist/core/util/collate.js +23 -0
  24. package/dist/core/version.js +1 -1
  25. package/dist/shell/audit-cli.js +46 -2
  26. package/dist/shell/commit-summary.js +2 -1
  27. package/dist/shell/drive.js +26 -10
  28. package/dist/shell/evidence-reasoning.js +4 -3
  29. package/dist/shell/execution-backend/registry.js +2 -1
  30. package/dist/shell/execution-backend/types.js +0 -9
  31. package/dist/shell/fs-atomic.js +14 -1
  32. package/dist/shell/multi-agent-operator-ux.js +4 -3
  33. package/dist/shell/observability.js +12 -11
  34. package/dist/shell/onramp.js +17 -1
  35. package/dist/shell/operator-ux-text.js +2 -1
  36. package/dist/shell/operator-ux.js +7 -6
  37. package/dist/shell/pipeline-cli.js +82 -72
  38. package/dist/shell/reclamation-io.js +2 -1
  39. package/dist/shell/report.js +2 -1
  40. package/dist/shell/run-store.js +17 -0
  41. package/dist/shell/state-explosion-cli.js +2 -1
  42. package/dist/shell/topology-io.js +2 -1
  43. package/dist/shell/trust-audit.js +41 -2
  44. package/dist/shell/worker-cli.js +9 -1
  45. package/dist/shell/worker-isolation.js +34 -9
  46. package/dist/shell/workflow-app-loader.js +3 -2
  47. package/dist/wiring/capability-table/basics.js +66 -0
  48. package/dist/wiring/capability-table/exec-backend.js +162 -0
  49. package/dist/wiring/capability-table/index.js +42 -0
  50. package/dist/wiring/capability-table/multi-agent.js +623 -0
  51. package/dist/wiring/capability-table/parity.js +466 -0
  52. package/dist/wiring/capability-table/pipeline.js +280 -0
  53. package/dist/wiring/capability-table/registry-core.js +189 -0
  54. package/dist/wiring/capability-table/reporting.js +394 -0
  55. package/dist/wiring/capability-table/scheduling-registry.js +558 -0
  56. package/dist/wiring/capability-table/state.js +173 -0
  57. package/dist/wiring/capability-table/trust-ledger.js +134 -0
  58. package/dist/wiring/capability-table/workflow-apps.js +366 -0
  59. package/docs/agent-delegation-drive.7.md +2 -0
  60. package/docs/cli-mcp-parity.7.md +7 -2
  61. package/docs/contract-migration-tooling.7.md +2 -0
  62. package/docs/control-plane-scheduling.7.md +2 -0
  63. package/docs/durable-state-and-locking.7.md +22 -0
  64. package/docs/evidence-adoption-reasoning-chain.7.md +2 -0
  65. package/docs/execution-backends.7.md +2 -0
  66. package/docs/index.md +1 -0
  67. package/docs/multi-agent-cli-mcp-surface.7.md +2 -0
  68. package/docs/multi-agent-eval-replay-harness.7.md +2 -0
  69. package/docs/multi-agent-operator-ux.7.md +2 -0
  70. package/docs/node-snapshot-diff-replay.7.md +2 -0
  71. package/docs/observability-cost-accounting.7.md +2 -0
  72. package/docs/project-index.md +25 -5
  73. package/docs/real-execution-backends.7.md +2 -0
  74. package/docs/release-and-migration.7.md +2 -0
  75. package/docs/release-tooling.7.md +2 -0
  76. package/docs/run-registry-control-plane.7.md +2 -0
  77. package/docs/run-retention-reclamation.7.md +2 -0
  78. package/docs/security-trust-hardening.7.md +30 -0
  79. package/docs/state-explosion-management.7.md +2 -0
  80. package/docs/team-collaboration.7.md +2 -0
  81. package/docs/trust-audit-anchor.7.md +69 -0
  82. package/docs/web-desktop-workbench.7.md +2 -0
  83. package/manifest/plugin.manifest.json +1 -1
  84. package/package.json +3 -1
  85. package/scripts/bump-version.js +24 -2
  86. package/scripts/canonical-apps.js +4 -4
  87. package/scripts/dogfood-release.js +1 -1
  88. package/scripts/golden-path.js +4 -4
  89. package/scripts/purity-baseline.json +68 -0
  90. package/scripts/purity-gate.js +239 -0
  91. package/scripts/release-check.js +8 -1
  92. package/scripts/version-sync-check.js +33 -12
  93. package/workflows/README.md +19 -0
@@ -28,9 +28,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
28
28
  exports.formatHelp = formatHelp;
29
29
  exports.formatCommandHelp = formatCommandHelp;
30
30
  exports.formatInfo = formatInfo;
31
+ exports.formatSearchResults = formatSearchResults;
31
32
  const capability_table_1 = require("../capability-table");
32
- /** src/orchestrator.ts:934-951 — the exact "More commands" token set, in
33
- * this exact order (space-joined in the source, pipe-joined for display). */
33
+ /** src/orchestrator.ts:934-951 — the "More commands" token set, in the old
34
+ * build's order (space-joined in the source, pipe-joined for display).
35
+ * One change from the old capture: `update` is gone. The verb had no code
36
+ * behind it in this build (`cw update` said "Unknown command"), so the
37
+ * help must not offer it. See parseargv.ts KNOWN_COMMANDS. */
34
38
  const MORE_COMMANDS_TOKENS = [
35
39
  "list", "search", "info", "init", "plan", "status", "next", "dispatch",
36
40
  "result", "state", "commit", "report", "app", "sandbox", "backend",
@@ -40,7 +44,7 @@ const MORE_COMMANDS_TOKENS = [
40
44
  "summary", "blackboard", "coordinator", "metrics", "operator", "sched",
41
45
  "gc", "telemetry", "migration", "demo", "workbench", "approve", "reject",
42
46
  "comment", "handoff", "ledger", "graph", "eval", "man", "version",
43
- "update", "fix",
47
+ "fix",
44
48
  ];
45
49
  const MORE_COMMANDS_WRAP_WIDTH = 76;
46
50
  /** src/orchestrator.ts:940-951 — greedily pack pipe-joined tokens into
@@ -205,7 +209,9 @@ const COMMAND_HELP_ROWS = {
205
209
  /** src/orchestrator.ts:899-933 — the top-level `cw help` text. Color is
206
210
  * intentionally NOT applied here (this milestone's conformance runs pipe
207
211
  * stdout, so NO_COLOR/non-TTY always wins); byte content matches the
208
- * plain-text capture at SPEC/cli-help/_root.txt exactly. */
212
+ * plain-text capture at SPEC/cli-help/_root.txt, but for the dead
213
+ * `update` lines, which were taken out on purpose (no code was behind
214
+ * the verb — see MORE_COMMANDS_TOKENS note above). */
209
215
  function formatHelp() {
210
216
  const moreCommandsLines = wrapPipeJoined(MORE_COMMANDS_TOKENS, MORE_COMMANDS_WRAP_WIDTH);
211
217
  const lines = [
@@ -214,7 +220,6 @@ function formatHelp() {
214
220
  ' -q "question" [-claude|-codex|-gemini|-deepseek] Ask a question, get a report',
215
221
  ' -q "question" --link <url> Review a remote repo by URL',
216
222
  " version Show version",
217
- " update Update to latest release",
218
223
  " doctor Check setup",
219
224
  " fix Show fix commands for setup issues",
220
225
  "",
@@ -312,3 +317,21 @@ function formatInfo(appId, data) {
312
317
  lines.push(` Run: cw quickstart ${appId} --repo . --question "..."`);
313
318
  return lines.join("\n");
314
319
  }
320
+ /** `cw search <keyword>`'s human text — byte-exact to the milestone-1
321
+ * carry-over's own formatSearchResults (moved here from cli/dispatch.ts
322
+ * so the search capability-table row, which lives in core/, can render
323
+ * its own text without core importing from cli/). */
324
+ function formatSearchResults(keyword, results) {
325
+ if (results.length === 0) {
326
+ return `No workflows matched "${keyword}".\n Tip: cw list for all available workflows.`;
327
+ }
328
+ const lines = [`${results.length} workflow${results.length === 1 ? "" : "s"} matching "${keyword}"`];
329
+ for (const r of results) {
330
+ lines.push(` ${r.id} — ${r.title}`);
331
+ const cut = r.summary.length > 120 ? `${r.summary.slice(0, 119)}…` : r.summary;
332
+ lines.push(` ${cut}`);
333
+ }
334
+ lines.push("");
335
+ lines.push("Use cw info <id> for full details.");
336
+ return lines.join("\n");
337
+ }
@@ -36,6 +36,7 @@ exports.listComments = listComments;
36
36
  exports.distinctTargets = distinctTargets;
37
37
  exports.formatReviewStatus = formatReviewStatus;
38
38
  exports.formatCommentList = formatCommentList;
39
+ const collate_1 = require("../util/collate");
39
40
  exports.COLLABORATION_SCHEMA_VERSION = 1;
40
41
  /** The single, honest stand-in for an absent identity. */
41
42
  exports.UNATTRIBUTED_ACTOR = {
@@ -213,7 +214,7 @@ function matchesAnyTarget(target, related) {
213
214
  return related.some((entry) => sameTarget(target, entry));
214
215
  }
215
216
  function compareByCreated(left, right) {
216
- return left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id);
217
+ return (0, collate_1.stableCompare)(left.createdAt, right.createdAt) || (0, collate_1.stableCompare)(left.id, right.id);
217
218
  }
218
219
  function disqualify(record, policy, selfIds) {
219
220
  const actor = record.actor;
@@ -453,7 +454,7 @@ function distinctTargets(state) {
453
454
  seen.set(targetKey(record.target), record.target);
454
455
  for (const record of state.handoffs)
455
456
  seen.set(targetKey(record.target), record.target);
456
- return [...seen.values()].sort((left, right) => targetKey(left).localeCompare(targetKey(right)));
457
+ return [...seen.values()].sort((left, right) => (0, collate_1.stableCompare)(targetKey(left), targetKey(right)));
457
458
  }
458
459
  function formatReviewStatus(report) {
459
460
  const lines = [];
@@ -47,6 +47,7 @@ exports.summarizeBlackboard = summarizeBlackboard;
47
47
  exports.listBlackboardMessages = listBlackboardMessages;
48
48
  exports.listBlackboardArtifacts = listBlackboardArtifacts;
49
49
  exports.buildBlackboardGraph = buildBlackboardGraph;
50
+ const collate_1 = require("../util/collate");
50
51
  exports.BLACKBOARD_SCHEMA_VERSION = 1;
51
52
  /** Dedup, SORTS. Coordinator-side sorting `unique` — byte-identical
52
53
  * behavior to core/multi-agent/runtime.ts's own copy, kept as a
@@ -420,7 +421,7 @@ function summarizeBlackboard(runId, state, blackboardId, defaultIndexPath) {
420
421
  ].sort();
421
422
  const readyForFanin = Boolean(board && !openQuestions.length && !conflicts.length && artifacts.length > 0 && missingEvidence.length === 0);
422
423
  const latestSnapshot = scoped(state.snapshots)
423
- .sort((left, right) => left.createdAt.localeCompare(right.createdAt))
424
+ .sort((left, right) => (0, collate_1.stableCompare)(left.createdAt, right.createdAt))
424
425
  .at(-1);
425
426
  return {
426
427
  runId,
@@ -454,12 +455,12 @@ function nextAction(runId, board, openQuestions, conflicts, artifacts) {
454
455
  function listBlackboardMessages(state, options = {}) {
455
456
  return state.messages
456
457
  .filter((message) => (!options.blackboardId || message.blackboardId === options.blackboardId) && (!options.topicId || message.topicId === options.topicId))
457
- .sort((left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id));
458
+ .sort((left, right) => (0, collate_1.stableCompare)(left.createdAt, right.createdAt) || (0, collate_1.stableCompare)(left.id, right.id));
458
459
  }
459
460
  function listBlackboardArtifacts(state, options = {}) {
460
461
  return state.artifacts
461
462
  .filter((artifact) => (!options.blackboardId || artifact.blackboardId === options.blackboardId) && (!options.topicId || artifact.topicId === options.topicId))
462
- .sort((left, right) => left.id.localeCompare(right.id));
463
+ .sort((left, right) => (0, collate_1.stableCompare)(left.id, right.id));
463
464
  }
464
465
  function buildBlackboardGraph(runId, state, recordPath, messagesPath) {
465
466
  const nodes = [];
@@ -20,6 +20,7 @@ exports.formatCommitId = formatCommitId;
20
20
  exports.gateFailureSeq = gateFailureSeq;
21
21
  const evidence_grounding_1 = require("../trust/evidence-grounding");
22
22
  const result_normalize_1 = require("./result-normalize");
23
+ const collate_1 = require("../util/collate");
23
24
  /** The sandbox profile that accepted a candidate's worker output — the
24
25
  * worker's own profile when present, else the backing task's. Pure port of
25
26
  * the old build's src/gates.ts sandboxProfileForCandidate. */
@@ -97,7 +98,7 @@ function findSelectionNode(run, selectionId) {
97
98
  function latestSelectionForCandidate(run, candidateId) {
98
99
  return [...(run.candidateSelections || [])]
99
100
  .filter((s) => s.candidateId === candidateId)
100
- .sort((a, b) => (b.selectedAt || "").localeCompare(a.selectedAt || ""))[0];
101
+ .sort((a, b) => (0, collate_1.stableCompare)(b.selectedAt || "", a.selectedAt || ""))[0];
101
102
  }
102
103
  function evidenceLocatorString(entry) {
103
104
  const ref = entry.locator || entry.path || entry.summary || entry.id;
@@ -29,6 +29,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
29
29
  exports.summarizeBlackboardDigest = summarizeBlackboardDigest;
30
30
  const hash_1 = require("../../hash");
31
31
  const helpers_1 = require("./helpers");
32
+ const collate_1 = require("../../util/collate");
32
33
  /** Deterministic structural summary of one (or the default) blackboard.
33
34
  * Every list is sorted by id (`byId`); `recentChanges` is the last 10 by
34
35
  * `updatedAt` desc, THEN re-sorted by id for the final list (matching the
@@ -63,7 +64,7 @@ function summarizeBlackboardDigest(run, blackboardId, now) {
63
64
  .map((topic) => {
64
65
  const topicMessages = messages
65
66
  .filter((m) => m.topicId === topic.id)
66
- .sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id));
67
+ .sort((a, b) => (0, collate_1.stableCompare)(a.createdAt, b.createdAt) || (0, collate_1.stableCompare)(a.id, b.id));
67
68
  const last = topicMessages[topicMessages.length - 1];
68
69
  return {
69
70
  id: `thread:${topic.id}`,
@@ -181,7 +182,7 @@ function summarizeBlackboardDigest(run, blackboardId, now) {
181
182
  updatedAt: record.updatedAt,
182
183
  status: record.status,
183
184
  }))
184
- .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt) || a.id.localeCompare(b.id))
185
+ .sort((a, b) => (0, collate_1.stableCompare)(b.updatedAt, a.updatedAt) || (0, collate_1.stableCompare)(a.id, b.id))
185
186
  .slice(0, 10)
186
187
  .map((record) => ({
187
188
  id: `recent:${record.id}`,
@@ -37,6 +37,7 @@ exports.buildCompactGraphFromView = buildCompactGraphFromView;
37
37
  const size_1 = require("./size");
38
38
  const helpers_1 = require("./helpers");
39
39
  Object.defineProperty(exports, "byId", { enumerable: true, get: function () { return helpers_1.byId; } });
40
+ const collate_1 = require("../../util/collate");
40
41
  const runtime_1 = require("../../multi-agent/runtime");
41
42
  const coordinator_1 = require("../../multi-agent/coordinator");
42
43
  const topology_1 = require("../../multi-agent/topology");
@@ -144,8 +145,8 @@ function runToGraphView(run) {
144
145
  return true;
145
146
  });
146
147
  return {
147
- nodes: [...nodes.values()].sort((a, b) => a.kind.localeCompare(b.kind) || a.id.localeCompare(b.id)),
148
- edges: dedupedEdges.sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to) || (a.label || "").localeCompare(b.label || "")),
148
+ nodes: [...nodes.values()].sort((a, b) => (0, collate_1.stableCompare)(a.kind, b.kind) || (0, collate_1.stableCompare)(a.id, b.id)),
149
+ edges: dedupedEdges.sort((a, b) => (0, collate_1.stableCompare)(a.from, b.from) || (0, collate_1.stableCompare)(a.to, b.to) || (0, collate_1.stableCompare)(a.label || "", b.label || "")),
149
150
  };
150
151
  }
151
152
  /** `buildCompactGraph(run, view, options)` — builds the graph view via
@@ -192,8 +193,8 @@ function runToGraphViewFromWorkflowRun(run) {
192
193
  edges.push(edge);
193
194
  }
194
195
  return {
195
- nodes: [...nodes.values()].sort((a, b) => a.kind.localeCompare(b.kind) || a.id.localeCompare(b.id)),
196
- edges: edges.sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to) || (a.label || "").localeCompare(b.label || "")),
196
+ nodes: [...nodes.values()].sort((a, b) => (0, collate_1.stableCompare)(a.kind, b.kind) || (0, collate_1.stableCompare)(a.id, b.id)),
197
+ edges: edges.sort((a, b) => (0, collate_1.stableCompare)(a.from, b.from) || (0, collate_1.stableCompare)(a.to, b.to) || (0, collate_1.stableCompare)(a.label || "", b.label || "")),
197
198
  };
198
199
  }
199
200
  function collapseRuleFor() {
@@ -470,7 +471,7 @@ function buildCompactGraphFromView(runId, full, view = "compact", options = {})
470
471
  // critical-path — docs/rebuild/PLAN.md byte-compat item 9).
471
472
  const synthetic = [];
472
473
  const collapsedNodeIds = new Map(); // sourceNodeId -> syntheticId
473
- for (const [bucketKey, ids] of [...buckets.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
474
+ for (const [bucketKey, ids] of [...buckets.entries()].sort((a, b) => (0, collate_1.stableCompare)(a[0], b[0]))) {
474
475
  if (view !== "critical-path" && ids.length < thresholds.collapseBucket) {
475
476
  for (const id of ids)
476
477
  keep.add(id);
@@ -519,9 +520,9 @@ function buildCompactGraphFromView(runId, full, view = "compact", options = {})
519
520
  edges.push({ from, to, label: edge.label });
520
521
  }
521
522
  return finalizeGraphRecord(runId, view, options, full, {
522
- nodes: nodes.sort((a, b) => a.kind.localeCompare(b.kind) || a.id.localeCompare(b.id)),
523
- edges: edges.sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to) || (a.label || "").localeCompare(b.label || "")),
524
- syntheticNodes: synthetic.sort((a, b) => a.id.localeCompare(b.id)),
523
+ nodes: nodes.sort((a, b) => (0, collate_1.stableCompare)(a.kind, b.kind) || (0, collate_1.stableCompare)(a.id, b.id)),
524
+ edges: edges.sort((a, b) => (0, collate_1.stableCompare)(a.from, b.from) || (0, collate_1.stableCompare)(a.to, b.to) || (0, collate_1.stableCompare)(a.label || "", b.label || "")),
525
+ syntheticNodes: synthetic.sort((a, b) => (0, collate_1.stableCompare)(a.id, b.id)),
525
526
  critical,
526
527
  });
527
528
  }
@@ -33,6 +33,7 @@ exports.slug = slug;
33
33
  const hash_1 = require("../../hash");
34
34
  Object.defineProperty(exports, "fingerprintRecords", { enumerable: true, get: function () { return hash_1.fingerprintRecords; } });
35
35
  Object.defineProperty(exports, "fingerprintStrings", { enumerable: true, get: function () { return hash_1.fingerprintStrings; } });
36
+ const collate_1 = require("../../util/collate");
36
37
  /** True for `failed`, `blocked`, `rejected`, `conflicting` — the never-
37
38
  * collapse status set (docs/rebuild/PLAN.md byte-compat item 9). */
38
39
  function isProtectedStatus(status) {
@@ -83,7 +84,7 @@ function unique(values) {
83
84
  return Array.from(new Set(values.filter(Boolean))).sort();
84
85
  }
85
86
  function byId(a, b) {
86
- return a.id.localeCompare(b.id);
87
+ return (0, collate_1.stableCompare)(a.id, b.id);
87
88
  }
88
89
  /** Whitespace-collapsed; over 80 chars becomes the first 77 chars + `...`. */
89
90
  function truncate(value) {
@@ -22,6 +22,7 @@ exports.verifyLedgerEntry = verifyLedgerEntry;
22
22
  exports.applyLedgerProposal = applyLedgerProposal;
23
23
  exports.resolveLedgerInbox = resolveLedgerInbox;
24
24
  const hash_1 = require("../hash");
25
+ const collate_1 = require("../util/collate");
25
26
  /** sha256 over the canonical content (every field except `id` and
26
27
  * `digest`, which are derived FROM it). Returns the full `sha256:<hex>`
27
28
  * form — the same prefixed spelling every other hash chain uses. */
@@ -187,7 +188,7 @@ function resolveLedgerInbox(entries) {
187
188
  reviews: answering.map((r) => r.id).sort(),
188
189
  };
189
190
  })
190
- .sort((a, b) => a.id.localeCompare(b.id));
191
+ .sort((a, b) => (0, collate_1.stableCompare)(a.id, b.id));
191
192
  const tally = (s) => proposals.filter((p) => p.resolution === s).length;
192
193
  return {
193
194
  proposals,
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ // core/types/execution-backend.ts — plain data shapes for the driver layer.
3
+ //
4
+ // MILESTONE 5 (docs/rebuild/PLAN.md build order, step 5). Byte-exact port of the shapes
5
+ // in the old build's src/types/execution-backend.ts and the sandbox slice of
6
+ // src/types/sandbox.ts that this subsystem needs. Types only — no logic —
7
+ // so this file lives in core/ (moved here from shell/execution-backend/
8
+ // types.ts, which now re-exports it for its 7 existing importers): the
9
+ // executor-boundary welds in core/types/boundary.ts need ResultEnvelope
10
+ // and ExecutionResultEnvelope, and neither can be cherry-picked out of
11
+ // this file alone without also carrying their whole dependency graph
12
+ // (ExecutionProvenance, SandboxAttestation, BackendLocality/BackendKind,
13
+ // BackendExecutionHandle, ...) — so the file moves as one piece, matching
14
+ // its own original header's claim that it was "safe to import from both
15
+ // shell/ (impure) and any future core/ caller."
16
+ //
17
+ // Evidence: SPEC/execution-backend.md.
18
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ // core/types/observability.ts — the one type shell/observability.ts's
3
+ // executor-boundary weld (core/types/boundary.ts) needs, moved here since
4
+ // it is plain data with no dependency on that (impure) file's logic.
5
+ // shell/observability.ts re-exports it so its own existing exports stay
6
+ // unchanged.
7
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.stableCompare = stableCompare;
4
+ // core/util/collate.ts — THE one string-ordering comparator for anything
5
+ // whose order feeds a hash, a cache key, or a byte-pinned output.
6
+ //
7
+ // Pure. No fs, no child_process, no net, no process.env, no Date.now(), no
8
+ // Math.random().
9
+ //
10
+ // A bare `a.localeCompare(b)` reads the HOST's default locale (LANG/LC_ALL),
11
+ // which is not part of CW's replay-determinism story: two machines with
12
+ // different locales sorting the SAME string set can walk it in a different
13
+ // order, so anything that hashes that order (a cache key) drifts silently
14
+ // across hosts, and anything that hashes CONTENT built from that order (an
15
+ // eval snapshot) can misreport a determinism regression that is really just
16
+ // a locale difference. `stableCompare` pins the locale explicitly to "en" —
17
+ // this is the SAME bytes Node's full-ICU build already produces under the
18
+ // ICU root locale (which is what a locale-stripped environment, e.g. this
19
+ // repo's own conformance harness, always runs under), so switching a bare
20
+ // `localeCompare` call to this one changes NO existing output.
21
+ function stableCompare(a, b) {
22
+ return a.localeCompare(b, "en");
23
+ }
@@ -7,7 +7,7 @@ exports.MIN_SUPPORTED_RUN_STATE_SCHEMA_VERSION = exports.LEGACY_RUN_STATE_SCHEMA
7
7
  // `cw version` output does not change bytes just because the code under it
8
8
  // was rebuilt. See SPEC/cli-surface.md "Exact outputs > Version" and
9
9
  // conformance/cases/version-basic.case.js (regex `/^\d+\.\d+\.\d+\n$/`).
10
- exports.CURRENT_COOL_WORKFLOW_VERSION = "0.2.1";
10
+ exports.CURRENT_COOL_WORKFLOW_VERSION = "0.2.2";
11
11
  // State-kernel schema version constants (SPEC/state-core.md "Version
12
12
  // constants"). Pinned to the old build's src/version.ts byte-for-byte.
13
13
  exports.WORKFLOW_APP_SCHEMA_VERSION = 1;
@@ -44,6 +44,7 @@ var __importStar = (this && this.__importStar) || (function () {
44
44
  })();
45
45
  Object.defineProperty(exports, "__esModule", { value: true });
46
46
  exports.auditVerifyCli = auditVerifyCli;
47
+ exports.auditHeadCli = auditHeadCli;
47
48
  exports.auditSummaryCli = auditSummaryCli;
48
49
  exports.auditMultiAgentCli = auditMultiAgentCli;
49
50
  exports.auditPolicyCli = auditPolicyCli;
@@ -65,12 +66,36 @@ const sandbox_profile_1 = require("./sandbox-profile");
65
66
  function invocationCwd(args) {
66
67
  return typeof args.cwd === "string" && args.cwd.trim() ? path.resolve(args.cwd) : process.cwd();
67
68
  }
69
+ /** Parse the optional truncation anchor off the CLI options / MCP args
70
+ * (`--expect-head <hash>` / `--expect-count <n>`; MCP: expectHead /
71
+ * expectCount). Fail-closed on a malformed count — a flag given without
72
+ * a usable value must never silently weaken the check it asked for. */
73
+ function anchorOption(args) {
74
+ const headRaw = args["expect-head"] ?? args.expectHead;
75
+ const countRaw = args["expect-count"] ?? args.expectCount;
76
+ const expectHead = optionalString(headRaw);
77
+ if (headRaw !== undefined && headRaw !== null && expectHead === undefined) {
78
+ throw new Error("audit verify: --expect-head requires a hash value");
79
+ }
80
+ let expectCount;
81
+ if (countRaw !== undefined && countRaw !== null) {
82
+ const parsed = Number(countRaw);
83
+ if (countRaw === true || !Number.isInteger(parsed) || parsed < 0) {
84
+ throw new Error("audit verify: --expect-count requires a non-negative integer");
85
+ }
86
+ expectCount = parsed;
87
+ }
88
+ if (expectHead === undefined && expectCount === undefined)
89
+ return undefined;
90
+ return { expectHead, expectCount };
91
+ }
68
92
  function auditVerifyCli(runId, args) {
69
93
  if (!runId)
70
94
  throw new Error("audit verify requires a run id (cw audit verify <run-id>)");
95
+ const anchor = anchorOption(args);
71
96
  const run = (0, run_store_1.loadRunFromCwd)(runId, invocationCwd(args));
72
- const v = (0, trust_audit_1.verifyTrustAudit)(run);
73
- return {
97
+ const v = (0, trust_audit_1.verifyTrustAudit)(run, anchor);
98
+ const result = {
74
99
  schemaVersion: 1,
75
100
  runId: run.id,
76
101
  present: v.present,
@@ -81,6 +106,25 @@ function auditVerifyCli(runId, args) {
81
106
  corruptLines: v.corruptLines,
82
107
  failedChecks: v.checks.filter((c) => !c.pass).map((c) => ({ name: c.name, code: c.code })),
83
108
  };
109
+ if (anchor) {
110
+ result.anchor = {
111
+ ...(anchor.expectHead !== undefined ? { expectHead: anchor.expectHead } : {}),
112
+ ...(anchor.expectCount !== undefined ? { expectCount: anchor.expectCount } : {}),
113
+ satisfied: !v.checks.some((c) => c.code === "trust-audit-truncated"),
114
+ };
115
+ }
116
+ return result;
117
+ }
118
+ /** `cw audit head <run>` — the chain head anchor (read-only projection).
119
+ * Capture it after a run (or before publishing/exporting); later,
120
+ * `cw audit verify <run> --expect-head <hash> --expect-count <n>`
121
+ * re-proves the log was not shortened since the capture. */
122
+ function auditHeadCli(runId, args) {
123
+ if (!runId)
124
+ throw new Error("audit head requires a run id (cw audit head <run-id>)");
125
+ const run = (0, run_store_1.loadRunFromCwd)(runId, invocationCwd(args));
126
+ const head = (0, trust_audit_1.trustAuditHead)(run);
127
+ return { schemaVersion: 1, runId: run.id, eventCount: head.eventCount, headHash: head.headHash };
84
128
  }
85
129
  /** MILESTONE 11 (reporting/observability, workbench audit panels) —
86
130
  * `cw audit summary`/`audit multi-agent`/`audit policy`/`audit judge`.
@@ -46,6 +46,7 @@ exports.commitSummaryCli = commitSummaryCli;
46
46
  exports.formatCommitSummaryText = formatCommitSummaryText;
47
47
  const path = __importStar(require("node:path"));
48
48
  const run_store_1 = require("./run-store");
49
+ const collate_1 = require("../core/util/collate");
49
50
  function formatCommitRow(commit) {
50
51
  return {
51
52
  id: commit.id,
@@ -63,7 +64,7 @@ function formatCommitRow(commit) {
63
64
  /** Byte-exact port of the old build's `summarizeOperatorCommits`
64
65
  * (src/operator-ux.ts:339-349). */
65
66
  function summarizeOperatorCommits(run) {
66
- const commits = [...(run.commits || [])].sort((left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id));
67
+ const commits = [...(run.commits || [])].sort((left, right) => (0, collate_1.stableCompare)(left.createdAt, right.createdAt) || (0, collate_1.stableCompare)(left.id, right.id));
67
68
  const rows = commits.map(formatCommitRow);
68
69
  return {
69
70
  total: rows.length,
@@ -7,15 +7,10 @@
7
7
  // milestone). Byte-exact port of the old build's src/drive.ts's
8
8
  // imperative shell around the pure decision core now in
9
9
  // core/pipeline/drive-decide.ts. Sub-workflow nesting and `--incremental`
10
- // are ported; the concurrent-round driver (driveConcurrentRound) is
11
- // scoped down to the serial driver run through a width loop, since no
12
- // case in this milestone's combined gate exercises true concurrent-batch
13
- // recording order (that is `--concurrency`/parallel-phase-specific and is
14
- // authored as its own future conformance case per Open risk 5) — the
15
- // `mode:"parallel"` architecture-review phases still complete correctly
16
- // through the serial per-task loop, just without the wall-clock-parallel
17
- // spawn optimization; this is flagged here rather than silently ported as
18
- // if fully equivalent.
10
+ // are ported; the concurrent-round driver (driveConcurrentRound, below)
11
+ // dispatches and settles a whole round's tasks in one batch (see
12
+ // `--concurrency`/`roundWidth`), pinned by
13
+ // v2/conformance/cases/pipeline-concurrent-round.case.js.
19
14
  //
20
15
  // Evidence: SPEC/pipeline-run.md "Drive loop — src/drive.ts".
21
16
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
@@ -77,6 +72,7 @@ const agent_config_1 = require("./agent-config");
77
72
  const registry_1 = require("./execution-backend/registry");
78
73
  const agent_1 = require("./execution-backend/agent");
79
74
  const hash_1 = require("../core/hash");
75
+ const collate_1 = require("../core/util/collate");
80
76
  const pipeline_1 = require("./pipeline");
81
77
  const reporter_1 = require("./reporter");
82
78
  const fs_atomic_1 = require("./fs-atomic");
@@ -191,7 +187,10 @@ function previousPhaseResultsDigest(run, task) {
191
187
  return undefined;
192
188
  const previousTaskIds = new Set(run.phases.slice(0, phaseIndex).flatMap((p) => p.taskIds));
193
189
  const records = [];
194
- for (const candidate of run.tasks.filter((t) => previousTaskIds.has(t.id)).sort((a, b) => a.id.localeCompare(b.id))) {
190
+ // stableCompare (not a bare localeCompare): this order feeds the sha256
191
+ // digest below, which feeds the incremental cache key — a host-locale-
192
+ // dependent order would silently move the cache key across machines.
193
+ for (const candidate of run.tasks.filter((t) => previousTaskIds.has(t.id)).sort((a, b) => (0, collate_1.stableCompare)(a.id, b.id))) {
195
194
  if (candidate.status !== "completed" || !candidate.resultPath || !fs.existsSync(candidate.resultPath)) {
196
195
  records.push(undefined);
197
196
  continue;
@@ -303,7 +302,24 @@ function processSelectedTask(ctx, selectedId, preparedOutcome, deferPersist = fa
303
302
  emitProgress(`↺ ${selected.label || selected.id} (${selected.phase}) — accepting cached result`);
304
303
  try {
305
304
  fs.writeFileSync(manifest.resultPath, fs.readFileSync(cachePath, "utf8"), "utf8");
305
+ // Not gated by requireAttestedTelemetry here: the underlying result was
306
+ // already gated (attested or explicitly overridden) at its FIRST
307
+ // acceptance, before it was cached. Re-blocking a cache hit would only
308
+ // punish the operator for their own earlier, already-audited accept.
309
+ // Still made visible, not silent: when the operator requires attested
310
+ // telemetry, record that this particular accept came from the cache
311
+ // rather than a freshly re-verified hop.
306
312
  (0, worker_isolation_1.recordWorkerOutput)(run, workerId, manifest.resultPath);
313
+ if (ctx.config.requireAttestedTelemetry) {
314
+ (0, trust_audit_1.recordTrustAuditEvent)(run, {
315
+ kind: "telemetry.cache-accept",
316
+ decision: "recorded",
317
+ source: "cw-validated",
318
+ workerId,
319
+ taskId: selected.id,
320
+ metadata: { reason: "result-cache hit; original attestation gate applied at first acceptance, not re-verified here" },
321
+ });
322
+ }
307
323
  // Advance the run lifecycle stage on accept, as the old build's
308
324
  // recordWorkerOutput wrapper did (run.loopStage = "observe").
309
325
  run.loopStage = "observe";
@@ -29,6 +29,7 @@ const fs_atomic_1 = require("./fs-atomic");
29
29
  const multi_agent_operator_ux_1 = require("./multi-agent-operator-ux");
30
30
  const trust_audit_1 = require("./trust-audit");
31
31
  const trust_policy_1 = require("../core/multi-agent/trust-policy");
32
+ const collate_1 = require("../core/util/collate");
32
33
  exports.EVIDENCE_REASONING_SCHEMA_VERSION = 1;
33
34
  function candidatesOf(run) {
34
35
  return (run.candidates || []);
@@ -56,7 +57,7 @@ function buildEvidenceReasoningReport(run, options = {}) {
56
57
  const counterfactuals = deriveCounterfactuals(run, scores);
57
58
  const chains = operator.evidence
58
59
  .map((evidence) => buildChain(run, evidence, { scores, auditEvents, counterfactuals }))
59
- .sort((left, right) => statusRank(left.evidenceStatus) - statusRank(right.evidenceStatus) || left.id.localeCompare(right.id));
60
+ .sort((left, right) => statusRank(left.evidenceStatus) - statusRank(right.evidenceStatus) || (0, collate_1.stableCompare)(left.id, right.id));
60
61
  const totals = summarizeTotals(chains);
61
62
  const currentFingerprint = fingerprintChains(chains);
62
63
  const persisted = options.index;
@@ -340,7 +341,7 @@ function refreshEvidenceReasoning(run) {
340
341
  generatedAt: new Date().toISOString(),
341
342
  sourceFingerprint: report.sourceFingerprint,
342
343
  totals: report.totals,
343
- entries: entries.sort((a, b) => a.id.localeCompare(b.id)),
344
+ entries: entries.sort((a, b) => (0, collate_1.stableCompare)(a.id, b.id)),
344
345
  paths: { reasoningDir: dir, indexPath, reportPath },
345
346
  nextAction: `node scripts/cw.js multi-agent reasoning ${run.id}`,
346
347
  };
@@ -601,5 +602,5 @@ function unique(values) {
601
602
  return Array.from(new Set(values.filter(Boolean))).sort();
602
603
  }
603
604
  function byRef(a, b) {
604
- return a.ref.localeCompare(b.ref);
605
+ return (0, collate_1.stableCompare)(a.ref, b.ref);
605
606
  }
@@ -33,6 +33,7 @@ const container_1 = require("./container");
33
33
  const remote_1 = require("./remote");
34
34
  const ci_1 = require("./ci");
35
35
  const agent_1 = require("./agent");
36
+ const collate_1 = require("../../core/util/collate");
36
37
  exports.EXECUTION_BACKEND_SCHEMA_VERSION = 1;
37
38
  exports.DEFAULT_BACKEND_ID = "node";
38
39
  exports.SANDBOX_DIMENSIONS = ["read", "write", "command", "network", "env"];
@@ -159,7 +160,7 @@ function registeredDrivers() {
159
160
  function listBackendDescriptors() {
160
161
  return registeredDrivers()
161
162
  .map((driver) => specDescriptor(driver.spec))
162
- .sort((left, right) => left.id.localeCompare(right.id));
163
+ .sort((left, right) => (0, collate_1.stableCompare)(left.id, right.id));
163
164
  }
164
165
  function backendIds() {
165
166
  return registeredDrivers()
@@ -1,11 +1,2 @@
1
1
  "use strict";
2
- // shell/execution-backend/types.ts — plain data shapes for the driver layer.
3
- //
4
- // MILESTONE 5 (docs/rebuild/PLAN.md build order, step 5). Byte-exact port of the shapes
5
- // in the old build's src/types/execution-backend.ts and the sandbox slice of
6
- // src/types/sandbox.ts that this subsystem needs. Types only — no logic — so
7
- // this file is safe to import from both shell/ (impure) and any future core/
8
- // caller without violating the core/shell purity split.
9
- //
10
- // Evidence: SPEC/execution-backend.md.
11
2
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -198,14 +198,25 @@ function durableAppendFileSync(file, data) {
198
198
  // only when still owned by this pid.
199
199
  // ---------------------------------------------------------------------------
200
200
  const FILE_LOCK_STALE_MS = 30_000;
201
+ // Lock paths this process holds right now. A nested withFileLock on the
202
+ // SAME target runs its fn directly (re-entrant) instead of waiting on its
203
+ // own lock file until the 240 tries run out — that lets a whole
204
+ // load -> change -> save cycle hold one lock while the save path inside
205
+ // it keeps its own withFileLock call unchanged.
206
+ const HELD_LOCKS = new Set();
201
207
  function sleepSync(ms) {
202
208
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
203
209
  }
204
210
  /** Run `fn` while holding an advisory lock for `targetPath`; always released
205
211
  * (unless the lock was stolen mid-operation, in which case releasing would
206
- * corrupt the thief's critical section, so it is deliberately NOT released). */
212
+ * corrupt the thief's critical section, so it is deliberately NOT released).
213
+ * Re-entrant inside one process: a nested call on the same target runs
214
+ * `fn` under the already-held lock. */
207
215
  function withFileLock(targetPath, fn) {
208
216
  const lock = `${targetPath}.lock`;
217
+ const heldKey = path.resolve(lock);
218
+ if (HELD_LOCKS.has(heldKey))
219
+ return fn();
209
220
  fs.mkdirSync(path.dirname(lock), { recursive: true });
210
221
  const pid = String(process.pid);
211
222
  let acquired = false;
@@ -234,6 +245,7 @@ function withFileLock(targetPath, fn) {
234
245
  }
235
246
  if (!acquired)
236
247
  throw new Error(`could not acquire file lock for ${targetPath}`);
248
+ HELD_LOCKS.add(heldKey);
237
249
  // Refresh mtime right before the critical section.
238
250
  try {
239
251
  fs.utimesSync(lock, new Date(), new Date());
@@ -263,6 +275,7 @@ function withFileLock(targetPath, fn) {
263
275
  return result;
264
276
  }
265
277
  finally {
278
+ HELD_LOCKS.delete(heldKey);
266
279
  try {
267
280
  // Only release if we still own the lock.
268
281
  const current = fs.readFileSync(lock, "utf8");
@@ -62,6 +62,7 @@ const coordinator_io_1 = require("./coordinator-io");
62
62
  const multi_agent_io_1 = require("./multi-agent-io");
63
63
  const topology_io_1 = require("./topology-io");
64
64
  const trust_audit_1 = require("./trust-audit");
65
+ const collate_1 = require("../core/util/collate");
65
66
  function maOf(run) {
66
67
  return run.multiAgent || {};
67
68
  }
@@ -275,7 +276,7 @@ function deriveDependencies(run) {
275
276
  for (const commit of commitsOf(run)) {
276
277
  add(commit.selectionId ? `${run.id}:selection:${commit.selectionId}` : undefined, String(commit.stateNodeId || `${run.id}:commit:${commit.id}`), "commits", commit.verifierGated ? "committed" : "checkpoint");
277
278
  }
278
- return rows.filter(uniqueById).sort((left, right) => left.from.localeCompare(right.from) || left.to.localeCompare(right.to));
279
+ return rows.filter(uniqueById).sort((left, right) => (0, collate_1.stableCompare)(left.from, right.from) || (0, collate_1.stableCompare)(left.to, right.to));
279
280
  }
280
281
  function deriveFailures(run, dependencies) {
281
282
  const rows = [];
@@ -337,7 +338,7 @@ function deriveFailures(run, dependencies) {
337
338
  const readySelection = selectionsOf(run).find((selection) => !commitsOf(run).some((commit) => commit.selectionId === selection.id && commit.verifierGated));
338
339
  if (readySelection)
339
340
  add(String(readySelection.id), "commit-gate", "not-ready", `selection ${readySelection.id} has no verifier-gated commit`, `node scripts/cw.js commit ${run.id} --selection ${readySelection.id} --reason "<verified rationale>"`, readySelection.candidateId);
340
- return rows.filter(uniqueByFailure).sort((left, right) => left.kind.localeCompare(right.kind) || left.id.localeCompare(right.id));
341
+ return rows.filter(uniqueByFailure).sort((left, right) => (0, collate_1.stableCompare)(left.kind, right.kind) || (0, collate_1.stableCompare)(left.id, right.id));
341
342
  }
342
343
  function deriveEvidence(run) {
343
344
  const rows = new Map();
@@ -442,7 +443,7 @@ function deriveEvidence(run) {
442
443
  return [...rows.values()]
443
444
  .map(normalizeEvidenceStatus)
444
445
  .map(withDisposition)
445
- .sort((left, right) => statusRank(left.status) - statusRank(right.status) || left.id.localeCompare(right.id));
446
+ .sort((left, right) => statusRank(left.status) - statusRank(right.status) || (0, collate_1.stableCompare)(left.id, right.id));
446
447
  }
447
448
  function formatDependencies(rows) {
448
449
  const lines = ["Dependencies"];