cool-workflow 0.1.95 → 0.1.97

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 (89) 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/workflow.js +3 -3
  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/dist/agent-config.js +2 -1
  11. package/dist/candidate-scoring.js +3 -3
  12. package/dist/capability-core.js +6 -1
  13. package/dist/cli/handlers/scheduling.js +7 -1
  14. package/dist/dispatch.js +12 -6
  15. package/dist/drive.js +10 -0
  16. package/dist/evidence-grounding.js +18 -13
  17. package/dist/execution-backend/agent.js +1 -1
  18. package/dist/execution-backend/probes.js +22 -6
  19. package/dist/execution-backend.js +61 -8
  20. package/dist/mcp-server.js +4 -0
  21. package/dist/node-snapshot.js +3 -3
  22. package/dist/onramp.js +2 -0
  23. package/dist/orchestrator/app-operations.js +6 -0
  24. package/dist/orchestrator/cli-options.js +8 -2
  25. package/dist/orchestrator/lifecycle-operations.js +16 -5
  26. package/dist/orchestrator/migration-operations.js +1 -1
  27. package/dist/orchestrator.js +26 -1
  28. package/dist/reclamation.js +8 -2
  29. package/dist/run-export.js +10 -1
  30. package/dist/run-registry/derive.js +4 -1
  31. package/dist/sandbox-profile.js +6 -1
  32. package/dist/scheduler.js +14 -14
  33. package/dist/schema-validate.js +8 -2
  34. package/dist/state-explosion/helpers.js +4 -21
  35. package/dist/state-explosion/size.js +63 -0
  36. package/dist/state-explosion.js +18 -71
  37. package/dist/state.js +47 -9
  38. package/dist/triggers.js +7 -1
  39. package/dist/trust-audit.js +27 -2
  40. package/dist/util/fingerprint.js +19 -0
  41. package/dist/util/fingerprint.test.js +27 -0
  42. package/dist/version.js +1 -1
  43. package/dist/workbench-host.js +17 -0
  44. package/dist/workbench.js +19 -17
  45. package/dist/worker-isolation.js +25 -1
  46. package/docs/agent-delegation-drive.7.md +66 -1
  47. package/docs/cli-mcp-parity.7.md +4 -0
  48. package/docs/contract-migration-tooling.7.md +4 -0
  49. package/docs/control-plane-scheduling.7.md +4 -0
  50. package/docs/demo.7.md +80 -0
  51. package/docs/doctor.7.md +97 -0
  52. package/docs/durable-state-and-locking.7.md +4 -0
  53. package/docs/evidence-adoption-reasoning-chain.7.md +4 -0
  54. package/docs/execution-backends.7.md +4 -0
  55. package/docs/fix.7.md +44 -0
  56. package/docs/init.7.md +62 -0
  57. package/docs/multi-agent-cli-mcp-surface.7.md +4 -0
  58. package/docs/multi-agent-eval-replay-harness.7.md +4 -0
  59. package/docs/multi-agent-operator-ux.7.md +4 -0
  60. package/docs/node-snapshot-diff-replay.7.md +4 -0
  61. package/docs/observability-cost-accounting.7.md +4 -0
  62. package/docs/pipeline-verbs.7.md +93 -0
  63. package/docs/project-index.md +20 -4
  64. package/docs/real-execution-backends.7.md +4 -0
  65. package/docs/release-and-migration.7.md +4 -0
  66. package/docs/release-tooling.7.md +20 -0
  67. package/docs/routine.7.md +73 -0
  68. package/docs/routines.md +30 -0
  69. package/docs/run-registry-control-plane.7.md +4 -0
  70. package/docs/run-retention-reclamation.7.md +4 -0
  71. package/docs/sandbox-profiles.7.md +15 -0
  72. package/docs/state-explosion-management.7.md +4 -0
  73. package/docs/team-collaboration.7.md +4 -0
  74. package/docs/web-desktop-workbench.7.md +4 -0
  75. package/manifest/README.md +16 -10
  76. package/manifest/plugin.manifest.json +1 -1
  77. package/package.json +5 -3
  78. package/scripts/agents/agent-adapter-core.js +25 -1
  79. package/scripts/agents/claude-p-agent.js +10 -2
  80. package/scripts/agents/codex-agent.js +22 -2
  81. package/scripts/agents/gemini-agent.js +10 -2
  82. package/scripts/agents/opencode-agent.js +10 -2
  83. package/scripts/canonical-apps.js +4 -4
  84. package/scripts/children/batch-delegate-child.js +10 -3
  85. package/scripts/children/http-delegate-child.js +2 -1
  86. package/scripts/dogfood-release.js +1 -1
  87. package/scripts/golden-path.js +4 -4
  88. package/scripts/release-flow.js +24 -6
  89. package/scripts/release-gate.sh +1 -1
@@ -14,6 +14,13 @@
14
14
  //
15
15
  // stdout: one JSON object { model, usage, result } for CW provenance.
16
16
  // stderr: optional live trace when CW_AGENT_STREAM=1 and attached to a TTY.
17
+ //
18
+ // SPEED: codex `exec` inherits the user's ~/.codex/config.toml, including a heavy
19
+ // `model_reasoning_effort` (e.g. "high"), which makes every read/grep turn slow.
20
+ // CW caps it for ITS runs only via `-c model_reasoning_effort=<effort>` — a
21
+ // per-run override that does NOT touch the user's interactive codex. Tune with
22
+ // CW_CODEX_REASONING_EFFORT (default "low"); set it to "medium"/"high" to opt back
23
+ // into more thinking.
17
24
 
18
25
  const fs = require("node:fs");
19
26
  const os = require("node:os");
@@ -25,6 +32,7 @@ const {
25
32
  emitReport,
26
33
  flushJsonLines,
27
34
  parseJsonLines,
35
+ persistStderr,
28
36
  writeResult
29
37
  } = require("./agent-adapter-core");
30
38
 
@@ -74,9 +82,14 @@ function recordJsonLine(line) {
74
82
 
75
83
  render.action("codex: reading the repo (read-only)…");
76
84
 
85
+ // Cap codex's reasoning effort for CW runs (speed) — overrides config.toml for
86
+ // THIS invocation only. Default "low"; CW_CODEX_REASONING_EFFORT opts back up.
87
+ const effort = process.env.CW_CODEX_REASONING_EFFORT || "low";
77
88
  const args = [
78
89
  "exec",
79
90
  "--json",
91
+ "-c",
92
+ `model_reasoning_effort=${effort}`,
80
93
  "--output-last-message",
81
94
  finalPath,
82
95
  "--sandbox",
@@ -108,6 +121,7 @@ child.stderr.on("data", (chunk) => {
108
121
 
109
122
  child.on("error", (error) => {
110
123
  render.finishLive();
124
+ persistStderr(resultPath, `codex spawn failed: ${error.message}`);
111
125
  process.stderr.write(`codex spawn failed: ${error.message}\n`);
112
126
  process.exit(1);
113
127
  });
@@ -118,11 +132,14 @@ child.on("close", (code) => {
118
132
  render.writeTranscript(transcriptPath);
119
133
  if (code !== 0) {
120
134
  const detail = childStderr.trim() || `codex exited ${code === null ? "(timeout/killed)" : code}`;
135
+ persistStderr(resultPath, detail);
121
136
  process.stderr.write(`${detail}\n`);
122
137
  process.exit(code === null ? 1 : code);
123
138
  }
124
139
  if (state.invalidJson) {
125
- process.stderr.write("codex --json produced a non-JSONL stdout line - refusing to trust the result\n");
140
+ const detail = "codex --json produced a non-JSONL stdout line - refusing to trust the result";
141
+ persistStderr(resultPath, childStderr.trim() || detail);
142
+ process.stderr.write(`${detail}\n`);
126
143
  process.exit(1);
127
144
  }
128
145
 
@@ -130,7 +147,9 @@ child.on("close", (code) => {
130
147
  try {
131
148
  resultText = fs.readFileSync(finalPath, "utf8");
132
149
  } catch {
133
- process.stderr.write("codex produced no final output file - refusing to fabricate a result\n");
150
+ const detail = "codex produced no final output file - refusing to fabricate a result";
151
+ persistStderr(resultPath, childStderr.trim() || detail);
152
+ process.stderr.write(`${detail}\n`);
134
153
  process.exit(1);
135
154
  } finally {
136
155
  try {
@@ -143,6 +162,7 @@ child.on("close", (code) => {
143
162
  try {
144
163
  writeResult(resultPath, resultText);
145
164
  } catch (error) {
165
+ persistStderr(resultPath, `codex produced no final result: ${error.message}`);
146
166
  process.stderr.write(`codex produced no final result: ${error.message}\n`);
147
167
  process.exit(1);
148
168
  }
@@ -22,6 +22,7 @@ const {
22
22
  emitReport,
23
23
  flushJsonLines,
24
24
  parseJsonLines,
25
+ persistStderr,
25
26
  writeResult
26
27
  } = require("./agent-adapter-core");
27
28
 
@@ -83,6 +84,7 @@ child.stderr.on("data", (chunk) => {
83
84
 
84
85
  child.on("error", (error) => {
85
86
  render.finishLive();
87
+ persistStderr(resultPath, `gemini spawn failed: ${error.message}`);
86
88
  process.stderr.write(`gemini spawn failed: ${error.message}\n`);
87
89
  process.exit(1);
88
90
  });
@@ -93,23 +95,29 @@ child.on("close", (code) => {
93
95
  render.writeTranscript(transcriptPath);
94
96
  if (code !== 0) {
95
97
  const detail = childStderr.trim() || `gemini exited ${code === null ? "(timeout/killed)" : code}`;
98
+ persistStderr(resultPath, detail);
96
99
  process.stderr.write(`${detail}\n`);
97
100
  process.exit(code === null ? 1 : code);
98
101
  }
99
102
  if (state.invalidJson) {
100
- process.stderr.write("gemini --output-format stream-json produced a non-JSONL stdout line - refusing to trust the result\n");
103
+ const detail = "gemini --output-format stream-json produced a non-JSONL stdout line - refusing to trust the result";
104
+ persistStderr(resultPath, childStderr.trim() || detail);
105
+ process.stderr.write(`${detail}\n`);
101
106
  process.exit(1);
102
107
  }
103
108
 
104
109
  const resultText = state.finalResult || state.textFragments.join("\n\n");
105
110
  if (!resultText.trim()) {
106
- process.stderr.write("gemini produced no result text - refusing to fabricate a result\n");
111
+ const detail = "gemini produced no result text - refusing to fabricate a result";
112
+ persistStderr(resultPath, childStderr.trim() || detail);
113
+ process.stderr.write(`${detail}\n`);
107
114
  process.exit(1);
108
115
  }
109
116
 
110
117
  try {
111
118
  writeResult(resultPath, resultText);
112
119
  } catch (error) {
120
+ persistStderr(resultPath, `gemini produced no final result: ${error.message}`);
113
121
  process.stderr.write(`gemini produced no final result: ${error.message}\n`);
114
122
  process.exit(1);
115
123
  }
@@ -27,6 +27,7 @@ const {
27
27
  emitReport,
28
28
  flushJsonLines,
29
29
  parseJsonLines,
30
+ persistStderr,
30
31
  writeResult
31
32
  } = require("./agent-adapter-core");
32
33
 
@@ -136,6 +137,7 @@ child.stderr.on("data", (chunk) => {
136
137
 
137
138
  child.on("error", (error) => {
138
139
  render.finishLive();
140
+ persistStderr(resultPath, `opencode spawn failed: ${error.message}`);
139
141
  process.stderr.write(`opencode spawn failed: ${error.message}\n`);
140
142
  process.exit(1);
141
143
  });
@@ -146,23 +148,29 @@ child.on("close", (code) => {
146
148
  render.writeTranscript(transcriptPath);
147
149
  if (code !== 0) {
148
150
  const detail = childStderr.trim() || `opencode exited ${code === null ? "(timeout/killed)" : code}`;
151
+ persistStderr(resultPath, detail);
149
152
  process.stderr.write(`${detail}\n`);
150
153
  process.exit(code === null ? 1 : code);
151
154
  }
152
155
  if (state.invalidJson) {
153
- process.stderr.write("opencode --format json produced a non-JSONL stdout line - refusing to trust the result\n");
156
+ const detail = "opencode --format json produced a non-JSONL stdout line - refusing to trust the result";
157
+ persistStderr(resultPath, childStderr.trim() || detail);
158
+ process.stderr.write(`${detail}\n`);
154
159
  process.exit(1);
155
160
  }
156
161
 
157
162
  const resultText = state.finalResult || state.lastMessageText || state.textFragments.join("\n\n");
158
163
  if (!resultText.trim()) {
159
- process.stderr.write("opencode produced no result text - refusing to fabricate a result\n");
164
+ const detail = "opencode produced no result text - refusing to fabricate a result";
165
+ persistStderr(resultPath, childStderr.trim() || detail);
166
+ process.stderr.write(`${detail}\n`);
160
167
  process.exit(1);
161
168
  }
162
169
 
163
170
  try {
164
171
  writeResult(resultPath, resultText);
165
172
  } catch (error) {
173
+ persistStderr(resultPath, `opencode produced no final result: ${error.message}`);
166
174
  process.stderr.write(`opencode produced no final result: ${error.message}\n`);
167
175
  process.exit(1);
168
176
  }
@@ -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.95",
86
+ "Cool Workflow v0.1.97",
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.95");
120
+ assert.equal(summary.version, "0.1.97");
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.95");
127
+ assert.equal(shown.app.version, "0.1.97");
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.95");
138
+ assert.equal(state.workflow.app.version, "0.1.97");
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`);
@@ -18,10 +18,17 @@
18
18
 
19
19
  const { spawn } = require("node:child_process");
20
20
  let raw = "";
21
+ const MAX_STDIN_BYTES = 32 * 1024 * 1024;
21
22
  process.stdin.setEncoding("utf8");
22
- process.stdin.on("data", (d) => (raw += d));
23
+ process.stdin.on("data", (d) => { if (raw.length < MAX_STDIN_BYTES) raw += d; });
23
24
  process.stdin.on("end", () => {
24
- const jobs = JSON.parse(raw);
25
+ let jobs;
26
+ try {
27
+ jobs = JSON.parse(raw);
28
+ } catch (e) {
29
+ process.stdout.write(JSON.stringify([{ spawnError: `invalid stdin JSON: ${String(e && e.message || e)}`, exitCode: null, stdout: "" }]));
30
+ return;
31
+ }
25
32
  if (!jobs.length) { process.stdout.write("[]"); return; }
26
33
  const out = new Array(jobs.length);
27
34
  let pending = jobs.length;
@@ -37,7 +44,7 @@ process.stdin.on("end", () => {
37
44
  };
38
45
  let child;
39
46
  try {
40
- child = spawn(job.binary, job.args, { cwd: job.cwd, env: process.env, shell: false });
47
+ child = spawn(job.binary, job.args, { cwd: job.cwd, env: job.env || process.env, shell: false });
41
48
  } catch (error) {
42
49
  settle({ spawnError: String((error && error.message) || error), exitCode: null, stdout: "" });
43
50
  return;
@@ -15,7 +15,8 @@
15
15
  // API request. Behavior MUST stay byte-identical to the previous embedded string.
16
16
 
17
17
  (async () => {
18
- const read = () => new Promise((res) => { let b = ""; process.stdin.on("data", (c) => (b += c)); process.stdin.on("end", () => res(b)); });
18
+ const MAX_STDIN_BYTES = 32 * 1024 * 1024;
19
+ const read = () => new Promise((res) => { let b = ""; process.stdin.on("data", (c) => { if (b.length < MAX_STDIN_BYTES) b += c; }); process.stdin.on("end", () => res(b)); });
19
20
  try {
20
21
  const job = JSON.parse((await read()) || "{}");
21
22
  const endpoint = process.env.CW_DELEGATE_ENDPOINT;
@@ -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.95";
9
+ const TARGET_VERSION = "0.1.97";
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.95");
36
+ assert.equal(appValidation.summary.version, "0.1.97");
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.95 end-to-end golden path."
45
+ "Prove the deterministic v0.1.97 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.95");
55
+ assert.equal(state.workflow.app.version, "0.1.97");
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\.95/);
198
+ assert.match(report, /Workflow App: end-to-end-golden-path@0\.1\.97/);
199
199
  assert.match(report, /## Candidates/);
200
200
  assert.match(report, /## Trust Audit/);
201
201
  assert.match(report, /## Acceptance Rationale/);
@@ -154,6 +154,11 @@ function runVendorPreflight() {
154
154
  }
155
155
 
156
156
  // ---- 2. independent reviewer, delegated to the configured agent -------------
157
+ // Default reviewer deadline. The zero-trust reviewer re-runs release-gate.sh,
158
+ // whose sequential test suite alone is ~12 min, then reads + reasons over the
159
+ // diff — so a 10-min default guaranteed a timeout on a real release. 30 min gives
160
+ // headroom; override with CW_AGENT_TIMEOUT_MS (or --agent-timeout-ms).
161
+ const REVIEWER_TIMEOUT_MS = 1800000;
157
162
  function reviewerPromptBody() {
158
163
  // Reuse the committed reviewer spec as the prompt; strip YAML frontmatter.
159
164
  const specPath = path.join(pluginRoot, "agents", "release-reviewer.md");
@@ -280,7 +285,7 @@ function delegateReview(resultPath, inputPath) {
280
285
  cwd: repoRoot,
281
286
  env: { ...process.env },
282
287
  encoding: "utf8",
283
- timeout: cfg.timeoutMs || 600000,
288
+ timeout: cfg.timeoutMs || REVIEWER_TIMEOUT_MS,
284
289
  shell: false,
285
290
  stdio: ["ignore", "pipe", "inherit"],
286
291
  maxBuffer: 32 * 1024 * 1024
@@ -310,7 +315,7 @@ function delegateReview(resultPath, inputPath) {
310
315
  say(`[2/3] reviewer — POSTing to endpoint ${cfg.endpoint} (model: ${cfg.model || "unreported"})`);
311
316
  const body = JSON.stringify({ prompt: subMap.prompt, model: cfg.model, sha: HEAD });
312
317
  const lib = cfg.endpoint.startsWith("https:") ? https : http;
313
- const text = postSync(lib, cfg.endpoint, body, cfg.timeoutMs || 600000);
318
+ const text = postSync(lib, cfg.endpoint, body, cfg.timeoutMs || REVIEWER_TIMEOUT_MS);
314
319
  if (text === null) die("reviewer endpoint call failed — no verdict trusted.");
315
320
  fs.writeFileSync(resultPath, text.endsWith("\n") ? text : `${text}\n`);
316
321
  }
@@ -502,15 +507,28 @@ function cut(resultPath, capability) {
502
507
  const bump = spawnSync("npm", ["run", "bump:version", "--", cutVersion], { cwd: pluginRoot, encoding: "utf8", stdio: "inherit" });
503
508
  if (bump.status !== 0) die("bump:version failed");
504
509
  // Regenerate the gated project index after the version bump (PR #87 gate).
505
- spawnSync("npm", ["run", "sync:project-index", "--", "--repo-only"], { cwd: pluginRoot, stdio: "inherit" });
506
- git(["add", "-A"]);
510
+ // Fail closed: a failed regen must not bake a stale index into the immutable tag.
511
+ const sync = spawnSync("npm", ["run", "sync:project-index", "--", "--repo-only"], { cwd: pluginRoot, encoding: "utf8", stdio: "inherit" });
512
+ if (sync.status !== 0) die("sync:project-index failed — refusing to cut with a stale project index");
513
+ // Stage ONLY tracked-file modifications (the bump surfaces, project-index, dist)
514
+ // plus the ONE intended new file: the reviewer verdict. NEVER `git add -A` — an
515
+ // untracked stray (e.g. the reviewer's narration transcript, which carries the
516
+ // operator's local home path) must never ride into the immutable tag commit
517
+ // (that tripped pii-redaction-smoke and red-failed release-gate for v0.1.96).
518
+ // `git add -u` touches tracked files only, so no untracked file can be swept in;
519
+ // the verdict is the single new path the cut is allowed to add.
520
+ git(["add", "-u"]);
521
+ git(["add", "--", path.relative(repoRoot, resultPath)]);
507
522
  const commit = git(["commit", "-m", `chore(release): record APPROVED reviewer verdict for v${cutVersion}`]);
508
523
  if (commit.code !== 0) die("verdict commit failed", commit.err);
509
524
  const tag = git(["tag", "-a", `v${cutVersion}`, "-m", `v${cutVersion}: ${capability || "release"}`]);
510
525
  if (tag.code !== 0) die("git tag failed", tag.err);
511
526
  if (PUSH) {
512
- git(["push", "origin", "HEAD"]);
513
- git(["push", "origin", `v${cutVersion}`]);
527
+ // Atomic: the verdict commit on HEAD and the tag land together or not at all.
528
+ // A non-atomic two-push could leave main advanced with no tag, so CI's
529
+ // release-gate (which fires on the tag) never runs and the release silently stalls.
530
+ const push = git(["push", "--atomic", "origin", "HEAD", `v${cutVersion}`]);
531
+ if (push.code !== 0) die("atomic push of HEAD + tag failed (nothing partially pushed)", push.err);
514
532
  }
515
533
  say(`tagged v${cutVersion}${PUSH ? " and pushed" : " (local only; push when ready)"}`);
516
534
  // Finishing step: create the GitHub Release for the just-pushed tag. Only when
@@ -30,7 +30,7 @@ say "[1/6] build"
30
30
  npm run --prefix plugins/cool-workflow build >/dev/null 2>&1 || fail "build failed"
31
31
 
32
32
  say "[2/6] tests"
33
- CW_TEST_CONCURRENCY=1 npm test --prefix plugins/cool-workflow >/dev/null 2>&1 || fail "tests failed"
33
+ CW_TEST_CONCURRENCY=1 npm run test:gate --prefix plugins/cool-workflow >/dev/null 2>&1 || fail "tests failed"
34
34
 
35
35
  if [[ -n "$PREV_TAG" ]]; then
36
36
  RANGE="$PREV_TAG..HEAD"