cool-workflow 0.2.5 → 0.2.6

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 (80) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/.gemini-plugin/plugin.json +1 -1
  4. package/.opencode-plugin/plugin.json +1 -1
  5. package/README.md +5 -3
  6. package/apps/architecture-review/app.json +1 -1
  7. package/apps/architecture-review-fast/app.json +1 -1
  8. package/apps/end-to-end-golden-path/app.json +1 -1
  9. package/apps/pr-review-fix-ci/app.json +1 -1
  10. package/apps/release-cut/app.json +1 -1
  11. package/apps/research-synthesis/app.json +1 -1
  12. package/dist/core/state/run-paths.js +3 -30
  13. package/dist/core/version.js +1 -1
  14. package/dist/mcp/server.js +95 -27
  15. package/dist/mcp/tool-process.js +181 -0
  16. package/dist/mcp-server.js +8 -1
  17. package/dist/shell/agent-config.js +11 -2
  18. package/dist/shell/audit-cli.js +1 -1
  19. package/dist/shell/drive.js +99 -89
  20. package/dist/shell/execution-backend/agent.js +18 -15
  21. package/dist/shell/execution-backend/local.js +1 -0
  22. package/dist/shell/fs-atomic.js +3 -0
  23. package/dist/shell/metrics-cli.js +1 -1
  24. package/dist/shell/multi-agent-cli.js +4 -1
  25. package/dist/shell/multi-agent-operator-ux.js +1 -1
  26. package/dist/shell/observability.js +7 -4
  27. package/dist/shell/operator-ux.js +1 -1
  28. package/dist/shell/perf-trace.js +136 -0
  29. package/dist/shell/pipeline.js +1 -1
  30. package/dist/shell/report-view-cli.js +2 -0
  31. package/dist/shell/run-export-cli.js +5 -2
  32. package/dist/shell/run-export.js +241 -27
  33. package/dist/shell/run-registry-io.js +8 -0
  34. package/dist/shell/run-store.js +27 -3
  35. package/dist/shell/state-explosion-cli.js +4 -1
  36. package/dist/shell/telemetry-demo.js +1 -1
  37. package/dist/shell/trust-audit.js +91 -43
  38. package/dist/shell/workbench.js +4 -1
  39. package/dist/wiring/capability-table/parity.js +10 -5
  40. package/docs/agent-delegation-drive.7.md +2 -0
  41. package/docs/cli-mcp-parity.7.md +21 -5
  42. package/docs/contract-migration-tooling.7.md +2 -0
  43. package/docs/control-plane-scheduling.7.md +2 -0
  44. package/docs/durable-state-and-locking.7.md +2 -0
  45. package/docs/evidence-adoption-reasoning-chain.7.md +2 -0
  46. package/docs/execution-backends.7.md +2 -0
  47. package/docs/mcp-app-surface.7.md +21 -0
  48. package/docs/multi-agent-cli-mcp-surface.7.md +2 -0
  49. package/docs/multi-agent-eval-replay-harness.7.md +2 -0
  50. package/docs/multi-agent-operator-ux.7.md +2 -0
  51. package/docs/node-snapshot-diff-replay.7.md +2 -0
  52. package/docs/observability-cost-accounting.7.md +2 -0
  53. package/docs/project-index.md +18 -4
  54. package/docs/real-execution-backends.7.md +2 -0
  55. package/docs/release-and-migration.7.md +2 -0
  56. package/docs/release-tooling.7.md +10 -0
  57. package/docs/run-registry-control-plane.7.md +35 -9
  58. package/docs/run-retention-reclamation.7.md +2 -0
  59. package/docs/state-explosion-management.7.md +2 -0
  60. package/docs/team-collaboration.7.md +2 -0
  61. package/docs/trust-audit-anchor.7.md +2 -0
  62. package/docs/web-desktop-workbench.7.md +48 -0
  63. package/manifest/plugin.manifest.json +1 -1
  64. package/package.json +3 -2
  65. package/scripts/canonical-apps.js +4 -4
  66. package/scripts/dogfood-release.js +1 -1
  67. package/scripts/gen-manifests.js +32 -61
  68. package/scripts/golden-path.js +4 -4
  69. package/scripts/parity-check.js +42 -23
  70. package/scripts/purity-baseline.json +0 -3
  71. package/scripts/release-flow.js +43 -13
  72. package/scripts/release-gate.js +30 -6
  73. package/scripts/schema-version-inventory.json +31 -0
  74. package/scripts/validate-run-state-schema.js +95 -4
  75. package/scripts/verify-release-verdict.js +139 -0
  76. package/ui/workbench/app.css +15 -7
  77. package/ui/workbench/app.js +118 -16
  78. package/ui/workbench/index.html +2 -0
  79. package/ui/workbench/inspection.js +51 -0
  80. package/ui/workbench/navigation.js +44 -0
@@ -110,82 +110,47 @@ function _resolveTemplate(template, src, pluginRootVar) {
110
110
 
111
111
  function build(src) {
112
112
  const { targets, vendors } = src;
113
- // Backward compat: if no `vendors` key, build from legacy targets
114
- if (!vendors || typeof vendors !== "object" || Object.keys(vendors).length === 0) {
115
- return buildLegacy(src);
116
- }
113
+ validateVendorRegistry(vendors);
117
114
  const outputs = [];
118
115
  for (const [vendorId, vendorDef] of Object.entries(vendors)) {
119
116
  const targetConfig = targets && targets[vendorId] ? targets[vendorId] : {};
120
117
  const pluginRootVar = targetConfig.pluginRootVar || "./";
121
- const vendorOutputs = vendorDef.outputs || [];
118
+ const vendorOutputs = vendorDef.outputs;
122
119
  for (const output of vendorOutputs) {
123
120
  const resolved = _resolveTemplate(output, src, pluginRootVar);
121
+ if (!isPlainObject(resolved) || typeof resolved.path !== "string" || !resolved.path.trim()) {
122
+ throw new Error(`vendor ${vendorId} has an output without a path`);
123
+ }
124
+ if (!isPlainObject(resolved.json)) {
125
+ throw new Error(`vendor ${vendorId} output ${resolved.path} has no JSON object`);
126
+ }
124
127
  outputs.push({
125
- path: typeof resolved.path === "string" ? resolved.path : `vendor-${vendorId}-${outputs.length}`,
126
- json: resolved.json || {}
128
+ path: resolved.path,
129
+ json: resolved.json
127
130
  });
128
131
  }
129
132
  }
130
133
  return outputs;
131
134
  }
132
135
 
133
- /** Legacy path: build from hardcoded shapes (backward compat if no `vendors` key). */
134
- function buildLegacy(src) {
135
- const { identity, descriptions, interface: ui, layout, mcp, targets } = src;
136
- const outputs = [];
137
-
138
- if (targets && targets.claude) {
139
- outputs.push({
140
- path: targets.claude.marketplace,
141
- json: { name: identity.name, owner: identity.author, metadata: { description: descriptions.standard, version: identity.version }, plugins: [{ name: identity.name, source: layout.pluginPathFromRepoRoot, description: descriptions.standard, category: ui.category.toLowerCase() }] }
142
- });
143
- outputs.push({
144
- path: targets.claude.plugin,
145
- json: { name: identity.name, description: descriptions.standard, version: identity.version, author: identity.author, homepage: identity.homepage, license: identity.license, keywords: identity.keywords }
146
- });
147
- outputs.push({
148
- path: targets.claude.mcp,
149
- json: legacyMcpConfig(mcp, layout, targets.claude.pluginRootVar)
150
- });
151
- }
152
- if (targets && targets.codex) {
153
- outputs.push({
154
- path: targets.codex.marketplace,
155
- json: { name: identity.name, interface: { displayName: ui.displayName }, plugins: [{ name: identity.name, source: { source: "local", path: layout.pluginPathFromRepoRoot }, policy: { installation: "AVAILABLE", authentication: "ON_INSTALL" }, category: ui.category }] }
156
- });
157
- outputs.push({
158
- path: targets.codex.plugin,
159
- json: { name: identity.name, version: identity.version, description: descriptions.standard, author: identity.author, keywords: identity.keywords, skills: layout.skillsDir, mcpServers: "./.codex-plugin/mcp.json", interface: { displayName: ui.displayName, shortDescription: descriptions.short, longDescription: descriptions.long, developerName: identity.author.name, category: ui.category, capabilities: ui.capabilities, brandColor: ui.brandColor, defaultPrompt: ui.defaultPrompt } }
160
- });
161
- outputs.push({
162
- path: targets.codex.mcp,
163
- json: legacyMcpConfig(mcp, layout, targets.codex.pluginRootVar)
164
- });
165
- }
166
- if (targets && targets.agents) {
167
- outputs.push({
168
- path: targets.agents.plugin,
169
- json: { name: identity.name, description: descriptions.standard, version: identity.version, author: identity.author, homepage: identity.homepage, license: identity.license, keywords: identity.keywords, skills: layout.skillsDir, mcpServers: "./.codex-plugin/mcp.json", interface: { displayName: ui.displayName, shortDescription: descriptions.short, longDescription: descriptions.long, developerName: identity.author.name, category: ui.category, capabilities: ui.capabilities, brandColor: ui.brandColor, defaultPrompt: ui.defaultPrompt } }
170
- });
171
- outputs.push({
172
- path: targets.agents.mcp,
173
- json: legacyMcpConfig(mcp, layout, targets.agents.pluginRootVar)
174
- });
175
- }
176
-
177
- return outputs;
136
+ function isPlainObject(value) {
137
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
178
138
  }
179
139
 
180
- function legacyMcpConfig(mcp, layout, pluginRootVar) {
181
- return {
182
- mcpServers: {
183
- [mcp.serverName]: {
184
- command: mcp.command,
185
- args: [`${pluginRootVar}${layout.mcpServerScript}`]
186
- }
140
+ /** The vendor table is required. A broken source must stop before the write
141
+ * loop; an old hard-coded shape is not a safe second source of truth. */
142
+ function validateVendorRegistry(vendors) {
143
+ if (!isPlainObject(vendors) || Object.keys(vendors).length === 0) {
144
+ throw new Error("manifest source needs a non-empty vendors object");
145
+ }
146
+ for (const [vendorId, vendorDef] of Object.entries(vendors)) {
147
+ if (!isPlainObject(vendorDef) || !Array.isArray(vendorDef.outputs) || vendorDef.outputs.length === 0) {
148
+ throw new Error(`vendor ${vendorId} needs a non-empty outputs array`);
149
+ }
150
+ for (const output of vendorDef.outputs) {
151
+ if (!isPlainObject(output)) throw new Error(`vendor ${vendorId} has an invalid output`);
187
152
  }
188
- };
153
+ }
189
154
  }
190
155
 
191
156
  function serialize(json) {
@@ -229,4 +194,10 @@ function main() {
229
194
  }
230
195
  }
231
196
 
232
- main();
197
+ try {
198
+ main();
199
+ } catch (error) {
200
+ const message = error instanceof Error ? error.message : String(error);
201
+ process.stderr.write(`gen-manifests: ${message}\n`);
202
+ process.exitCode = 1;
203
+ }
@@ -33,7 +33,7 @@ function main() {
33
33
  const appValidation = runJson(["app", "validate", "end-to-end-golden-path"], pluginRoot);
34
34
  assert.equal(appValidation.valid, true);
35
35
  assert.equal(appValidation.summary.id, "end-to-end-golden-path");
36
- assert.equal(appValidation.summary.version, "0.2.5");
36
+ assert.equal(appValidation.summary.version, "0.2.6");
37
37
 
38
38
  const plan = runJson(
39
39
  [
@@ -42,7 +42,7 @@ function main() {
42
42
  "--repo",
43
43
  tmp,
44
44
  "--question",
45
- "Prove the deterministic v0.2.5 end-to-end golden path."
45
+ "Prove the deterministic v0.2.6 end-to-end golden path."
46
46
  ],
47
47
  pluginRoot
48
48
  );
@@ -52,7 +52,7 @@ function main() {
52
52
 
53
53
  let state = readJson(plan.statePath);
54
54
  assert.equal(state.workflow.app.id, "end-to-end-golden-path");
55
- assert.equal(state.workflow.app.version, "0.2.5");
55
+ assert.equal(state.workflow.app.version, "0.2.6");
56
56
  assert.equal(state.loopStage, "interpret");
57
57
 
58
58
  const dispatch = runJson(["dispatch", plan.runId, "--limit", "1", "--sandbox", "readonly"], tmp);
@@ -195,7 +195,7 @@ function main() {
195
195
  assert.equal(reportPath, plan.reportPath);
196
196
  assert.ok(fs.existsSync(reportPath));
197
197
  const report = fs.readFileSync(reportPath, "utf8");
198
- assert.match(report, /Workflow App: end-to-end-golden-path@0\.2\.5/);
198
+ assert.match(report, /Workflow App: end-to-end-golden-path@0\.2\.6/);
199
199
  assert.match(report, /## Candidates/);
200
200
  assert.match(report, /## Trust Audit/);
201
201
  assert.match(report, /## Acceptance Rationale/);
@@ -8,9 +8,9 @@
8
8
  //
9
9
  // FAIL CLOSED ON DRIFT (BSD discipline, same shape as gen-manifests --check):
10
10
  // 1. STATIC parity — the declared capability registry must exactly match the
11
- // live MCP tool list (tools/list) and the CLI dispatch tokens parsed from
12
- // the built CLI dispatch surface. A tool or command on one surface but not
13
- // the other, or not declared in src/capability-registry.ts, is release-blocking.
11
+ // live MCP tool list (tools/list), every declared CLI path must resolve
12
+ // through the live dispatcher, and real `cw help` must match the declared
13
+ // help surface. Any gap is release-blocking.
14
14
  // 2. PAYLOAD parity — for every capability declared `payloadIdentical`, the
15
15
  // `cw <cmd> --json` payload must equal the `cw_<tool>` MCP result on a real
16
16
  // bootstrap run (whitespace + generation-moment ISO timestamps aside).
@@ -38,7 +38,6 @@ const node = process.execPath;
38
38
  const cli = path.join(pluginRoot, "dist", "cli.js");
39
39
  const mcpServer = path.join(pluginRoot, "dist", "mcp-server.js");
40
40
  const registry = require(path.join(pluginRoot, "dist", "core", "capability-table.js"));
41
- const { formatHelp } = require(path.join(pluginRoot, "dist", "core", "format", "help.js"));
42
41
  const { loadRunFromCwd, saveCheckpoint } = require(path.join(pluginRoot, "dist", "shell", "run-store.js"));
43
42
  const { appendRunNode, createStateNode } = require(path.join(pluginRoot, "dist", "core", "state", "state-node.js"));
44
43
 
@@ -71,15 +70,6 @@ function liveMcpTools() {
71
70
  return JSON.parse(line).result.tools.map((tool) => tool.name);
72
71
  }
73
72
 
74
- function cliDispatchTokens() {
75
- // v2 dispatch is table-driven (findCapabilityByCliPath over REGISTRY), not a
76
- // switch, so the token set is the union of every CLI capability's accepted
77
- // first tokens (cli.caseTokens where an alias set is declared, else cli.path).
78
- // The reachability check below then proves each declared path resolves through
79
- // the live dispatcher — replacing the old `case "x":` dist source-grep.
80
- return [...new Set(registry.cliCapabilities().flatMap((cap) => cap.cli.caseTokens ?? cap.cli.path))];
81
- }
82
-
83
73
  // Old-build top-level verbs kept in the frozen help "More commands" index line
84
74
  // but folded in v2 into subcommands / the shell layer: init->app.init,
85
75
  // search->run.search (declaredCliHelpTokens collapses both to app/run), and
@@ -87,8 +77,8 @@ function cliDispatchTokens() {
87
77
  // discovery text, not dispatch-parity tokens — exclude them.
88
78
  const HELP_INDEX_ONLY_TOKENS = new Set(["init", "search", "update"]);
89
79
 
90
- function cliHelpTokens() {
91
- const lines = formatHelp().split(/\r?\n/);
80
+ function cliHelpTokens(helpText) {
81
+ const lines = helpText.split(/\r?\n/);
92
82
  const tokens = new Set();
93
83
  for (const line of lines) {
94
84
  // Command lines start with exactly 2 spaces; examples are 4-space.
@@ -104,6 +94,29 @@ function cliHelpTokens() {
104
94
  return [...tokens].sort();
105
95
  }
106
96
 
97
+ function liveCliHelpTokens() {
98
+ const helpText = execFileSync(node, [cli, "help", "--no-color"], {
99
+ cwd: pluginRoot,
100
+ encoding: "utf8",
101
+ env: { ...process.env, NO_COLOR: "1" }
102
+ });
103
+ return cliHelpTokens(helpText);
104
+ }
105
+
106
+ /** The CLI dispatcher calls this same lookup. Keep the probe outside the
107
+ * registry report so the live gate does not compare a token list made from the
108
+ * registry with another list made from the same registry. */
109
+ function cliReachabilityIssues(table) {
110
+ const issues = [];
111
+ for (const cap of table.cliCapabilities()) {
112
+ const resolved = table.findCapabilityByCliPath(cap.cli.path);
113
+ if (!resolved || !resolved.cli || typeof resolved.cli.handler !== "function") {
114
+ issues.push(`${cap.capability}: CLI path ${cap.cli.path.join(" ")} does not resolve through the dispatcher`);
115
+ }
116
+ }
117
+ return issues;
118
+ }
119
+
107
120
  // ---- 2. payload identity ---------------------------------------------------
108
121
  // Generation-moment ISO timestamps are presentation metadata, not capability
109
122
  // data: the same field carries the wall-clock instant of the render. Neutralize
@@ -907,9 +920,11 @@ async function payloadParity() {
907
920
  async function main() {
908
921
  const check = process.argv.includes("--check");
909
922
  const tools = liveMcpTools();
910
- const tokens = cliDispatchTokens();
911
- const helpTokens = cliHelpTokens();
912
- const report = registry.buildParityReport({ mcpTools: tools, cliTokens: tokens, helpTokens });
923
+ const helpTokens = liveCliHelpTokens();
924
+ const report = registry.buildParityReport({ mcpTools: tools, helpTokens });
925
+ const reachabilityIssues = cliReachabilityIssues(registry);
926
+ report.registryLint.push(...reachabilityIssues);
927
+ report.ok = report.ok && reachabilityIssues.length === 0;
913
928
  const payload = await payloadParity();
914
929
 
915
930
  const ok = report.ok && payload.mismatches.length === 0;
@@ -940,13 +955,17 @@ async function main() {
940
955
  if (report.payloadProbeInvalidClassifications.length) lines.push(` - payload probe classifications for invalid capabilities: ${report.payloadProbeInvalidClassifications.join(", ")}`);
941
956
  if (report.registryLint.length) lines.push(` - registry lint: ${report.registryLint.join("; ")}`);
942
957
  if (payload.mismatches.length) lines.push(` - cw --json != cw_<tool> payload for: ${payload.mismatches.join(", ")}`);
943
- lines.push("Reconcile src/capability-registry.ts, cli.ts, and mcp-server.ts so both surfaces render one data source.\n");
958
+ lines.push("Reconcile src/wiring/capability-table, src/cli, and src/mcp so both surfaces render one data source.\n");
944
959
  process.stderr.write(lines.join("\n"));
945
960
  process.exitCode = 1;
946
961
  }
947
962
  }
948
963
 
949
- main().catch((error) => {
950
- process.stderr.write(`parity-check: ${error instanceof Error ? error.message : String(error)}\n`);
951
- process.exitCode = 1;
952
- });
964
+ module.exports = { cliHelpTokens, cliReachabilityIssues };
965
+
966
+ if (require.main === module) {
967
+ main().catch((error) => {
968
+ process.stderr.write(`parity-check: ${error instanceof Error ? error.message : String(error)}\n`);
969
+ process.exitCode = 1;
970
+ });
971
+ }
@@ -1,8 +1,5 @@
1
1
  {
2
2
  "builtinViolations": {
3
- "core/state/run-paths.ts": [
4
- "node:fs"
5
- ],
6
3
  "core/trust/telemetry-attestation.ts": [
7
4
  "node:fs"
8
5
  ]
@@ -229,12 +229,12 @@ function buildReviewerInput(resultPath) {
229
229
  " <one concrete sentence: the user-visible capability this ships>",
230
230
  "or:",
231
231
  " REJECTED",
232
- " <numbered gate failures with file:line references>",
232
+ " kind: semantic-review",
233
+ " 1. <a semantic code finding with repo-relative file:line evidence>",
233
234
  "",
234
- "If your host wrapper asks for Markdown, JSON, or a fenced result block, still put",
235
- "the exact APPROVED/REJECTED verdict line above as a standalone line outside any",
236
- "prose or code fence. The release tool extracts only strict standalone verdict",
237
- "lines; prose such as 'Verdict: APPROVED' is not trusted.",
235
+ "The control plane already ran the deterministic gate. Do not run it again",
236
+ "or reject with an unsupported claim that it failed. A rejection without the",
237
+ "semantic-review form above is invalid reviewer output and remains fail closed.",
238
238
  ""
239
239
  ].join("\n");
240
240
  }
@@ -271,7 +271,7 @@ function extractVerdictFromStdout(stdout, resultPath) {
271
271
  const rejectedLine = lines.find((line) => isRejectedLine(line));
272
272
  if (rejectedLine) {
273
273
  process.stderr.write(`reviewer REJECTED via stdout — full output:\n${stdout.trim()}\n`);
274
- return rejectedLine.trim();
274
+ return lines.slice(lines.indexOf(rejectedLine)).join("\n").trim();
275
275
  }
276
276
  const approvedLine = lines.find((line) => /^APPROVED\s+\S+/.test(line.trim()));
277
277
  if (approvedLine) {
@@ -432,14 +432,19 @@ function verifyVerdict(resultPath) {
432
432
  const text = fs.readFileSync(resultPath, "utf8");
433
433
  const lines = text.split(/\r?\n/);
434
434
  const firstLine = lines[0] || "";
435
+ if (isRejectedLine(firstLine)) {
436
+ const rejection = validateSemanticRejection(lines);
437
+ if (!rejection.ok) {
438
+ die(`invalid reviewer output — ${rejection.reason}. Release remains blocked; this is not a verified code rejection.`, text.trim());
439
+ }
440
+ die("reviewer rejected the release with semantic evidence.", text.trim());
441
+ }
435
442
  if (firstLine !== `APPROVED ${HEAD}`) {
436
- if (!isRejectedLine(firstLine)) {
437
- const normalized = extractVerdictFromStdout(text, resultPath);
438
- if (normalized && normalized.split(/\r?\n/)[0] === `APPROVED ${HEAD}`) {
439
- fs.writeFileSync(resultPath, `${normalized}\n`);
440
- const normalizedLines = normalized.split(/\r?\n/);
441
- return (normalizedLines[1] || "").trim();
442
- }
443
+ const normalized = extractVerdictFromStdout(text, resultPath);
444
+ if (normalized && normalized.split(/\r?\n/)[0] === `APPROVED ${HEAD}`) {
445
+ fs.writeFileSync(resultPath, `${normalized}\n`);
446
+ const normalizedLines = normalized.split(/\r?\n/);
447
+ return (normalizedLines[1] || "").trim();
443
448
  }
444
449
  die(`verdict first line must be exactly "APPROVED ${HEAD}" — release blocked.`, text.trim());
445
450
  }
@@ -447,6 +452,31 @@ function verifyVerdict(resultPath) {
447
452
  return cap;
448
453
  }
449
454
 
455
+ function validateSemanticRejection(lines) {
456
+ if (lines[0] !== "REJECTED") {
457
+ return { ok: false, reason: 'a rejection must start with exact "REJECTED"' };
458
+ }
459
+ if (lines[1] !== "kind: semantic-review") {
460
+ return { ok: false, reason: 'a rejection must declare exact "kind: semantic-review"' };
461
+ }
462
+ const findings = lines.slice(2).filter((line) => /^\d+\.\s+/.test(line));
463
+ if (!findings.length) return { ok: false, reason: "a semantic rejection needs a numbered finding" };
464
+ for (const finding of findings) {
465
+ const match = finding.match(/^\d+\.\s+.*?\b([A-Za-z0-9][A-Za-z0-9_./-]*):(\d+)\b/);
466
+ if (!match) return { ok: false, reason: "each semantic finding needs a repo-relative file:line locator" };
467
+ const rel = match[1];
468
+ const line = Number(match[2]);
469
+ if (rel.startsWith("/") || rel.split("/").includes("..") || !Number.isSafeInteger(line) || line < 1) {
470
+ return { ok: false, reason: "a semantic finding has an unsafe file:line locator" };
471
+ }
472
+ const target = path.join(repoRoot, rel);
473
+ if (!fs.existsSync(target) || !fs.statSync(target).isFile()) {
474
+ return { ok: false, reason: `semantic evidence path does not exist: ${rel}` };
475
+ }
476
+ }
477
+ return { ok: true };
478
+ }
479
+
450
480
  // ---- verdict signing (optional, opt-in — see the header comment) -----------
451
481
  function resolveVerdictPrivateKey(value) {
452
482
  const trimmed = String(value).trim();
@@ -67,23 +67,47 @@ if (!PREV_TAG) {
67
67
  const MARKER_DIR = path.join(REPO_ROOT, ".cw-release");
68
68
  fs.mkdirSync(MARKER_DIR, { recursive: true });
69
69
 
70
+ const MAX_CHILD_DIAGNOSTIC_BYTES = 16 * 1024;
71
+
72
+ function boundedDiagnostic(text) {
73
+ const value = String(text || "").trim();
74
+ if (value.length <= MAX_CHILD_DIAGNOSTIC_BYTES) return value;
75
+ return `…${value.slice(-MAX_CHILD_DIAGNOSTIC_BYTES)}`;
76
+ }
77
+
78
+ function reportNpmFailure(label, result) {
79
+ // Failure facts are local diagnostics, never stdout data and never a saved
80
+ // release record. Keep the tail so a large smoke run cannot flood a terminal.
81
+ const out = boundedDiagnostic(result.stdout);
82
+ const err = boundedDiagnostic(result.stderr);
83
+ process.stderr.write(`release-gate: ${label} failed (exit ${result.status ?? "unknown"})\n`);
84
+ if (err) process.stderr.write(`[stderr]\n${err}\n`);
85
+ if (out) process.stderr.write(`[stdout]\n${out}\n`);
86
+ }
87
+
70
88
  function runNpm(args, extraEnv) {
71
- // stdio ignored on purpose (the old script sent both streams to /dev/null):
72
- // the gate's verdict is the exit code; a red step is re-run by hand for detail.
73
89
  const r = spawnSync("npm", args, {
74
90
  cwd: REPO_ROOT,
75
- stdio: "ignore",
91
+ encoding: "utf8",
76
92
  env: extraEnv ? { ...process.env, ...extraEnv } : process.env,
77
93
  });
78
- return !r.error && r.status === 0;
94
+ return r;
79
95
  }
80
96
 
81
97
  // --- 1. Build & tests (run, don't trust pasted output) -----------------
82
98
  say("[1/6] build");
83
- if (!runNpm(["run", "--prefix", "plugins/cool-workflow", "build"])) fail("build failed");
99
+ const build = runNpm(["run", "--prefix", "plugins/cool-workflow", "build"]);
100
+ if (build.error || build.status !== 0) {
101
+ fail("build failed");
102
+ reportNpmFailure("build", build);
103
+ }
84
104
 
85
105
  say("[2/6] tests");
86
- if (!runNpm(["run", "test:gate", "--prefix", "plugins/cool-workflow"], { CW_TEST_CONCURRENCY: "1" })) fail("tests failed");
106
+ const tests = runNpm(["run", "test:gate", "--prefix", "plugins/cool-workflow"], { CW_TEST_CONCURRENCY: "1" });
107
+ if (tests.error || tests.status !== 0) {
108
+ fail("tests failed");
109
+ reportNpmFailure("tests", tests);
110
+ }
87
111
 
88
112
  if (PREV_TAG) {
89
113
  const RANGE = `${PREV_TAG}..HEAD`;
@@ -0,0 +1,31 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "domains": [
4
+ { "domain": "agent-config", "constant": "AGENT_CONFIG_SCHEMA_VERSION", "source": "shell/agent-config.ts" },
5
+ { "domain": "blackboard", "constant": "BLACKBOARD_SCHEMA_VERSION", "source": "core/multi-agent/coordinator.ts" },
6
+ { "domain": "candidate", "constant": "CANDIDATE_SCHEMA_VERSION", "source": "core/multi-agent/candidate-scoring.ts" },
7
+ { "domain": "collaboration", "constant": "COLLABORATION_SCHEMA_VERSION", "source": "core/multi-agent/collaboration.ts" },
8
+ { "domain": "contract-migration", "constant": "CONTRACT_MIGRATION_SCHEMA_VERSION", "source": "core/state/contract-migration.ts" },
9
+ { "domain": "current-run-state", "constant": "CURRENT_RUN_STATE_SCHEMA_VERSION", "source": "core/version.ts" },
10
+ { "domain": "drive", "constant": "DRIVE_SCHEMA_VERSION", "source": "core/pipeline/drive-decide.ts" },
11
+ { "domain": "error-feedback", "constant": "ERROR_FEEDBACK_SCHEMA_VERSION", "source": "core/pipeline/error-feedback.ts" },
12
+ { "domain": "evidence-reasoning", "constant": "EVIDENCE_REASONING_SCHEMA_VERSION", "source": "shell/evidence-reasoning.ts" },
13
+ { "domain": "execution-backend", "constant": "EXECUTION_BACKEND_SCHEMA_VERSION", "source": "shell/execution-backend/registry.ts" },
14
+ { "domain": "legacy-run-state", "constant": "LEGACY_RUN_STATE_SCHEMA_VERSION", "source": "core/version.ts" },
15
+ { "domain": "metrics", "constant": "METRICS_SCHEMA_VERSION", "source": "shell/observability.ts" },
16
+ { "domain": "minimum-run-state", "constant": "MIN_SUPPORTED_RUN_STATE_SCHEMA_VERSION", "source": "core/version.ts" },
17
+ { "domain": "multi-agent", "constant": "MULTI_AGENT_SCHEMA_VERSION", "source": "core/multi-agent/runtime.ts" },
18
+ { "domain": "node-snapshot", "constant": "NODE_SNAPSHOT_SCHEMA_VERSION", "source": "core/state/node-snapshot.ts" },
19
+ { "domain": "pipeline-contract", "constant": "PIPELINE_CONTRACT_SCHEMA_VERSION", "source": "core/state/state-node.ts" },
20
+ { "domain": "run-registry", "constant": "RUN_REGISTRY_SCHEMA_VERSION", "source": "shell/run-registry-io.ts" },
21
+ { "domain": "sandbox-profile", "constant": "SANDBOX_PROFILE_SCHEMA_VERSION", "source": "shell/sandbox-profile.ts" },
22
+ { "domain": "scheduling", "constant": "SCHEDULING_SCHEMA_VERSION", "source": "shell/scheduling-io.ts" },
23
+ { "domain": "state-explosion", "constant": "STATE_EXPLOSION_SCHEMA_VERSION", "source": "core/state/state-explosion/size.ts" },
24
+ { "domain": "state-node", "constant": "STATE_NODE_SCHEMA_VERSION", "source": "core/state/state-node.ts" },
25
+ { "domain": "telemetry-ledger", "constant": "TELEMETRY_LEDGER_SCHEMA_VERSION", "source": "core/trust/telemetry-ledger.ts" },
26
+ { "domain": "topology", "constant": "TOPOLOGY_SCHEMA_VERSION", "source": "core/multi-agent/topology.ts" },
27
+ { "domain": "trust-audit", "constant": "TRUST_AUDIT_SCHEMA_VERSION", "source": "shell/trust-audit.ts" },
28
+ { "domain": "worker-isolation", "constant": "WORKER_ISOLATION_SCHEMA_VERSION", "source": "shell/worker-isolation.ts" },
29
+ { "domain": "workflow-app", "constant": "WORKFLOW_APP_SCHEMA_VERSION", "source": "core/version.ts" }
30
+ ]
31
+ }
@@ -19,6 +19,8 @@ const path = require("node:path");
19
19
  const PLUGIN_ROOT = path.resolve(__dirname, "..");
20
20
  const TYPES_FILE = path.join(PLUGIN_ROOT, "src", "core", "state", "types.ts");
21
21
  const SCHEMA_MODULE = path.join(PLUGIN_ROOT, "dist", "core", "state", "schema.js");
22
+ const SOURCE_ROOT = path.join(PLUGIN_ROOT, "src");
23
+ const VERSION_INVENTORY_FILE = path.join(__dirname, "schema-version-inventory.json");
22
24
 
23
25
  function main() {
24
26
  const errors = [];
@@ -43,6 +45,15 @@ function main() {
43
45
  const requiredKeys = new Set(schema.REQUIRED_TOP_LEVEL_KEYS || []);
44
46
  const optionalKeys = new Set(schema.OPTIONAL_TOP_LEVEL_KEYS || []);
45
47
 
48
+ let versionInventory;
49
+ try {
50
+ versionInventory = JSON.parse(fs.readFileSync(VERSION_INVENTORY_FILE, "utf8"));
51
+ } catch (error) {
52
+ console.error(`validate-run-state-schema: FATAL — cannot load ${VERSION_INVENTORY_FILE}.`);
53
+ process.exit(1);
54
+ }
55
+ errors.push(...validateSchemaVersionInventory(versionInventory, readTypeScriptSources(SOURCE_ROOT)));
56
+
46
57
  // ---- 3. Cross-check: every type key must be either required or optional ----
47
58
  for (const key of runKeys) {
48
59
  if (requiredKeys.has(key)) continue;
@@ -70,9 +81,8 @@ function main() {
70
81
  console.error("validate-run-state-schema: FAIL CLOSED — schema drift detected.");
71
82
  for (const e of errors) console.error(` ${e}`);
72
83
  console.error(
73
- `\nFix: update REQUIRED_TOP_LEVEL_KEYS and/or OPTIONAL_TOP_LEVEL_KEYS in ` +
74
- `src/run-state-schema.ts, then update normalizeRunState() and validateMigratedRunState() ` +
75
- `in src/state-migrations.ts to handle any new required fields.`
84
+ `\nFix: update the run-state key lists or schema-version-inventory.json and its canonical source. ` +
85
+ `Keep each schema domain separate.`
76
86
  );
77
87
  process.exit(1);
78
88
  }
@@ -82,6 +92,85 @@ function main() {
82
92
  `validate-run-state-schema: ok — ${runKeys.size} WorkflowRun keys accounted for ` +
83
93
  `(${requiredKeys.size} required + ${optionalKeys.size} optional = ${total})`
84
94
  );
95
+ console.error(
96
+ `validate-run-state-schema: ok — ${versionInventory.domains.length} schema version domains have one definition each`
97
+ );
98
+ }
99
+
100
+ function readTypeScriptSources(root, base = root) {
101
+ const files = [];
102
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
103
+ const absolute = path.join(root, entry.name);
104
+ if (entry.isDirectory()) {
105
+ files.push(...readTypeScriptSources(absolute, base));
106
+ continue;
107
+ }
108
+ if (!entry.isFile() || !entry.name.endsWith(".ts")) continue;
109
+ files.push({
110
+ path: path.relative(base, absolute).split(path.sep).join("/"),
111
+ source: fs.readFileSync(absolute, "utf8"),
112
+ });
113
+ }
114
+ return files;
115
+ }
116
+
117
+ function validateSchemaVersionInventory(inventory, sourceFiles) {
118
+ const errors = [];
119
+ if (!inventory || inventory.schemaVersion !== 1 || !Array.isArray(inventory.domains)) {
120
+ return ["Schema version inventory must have schemaVersion 1 and a domains array."];
121
+ }
122
+
123
+ const entriesByConstant = new Map();
124
+ const domains = new Set();
125
+ for (const entry of inventory.domains) {
126
+ if (!entry || typeof entry.domain !== "string" || typeof entry.constant !== "string" || typeof entry.source !== "string") {
127
+ errors.push("Schema version inventory entries need domain, constant, and source strings.");
128
+ continue;
129
+ }
130
+ if (domains.has(entry.domain)) errors.push(`Schema version domain "${entry.domain}" is listed more than once.`);
131
+ domains.add(entry.domain);
132
+ if (entriesByConstant.has(entry.constant)) {
133
+ errors.push(`Schema version constant "${entry.constant}" is listed more than once.`);
134
+ } else {
135
+ entriesByConstant.set(entry.constant, entry);
136
+ }
137
+ }
138
+
139
+ const definitions = new Map();
140
+ const pattern = /^\s*(?:export\s+)?const\s+([A-Z][A-Z0-9_]*_SCHEMA_VERSION)\s*(?::[^=;]+)?=/gm;
141
+ for (const file of sourceFiles) {
142
+ pattern.lastIndex = 0;
143
+ let match;
144
+ while ((match = pattern.exec(file.source)) !== null) {
145
+ const found = definitions.get(match[1]) || [];
146
+ found.push(file.path);
147
+ definitions.set(match[1], found);
148
+ }
149
+ }
150
+
151
+ for (const [constant, sources] of definitions) {
152
+ const entry = entriesByConstant.get(constant);
153
+ if (!entry) {
154
+ errors.push(`Unknown schema version domain: "${constant}" has a definition in ${sources.join(", ")}.`);
155
+ continue;
156
+ }
157
+ if (sources.length > 1) {
158
+ errors.push(`Schema version constant "${constant}" has more than one definition: ${sources.join(", ")}.`);
159
+ }
160
+ for (const source of sources) {
161
+ if (source !== entry.source) {
162
+ errors.push(`Schema version constant "${constant}" must be defined in ${entry.source}, not ${source}.`);
163
+ }
164
+ }
165
+ }
166
+
167
+ for (const [constant, entry] of entriesByConstant) {
168
+ if (!definitions.has(constant)) {
169
+ errors.push(`Schema version inventory entry "${entry.domain}" has no definition for ${constant}.`);
170
+ }
171
+ }
172
+
173
+ return errors;
85
174
  }
86
175
 
87
176
  /**
@@ -123,4 +212,6 @@ function extractInterfaceKeys(source, interfaceName) {
123
212
  return keys;
124
213
  }
125
214
 
126
- main();
215
+ if (require.main === module) main();
216
+
217
+ module.exports = { extractInterfaceKeys, validateSchemaVersionInventory };