cool-workflow 0.1.79 → 0.1.81

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/README.md +51 -3
  4. package/apps/architecture-review/app.json +1 -1
  5. package/apps/architecture-review-fast/app.json +64 -0
  6. package/apps/architecture-review-fast/workflow.js +153 -0
  7. package/apps/end-to-end-golden-path/app.json +1 -1
  8. package/apps/pr-review-fix-ci/app.json +1 -1
  9. package/apps/release-cut/app.json +1 -1
  10. package/apps/research-synthesis/app.json +1 -1
  11. package/dist/agent-config.js +21 -7
  12. package/dist/candidate-scoring.js +42 -22
  13. package/dist/capability-core.js +132 -17
  14. package/dist/capability-registry.js +138 -168
  15. package/dist/cli.js +97 -98
  16. package/dist/collaboration.js +5 -6
  17. package/dist/commit.js +20 -6
  18. package/dist/compare.js +18 -0
  19. package/dist/coordinator/classify.js +45 -0
  20. package/dist/coordinator/paths.js +42 -0
  21. package/dist/coordinator/util.js +129 -0
  22. package/dist/coordinator.js +127 -300
  23. package/dist/dispatch.js +35 -0
  24. package/dist/drive.js +79 -6
  25. package/dist/error-feedback.js +8 -4
  26. package/dist/evidence-reasoning.js +3 -3
  27. package/dist/execution-backend/agent.js +331 -0
  28. package/dist/execution-backend/probes.js +96 -0
  29. package/dist/execution-backend/util.js +47 -0
  30. package/dist/execution-backend.js +73 -421
  31. package/dist/mcp-server.js +79 -183
  32. package/dist/multi-agent/graph.js +84 -0
  33. package/dist/multi-agent/helpers.js +145 -0
  34. package/dist/multi-agent/paths.js +22 -0
  35. package/dist/multi-agent-eval/format.js +194 -0
  36. package/dist/multi-agent-eval/normalize.js +51 -0
  37. package/dist/multi-agent-eval.js +39 -244
  38. package/dist/multi-agent-host.js +0 -19
  39. package/dist/multi-agent.js +125 -314
  40. package/dist/node-snapshot.js +3 -3
  41. package/dist/observability/format.js +61 -0
  42. package/dist/observability/intake.js +98 -0
  43. package/dist/observability.js +14 -160
  44. package/dist/operator-ux/format.js +364 -0
  45. package/dist/operator-ux.js +22 -363
  46. package/dist/orchestrator/lifecycle-operations.js +2 -1
  47. package/dist/orchestrator/report.js +8 -0
  48. package/dist/orchestrator.js +26 -9
  49. package/dist/reclamation.js +26 -21
  50. package/dist/run-export.js +494 -25
  51. package/dist/run-registry/derive.js +172 -0
  52. package/dist/run-registry/format.js +124 -0
  53. package/dist/run-registry/gc.js +251 -0
  54. package/dist/run-registry/policy.js +16 -0
  55. package/dist/run-registry/queue.js +116 -0
  56. package/dist/run-registry.js +89 -597
  57. package/dist/run-state-schema.js +1 -0
  58. package/dist/sandbox-profile.js +43 -2
  59. package/dist/state-explosion/format.js +159 -0
  60. package/dist/state-explosion/helpers.js +82 -0
  61. package/dist/state-explosion.js +165 -304
  62. package/dist/state-node.js +19 -4
  63. package/dist/telemetry-attestation.js +55 -0
  64. package/dist/telemetry-demo.js +15 -3
  65. package/dist/telemetry-ledger.js +60 -15
  66. package/dist/topology.js +25 -8
  67. package/dist/triggers.js +33 -14
  68. package/dist/trust-audit.js +145 -33
  69. package/dist/version.js +1 -1
  70. package/dist/worker-isolation/helpers.js +51 -0
  71. package/dist/worker-isolation/paths.js +46 -0
  72. package/dist/worker-isolation.js +39 -115
  73. package/docs/agent-delegation-drive.7.md +71 -0
  74. package/docs/canonical-workflow-apps.7.md +37 -0
  75. package/docs/cli-mcp-parity.7.md +16 -0
  76. package/docs/contract-migration-tooling.7.md +6 -0
  77. package/docs/control-plane-scheduling.7.md +6 -0
  78. package/docs/dogfood/resume-drive-real-agent-2026-06-14.md +40 -0
  79. package/docs/durable-state-and-locking.7.md +8 -0
  80. package/docs/evidence-adoption-reasoning-chain.7.md +6 -0
  81. package/docs/execution-backends.7.md +6 -0
  82. package/docs/index.md +2 -0
  83. package/docs/launch/demo.tape +28 -0
  84. package/docs/launch/launch-kit.md +96 -17
  85. package/docs/launch/pre-launch-checklist.md +53 -0
  86. package/docs/multi-agent-cli-mcp-surface.7.md +8 -0
  87. package/docs/multi-agent-eval-replay-harness.7.md +6 -0
  88. package/docs/multi-agent-operator-ux.7.md +6 -0
  89. package/docs/multi-agent-trust-policy-audit.7.md +27 -0
  90. package/docs/node-snapshot-diff-replay.7.md +6 -0
  91. package/docs/observability-cost-accounting.7.md +6 -0
  92. package/docs/project-index.md +27 -6
  93. package/docs/real-execution-backends.7.md +6 -0
  94. package/docs/release-and-migration.7.md +8 -0
  95. package/docs/release-tooling.7.md +6 -0
  96. package/docs/routines.md +23 -0
  97. package/docs/run-registry-control-plane.7.md +89 -2
  98. package/docs/run-retention-reclamation.7.md +8 -0
  99. package/docs/source-context-profiles.7.md +119 -0
  100. package/docs/state-explosion-management.7.md +13 -0
  101. package/docs/team-collaboration.7.md +6 -0
  102. package/docs/trust-model.md +267 -0
  103. package/docs/unix-principles.md +49 -1
  104. package/docs/vendor-manifest-loadability.7.md +43 -0
  105. package/docs/web-desktop-workbench.7.md +6 -0
  106. package/manifest/plugin.manifest.json +1 -1
  107. package/manifest/source-context-profiles.json +142 -0
  108. package/package.json +4 -1
  109. package/scripts/agents/builtin-templates.json +7 -0
  110. package/scripts/agents/claude-p-agent.js +129 -43
  111. package/scripts/architecture-review-fast.js +362 -0
  112. package/scripts/bump-version.js +5 -10
  113. package/scripts/canonical-apps-list.js +64 -0
  114. package/scripts/canonical-apps.js +36 -4
  115. package/scripts/coverage-gate.js +211 -0
  116. package/scripts/dogfood-release.js +1 -1
  117. package/scripts/golden-path.js +4 -4
  118. package/scripts/parity-check.js +5 -0
  119. package/scripts/release-check.js +5 -1
  120. package/scripts/source-context.js +291 -0
  121. package/scripts/version-sync-check.js +5 -7
  122. package/skills/ci-triage/SKILL.md +50 -0
  123. package/skills/ci-triage/agents/openai.yaml +4 -0
  124. package/skills/cool-workflow/SKILL.md +4 -1
  125. package/skills/deploy-check/SKILL.md +55 -0
  126. package/skills/deploy-check/agents/openai.yaml +4 -0
  127. package/skills/design-qa/SKILL.md +49 -0
  128. package/skills/design-qa/agents/openai.yaml +4 -0
  129. package/skills/pr-review/SKILL.md +45 -0
  130. package/skills/pr-review/agents/openai.yaml +4 -0
  131. package/dist/capability-dispatcher.js +0 -86
@@ -58,6 +58,7 @@ exports.OPTIONAL_TOP_LEVEL_KEYS = [
58
58
  "audit",
59
59
  "workers",
60
60
  "sandboxProfiles",
61
+ "customSandboxProfiles",
61
62
  "candidates",
62
63
  "candidateSelections",
63
64
  "multiAgent",
@@ -124,7 +124,37 @@ function showBundledSandboxProfile(id, context = defaultSandboxContext()) {
124
124
  return resolveSandboxProfile(profile, context);
125
125
  }
126
126
  function resolveSandboxProfileById(id, context = defaultSandboxContext()) {
127
- return showBundledSandboxProfile(id || exports.DEFAULT_SANDBOX_PROFILE_ID, context);
127
+ const requested = id || exports.DEFAULT_SANDBOX_PROFILE_ID;
128
+ if (isBundledSandboxProfileId(requested))
129
+ return showBundledSandboxProfile(requested, context);
130
+ // A non-bundled id that resolves to a readable profile FILE is a CUSTOM profile:
131
+ // validate and ENFORCE it (the resolved policy snapshots onto the worker scope).
132
+ // This closes the gap where `sandbox validate` accepted a custom profile that
133
+ // dispatch/worker-isolation then refused — validated but never enforceable.
134
+ // A non-bundled, non-file id still fails closed via showBundledSandboxProfile.
135
+ const absolute = node_path_1.default.resolve(requested);
136
+ if (node_fs_1.default.existsSync(absolute) && node_fs_1.default.statSync(absolute).isFile()) {
137
+ const result = validateSandboxProfileFile(requested, context);
138
+ if (!result.valid || !result.profile) {
139
+ throw new SandboxProfileError("sandbox-profile-invalid", `Custom sandbox profile is invalid: ${requested}`, {
140
+ details: { issues: result.issues }
141
+ });
142
+ }
143
+ return result.profile;
144
+ }
145
+ // H7: a custom profile loaded from a FILE at dispatch persists as a DEFINITION
146
+ // in run.customSandboxProfiles (threaded here as context.customProfiles). After a
147
+ // worker scope snapshot is lost, the boundary re-resolves by the profile's
148
+ // LOGICAL id (e.g. "my-custom"), and the dispatch-time file path is gone. Resolve
149
+ // the persisted definition against the CURRENT (worker) context so worker-specific
150
+ // path tokens ($workerDir etc.) bind to THIS worker — re-enforcing the same
151
+ // policy instead of throwing not-found. This runs only after the bundled +
152
+ // file-path branches, so a custom id never shadows a bundled or on-disk profile.
153
+ const customDefinition = context.customProfiles?.[requested];
154
+ if (customDefinition) {
155
+ return resolveSandboxProfile(customDefinition, context);
156
+ }
157
+ return showBundledSandboxProfile(requested, context);
128
158
  }
129
159
  function resolveSandboxProfile(profile, context = defaultSandboxContext()) {
130
160
  const issues = validateSandboxProfileDefinition(profile, context);
@@ -189,6 +219,14 @@ function validateSandboxProfileFile(profileFile, context = defaultSandboxContext
189
219
  issues.push(issue("sandbox-profile-invalid", `Profile file is not valid JSON: ${messageOf(error)}`, absolutePath));
190
220
  return { valid: false, profileFile: absolutePath, issues };
191
221
  }
222
+ // Fail closed if a CUSTOM file reuses a BUNDLED id (H7 hardening): resolution is
223
+ // bundled-first, so a custom "default"/"workspace-write"/... would be silently
224
+ // shadowed by the WIDER bundled policy on a snapshot-loss re-resolve — widening
225
+ // the sandbox with no error. Reserve the bundled names for bundled profiles.
226
+ if (profile && typeof profile.id === "string" && isBundledSandboxProfileId(profile.id)) {
227
+ issues.push(issue("sandbox-profile-invalid", `Custom sandbox profile id "${profile.id}" is reserved (collides with a bundled profile); choose a different id`, absolutePath));
228
+ return { valid: false, profileFile: absolutePath, issues };
229
+ }
192
230
  issues.push(...validateSandboxProfileDefinition(profile, context));
193
231
  if (issues.length)
194
232
  return { valid: false, profileFile: absolutePath, issues };
@@ -280,7 +318,10 @@ function upsertRunSandboxPolicy(run, policy) {
280
318
  function sandboxContextForRun(run) {
281
319
  return {
282
320
  cwd: run.cwd,
283
- runDir: run.paths.runDir
321
+ runDir: run.paths.runDir,
322
+ // H7: thread persisted custom profile DEFINITIONS so a boundary re-resolve by
323
+ // logical id can find + re-resolve a custom profile after snapshot loss.
324
+ customProfiles: run.customSandboxProfiles
284
325
  };
285
326
  }
286
327
  function sandboxContextForValidation(cwd = process.cwd()) {
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatStateExplosionReport = formatStateExplosionReport;
4
+ exports.formatCompactGraph = formatCompactGraph;
5
+ exports.formatBlackboardDigest = formatBlackboardDigest;
6
+ exports.stateExplosionReportLines = stateExplosionReportLines;
7
+ function formatStateExplosionReport(report) {
8
+ const lines = [];
9
+ const size = report.stateSize;
10
+ lines.push(`State Explosion Report: ${report.runId}`);
11
+ lines.push(`Freshness: ${report.freshness.status}${report.freshness.staleScopes.length ? ` (stale: ${report.freshness.staleScopes.join(", ")})` : ""}`);
12
+ lines.push("");
13
+ lines.push("State Size");
14
+ lines.push(` records=${size.total}; graph nodes=${size.graphNodes}; graph edges=${size.graphEdges}; messages=${size.messages}; compaction=${size.compactionRecommended ? "recommended" : "not needed"}`);
15
+ for (const reason of size.reasons)
16
+ lines.push(` - ${reason}`);
17
+ lines.push("");
18
+ lines.push("Compact Graph");
19
+ lines.push(` full=${report.compactGraph.fullNodeCount} nodes/${report.compactGraph.fullEdgeCount} edges -> compact=${report.compactGraph.compactNodeCount} nodes/${report.compactGraph.compactEdgeCount} edges`);
20
+ if (report.compactGraph.collapsedNodeCount > 0) {
21
+ lines.push(` Graph compacted: ${report.compactGraph.collapsedNodeCount} nodes collapsed into ${report.compactGraph.syntheticNodes.length} summary nodes`);
22
+ }
23
+ for (const syn of report.compactGraph.syntheticNodes) {
24
+ lines.push(` [${syn.dominantStatus}] ${syn.id} collapses ${syn.collapsedNodeCount} nodes/${syn.collapsedEdgeCount} edges${syn.blockedReason ? ` blocked=${syn.blockedReason}` : ""}; expand: ${syn.expansionCommand}`);
25
+ }
26
+ lines.push("");
27
+ lines.push("Blackboard Digest");
28
+ lines.push(` topics=${report.blackboardDigest.topicRollups.length}; threads=${report.blackboardDigest.threadSummaries.length}; unresolved=${report.blackboardDigest.unresolvedQuestions.length}; conflicts=${report.blackboardDigest.conflicts.length}; decisions=${report.blackboardDigest.decisions.length}; artifacts=${report.blackboardDigest.artifacts.length}`);
29
+ for (const topic of report.blackboardDigest.topicRollups.slice(0, 20))
30
+ lines.push(` - ${topic.label}; expand: ${topic.expansionCommand}`);
31
+ lines.push("");
32
+ lines.push("Critical Path");
33
+ if (!report.criticalPathGraph.criticalPath.length)
34
+ lines.push(" none");
35
+ for (const id of report.criticalPathGraph.criticalPath.slice(0, 40))
36
+ lines.push(` -> ${id}`);
37
+ lines.push("");
38
+ lines.push("Failures / Blockers");
39
+ if (!report.operatorDigest.failures.length)
40
+ lines.push(" none");
41
+ for (const failure of report.operatorDigest.failures.slice(0, 30))
42
+ lines.push(` [${failure.status}] ${failure.kind} ${failure.id}: ${failure.reason}; next=${failure.nextCommand}`);
43
+ lines.push("");
44
+ lines.push("Evidence Digest");
45
+ lines.push(` adopted=${report.operatorDigest.evidenceDigest.adopted}; missing=${report.operatorDigest.evidenceDigest.missing}; rejected=${report.operatorDigest.evidenceDigest.rejected}`);
46
+ lines.push("");
47
+ lines.push("Trust / Policy Digest");
48
+ lines.push(` events=${report.operatorDigest.trustDigest.events}; policyViolations=${report.operatorDigest.trustDigest.policyViolations}; judgeRationales=${report.operatorDigest.trustDigest.judgeRationales}`);
49
+ for (const violation of report.blackboardDigest.policyViolations.slice(0, 20))
50
+ lines.push(` [policy] ${violation.label}; expand: ${violation.expansionCommand}`);
51
+ lines.push("");
52
+ lines.push("Hidden Source Records");
53
+ if (!report.hiddenSourceRecords.length)
54
+ lines.push(" none (all records shown)");
55
+ for (const hidden of report.hiddenSourceRecords)
56
+ lines.push(` ${hidden.kind}: ${hidden.count} records hidden; expand: ${hidden.expansionCommand}`);
57
+ lines.push("");
58
+ lines.push("Expansion Commands");
59
+ for (const command of report.expansionCommands)
60
+ lines.push(` ${command}`);
61
+ lines.push("");
62
+ lines.push("Next Action");
63
+ lines.push(` ${report.nextAction}`);
64
+ return lines.join("\n");
65
+ }
66
+ function formatCompactGraph(graph) {
67
+ const lines = [];
68
+ lines.push(`Compact Graph (${graph.view}): ${graph.runId}`);
69
+ lines.push(` full=${graph.fullNodeCount} nodes/${graph.fullEdgeCount} edges -> view=${graph.compactNodeCount} nodes/${graph.compactEdgeCount} edges`);
70
+ if (graph.collapsedNodeCount > 0) {
71
+ lines.push(` Graph compacted: ${graph.collapsedNodeCount} nodes collapsed into ${graph.syntheticNodes.length} summary nodes`);
72
+ }
73
+ lines.push("");
74
+ lines.push("Critical Path");
75
+ if (!graph.criticalPath.length)
76
+ lines.push(" none");
77
+ for (const id of graph.criticalPath.slice(0, 40))
78
+ lines.push(` -> ${id}`);
79
+ lines.push("");
80
+ lines.push("Summary Nodes");
81
+ if (!graph.syntheticNodes.length)
82
+ lines.push(" none");
83
+ for (const syn of graph.syntheticNodes) {
84
+ lines.push(` [${syn.dominantStatus}] ${syn.id}: ${syn.collapsedNodeCount} nodes / ${syn.collapsedEdgeCount} edges${syn.blockedReason ? ` blocked=${syn.blockedReason}` : ""}`);
85
+ lines.push(` expand: ${syn.expansionCommand}`);
86
+ }
87
+ lines.push("");
88
+ lines.push("Blockers");
89
+ if (!graph.blockedReasons.length)
90
+ lines.push(" none");
91
+ for (const reason of graph.blockedReasons.slice(0, 20))
92
+ lines.push(` ${reason}`);
93
+ lines.push("");
94
+ lines.push("Nodes");
95
+ for (const node of graph.nodes.slice(0, 80)) {
96
+ lines.push(` [${node.status}] ${node.kind} ${node.id}${node.synthetic ? ` (summary of ${node.synthetic.collapsedNodeCount})` : ""}`);
97
+ }
98
+ if (graph.nodes.length > 80)
99
+ lines.push(` ... ${graph.nodes.length - 80} more`);
100
+ lines.push("");
101
+ lines.push("Next Action");
102
+ lines.push(` ${graph.nextAction}`);
103
+ return lines.join("\n");
104
+ }
105
+ function formatBlackboardDigest(record) {
106
+ const lines = [];
107
+ lines.push(`Blackboard Digest: ${record.runId}${record.blackboardId ? ` (${record.blackboardId})` : ""}`);
108
+ lines.push(` freshness=${record.status}; included=${record.includedCount}; omitted=${record.omittedCount}`);
109
+ const section = (title, entries) => {
110
+ lines.push("");
111
+ lines.push(title);
112
+ if (!entries.length) {
113
+ lines.push(" none");
114
+ return;
115
+ }
116
+ for (const entry of entries.slice(0, 25))
117
+ lines.push(` [${entry.status}] ${entry.label}; expand: ${entry.expansionCommand}`);
118
+ if (entries.length > 25)
119
+ lines.push(` ... ${entries.length - 25} more`);
120
+ };
121
+ section("Topic Rollups", record.topicRollups);
122
+ section("Thread Summaries", record.threadSummaries);
123
+ section("Unresolved Questions", record.unresolvedQuestions);
124
+ section("Conflicts", record.conflicts);
125
+ section("Decisions", record.decisions);
126
+ section("Artifacts", record.artifacts);
127
+ section("Adopted Evidence", record.adoptedEvidence);
128
+ section("Missing Evidence", record.missingEvidence);
129
+ section("Policy Violations", record.policyViolations);
130
+ section("Judge Rationale", record.judgeRationale);
131
+ section("Recent Changes", record.recentChanges);
132
+ section("High-Signal Records", record.highSignal);
133
+ lines.push("");
134
+ lines.push("Next Action");
135
+ lines.push(` ${record.nextAction}`);
136
+ return lines.join("\n");
137
+ }
138
+ function stateExplosionReportLines(report) {
139
+ // Markdown lines for inclusion in the run report.md State Size section.
140
+ const size = report.stateSize;
141
+ const lines = [
142
+ `- Records: ${size.total}; graph nodes: ${size.graphNodes}; graph edges: ${size.graphEdges}; messages: ${size.messages}`,
143
+ `- Compaction: ${size.compactionRecommended ? "recommended" : "not needed"}`,
144
+ `- Summary freshness: ${report.freshness.status}`
145
+ ];
146
+ for (const reason of size.reasons)
147
+ lines.push(` - ${reason}`);
148
+ if (report.compactGraph.collapsedNodeCount > 0) {
149
+ lines.push(`- Graph compacted: ${report.compactGraph.collapsedNodeCount} nodes collapsed into ${report.compactGraph.syntheticNodes.length} summary nodes`);
150
+ lines.push(` - Use: \`node scripts/cw.js multi-agent graph ${report.runId} --view full --json\``);
151
+ }
152
+ if (report.hiddenSourceRecords.length) {
153
+ for (const hidden of report.hiddenSourceRecords) {
154
+ lines.push(`- Hidden ${hidden.kind}: ${hidden.count} records; expand: \`${hidden.expansionCommand}\``);
155
+ }
156
+ }
157
+ lines.push(`- Next: \`${report.nextAction}\``);
158
+ return lines;
159
+ }
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.isProtectedStatus = isProtectedStatus;
7
+ exports.dominantStatus = dominantStatus;
8
+ exports.parentMap = parentMap;
9
+ exports.fingerprintRecords = fingerprintRecords;
10
+ exports.fingerprintStrings = fingerprintStrings;
11
+ exports.stableLine = stableLine;
12
+ exports.sortKeys = sortKeys;
13
+ exports.stripRunId = stripRunId;
14
+ exports.unique = unique;
15
+ exports.byId = byId;
16
+ exports.truncate = truncate;
17
+ exports.slug = slug;
18
+ // Pure, stateless helpers for the state-explosion derived-index layer —
19
+ // status priority, fingerprinting, deterministic key-sorting, id/string
20
+ // utilities. Carved out of state-explosion.ts (FreeBSD-audit god-module carve)
21
+ // so the report/graph/digest builders no longer bundle the primitive helper
22
+ // layer. Nothing here touches run state beyond its arguments; every function is
23
+ // pure (`fingerprintStrings` is re-exported from state-explosion.ts to keep the
24
+ // public surface byte-identical for importers).
25
+ const node_crypto_1 = __importDefault(require("node:crypto"));
26
+ function isProtectedStatus(status) {
27
+ return ["failed", "blocked", "rejected", "conflicting"].includes(status);
28
+ }
29
+ function dominantStatus(statuses) {
30
+ for (const priority of ["failed", "blocked", "rejected", "conflicting", "running", "pending"]) {
31
+ if (statuses.includes(priority))
32
+ return priority;
33
+ }
34
+ return statuses[0] || "completed";
35
+ }
36
+ function parentMap(edges) {
37
+ const parents = new Map();
38
+ for (const edge of edges) {
39
+ if (!parents.has(edge.to))
40
+ parents.set(edge.to, edge.from);
41
+ }
42
+ return parents;
43
+ }
44
+ function fingerprintRecords(records) {
45
+ return fingerprintStrings(records.map((r) => `${r.id}:${r.status || ""}`).sort());
46
+ }
47
+ function fingerprintStrings(values) {
48
+ const hash = node_crypto_1.default.createHash("sha256");
49
+ hash.update(JSON.stringify([...values].sort()));
50
+ return `sha256:${hash.digest("hex").slice(0, 32)}`;
51
+ }
52
+ function stableLine(value) {
53
+ return JSON.stringify(sortKeys(value));
54
+ }
55
+ function sortKeys(value) {
56
+ if (Array.isArray(value))
57
+ return value.map(sortKeys);
58
+ if (value && typeof value === "object") {
59
+ const record = value;
60
+ const result = {};
61
+ for (const key of Object.keys(record).sort())
62
+ result[key] = sortKeys(record[key]);
63
+ return result;
64
+ }
65
+ return value;
66
+ }
67
+ function stripRunId(run, id) {
68
+ return id.startsWith(`${run.id}:`) ? id.slice(run.id.length + 1) : id;
69
+ }
70
+ function unique(values) {
71
+ return Array.from(new Set(values.filter(Boolean))).sort();
72
+ }
73
+ function byId(a, b) {
74
+ return a.id.localeCompare(b.id);
75
+ }
76
+ function truncate(value) {
77
+ const single = value.replace(/\s+/g, " ").trim();
78
+ return single.length > 80 ? `${single.slice(0, 77)}...` : single;
79
+ }
80
+ function slug(value) {
81
+ return value.replace(/[^a-zA-Z0-9._:-]/g, "-");
82
+ }