cool-workflow 0.1.90 → 0.1.92

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 (55) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/README.md +78 -374
  4. package/apps/architecture-review/app.json +1 -1
  5. package/apps/architecture-review-fast/app.json +1 -1
  6. package/apps/end-to-end-golden-path/app.json +1 -1
  7. package/apps/pr-review-fix-ci/app.json +1 -1
  8. package/apps/release-cut/app.json +1 -1
  9. package/apps/research-synthesis/app.json +1 -1
  10. package/apps/research-synthesis/workflow.js +1 -1
  11. package/dist/capability-core.js +31 -0
  12. package/dist/cli/command-surface.js +55 -8
  13. package/dist/doctor.js +1 -0
  14. package/dist/drive.js +2 -1
  15. package/dist/orchestrator/report.js +6 -1
  16. package/dist/orchestrator.js +3 -0
  17. package/dist/reporter.js +67 -0
  18. package/dist/term.js +73 -1
  19. package/dist/version.js +1 -1
  20. package/docs/agent-delegation-drive.7.md +41 -22
  21. package/docs/canonical-workflow-apps.7.md +12 -0
  22. package/docs/cli-mcp-parity.7.md +4 -0
  23. package/docs/contract-migration-tooling.7.md +4 -0
  24. package/docs/control-plane-scheduling.7.md +4 -0
  25. package/docs/durable-state-and-locking.7.md +4 -0
  26. package/docs/evidence-adoption-reasoning-chain.7.md +4 -0
  27. package/docs/execution-backends.7.md +4 -0
  28. package/docs/multi-agent-cli-mcp-surface.7.md +4 -0
  29. package/docs/multi-agent-eval-replay-harness.7.md +4 -0
  30. package/docs/multi-agent-operator-ux.7.md +4 -0
  31. package/docs/node-snapshot-diff-replay.7.md +4 -0
  32. package/docs/observability-cost-accounting.7.md +4 -0
  33. package/docs/project-index.md +7 -3
  34. package/docs/real-execution-backends.7.md +4 -0
  35. package/docs/release-and-migration.7.md +4 -0
  36. package/docs/release-tooling.7.md +13 -0
  37. package/docs/run-registry-control-plane.7.md +4 -0
  38. package/docs/run-retention-reclamation.7.md +4 -0
  39. package/docs/state-explosion-management.7.md +4 -0
  40. package/docs/team-collaboration.7.md +4 -0
  41. package/docs/web-desktop-workbench.7.md +4 -0
  42. package/manifest/plugin.manifest.json +1 -1
  43. package/package.json +3 -1
  44. package/scripts/agents/agent-adapter-core.js +289 -4
  45. package/scripts/agents/claude-p-agent.js +45 -19
  46. package/scripts/agents/codex-agent.js +11 -8
  47. package/scripts/agents/gemini-agent.js +11 -8
  48. package/scripts/agents/opencode-agent.js +11 -8
  49. package/scripts/bump-version.js +4 -1
  50. package/scripts/canonical-apps.js +4 -4
  51. package/scripts/dogfood-release.js +1 -1
  52. package/scripts/golden-path.js +4 -4
  53. package/scripts/release-check.js +5 -1
  54. package/scripts/sync-readme.js +123 -0
  55. package/scripts/version-sync-check.js +5 -1
@@ -20,10 +20,10 @@ const path = require("node:path");
20
20
  const { spawn } = require("node:child_process");
21
21
  const {
22
22
  buildPrompt,
23
+ createRenderer,
23
24
  emitReport,
24
25
  flushJsonLines,
25
26
  parseJsonLines,
26
- trace,
27
27
  writeResult
28
28
  } = require("./agent-adapter-core");
29
29
 
@@ -42,7 +42,9 @@ try {
42
42
  }
43
43
 
44
44
  const prompt = buildPrompt(inputPath);
45
- const state = { provider: "codex", buffer: "", model: undefined, usage: undefined };
45
+ const render = createRenderer({ env: process.env, stderr: process.stderr, label: "codex" });
46
+ const transcriptPath = path.join(path.dirname(resultPath), "transcript.md");
47
+ const state = { provider: "codex", buffer: "", model: undefined, usage: undefined, renderer: render };
46
48
  const capturedStdout = [];
47
49
  let childStderr = "";
48
50
  function recordJsonLine(line) {
@@ -54,7 +56,7 @@ function recordJsonLine(line) {
54
56
  }
55
57
  }
56
58
 
57
- trace("* codex: reading the repo (read-only)...");
59
+ render.action("codex: reading the repo (read-only)");
58
60
 
59
61
  const args = [
60
62
  "exec",
@@ -83,21 +85,23 @@ child.stdout.on("data", (chunk) => {
83
85
  parseJsonLines("codex", chunk, state, recordJsonLine);
84
86
  });
85
87
 
88
+ // Capture codex's own stderr (do NOT inherit) so it can never corrupt the live region; it's
89
+ // surfaced only on a non-zero exit.
86
90
  child.stderr.setEncoding("utf8");
87
91
  child.stderr.on("data", (chunk) => {
88
- childStderr += chunk;
89
- if (process.env.CW_AGENT_STREAM !== "0" && process.env.CW_NO_STREAM !== "1" && process.stderr.isTTY) {
90
- process.stderr.write(chunk);
91
- }
92
+ if (childStderr.length < 1024 * 1024) childStderr += chunk;
92
93
  });
93
94
 
94
95
  child.on("error", (error) => {
96
+ render.finishLive();
95
97
  process.stderr.write(`codex spawn failed: ${error.message}\n`);
96
98
  process.exit(1);
97
99
  });
98
100
 
99
101
  child.on("close", (code) => {
100
102
  flushJsonLines("codex", state, recordJsonLine);
103
+ render.finishLive();
104
+ render.writeTranscript(transcriptPath);
101
105
  if (code !== 0) {
102
106
  const detail = childStderr.trim() || `codex exited ${code === null ? "(timeout/killed)" : code}`;
103
107
  process.stderr.write(`${detail}\n`);
@@ -129,6 +133,5 @@ child.on("close", (code) => {
129
133
  process.exit(1);
130
134
  }
131
135
 
132
- trace("* done - result captured");
133
136
  emitReport(state.model, state.usage, resultText);
134
137
  });
@@ -14,13 +14,14 @@
14
14
  // stdout: one JSON object { model, usage, result } for CW provenance.
15
15
  // stderr: optional live trace when CW_AGENT_STREAM=1 and attached to a TTY.
16
16
 
17
+ const path = require("node:path");
17
18
  const { spawn } = require("node:child_process");
18
19
  const {
19
20
  buildPrompt,
21
+ createRenderer,
20
22
  emitReport,
21
23
  flushJsonLines,
22
24
  parseJsonLines,
23
- trace,
24
25
  writeResult
25
26
  } = require("./agent-adapter-core");
26
27
 
@@ -32,7 +33,9 @@ if (!inputPath || !resultPath) {
32
33
  }
33
34
 
34
35
  const prompt = buildPrompt(inputPath);
35
- const state = { provider: "gemini", buffer: "", model: undefined, usage: undefined, textFragments: [], finalResult: undefined };
36
+ const render = createRenderer({ env: process.env, stderr: process.stderr, label: "gemini" });
37
+ const transcriptPath = path.join(path.dirname(resultPath), "transcript.md");
38
+ const state = { provider: "gemini", buffer: "", model: undefined, usage: undefined, textFragments: [], finalResult: undefined, renderer: render };
36
39
  let childStderr = "";
37
40
  function recordJsonLine(line) {
38
41
  let ev;
@@ -51,7 +54,7 @@ function recordJsonLine(line) {
51
54
  }
52
55
  }
53
56
 
54
- trace("* gemini: reading the repo (read-only)...");
57
+ render.action("gemini: reading the repo (read-only)");
55
58
 
56
59
  const args = [
57
60
  "-p",
@@ -72,21 +75,22 @@ child.stdout.on("data", (chunk) => {
72
75
  parseJsonLines("gemini", chunk, state, recordJsonLine);
73
76
  });
74
77
 
78
+ // Capture gemini's own stderr (do NOT inherit) so it can never corrupt the live region.
75
79
  child.stderr.setEncoding("utf8");
76
80
  child.stderr.on("data", (chunk) => {
77
- childStderr += chunk;
78
- if (process.env.CW_AGENT_STREAM !== "0" && process.env.CW_NO_STREAM !== "1" && process.stderr.isTTY) {
79
- process.stderr.write(chunk);
80
- }
81
+ if (childStderr.length < 1024 * 1024) childStderr += chunk;
81
82
  });
82
83
 
83
84
  child.on("error", (error) => {
85
+ render.finishLive();
84
86
  process.stderr.write(`gemini spawn failed: ${error.message}\n`);
85
87
  process.exit(1);
86
88
  });
87
89
 
88
90
  child.on("close", (code) => {
89
91
  flushJsonLines("gemini", state, recordJsonLine);
92
+ render.finishLive();
93
+ render.writeTranscript(transcriptPath);
90
94
  if (code !== 0) {
91
95
  const detail = childStderr.trim() || `gemini exited ${code === null ? "(timeout/killed)" : code}`;
92
96
  process.stderr.write(`${detail}\n`);
@@ -110,6 +114,5 @@ child.on("close", (code) => {
110
114
  process.exit(1);
111
115
  }
112
116
 
113
- trace("* done - result captured");
114
117
  emitReport(state.model, state.usage, resultText);
115
118
  });
@@ -19,13 +19,14 @@
19
19
  // via execution-backend boundary controls. If OpenCode adds a cleaner read-only
20
20
  // flag, prefer that here.
21
21
 
22
+ const path = require("node:path");
22
23
  const { spawn } = require("node:child_process");
23
24
  const {
24
25
  buildPrompt,
26
+ createRenderer,
25
27
  emitReport,
26
28
  flushJsonLines,
27
29
  parseJsonLines,
28
- trace,
29
30
  writeResult
30
31
  } = require("./agent-adapter-core");
31
32
 
@@ -37,7 +38,9 @@ if (!inputPath || !resultPath) {
37
38
  }
38
39
 
39
40
  const prompt = buildPrompt(inputPath);
40
- const state = { provider: "opencode", buffer: "", model: undefined, usage: undefined, textFragments: [], finalResult: undefined };
41
+ const render = createRenderer({ env: process.env, stderr: process.stderr, label: "opencode" });
42
+ const transcriptPath = path.join(path.dirname(resultPath), "transcript.md");
43
+ const state = { provider: "opencode", buffer: "", model: undefined, usage: undefined, textFragments: [], finalResult: undefined, renderer: render };
41
44
  let childStderr = "";
42
45
  function recordJsonLine(line) {
43
46
  let ev;
@@ -55,7 +58,7 @@ function recordJsonLine(line) {
55
58
  }
56
59
  }
57
60
 
58
- trace("* opencode: reading the repo...");
61
+ render.action("opencode: reading the repo");
59
62
 
60
63
  const args = [
61
64
  "run",
@@ -76,21 +79,22 @@ child.stdout.on("data", (chunk) => {
76
79
  parseJsonLines("opencode", chunk, state, recordJsonLine);
77
80
  });
78
81
 
82
+ // Capture opencode's own stderr (do NOT inherit) so it can never corrupt the live region.
79
83
  child.stderr.setEncoding("utf8");
80
84
  child.stderr.on("data", (chunk) => {
81
- childStderr += chunk;
82
- if (process.env.CW_AGENT_STREAM !== "0" && process.env.CW_NO_STREAM !== "1" && process.stderr.isTTY) {
83
- process.stderr.write(chunk);
84
- }
85
+ if (childStderr.length < 1024 * 1024) childStderr += chunk;
85
86
  });
86
87
 
87
88
  child.on("error", (error) => {
89
+ render.finishLive();
88
90
  process.stderr.write(`opencode spawn failed: ${error.message}\n`);
89
91
  process.exit(1);
90
92
  });
91
93
 
92
94
  child.on("close", (code) => {
93
95
  flushJsonLines("opencode", state, recordJsonLine);
96
+ render.finishLive();
97
+ render.writeTranscript(transcriptPath);
94
98
  if (code !== 0) {
95
99
  const detail = childStderr.trim() || `opencode exited ${code === null ? "(timeout/killed)" : code}`;
96
100
  process.stderr.write(`${detail}\n`);
@@ -114,6 +118,5 @@ child.on("close", (code) => {
114
118
  process.exit(1);
115
119
  }
116
120
 
117
- trace("* done - result captured");
118
121
  emitReport(state.model, state.usage, resultText);
119
122
  });
@@ -167,7 +167,10 @@ function contentSurfaceFiles(next) {
167
167
  // All files the version-sync-check script validates for VERSION or vX.Y.Z.
168
168
  // Keep in sync with scripts/version-sync-check.js.
169
169
  return [
170
- { path: "plugins/cool-workflow/README.md", needle: `v${next}`, desc: "README version tag" },
170
+ // The npm package README (plugins/cool-workflow/README.md) is GENERATED from the GitHub
171
+ // README.md by scripts/sync-readme.js and shows the version via the live npm/release shields
172
+ // badges, not a hand-maintained literal — so it is NOT a version-bump surface. After a bump,
173
+ // `npm run sync:readme` keeps it byte-identical to the GitHub README (guarded by readme:check).
171
174
  { path: "plugins/cool-workflow/docs/multi-agent-cli-mcp-surface.7.md", needle: next, desc: "multi-agent CLI/MCP surface doc" },
172
175
  { path: "plugins/cool-workflow/docs/multi-agent-operator-ux.7.md", needle: next, desc: "multi-agent operator UX doc" },
173
176
  { path: "plugins/cool-workflow/docs/multi-agent-eval-replay-harness.7.md", needle: next, desc: "multi-agent eval/replay doc" },
@@ -83,7 +83,7 @@ const canonicalApps = [
83
83
  "--source",
84
84
  "plugins/cool-workflow/docs/workflow-app-framework.7.md",
85
85
  "--scope",
86
- "Cool Workflow v0.1.90",
86
+ "Cool Workflow v0.1.92",
87
87
  "--freshness",
88
88
  "as of release preparation"
89
89
  ]
@@ -117,14 +117,14 @@ function main() {
117
117
  assert.ok(summary, `${app.id} must appear in app list`);
118
118
  assert.equal(summary.sourceKind, "app-directory");
119
119
  assert.equal(summary.legacy, false);
120
- assert.equal(summary.version, "0.1.90");
120
+ assert.equal(summary.version, "0.1.92");
121
121
 
122
122
  const validation = runJson(["app", "validate", manifestPath]);
123
123
  assert.equal(validation.valid, true, `${app.id} manifest must validate`);
124
124
 
125
125
  const shown = runJson(["app", "show", app.id]);
126
126
  assert.equal(shown.app.id, app.id);
127
- assert.equal(shown.app.version, "0.1.90");
127
+ assert.equal(shown.app.version, "0.1.92");
128
128
  assert.ok(shown.app.metadata.canonical, `${app.id} must be marked canonical`);
129
129
  assert.ok(shown.app.sandboxProfiles.length > 0, `${app.id} must declare sandbox profiles`);
130
130
  assertTaskIdsUnique(shown);
@@ -135,7 +135,7 @@ function main() {
135
135
  const plan = runJson(["plan", app.id, ...app.args(workspace)]);
136
136
  const state = JSON.parse(fs.readFileSync(plan.statePath, "utf8"));
137
137
  assert.equal(state.workflow.app.id, app.id);
138
- assert.equal(state.workflow.app.version, "0.1.90");
138
+ assert.equal(state.workflow.app.version, "0.1.92");
139
139
  assert.equal(state.workflow.app.metadata.canonical, true);
140
140
  assert.ok(state.tasks.some((task) => task.requiresEvidence), `${app.id} plan must include evidence gates`);
141
141
  assert.ok(state.tasks.every((task) => task.sandboxProfileId), `${app.id} plan must include sandbox hints`);
@@ -6,7 +6,7 @@ const fs = require("node:fs");
6
6
  const path = require("node:path");
7
7
  const { CoolWorkflowRunner } = require("../dist/orchestrator.js");
8
8
 
9
- const TARGET_VERSION = "0.1.90";
9
+ const TARGET_VERSION = "0.1.92";
10
10
  const PREVIOUS_VERSION = "0.1.31";
11
11
  const pluginRoot = path.resolve(__dirname, "..");
12
12
  const repoRoot = path.resolve(pluginRoot, "..", "..");
@@ -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.1.90");
36
+ assert.equal(appValidation.summary.version, "0.1.92");
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.1.90 end-to-end golden path."
45
+ "Prove the deterministic v0.1.92 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.1.90");
55
+ assert.equal(state.workflow.app.version, "0.1.92");
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\.1\.90/);
198
+ assert.match(report, /Workflow App: end-to-end-golden-path@0\.1\.92/);
199
199
  assert.match(report, /## Candidates/);
200
200
  assert.match(report, /## Trust Audit/);
201
201
  assert.match(report, /## Acceptance Rationale/);
@@ -74,7 +74,11 @@ const checks = [
74
74
  // drifted from a fresh source scan. The teeth-on-the-gate live in
75
75
  // test/project-index-sync-smoke.js (run by `npm test`); this entry gives the
76
76
  // real-doc check its own visible PASS/FAIL line in the release summary.
77
- { name: "project index sync", command: ["npm", "run", "index:check"] }
77
+ { name: "project index sync", command: ["npm", "run", "index:check"] },
78
+ // Fail closed if the npm README (plugins/cool-workflow/README.md) has drifted
79
+ // from the GitHub README.md it is generated from. Keeps the two pages identical
80
+ // by construction; teeth live in test/readme-sync-smoke.js (run by `npm test`).
81
+ { name: "readme sync", command: ["npm", "run", "readme:check"] }
78
82
  ];
79
83
 
80
84
  function main() {
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // sync-readme — keep the npm package README in lockstep with the GitHub README.
5
+ //
6
+ // The repo-root README.md is the SINGLE SOURCE OF TRUTH. This generates the npm
7
+ // page (plugins/cool-workflow/README.md) from it, changing ONLY what npm needs to
8
+ // render: relative URLs become absolute, because npm cannot resolve a repo's
9
+ // relative `docs/assets/...` image paths or `](LICENSE)` links the way GitHub can.
10
+ //
11
+ // <img src="docs/assets/x.svg"> -> raw.githubusercontent.com/<owner>/<repo>/main/docs/assets/x.svg
12
+ // ](LICENSE) / ](plugins/...) -> github.com/<owner>/<repo>/blob/main/...
13
+ //
14
+ // The text content is otherwise identical, so the two pages can never drift.
15
+ //
16
+ // --check : regenerate in memory, compare to the committed npm README, write
17
+ // nothing, exit non-zero on drift (the release/CI gate). Mirrors
18
+ // sync-project-index.js. Fork-safe: the owner/repo in the absolute URLs
19
+ // is normalized out of the compare, so a fork clone never false-REDs.
20
+
21
+ const fs = require("node:fs");
22
+ const path = require("node:path");
23
+ const { spawnSync } = require("node:child_process");
24
+
25
+ const pluginRoot = path.resolve(__dirname, "..");
26
+ const repoRoot = path.resolve(pluginRoot, "..", "..");
27
+ const sourceReadme = path.join(repoRoot, "README.md");
28
+ // CW_README_PATH overrides the npm README that --check compares against (used by
29
+ // the smoke to point --check at a throwaway drifted fixture). Default: the real one.
30
+ const targetReadme = process.env.CW_README_PATH
31
+ ? path.resolve(process.env.CW_README_PATH)
32
+ : path.join(pluginRoot, "README.md");
33
+
34
+ const repoUrl = normalizeGitRemote(git(["config", "--get", "remote.origin.url"]).trim())
35
+ || "https://github.com/coo1white/cool-workflow";
36
+ const ownerRepo = repoUrl.replace(/^https:\/\/github\.com\//, "");
37
+ const RAW_BASE = `https://raw.githubusercontent.com/${ownerRepo}/main`;
38
+ const BLOB_BASE = `${repoUrl}/blob/main`;
39
+
40
+ const MARKER =
41
+ "<!-- AUTO-GENERATED from /README.md by scripts/sync-readme.js — edit the root README, " +
42
+ "then run `npm run sync:readme`. DO NOT edit this file directly. -->";
43
+
44
+ const CHECK = process.argv.includes("--check");
45
+
46
+ function render() {
47
+ if (!fs.existsSync(sourceReadme)) {
48
+ process.stderr.write(`sync-readme FAILED: source ${path.relative(repoRoot, sourceReadme)} does not exist.\n`);
49
+ process.exit(1);
50
+ }
51
+ const source = fs.readFileSync(sourceReadme, "utf8");
52
+ // NOTE: a relative path must be free of a literal "(", ")", or '"' — the [^)]+ / [^"]+
53
+ // captures below stop at those. The repo README has none; readme:check catches any drift.
54
+ const absolutized = source
55
+ // HTML image sources: <img src="docs/assets/x.svg"> -> raw URL (skip already-absolute / anchors)
56
+ .replace(/\bsrc="(?!https?:|#|data:)([^"]+)"/g, (_m, rel) => `src="${RAW_BASE}/${rel}"`)
57
+ // Markdown links: ](LICENSE) / ](plugins/...) -> blob URL (skip absolute / in-page anchors / mailto)
58
+ .replace(/\]\((?!https?:|#|mailto:)([^)]+)\)/g, (_m, rel) => `](${BLOB_BASE}/${rel})`);
59
+ return `${MARKER}\n${absolutized}`;
60
+ }
61
+
62
+ function normalizeForCompare(text) {
63
+ // Strip the owner/repo from the generated absolute URLs so a fork (whose git
64
+ // remote differs) does not false-RED against the canonical committed README.
65
+ // The CONTENT is what this gate guards; the owner/repo is environment-derived.
66
+ return text
67
+ .replace(/\r\n/g, "\n")
68
+ .replace(/https:\/\/raw\.githubusercontent\.com\/[^/]+\/[^/]+\/main/g, "<RAW>")
69
+ .replace(/https:\/\/github\.com\/[^/]+\/[^/]+\/blob\/main/g, "<BLOB>");
70
+ }
71
+
72
+ function main() {
73
+ const rendered = render();
74
+ const rel = path.relative(repoRoot, targetReadme);
75
+
76
+ if (CHECK) {
77
+ if (!fs.existsSync(targetReadme)) {
78
+ process.stderr.write(`readme check FAILED: ${rel} does not exist.\nRun: (cd plugins/cool-workflow && npm run sync:readme)\n`);
79
+ process.exit(1);
80
+ }
81
+ const committed = normalizeForCompare(fs.readFileSync(targetReadme, "utf8"));
82
+ const fresh = normalizeForCompare(rendered);
83
+ if (committed === fresh) {
84
+ process.stdout.write(`${JSON.stringify({ ok: true, check: true, source: "README.md", doc: rel }, null, 2)}\n`);
85
+ return;
86
+ }
87
+ const a = committed.split("\n");
88
+ const b = fresh.split("\n");
89
+ let i = 0;
90
+ while (i < a.length && i < b.length && a[i] === b[i]) i++;
91
+ process.stderr.write(
92
+ `readme check FAILED: ${rel} is stale (does not match the GitHub README.md).\n` +
93
+ `First difference at line ${i + 1}:\n` +
94
+ ` committed: ${JSON.stringify(a[i] ?? "<missing>")}\n` +
95
+ ` expected: ${JSON.stringify(b[i] ?? "<missing>")}\n` +
96
+ `Regenerate with: (cd plugins/cool-workflow && npm run sync:readme)\n`
97
+ );
98
+ process.exit(1);
99
+ }
100
+
101
+ fs.writeFileSync(targetReadme, rendered, "utf8");
102
+ process.stdout.write(`${JSON.stringify({
103
+ ok: true,
104
+ source: "README.md",
105
+ output: rel,
106
+ repoUrl
107
+ }, null, 2)}\n`);
108
+ }
109
+
110
+ function git(args) {
111
+ const result = spawnSync("git", args, { cwd: repoRoot, encoding: "utf8" });
112
+ return result.status === 0 ? result.stdout : "";
113
+ }
114
+
115
+ function normalizeGitRemote(remote) {
116
+ if (!remote) return "";
117
+ if (remote.endsWith(".git")) remote = remote.slice(0, -4);
118
+ const sshMatch = remote.match(/^git@github\.com:(.+)$/);
119
+ if (sshMatch) return `https://github.com/${sshMatch[1]}`;
120
+ return remote;
121
+ }
122
+
123
+ main();
@@ -92,7 +92,11 @@ function main() {
92
92
  checkIncludes("plugins/cool-workflow/dist/mcp-server.js", "CURRENT_COOL_WORKFLOW_VERSION", checks);
93
93
  checkIncludes("plugins/cool-workflow/dist/workflow-app-framework.js", "CURRENT_COOL_WORKFLOW_VERSION", checks);
94
94
 
95
- checkIncludes("plugins/cool-workflow/README.md", `v${VERSION}`, checks);
95
+ // The npm package README (plugins/cool-workflow/README.md) is GENERATED from the GitHub
96
+ // README.md (scripts/sync-readme.js) and conveys the version via the live npm/release shields
97
+ // badges — not a hand-maintained literal — exactly like the GitHub page. It is no longer
98
+ // version-pinned here; readme:check guards that the two pages stay identical, and the surfaces
99
+ // above (version.ts/dist/package.json/manifests/docs) still pin the release.
96
100
  checkIncludes("plugins/cool-workflow/docs/index.md", "release and migration", checks);
97
101
  checkIncludes("plugins/cool-workflow/docs/multi-agent-topologies.7.md", "Multi-Agent Topologies", checks);
98
102
  checkIncludes("plugins/cool-workflow/docs/multi-agent-cli-mcp-surface.7.md", VERSION, checks);