cool-workflow 0.2.2 → 0.2.4

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 (141) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/apps/architecture-review/app.json +1 -1
  4. package/apps/architecture-review-fast/app.json +1 -1
  5. package/apps/end-to-end-golden-path/app.json +1 -1
  6. package/apps/pr-review-fix-ci/app.json +1 -1
  7. package/apps/release-cut/app.json +1 -1
  8. package/apps/research-synthesis/app.json +1 -1
  9. package/dist/cli/dispatch.js +13 -6
  10. package/dist/cli/entry.js +41 -1
  11. package/dist/cli/io.js +12 -22
  12. package/dist/cli/parseargv.js +1 -0
  13. package/dist/core/capability-data.js +3 -2
  14. package/dist/core/format/completion.js +68 -0
  15. package/dist/core/format/help.js +31 -6
  16. package/dist/core/format/safe-json.js +73 -0
  17. package/dist/core/format/state-explosion-text.js +1 -1
  18. package/dist/core/multi-agent/candidate-scoring.js +5 -1
  19. package/dist/core/multi-agent/collaboration.js +3 -3
  20. package/dist/core/multi-agent/coordinator.js +5 -5
  21. package/dist/core/multi-agent/runtime.js +4 -4
  22. package/dist/core/multi-agent/topology.js +3 -3
  23. package/dist/core/pipeline/dispatch.js +18 -4
  24. package/dist/core/pipeline/drive-decide.js +2 -1
  25. package/dist/core/state/migrations.js +16 -1
  26. package/dist/core/state/state-explosion/digest.js +13 -13
  27. package/dist/core/state/state-explosion/graph.js +17 -11
  28. package/dist/core/state/state-explosion/report.js +6 -6
  29. package/dist/core/util/cli-args.js +33 -0
  30. package/dist/core/util/numeric-flag.js +40 -0
  31. package/dist/core/version.js +1 -1
  32. package/dist/mcp/server.js +99 -11
  33. package/dist/shell/audit-cli.js +57 -25
  34. package/dist/shell/coordinator-io.js +73 -13
  35. package/dist/shell/dispatch.js +1 -1
  36. package/dist/shell/doctor.js +80 -1
  37. package/dist/shell/drive.js +251 -49
  38. package/dist/shell/eval-text.js +2 -2
  39. package/dist/shell/evidence-reasoning.js +4 -4
  40. package/dist/shell/execution-backend/agent.js +30 -2
  41. package/dist/shell/execution-backend/container.js +4 -1
  42. package/dist/shell/execution-backend/local.js +19 -11
  43. package/dist/shell/feedback-cli.js +6 -6
  44. package/dist/shell/fs-atomic.js +218 -29
  45. package/dist/shell/man-cli.js +6 -0
  46. package/dist/shell/metrics-cli.js +2 -1
  47. package/dist/shell/multi-agent-cli.js +367 -323
  48. package/dist/shell/multi-agent-host.js +9 -9
  49. package/dist/shell/multi-agent-operator-ux.js +80 -38
  50. package/dist/shell/node-store.js +10 -4
  51. package/dist/shell/observability.js +1 -1
  52. package/dist/shell/operator-ux-text.js +22 -22
  53. package/dist/shell/operator-ux.js +15 -14
  54. package/dist/shell/orchestrator.js +49 -38
  55. package/dist/shell/pipeline-cli.js +107 -42
  56. package/dist/shell/reclamation-io.js +88 -10
  57. package/dist/shell/registry-cli.js +23 -17
  58. package/dist/shell/remote-source.js +13 -8
  59. package/dist/shell/report-cli.js +45 -0
  60. package/dist/shell/report.js +2 -1
  61. package/dist/shell/run-registry-io.js +78 -19
  62. package/dist/shell/run-store.js +74 -2
  63. package/dist/shell/scheduling-io.js +12 -13
  64. package/dist/shell/state-cli.js +2 -7
  65. package/dist/shell/state-explosion-cli.js +17 -9
  66. package/dist/shell/topology-io.js +36 -5
  67. package/dist/shell/trust-audit.js +224 -22
  68. package/dist/shell/trust-policy-io.js +1 -1
  69. package/dist/shell/worker-cli.js +35 -31
  70. package/dist/shell/workflow-app-loader.js +67 -1
  71. package/dist/wiring/capability-table/basics.js +33 -8
  72. package/dist/wiring/capability-table/exec-backend.js +28 -19
  73. package/dist/wiring/capability-table/multi-agent.js +187 -180
  74. package/dist/wiring/capability-table/parity.js +1 -0
  75. package/dist/wiring/capability-table/pipeline.js +64 -52
  76. package/dist/wiring/capability-table/registry-core.js +30 -11
  77. package/dist/wiring/capability-table/reporting.js +147 -106
  78. package/dist/wiring/capability-table/scheduling-registry.js +201 -167
  79. package/dist/wiring/capability-table/state.js +59 -51
  80. package/dist/wiring/capability-table/trust-ledger.js +52 -28
  81. package/dist/wiring/capability-table/workflow-apps.js +85 -70
  82. package/docs/agent-delegation-drive.7.md +4 -0
  83. package/docs/candidate-scoring.7.md +1 -1
  84. package/docs/canonical-workflow-apps.7.md +7 -7
  85. package/docs/cli-mcp-parity.7.md +26 -16
  86. package/docs/contract-migration-tooling.7.md +4 -0
  87. package/docs/control-plane-scheduling.7.md +4 -0
  88. package/docs/coordinator-blackboard.7.md +17 -17
  89. package/docs/dogfood-one-real-repo.7.md +11 -11
  90. package/docs/durable-state-and-locking.7.md +4 -0
  91. package/docs/end-to-end-golden-path.7.md +14 -14
  92. package/docs/evidence-adoption-reasoning-chain.7.md +4 -0
  93. package/docs/execution-backends.7.md +4 -0
  94. package/docs/getting-started.md +37 -37
  95. package/docs/multi-agent-cli-mcp-surface.7.md +21 -17
  96. package/docs/multi-agent-eval-replay-harness.7.md +17 -13
  97. package/docs/multi-agent-operator-ux.7.md +23 -19
  98. package/docs/multi-agent-runtime-core.7.md +22 -22
  99. package/docs/multi-agent-topologies.7.md +6 -6
  100. package/docs/multi-agent-trust-policy-audit.7.md +11 -11
  101. package/docs/node-snapshot-diff-replay.7.md +4 -0
  102. package/docs/observability-cost-accounting.7.md +4 -0
  103. package/docs/operator-ux.7.md +34 -34
  104. package/docs/pipeline-runner.7.md +4 -4
  105. package/docs/project-index.md +31 -4
  106. package/docs/real-execution-backends.7.md +4 -0
  107. package/docs/release-and-migration.7.md +5 -1
  108. package/docs/release-tooling.7.md +22 -0
  109. package/docs/routines.md +4 -4
  110. package/docs/run-registry-control-plane.7.md +23 -19
  111. package/docs/run-retention-reclamation.7.md +24 -3
  112. package/docs/scheduled-tasks.md +14 -14
  113. package/docs/security-trust-hardening.7.md +15 -15
  114. package/docs/state-explosion-management.7.md +14 -10
  115. package/docs/team-collaboration.7.md +4 -0
  116. package/docs/trust-audit-anchor.7.md +4 -0
  117. package/docs/unix-principles.md +3 -1
  118. package/docs/verifier-gated-commit.7.md +1 -1
  119. package/docs/web-desktop-workbench.7.md +4 -0
  120. package/docs/workflow-app-framework.7.md +13 -13
  121. package/manifest/plugin.manifest.json +1 -1
  122. package/package.json +3 -2
  123. package/scripts/agents/claude-p-agent.js +2 -2
  124. package/scripts/block-unapproved-tag.sh +42 -6
  125. package/scripts/bump-version.js +27 -10
  126. package/scripts/canonical-apps.js +4 -4
  127. package/scripts/children/batch-delegate-child.js +52 -2
  128. package/scripts/dogfood-release.js +1 -1
  129. package/scripts/fake-date-for-reproduction.js +44 -0
  130. package/scripts/golden-path.js +4 -4
  131. package/scripts/purity-baseline.json +0 -27
  132. package/scripts/release-flow.js +194 -13
  133. package/scripts/release-oneclick.js +407 -0
  134. package/scripts/verdict-keygen.js +83 -0
  135. package/scripts/verify-bump-reproduction.sh +148 -0
  136. package/scripts/verify-verdict-signature.js +61 -0
  137. package/scripts/version-sync-check.js +39 -22
  138. package/skills/cool-workflow/SKILL.md +9 -9
  139. package/skills/cool-workflow/references/commands.md +89 -88
  140. package/ui/workbench/app.css +37 -1
  141. package/ui/workbench/app.js +124 -6
@@ -15,15 +15,22 @@
15
15
  // Modes:
16
16
  // node release-flow.js [--check] gate + review, no mutation (default)
17
17
  // node release-flow.js --cut --version x.y.z [--push]
18
- // also bump:version, commit verdict, tag,
19
- // (push), and — when --push — create the
20
- // GitHub Release for the tag (idempotent;
21
- // opt out with --no-release)
18
+ // preflight (fail-fast checks BEFORE the
19
+ // gate/vendor/reviewer spend anything),
20
+ // then gate + review, then bump:version,
21
+ // commit verdict, tag, and — when --push —
22
+ // push ONLY the tag (refs/tags/vx.y.z; the
23
+ // verdict commit is a one-hop leaf off the
24
+ // reviewed main tip, it never lands on a
25
+ // branch) and create the GitHub Release
26
+ // (idempotent; opt out with --no-release)
22
27
  // node release-flow.js --release --version x.y.z create-or-skip the GitHub Release for an
23
28
  // already-pushed tag (backfill); no gate/cut.
24
29
  // Fails closed if gh can't create it; add
25
30
  // --soft for best-effort (skip-not-fail).
26
- // Flags also accepted: --prev-tag <t>, --agent-command "...", --agent-model m, --dry-run
31
+ // Flags also accepted: --prev-tag <t>, --agent-command "...", --agent-model m, --dry-run,
32
+ // --preflight-only (run the cut preflight and stop — used by release-oneclick.js),
33
+ // --allow-stale-head (skip the HEAD == origin/main tip check, deliberate re-cuts only)
27
34
  //
28
35
  // VERDICT CONTRACT (auto-generated by --cut, never write by hand):
29
36
  // Path: .cw-release/review-<FULLSHA>.verdict
@@ -31,6 +38,17 @@
31
38
  // Both release-flow.js --cut and CI's release-gate.yml verify this exact path and
32
39
  // format. The file MUST be committed to the tag's history.
33
40
  //
41
+ // VERDICT SIGNING (optional, opt-in — see scripts/verdict-keygen.js):
42
+ // Set CW_RELEASE_VERDICT_PRIVKEY (a path or inline PEM) and this script signs
43
+ // the verdict file's exact bytes, writing a sidecar
44
+ // .cw-release/review-<FULLSHA>.verdict.sig (base64 ed25519). Once
45
+ // .cw-release/verdict-signing.pub is committed to the repo, release-gate.yml,
46
+ // npm-publish.yml, and block-unapproved-tag.sh all start REQUIRING a valid
47
+ // signature in addition to the APPROVED text check — closing the gap where
48
+ // anyone with shell access could hand-write a passing verdict file. Until
49
+ // that public key is committed, every check stays exactly as before
50
+ // (grep-only) — this is inert by default, never a silent behavior change.
51
+ //
34
52
  // Test seams (smoke/operator only, never the delegated agent):
35
53
  // CW_RELEASE_FLOW_GATE_CMD overrides the deterministic gate command
36
54
  // (default: `bash <thisdir>/release-gate.sh`) so the smoke can exercise the
@@ -48,6 +66,7 @@ const fs = require("node:fs");
48
66
  const path = require("node:path");
49
67
  const https = require("node:https");
50
68
  const http = require("node:http");
69
+ const crypto = require("node:crypto");
51
70
  const { spawnSync } = require("node:child_process");
52
71
 
53
72
  const scriptsDir = __dirname;
@@ -364,6 +383,46 @@ function verifyVerdict(resultPath) {
364
383
  return cap;
365
384
  }
366
385
 
386
+ // ---- verdict signing (optional, opt-in — see the header comment) -----------
387
+ function resolveVerdictPrivateKey(value) {
388
+ const trimmed = String(value).trim();
389
+ if (trimmed.includes("BEGIN") && trimmed.includes("KEY")) return trimmed;
390
+ if (fs.existsSync(trimmed)) return fs.readFileSync(trimmed, "utf8");
391
+ die(`CW_RELEASE_VERDICT_PRIVKEY does not point to a readable key file or inline PEM: ${trimmed}`);
392
+ }
393
+
394
+ /** Called only after verifyVerdict() has confirmed resultPath's bytes are a
395
+ * genuine "APPROVED <HEAD>" verdict. A no-op unless CW_RELEASE_VERDICT_PRIVKEY
396
+ * is set. When it IS set, a signing failure fails closed (die()) rather than
397
+ * silently emitting an unsigned verdict — an operator who deliberately
398
+ * configured a signing key should never end up with the same on-disk state
399
+ * as one who didn't. */
400
+ function signVerdictIfConfigured(resultPath) {
401
+ const raw = process.env.CW_RELEASE_VERDICT_PRIVKEY;
402
+ if (!raw || !raw.trim()) return;
403
+ const pem = resolveVerdictPrivateKey(raw);
404
+ const sigPath = `${resultPath}.sig`;
405
+ try {
406
+ const key = crypto.createPrivateKey(pem);
407
+ // crypto.sign(null, ...) does NOT throw for an RSA or EC key — it silently
408
+ // signs with that algorithm's own default digest, producing a
409
+ // syntactically valid signature that can never verify against the
410
+ // committed ed25519 public key. Catching that here, at signing time,
411
+ // means a misconfigured key dies closed locally instead of only being
412
+ // caught later by CI (or not at all, if the repo hasn't committed
413
+ // verdict-signing.pub yet).
414
+ if (key.asymmetricKeyType !== "ed25519") {
415
+ die(`CW_RELEASE_VERDICT_PRIVKEY is a ${key.asymmetricKeyType || "unknown"} key, not ed25519 — generate one with scripts/verdict-keygen.js`);
416
+ }
417
+ const message = fs.readFileSync(resultPath);
418
+ const signature = crypto.sign(null, message, key).toString("base64");
419
+ fs.writeFileSync(sigPath, `${signature}\n`);
420
+ } catch (error) {
421
+ die(`failed to sign the verdict with CW_RELEASE_VERDICT_PRIVKEY: ${error.message}`);
422
+ }
423
+ say(`verdict signed → ${path.relative(repoRoot, sigPath)}`);
424
+ }
425
+
367
426
  // ---- GitHub Release (optional, presentation/distribution) ------------------
368
427
  // NOT a correctness gate: the load-bearing artifacts (the tag + the
369
428
  // provenance-attested npm publish) already exist when this runs. So `gh` is NOT
@@ -398,13 +457,22 @@ function prevTagOf(version) {
398
457
  return git(["describe", "--tags", "--abbrev=0", `v${version}^`]).out || "";
399
458
  }
400
459
 
460
+ // Full regex escape (same shape as parity-check.js's helper — scripts stay
461
+ // zero-dependency single files, so it is copied, not required). The version
462
+ // is SEMVER-validated on every path that reaches here, but escaping every
463
+ // metacharacter costs nothing and keeps the RegExp construction safe on its
464
+ // own terms.
465
+ function escapeRegExp(value) {
466
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
467
+ }
468
+
401
469
  // Extract the `## <version>` section body from the CHANGELOG AS SHIPPED AT THE
402
470
  // TAG (git show), so the notes reflect what that tag actually carried.
403
471
  function changelogSection(version) {
404
472
  const show = git(["show", `v${version}:CHANGELOG.md`]);
405
473
  if (show.code !== 0 || !show.out) return "";
406
474
  const lines = show.out.split(/\r?\n/);
407
- const esc = version.replace(/\./g, "\\.");
475
+ const esc = escapeRegExp(version);
408
476
  const startRe = new RegExp(`^## \\[?${esc}\\b`);
409
477
  let start = -1;
410
478
  for (let i = 0; i < lines.length; i++) if (startRe.test(lines[i])) { start = i; break; }
@@ -506,10 +574,102 @@ function releaseGitHub(version, { required = false } = {}) {
506
574
  return true;
507
575
  }
508
576
 
577
+ // ---- 2b. cut preflight (fail fast, BEFORE any paid step) --------------------
578
+ // Every check here answers one question: "will this cut die AFTER the gate,
579
+ // the live vendor preflight, and the reviewer have already run?" Each check
580
+ // was a real post-reviewer failure in the v0.2.3 cut (2026-07-10) — a missing
581
+ // --version, an unsigned verdict the CI gate then rejected, a left-over local
582
+ // tag, and a two-hop recovery that broke the HEAD~1 topology rule. All of
583
+ // them are answerable in under a second, so answer them FIRST.
584
+ //
585
+ // Fixture safety: each check is gated on the thing it checks being present
586
+ // (a committed signing pubkey, a CHANGELOG.md, an `origin` remote), so the
587
+ // smoke fixtures — bare repos with none of those — pass through untouched.
588
+ function preflightCut() {
589
+ say("[0/3] cut preflight");
590
+ // (a) version — cheap and load-bearing for every later step. Checking it
591
+ // here (not only inside cut()) is what stops a full gate+reviewer run from
592
+ // being thrown away over a missing flag.
593
+ if (!cutVersion || !/^\d+\.\d+\.\d+$/.test(cutVersion)) die("--cut requires --version x.y.z");
594
+
595
+ // (b) signing — once .cw-release/verdict-signing.pub is committed, CI's
596
+ // release-gate REQUIRES a valid signature; a cut without the private key
597
+ // configured would push an unsigned verdict that fails only in CI. Fail
598
+ // closed here instead, and also prove the configured key IS the committed
599
+ // key (a wrong key signs "successfully" and still fails in CI).
600
+ const pubPath = path.join(repoRoot, ".cw-release", "verdict-signing.pub");
601
+ if (fs.existsSync(pubPath)) {
602
+ const raw = process.env.CW_RELEASE_VERDICT_PRIVKEY;
603
+ if (!raw || !raw.trim()) {
604
+ die(
605
+ "verdict-signing.pub is committed, so CI requires a SIGNED verdict — set the private key first",
606
+ 'fix: export CW_RELEASE_VERDICT_PRIVKEY=~/.cw-keys/verdict-signing.key'
607
+ );
608
+ }
609
+ const pem = resolveVerdictPrivateKey(raw);
610
+ let key;
611
+ try {
612
+ key = crypto.createPrivateKey(pem);
613
+ } catch (error) {
614
+ die(`CW_RELEASE_VERDICT_PRIVKEY is not a readable private key: ${error.message}`);
615
+ }
616
+ if (key.asymmetricKeyType !== "ed25519") {
617
+ die(`CW_RELEASE_VERDICT_PRIVKEY is a ${key.asymmetricKeyType || "unknown"} key, not ed25519 — generate one with scripts/verdict-keygen.js`);
618
+ }
619
+ const derivedPub = crypto.createPublicKey(key).export({ type: "spki", format: "pem" }).toString().trim();
620
+ const committedPub = fs.readFileSync(pubPath, "utf8").trim();
621
+ if (derivedPub !== committedPub) {
622
+ die("CW_RELEASE_VERDICT_PRIVKEY does not match the committed .cw-release/verdict-signing.pub — wrong key; CI would reject its signature");
623
+ }
624
+ }
625
+
626
+ // (c) tag must not exist yet — a stale local tag makes cut() die at the very
627
+ // last step ("tag 'vX.Y.Z' already exists") after everything else has run.
628
+ if (git(["tag", "-l", `v${cutVersion}`]).out) {
629
+ die(`tag v${cutVersion} already exists locally — delete it first (git tag -d v${cutVersion}) or pick the next version`);
630
+ }
631
+
632
+ // (d) CHANGELOG must already carry the release section — release notes get
633
+ // pasted verbatim into the GitHub Release, and bump:version's content gate
634
+ // would only catch this inside cut(), after the reviewer.
635
+ const changelogPath = path.join(repoRoot, "CHANGELOG.md");
636
+ if (fs.existsSync(changelogPath) && !fs.readFileSync(changelogPath, "utf8").includes(`## ${cutVersion}`)) {
637
+ die(`CHANGELOG.md has no "## ${cutVersion}" section — write the release notes first`);
638
+ }
639
+
640
+ // (e) clean tree (tracked files only — cut()'s `git add -u` can never sweep
641
+ // an untracked stray in, so untracked files are not a cut hazard).
642
+ const dirty = git(["status", "--porcelain", "-uno"]).out;
643
+ if (dirty) die("working tree has tracked modifications — commit or stash them before a cut", dirty);
644
+
645
+ // (f) network-dependent checks, only when this cut will actually push and
646
+ // only when an `origin` remote exists (smoke fixtures have none):
647
+ if (PUSH && git(["remote"]).out.split("\n").includes("origin")) {
648
+ // remote tag must not exist (someone/some run already published this version)
649
+ const remoteTag = git(["ls-remote", "origin", `refs/tags/v${cutVersion}`]);
650
+ if (remoteTag.code === 0 && remoteTag.out) {
651
+ die(`tag v${cutVersion} already exists on origin — this version is already published; pick the next version`);
652
+ }
653
+ // HEAD must be the current origin/main tip — cutting a stale HEAD publishes
654
+ // old code (nearly happened on the v0.2.2 cut).
655
+ if (git(["fetch", "origin", "main", "--quiet"]).code !== 0) {
656
+ die("git fetch origin main failed — a --push cut needs the network anyway; fix connectivity first");
657
+ }
658
+ const mainTip = git(["rev-parse", "origin/main"]).out;
659
+ if (mainTip !== HEAD && !has("--allow-stale-head")) {
660
+ die(
661
+ `HEAD (${HEAD.slice(0, 12)}) is not the origin/main tip (${mainTip.slice(0, 12)}) — a cut must release exactly what main has`,
662
+ "fix: git fetch origin main && git checkout -B <branch> origin/main (or pass --allow-stale-head if this is deliberate)"
663
+ );
664
+ }
665
+ }
666
+ say("cut preflight OK");
667
+ }
668
+
509
669
  // ---- 3. optional cut (bump + commit verdict + tag + push) ------------------
510
670
  function cut(resultPath, capability) {
511
671
  if (!cutVersion || !/^\d+\.\d+\.\d+$/.test(cutVersion)) die("--cut requires --version x.y.z");
512
- if (DRY_RUN) { say(`[dry-run] would: bump:version ${cutVersion}, commit verdict, tag v${cutVersion}${PUSH ? ", push" : ""}`); return; }
672
+ if (DRY_RUN) { say(`[dry-run] would: bump:version ${cutVersion}, commit verdict, tag v${cutVersion}${PUSH ? `, push refs/tags/v${cutVersion} (tag only, no branch)` : ""}`); return; }
513
673
  const bump = spawnSync("npm", ["run", "bump:version", "--", cutVersion], { cwd: pluginRoot, encoding: "utf8", stdio: "inherit" });
514
674
  if (bump.status !== 0) die("bump:version failed");
515
675
  // Regenerate the gated project index after the version bump (PR #87 gate).
@@ -522,19 +682,27 @@ function cut(resultPath, capability) {
522
682
  // operator's local home path) must never ride into the immutable tag commit
523
683
  // (that tripped pii-redaction-smoke and red-failed release-gate for v0.1.96).
524
684
  // `git add -u` touches tracked files only, so no untracked file can be swept in;
525
- // the verdict is the single new path the cut is allowed to add.
685
+ // the verdict (and its .sig sidecar, when verdict signing is configured) are
686
+ // the only new paths the cut is allowed to add.
526
687
  git(["add", "-u"]);
527
688
  git(["add", "--", path.relative(repoRoot, resultPath)]);
689
+ const sigPath = `${resultPath}.sig`;
690
+ if (fs.existsSync(sigPath)) git(["add", "--", path.relative(repoRoot, sigPath)]);
528
691
  const commit = git(["commit", "-m", `chore(release): record APPROVED reviewer verdict for v${cutVersion}`]);
529
692
  if (commit.code !== 0) die("verdict commit failed", commit.err);
530
693
  const tag = git(["tag", "-a", `v${cutVersion}`, "-m", `v${cutVersion}: ${capability || "release"}`]);
531
694
  if (tag.code !== 0) die("git tag failed", tag.err);
532
695
  if (PUSH) {
533
- // Atomic: the verdict commit on HEAD and the tag land together or not at all.
534
- // A non-atomic two-push could leave main advanced with no tag, so CI's
535
- // release-gate (which fires on the tag) never runs and the release silently stalls.
536
- const push = git(["push", "--atomic", "origin", "HEAD", `v${cutVersion}`]);
537
- if (push.code !== 0) die("atomic push of HEAD + tag failed (nothing partially pushed)", push.err);
696
+ // Push ONLY the tag. The verdict commit is a one-hop leaf on the reviewed
697
+ // main-tip commit, reachable through the tag alone it does not need to
698
+ // be on any branch. Pushing HEAD here used to (a) hit main's branch
699
+ // protection (enforce_admins blocks even the owner's direct push) or
700
+ // (b) mint a stray remote branch named after whatever branch the cut ran
701
+ // from. Tags are not protected in this repo, and CI's release-gate checks
702
+ // the TAG's own history (verdict at HEAD or HEAD~1), so the tag alone is
703
+ // the complete release artifact — proven live by the v0.2.3 cut.
704
+ const push = git(["push", "origin", `refs/tags/v${cutVersion}`]);
705
+ if (push.code !== 0) die("tag push failed (nothing was pushed)", push.err);
538
706
  }
539
707
  say(`tagged v${cutVersion}${PUSH ? " and pushed" : " (local only; push when ready)"}`);
540
708
  // Finishing step: create the GitHub Release for the just-pushed tag. Only when
@@ -559,6 +727,18 @@ function main() {
559
727
  return;
560
728
  }
561
729
 
730
+ // Fail-fast checks FIRST — nothing below (gate, live vendor calls, the
731
+ // reviewer) runs until every cheap precondition holds. --preflight-only
732
+ // lets an orchestrator (release-oneclick.js) run just this step. Without
733
+ // --cut there is no preflight to run, so answering ok:true would be a
734
+ // green light with zero checks done — refuse instead.
735
+ if (has("--preflight-only") && !MODE_CUT) die("--preflight-only requires --cut (there is no preflight to run in check/release mode)");
736
+ if (MODE_CUT) preflightCut();
737
+ if (has("--preflight-only")) {
738
+ process.stdout.write(`${JSON.stringify({ ok: true, mode: "preflight", version: cutVersion || null }, null, 2)}\n`);
739
+ return;
740
+ }
741
+
562
742
  const markerDir = path.join(repoRoot, ".cw-release");
563
743
  fs.mkdirSync(markerDir, { recursive: true });
564
744
  const resultPath = path.join(markerDir, `review-${HEAD}.verdict`);
@@ -569,6 +749,7 @@ function main() {
569
749
  fs.writeFileSync(inputPath, buildReviewerInput(resultPath));
570
750
  delegateReview(resultPath, inputPath);
571
751
  const capability = verifyVerdict(resultPath);
752
+ signVerdictIfConfigured(resultPath);
572
753
 
573
754
  if (MODE_CUT) cut(resultPath, capability);
574
755