cool-workflow 0.2.4 → 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 (155) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/.gemini-plugin/mcp.json +10 -0
  4. package/.gemini-plugin/plugin.json +40 -0
  5. package/.opencode-plugin/mcp.json +10 -0
  6. package/.opencode-plugin/plugin.json +40 -0
  7. package/README.md +204 -48
  8. package/apps/architecture-review/app.json +1 -1
  9. package/apps/architecture-review-fast/app.json +1 -1
  10. package/apps/end-to-end-golden-path/app.json +1 -1
  11. package/apps/pr-review-fix-ci/app.json +1 -1
  12. package/apps/release-cut/app.json +1 -1
  13. package/apps/research-synthesis/app.json +1 -1
  14. package/dist/cli/dispatch.js +32 -4
  15. package/dist/cli/entry.js +11 -19
  16. package/dist/cli/global-flags.js +113 -0
  17. package/dist/cli/parseargv.js +7 -1
  18. package/dist/core/capability-data.js +337 -8
  19. package/dist/core/format/help.js +73 -3
  20. package/dist/core/format/recovery-hint.js +32 -0
  21. package/dist/core/multi-agent/collaboration.js +35 -6
  22. package/dist/core/multi-agent/runtime.js +7 -0
  23. package/dist/core/multi-agent/trust-policy.js +7 -1
  24. package/dist/core/pipeline/contract.js +7 -0
  25. package/dist/core/pipeline/error-feedback.js +2 -2
  26. package/dist/core/state/run-paths.js +3 -30
  27. package/dist/core/trust/evidence-grounding.js +13 -1
  28. package/dist/core/util/cli-args.js +22 -0
  29. package/dist/core/version.js +1 -1
  30. package/dist/mcp/dispatch.js +22 -2
  31. package/dist/mcp/server.js +215 -36
  32. package/dist/mcp/tool-process.js +181 -0
  33. package/dist/mcp-server.js +28 -1
  34. package/dist/shell/agent-config.js +11 -2
  35. package/dist/shell/audit-cli.js +1 -1
  36. package/dist/shell/commit.js +8 -2
  37. package/dist/shell/coordinator-io.js +73 -1
  38. package/dist/shell/drive.js +195 -128
  39. package/dist/shell/error-feedback-io.js +6 -0
  40. package/dist/shell/execution-backend/agent.js +201 -26
  41. package/dist/shell/execution-backend/container.js +44 -10
  42. package/dist/shell/execution-backend/local.js +33 -0
  43. package/dist/shell/fs-atomic.js +96 -12
  44. package/dist/shell/ledger-cli.js +9 -2
  45. package/dist/shell/metrics-cli.js +1 -1
  46. package/dist/shell/multi-agent-cli.js +9 -2
  47. package/dist/shell/multi-agent-operator-ux.js +1 -1
  48. package/dist/shell/observability.js +7 -4
  49. package/dist/shell/onramp.js +48 -5
  50. package/dist/shell/operator-ux.js +1 -1
  51. package/dist/shell/perf-trace.js +136 -0
  52. package/dist/shell/pipeline.js +3 -2
  53. package/dist/shell/reclamation-io.js +10 -9
  54. package/dist/shell/report-view-cli.js +2 -0
  55. package/dist/shell/run-export-cli.js +5 -2
  56. package/dist/shell/run-export.js +293 -31
  57. package/dist/shell/run-registry-io.js +8 -0
  58. package/dist/shell/run-store.js +183 -3
  59. package/dist/shell/scheduler-io.js +101 -10
  60. package/dist/shell/state-explosion-cli.js +4 -1
  61. package/dist/shell/telemetry-demo.js +1 -1
  62. package/dist/shell/telemetry-ledger-io.js +36 -24
  63. package/dist/shell/trust-audit.js +178 -36
  64. package/dist/shell/workbench-host.js +121 -10
  65. package/dist/shell/workbench.js +83 -6
  66. package/dist/shell/worker-isolation.js +1 -1
  67. package/dist/wiring/capability-table/basics.js +5 -0
  68. package/dist/wiring/capability-table/exec-backend.js +40 -22
  69. package/dist/wiring/capability-table/parity.js +10 -5
  70. package/dist/wiring/capability-table/pipeline.js +32 -0
  71. package/dist/wiring/capability-table/registry-core.js +26 -3
  72. package/dist/wiring/capability-table/reporting.js +7 -1
  73. package/dist/wiring/capability-table/scheduling-registry.js +8 -2
  74. package/dist/wiring/capability-table/trust-ledger.js +54 -12
  75. package/docs/agent-delegation-drive.7.md +17 -0
  76. package/docs/cli-mcp-parity.7.md +46 -5
  77. package/docs/contract-migration-tooling.7.md +4 -0
  78. package/docs/control-plane-scheduling.7.md +38 -0
  79. package/docs/cross-agent-ledger.7.md +20 -8
  80. package/docs/durable-state-and-locking.7.md +4 -0
  81. package/docs/evidence-adoption-reasoning-chain.7.md +4 -0
  82. package/docs/execution-backends.7.md +4 -0
  83. package/docs/fix.7.md +4 -4
  84. package/docs/getting-started.md +40 -32
  85. package/docs/index.md +17 -0
  86. package/docs/launch/demo.tape +4 -3
  87. package/docs/mcp-app-surface.7.md +27 -0
  88. package/docs/multi-agent-cli-mcp-surface.7.md +4 -0
  89. package/docs/multi-agent-eval-replay-harness.7.md +4 -0
  90. package/docs/multi-agent-operator-ux.7.md +4 -0
  91. package/docs/node-snapshot-diff-replay.7.md +4 -0
  92. package/docs/observability-cost-accounting.7.md +4 -0
  93. package/docs/project-index.md +53 -9
  94. package/docs/real-execution-backends.7.md +4 -0
  95. package/docs/release-and-migration.7.md +4 -0
  96. package/docs/release-tooling.7.md +12 -0
  97. package/docs/routine.7.md +22 -0
  98. package/docs/run-registry-control-plane.7.md +37 -9
  99. package/docs/run-retention-reclamation.7.md +12 -4
  100. package/docs/state-explosion-management.7.md +4 -0
  101. package/docs/team-collaboration.7.md +4 -0
  102. package/docs/trust-audit-anchor.7.md +4 -0
  103. package/docs/web-desktop-workbench.7.md +72 -2
  104. package/docs/workflow-app-framework.7.md +31 -0
  105. package/manifest/plugin.manifest.json +1 -1
  106. package/manifest/source-context-profiles.json +4 -3
  107. package/package.json +6 -3
  108. package/scripts/agents/agent-adapter-core.js +26 -0
  109. package/scripts/agents/claude-p-agent.js +4 -1
  110. package/scripts/agents/codex-agent.js +4 -0
  111. package/scripts/agents/cw-attest-wrap.js +1 -1
  112. package/scripts/agents/gemini-agent.js +4 -0
  113. package/scripts/agents/opencode-agent.js +5 -0
  114. package/scripts/block-unapproved-tag.js +160 -0
  115. package/scripts/canonical-apps.js +4 -4
  116. package/scripts/children/batch-delegate-child.js +72 -4
  117. package/scripts/children/http-batch-delegate-child.js +132 -0
  118. package/scripts/children/http-delegate-child.js +8 -0
  119. package/scripts/dogfood-release.js +2 -2
  120. package/scripts/fake-date-for-reproduction.js +1 -1
  121. package/scripts/gen-manifests.js +32 -61
  122. package/scripts/golden-path.js +4 -4
  123. package/scripts/lang-policy-check.js +82 -0
  124. package/scripts/parity-check.js +42 -23
  125. package/scripts/purity-baseline.json +0 -3
  126. package/scripts/purity-gate.js +16 -3
  127. package/scripts/release-check.js +2 -1
  128. package/scripts/release-flow.js +169 -41
  129. package/scripts/release-gate.js +208 -0
  130. package/scripts/release-oneclick.js +38 -7
  131. package/scripts/release-tags.js +47 -0
  132. package/scripts/schema-version-inventory.json +31 -0
  133. package/scripts/validate-run-state-schema.js +95 -4
  134. package/scripts/verdict-keygen.js +2 -2
  135. package/scripts/verify-bump-reproduction.js +193 -0
  136. package/scripts/verify-release-verdict.js +139 -0
  137. package/scripts/verify-verdict-signature.js +1 -1
  138. package/ui/workbench/app.css +50 -10
  139. package/ui/workbench/app.js +269 -34
  140. package/ui/workbench/index.html +4 -2
  141. package/ui/workbench/inspection.js +51 -0
  142. package/ui/workbench/navigation.js +44 -0
  143. package/docs/agent-framework.md +0 -177
  144. package/docs/designs/handoff-ledger.md +0 -145
  145. package/docs/dogfood/resume-drive-real-agent-2026-06-14.md +0 -40
  146. package/docs/launch/launch-kit.md +0 -195
  147. package/docs/launch/pre-launch-checklist.md +0 -53
  148. package/docs/readme-v0.1.87-full.md +0 -301
  149. package/docs/routines.md +0 -101
  150. package/docs/scheduled-tasks.md +0 -80
  151. package/scripts/agents/claude-p-agent.sh +0 -9
  152. package/scripts/block-unapproved-tag.sh +0 -75
  153. package/scripts/release-gate.sh +0 -94
  154. package/scripts/verify-bump-reproduction.sh +0 -148
  155. package/scripts/verify-container-selfref.js +0 -64
@@ -0,0 +1,193 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ // verify-bump-reproduction.js — closes the HEAD~1 verdict-replay bypass.
4
+ //
5
+ // release-gate.yml / npm-publish.yml tolerate a verdict written for the tag
6
+ // commit's PARENT (release-flow.js's cut() reviews content, then adds a
7
+ // mechanical version-bump + the verdict as a NEW child commit that actually
8
+ // gets tagged). A validly-signed verdict at the parent proves someone once
9
+ // approved THAT commit — it does NOT prove the child (the thing actually being
10
+ // tagged) is the deterministic bump cut() would have produced, rather than an
11
+ // attacker's own commit smuggling in arbitrary changes on top of an old,
12
+ // genuinely-approved parent (a real signature on a REPLAYED verdict).
13
+ //
14
+ // This script independently reproduces the bump: checks out the approved
15
+ // parent into a scratch worktree, runs the SAME bump:version/sync:project-index
16
+ // steps cut() runs, stages the SAME way cut() stages (git add -u + the
17
+ // explicit verdict/.sig paths, never -A), and requires the resulting tree to
18
+ // match the ACTUAL tagged commit's tree byte-for-byte. Any difference —
19
+ // anywhere, including a single added file — fails closed.
20
+ //
21
+ // (Node port of the former verify-bump-reproduction.sh; behavior kept line
22
+ // for line — same steps, same messages, same exit map.)
23
+ //
24
+ // Usage: verify-bump-reproduction.js <approved-parent-sha> <tagged-sha> <verdict-repo-relative-path> [sig-repo-relative-path]
25
+ //
26
+ // Exit 0: the tagged commit's tree matches the reproduced bump exactly.
27
+ // Exit 1: mismatch, or any step failed (install, bump, sync, worktree, etc).
28
+
29
+ const fs = require("node:fs");
30
+ const os = require("node:os");
31
+ const path = require("node:path");
32
+ const { spawnSync } = require("node:child_process");
33
+
34
+ // Where THIS SCRIPT ITSELF lives — distinct from REPO_ROOT (the git repo being
35
+ // operated ON, resolved below from the invocation cwd). These coincide in
36
+ // ordinary production use (the script runs from within the same checkout it
37
+ // verifies), but conflating them is wrong: REPO_ROOT could be an ARBITRARY git
38
+ // context (e.g. a test harness's throwaway clone of old history, which never
39
+ // has this script's own sibling files, since they postdate whatever was
40
+ // cloned). Sibling files this script depends on must be found relative to
41
+ // where it is installed, never relative to the repo it happens to be pointed at.
42
+ const SCRIPT_DIR = __dirname;
43
+
44
+ function err(msg) {
45
+ process.stderr.write(`${msg}\n`);
46
+ }
47
+
48
+ const [, , PARENT, TAGGED, VERDICT_REL, SIG_REL = ""] = process.argv;
49
+ if (!PARENT || !TAGGED || !VERDICT_REL) {
50
+ err("usage: verify-bump-reproduction.js <parent-sha> <tagged-sha> <verdict-rel-path> [sig-rel-path]");
51
+ process.exit(1);
52
+ }
53
+
54
+ function run(cmd, args, opts = {}) {
55
+ return spawnSync(cmd, args, { encoding: "utf8", ...opts });
56
+ }
57
+ function gitIn(dir, args, opts = {}) {
58
+ return run("git", ["-C", dir, ...args], opts);
59
+ }
60
+
61
+ const top = run("git", ["rev-parse", "--show-toplevel"]);
62
+ if (top.status !== 0) process.exit(1);
63
+ const REPO_ROOT = top.stdout.trim();
64
+
65
+ const SCRATCH_PARENT = fs.mkdtempSync(path.join(os.tmpdir(), "cw-bump-repro-"));
66
+ const SCRATCH = path.join(SCRATCH_PARENT, "wt");
67
+ function cleanup() {
68
+ gitIn(REPO_ROOT, ["worktree", "remove", "--force", SCRATCH], { stdio: "ignore" });
69
+ fs.rmSync(SCRATCH_PARENT, { recursive: true, force: true });
70
+ }
71
+
72
+ function main() {
73
+ const wt = gitIn(REPO_ROOT, ["worktree", "add", "--quiet", "--detach", SCRATCH, PARENT], { stdio: "ignore" });
74
+ if (wt.status !== 0) {
75
+ err(`verify-bump-reproduction: could not create a scratch worktree at ${PARENT}`);
76
+ return 1;
77
+ }
78
+
79
+ // The target version comes from the TAGGED commit (not the parent, which may
80
+ // still be pre-bump) — read via git plumbing, no separate checkout needed.
81
+ const pkgShow = gitIn(REPO_ROOT, ["show", `${TAGGED}:plugins/cool-workflow/package.json`]);
82
+ let VERSION = "";
83
+ if (pkgShow.status === 0) {
84
+ try {
85
+ VERSION = JSON.parse(pkgShow.stdout).version || "";
86
+ } catch {
87
+ VERSION = "";
88
+ }
89
+ }
90
+ if (!VERSION) {
91
+ err(`verify-bump-reproduction: could not read package.json version from tagged commit ${TAGGED}`);
92
+ return 1;
93
+ }
94
+
95
+ // sync-project-index.js embeds a wall-clock "Generated on <date>" line. Pin it
96
+ // to the TAGGED commit's own committer date (UTC) via a Node --require preload
97
+ // that overrides the global Date constructor (fake-date-for-reproduction.js),
98
+ // NOT an application-level env var: the scratch worktree runs the APPROVED
99
+ // PARENT's OWN checked-out copy of sync-project-index.js, which for every
100
+ // release cut before this mechanism existed has no idea any such env var is
101
+ // meant to be read — an app-level opt-in is a no-op against code that doesn't
102
+ // know to opt in, so it can only ever "work" for releases cut after the
103
+ // mechanism landed (confirmed empirically: re-running an already-shipped
104
+ // release's real approved-parent/tagged pair on a later calendar day produced
105
+ // a tree mismatch whose ONLY diff was that one date line). A global override
106
+ // of the Date constructor, injected before any application code runs, is
107
+ // transparent to the code being executed regardless of which version it is.
108
+ const dateShow = gitIn(REPO_ROOT, ["show", "-s", "--format=%cd", "--date=format-local:%Y-%m-%d", TAGGED], {
109
+ env: { ...process.env, TZ: "UTC" },
110
+ });
111
+ const CUT_DATE = dateShow.status === 0 ? dateShow.stdout.trim() : "";
112
+ if (!CUT_DATE) {
113
+ err("verify-bump-reproduction: could not read the tagged commit's committer date");
114
+ return 1;
115
+ }
116
+ const FAKE_DATE_PRELOAD = path.join(SCRIPT_DIR, "fake-date-for-reproduction.js");
117
+
118
+ // Each step's exit code is checked EXPLICITLY and immediately (not via one
119
+ // combined chain) — a chain's overall success can silently survive an earlier
120
+ // step's failure if a future edit ever loosens it, which would flip this
121
+ // fail-closed gate to fail-open without changing its outward shape.
122
+ // stdout/stderr from each step are captured (not discarded) so a real
123
+ // failure — a transient npm registry hiccup vs. an actual reproduction
124
+ // mismatch — is distinguishable in the workflow log instead of both
125
+ // collapsing into the same generic message.
126
+ const stepCwd = path.join(SCRATCH, "plugins", "cool-workflow");
127
+ const stepEnv = {
128
+ ...process.env,
129
+ NODE_OPTIONS: `--require ${FAKE_DATE_PRELOAD}`,
130
+ CW_FAKE_DATE: CUT_DATE,
131
+ };
132
+ function runStep(label, cmd, args) {
133
+ const r = run(cmd, args, { cwd: stepCwd, env: stepEnv });
134
+ const out = (r.stdout || "") + (r.stderr || "");
135
+ const code = r.error ? 1 : r.status;
136
+ if (code !== 0) {
137
+ err(`verify-bump-reproduction: ${label} failed (exit ${code}):`);
138
+ err(out);
139
+ return false;
140
+ }
141
+ return true;
142
+ }
143
+
144
+ const bumpOk =
145
+ runStep("npm install", "npm", ["install", "--no-package-lock", "--ignore-scripts"]) &&
146
+ runStep("bump:version", "npm", ["run", "bump:version", "--", VERSION]) &&
147
+ runStep("sync:project-index", "npm", ["run", "sync:project-index", "--", "--repo-only"]);
148
+ if (!bumpOk) {
149
+ err(`verify-bump-reproduction: reproducing the bump for ${PARENT} -> v${VERSION} failed`);
150
+ return 1;
151
+ }
152
+
153
+ fs.mkdirSync(path.join(SCRATCH, ".cw-release"), { recursive: true });
154
+ try {
155
+ fs.copyFileSync(path.join(REPO_ROOT, VERDICT_REL), path.join(SCRATCH, VERDICT_REL));
156
+ } catch {
157
+ return 1;
158
+ }
159
+ if (SIG_REL && fs.existsSync(path.join(REPO_ROOT, SIG_REL))) {
160
+ try {
161
+ fs.copyFileSync(path.join(REPO_ROOT, SIG_REL), path.join(SCRATCH, SIG_REL));
162
+ } catch {
163
+ return 1;
164
+ }
165
+ }
166
+
167
+ // Mirror cut()'s OWN staging exactly (git-add -u + the explicit verdict/.sig
168
+ // paths) — never -A, so an untracked stray left by npm/tooling in the scratch
169
+ // worktree can never silently ride into what we compare.
170
+ gitIn(SCRATCH, ["add", "-u"]);
171
+ gitIn(SCRATCH, ["add", "--", VERDICT_REL]);
172
+ if (SIG_REL && fs.existsSync(path.join(SCRATCH, SIG_REL))) {
173
+ gitIn(SCRATCH, ["add", "--", SIG_REL]);
174
+ }
175
+
176
+ const EXPECTED_TREE = gitIn(SCRATCH, ["write-tree"]).stdout.trim();
177
+ const ACTUAL_TREE = gitIn(REPO_ROOT, ["rev-parse", `${TAGGED}^{tree}`]).stdout.trim();
178
+
179
+ if (EXPECTED_TREE !== ACTUAL_TREE) {
180
+ err(`verify-bump-reproduction: ${TAGGED}'s tree (${ACTUAL_TREE}) does not match the deterministic bump reproduced from approved parent ${PARENT} (${EXPECTED_TREE}) — refusing`);
181
+ return 1;
182
+ }
183
+
184
+ return 0;
185
+ }
186
+
187
+ let code = 1;
188
+ try {
189
+ code = main();
190
+ } finally {
191
+ cleanup();
192
+ }
193
+ process.exit(code);
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ // verify-release-verdict.js — the one true "find a signed, committed release
4
+ // verdict for this commit" check. Both CI workflows call it:
5
+ // .github/workflows/release-gate.yml (cwd = repo root)
6
+ // .github/workflows/npm-publish.yml (cwd = plugins/cool-workflow)
7
+ // Before this file, each workflow kept its own ~35-line bash copy of this
8
+ // same loop, and the two copies could drift apart. Now the logic lives here
9
+ // only, and each workflow step is one line.
10
+ //
11
+ // One more win: a GitHub Actions `run:` step prints its FULL script text to
12
+ // the log before it runs. With the old inline bash, the "::error::" lines of
13
+ // the fail branch were printed on EVERY run — even a fully green one — and
14
+ // GitHub's log UI marked them red, which made a good release look bad. With
15
+ // a one-line `run:`, the "::error::" text only ever gets printed when this
16
+ // script truly fails.
17
+ //
18
+ // WHY the check is shaped this way (was: near-same comments in both YAMLs):
19
+ // The tag commit itself bumps version/changelog; the reviewed sha is its
20
+ // parent. So a verdict for HEAD or HEAD~1 is accepted. But a validly-signed
21
+ // verdict for the PARENT alone does not prove HEAD is the same mechanical
22
+ // bump release-flow.js's cut() would have made on top of the approved
23
+ // parent — it could be an attacker's own commit smuggling in changes on top
24
+ // of an old, truly-approved parent, replaying that parent's already-public
25
+ // verdict + signature (public git objects; no secret is needed to copy
26
+ // them). So when the match is at HEAD~1, HEAD must ALSO reproduce exactly
27
+ // as the deterministic bump (verify-bump-reproduction.js).
28
+ //
29
+ // The pubkey is read from origin/main, NOT from the checked-out tree. The
30
+ // checked-out tree IS the commit under test — an attacker's tag could just
31
+ // delete .cw-release/verdict-signing.pub and fall back to a no-signature
32
+ // check. origin/main is a ref the tag being judged cannot rewrite. Until a
33
+ // pubkey is committed there, this stays a first-line-only text check
34
+ // (opt-in, backward compatible — see scripts/verdict-keygen.js).
35
+ //
36
+ // cwd does not matter: everything is anchored on
37
+ // `git rev-parse --show-toplevel`, and the two helper scripts are loaded
38
+ // from this script's own directory (never from the repo under test).
39
+ //
40
+ // Usage: verify-release-verdict.js [--context "<one sentence for the fail message>"]
41
+ // Exit 0: a valid verdict was found. Exit 1: none found — the two
42
+ // "::error::" lines are printed here and nowhere else.
43
+
44
+ const fs = require("node:fs");
45
+ const os = require("node:os");
46
+ const path = require("node:path");
47
+ const { spawnSync } = require("node:child_process");
48
+
49
+ // Where THIS SCRIPT lives — the helper scripts are siblings of the installed
50
+ // script, never files of the (possibly throwaway) repo being checked. Same
51
+ // rule as in verify-bump-reproduction.js.
52
+ const VERIFY_SIG = path.join(__dirname, "verify-verdict-signature.js");
53
+ const VERIFY_BUMP = path.join(__dirname, "verify-bump-reproduction.js");
54
+
55
+ function git(args) {
56
+ const r = spawnSync("git", args, { encoding: "utf8" });
57
+ return r.status === 0 ? r.stdout.trim() : "";
58
+ }
59
+
60
+ function main() {
61
+ let context = "The release flow was bypassed.";
62
+ const argv = process.argv.slice(2);
63
+ for (let i = 0; i < argv.length; i++) {
64
+ if (argv[i] === "--context" && argv[i + 1]) context = argv[++i];
65
+ }
66
+
67
+ const REPO_ROOT = git(["rev-parse", "--show-toplevel"]);
68
+ if (!REPO_ROOT) {
69
+ process.stderr.write("verify-release-verdict: not inside a git repo\n");
70
+ return 1;
71
+ }
72
+
73
+ const SHA = git(["-C", REPO_ROOT, "rev-parse", "HEAD^{commit}"]);
74
+ if (!SHA) {
75
+ process.stderr.write("verify-release-verdict: could not resolve HEAD\n");
76
+ return 1;
77
+ }
78
+ const PARENT = git(["-C", REPO_ROOT, "rev-parse", "HEAD~1"]) || "none";
79
+
80
+ let PUBKEY = "";
81
+ const pk = spawnSync("git", ["-C", REPO_ROOT, "show", "origin/main:.cw-release/verdict-signing.pub"], {
82
+ encoding: "utf8",
83
+ });
84
+ if (pk.status === 0 && pk.stdout.trim() !== "") {
85
+ PUBKEY = path.join(fs.mkdtempSync(path.join(os.tmpdir(), "cw-verdict-pub-")), "verdict-signing.pub");
86
+ fs.writeFileSync(PUBKEY, pk.stdout);
87
+ }
88
+
89
+ for (const C of [SHA, PARENT]) {
90
+ if (C === "none") continue;
91
+ const VERDICT_REL = `.cw-release/review-${C}.verdict`;
92
+ const VERDICT = path.join(REPO_ROOT, VERDICT_REL);
93
+ if (!fs.existsSync(VERDICT)) continue;
94
+ // The first line must say EXACTLY "APPROVED <this candidate's sha>". A
95
+ // starts-with check is not enough: the signature binds the BYTES, never
96
+ // the file NAME, so a real signed verdict for some other sha could be
97
+ // parked under this candidate's filename and still verify.
98
+ const FIRST_LINE = fs.readFileSync(VERDICT, "utf8").split("\n", 1)[0];
99
+ if (FIRST_LINE !== `APPROVED ${C}`) continue;
100
+
101
+ if (PUBKEY) {
102
+ const SIG_REL = `${VERDICT_REL}.sig`;
103
+ const SIG = path.join(REPO_ROOT, SIG_REL);
104
+ const sigOk =
105
+ fs.existsSync(SIG) &&
106
+ spawnSync(process.execPath, [VERIFY_SIG, VERDICT, SIG, PUBKEY], { stdio: "inherit" }).status === 0;
107
+ if (sigOk) {
108
+ if (C !== SHA) {
109
+ const bump = spawnSync(process.execPath, [VERIFY_BUMP, C, SHA, VERDICT_REL, SIG_REL], {
110
+ cwd: REPO_ROOT,
111
+ stdio: "inherit",
112
+ });
113
+ if (bump.status !== 0) {
114
+ console.log(
115
+ `verdict for ${C} has a valid signature but HEAD does not reproduce as its deterministic bump — trying the other candidate`
116
+ );
117
+ continue;
118
+ }
119
+ }
120
+ console.log(`verdict found + signature verified for ${C}`);
121
+ return 0;
122
+ }
123
+ console.log(`verdict for ${C} has no valid signature — trying the other candidate`);
124
+ continue;
125
+ }
126
+ console.log(`verdict found for ${C} (no verdict-signing.pub committed yet — signature not required)`);
127
+ return 0;
128
+ }
129
+
130
+ console.log(
131
+ `::error::No committed APPROVED verdict for this tag (with a valid signature, if .cw-release/verdict-signing.pub is committed). ${context}`
132
+ );
133
+ console.log(
134
+ "::error::Expected: .cw-release/review-<sha>.verdict with first line 'APPROVED <sha>' — auto-created by release-flow.js --cut"
135
+ );
136
+ return 1;
137
+ }
138
+
139
+ process.exit(main());
@@ -4,7 +4,7 @@
4
4
  // verify-verdict-signature.js — verify an ed25519 signature over a committed
5
5
  // release verdict file, against the committed public key
6
6
  // (.cw-release/verdict-signing.pub). Portable (node + node:crypto only) so
7
- // CI (release-gate.yml, npm-publish.yml) and the local block-unapproved-tag.sh
7
+ // CI (release-gate.yml, npm-publish.yml) and the local block-unapproved-tag.js
8
8
  // hook can all shell out to the SAME check instead of three drifting copies.
9
9
  //
10
10
  // Exit 0: signature verifies. Exit 1: missing file, unreadable key, or a
@@ -27,21 +27,31 @@
27
27
  }
28
28
  }
29
29
  * { box-sizing: border-box; }
30
+ /* The body is a flex column and main takes the leftover height (flex: 1 +
31
+ min-height: 0 so the inner panes can scroll) — no hardcoded header
32
+ height, so a header that wraps to two lines no longer pushes the panes
33
+ past the bottom of the window. */
30
34
  body {
31
35
  margin: 0;
32
36
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
33
37
  background: var(--bg);
34
38
  color: var(--ink);
35
39
  font-size: 14px;
40
+ display: flex;
41
+ flex-direction: column;
42
+ height: 100vh;
36
43
  }
37
44
  header { padding: 14px 20px; border-bottom: 1px solid var(--line); }
38
45
  header h1 { margin: 0; font-size: 18px; font-weight: 600; }
39
46
  header .muted, .muted { color: var(--muted); font-weight: 400; }
40
47
  header .trust { margin: 4px 0 0; font-size: 12px; color: var(--muted); }
41
48
  code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
42
- main { display: grid; grid-template-columns: 320px 1fr; height: calc(100vh - 64px); }
49
+ main { display: grid; grid-template-columns: 320px 1fr; flex: 1; min-height: 0; }
43
50
  .sidebar { border-right: 1px solid var(--line); overflow-y: auto; padding: 12px; }
44
51
  .bar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
52
+ /* The sidebar heading is a real <h2> (document outline / screen-reader
53
+ navigation) styled to the size the old <strong> label had. */
54
+ .bar h2 { margin: 0; font-size: 14px; font-weight: 600; }
45
55
  button {
46
56
  background: var(--panel-2); color: var(--ink); border: 1px solid var(--line);
47
57
  border-radius: 6px; padding: 4px 10px; cursor: pointer; font-size: 12px;
@@ -51,16 +61,30 @@ input[type="search"] {
51
61
  width: 100%; background: var(--panel); color: var(--ink);
52
62
  border: 1px solid var(--line); border-radius: 6px; padding: 6px 8px; margin-bottom: 8px;
53
63
  }
54
- .freshness { font-size: 11px; color: var(--muted); margin-bottom: 8px; }
64
+ .freshness { font-size: 12px; color: var(--muted); margin-bottom: 8px; }
55
65
  .run-list { list-style: none; margin: 0; padding: 0; }
56
- .run-list li {
66
+ .run-list > li { margin-bottom: 6px; }
67
+ .run-list > li:not(.run-entry),
68
+ .run-list .run-entry > button {
57
69
  padding: 8px; border: 1px solid var(--line); border-radius: 6px;
58
- margin-bottom: 6px; cursor: pointer; background: var(--panel);
70
+ cursor: pointer; background: var(--panel);
59
71
  }
60
- .run-list li:hover { border-color: var(--accent); }
61
- .run-list li.active { border-color: var(--accent); background: var(--panel-2); }
72
+ /* Only the run-row <button> needs a plain-element reset: a <button> has
73
+ its own default display/width/font/color, a plain <li> (used only for
74
+ the error and empty-state rows) does not and must keep its own color
75
+ (.err red, .muted gray) — see app.js. */
76
+ .run-list .run-entry > button {
77
+ display: block; width: 100%; text-align: left; font: inherit; color: inherit;
78
+ }
79
+ .run-list > li:not(.run-entry):hover, .run-list .run-entry > button:hover { border-color: var(--accent); }
80
+ .run-list .run-entry > button.active { border-color: var(--accent); background: var(--panel-2); }
81
+ /* Run rows are now real <button> elements (for Tab/Enter/Space); make the
82
+ keyboard focus ring visible — there is no :focus-visible rule for this
83
+ element otherwise. */
84
+ .run-list button:focus-visible,
85
+ .tabs button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
62
86
  .run-list .rid { font-family: ui-monospace, monospace; font-size: 12px; display: flex; align-items: center; gap: 6px; }
63
- .run-list .meta { color: var(--muted); font-size: 11px; margin-top: 2px; }
87
+ .run-list .meta { color: var(--muted); font-size: 12px; margin-top: 2px; }
64
88
  .status-dot {
65
89
  display: inline-block; width: 8px; height: 8px; border-radius: 50%; flex: none;
66
90
  background: var(--muted);
@@ -81,25 +105,41 @@ input[type="search"] {
81
105
  padding: 8px 12px; border-bottom: 1px solid var(--line);
82
106
  }
83
107
  .panel-card > .head .title { font-weight: 600; }
84
- .panel-card > .head .src { font-size: 11px; color: var(--muted); font-family: ui-monospace, monospace; }
85
- .badge { font-size: 11px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--line); }
108
+ .panel-card > .head .src { font-size: 12px; color: var(--muted); font-family: ui-monospace, monospace; }
109
+ .badge { font-size: 12px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--line); }
86
110
  .badge.present { color: var(--present); border-color: var(--present); }
87
111
  .badge.absent { color: var(--absent); border-color: var(--absent); }
88
112
  .badge.stale { color: var(--absent); border-color: var(--absent); }
89
113
  .badge.valid { color: var(--present); border-color: var(--present); }
90
114
  .badge.missing, .badge.bad { color: var(--bad); border-color: var(--bad); }
115
+ /* Lifecycle states shown as a badge in the run detail header (the other
116
+ lifecycle values reuse the neutral/present/absent colors above). */
117
+ .badge.blocked { color: var(--absent); border-color: var(--absent); }
118
+ .badge.failed { color: var(--bad); border-color: var(--bad); }
119
+ .badge.completed { color: var(--present); border-color: var(--present); }
120
+ .badge.running { color: var(--accent); border-color: var(--accent); }
121
+ /* One plain next-step line under the header when a run is blocked/failed. */
122
+ .recovery-hint { margin: 0 0 12px; font-size: 12px; color: var(--absent); }
123
+ /* Second line of the empty-list state: the suggested next command. */
124
+ .run-list li .hint { margin-top: 4px; font-size: 12px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
91
125
  pre.json {
92
126
  margin: 0; padding: 12px; overflow-x: auto; font-size: 12px;
93
127
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; line-height: 1.5;
94
128
  color: var(--ink); white-space: pre; max-height: 460px;
95
129
  }
96
130
  .panel-card .absent-note { padding: 10px 12px; color: var(--absent); font-size: 12px; }
131
+ .what-matters { padding: 10px 12px; border-top: 1px solid var(--line); background: var(--panel-2); }
132
+ .what-matters-title { margin: 0 0 8px; font-size: 12px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted); }
133
+ .action-fact { display: grid; grid-template-columns: minmax(100px, 140px) 1fr; gap: 10px; margin-top: 6px; }
134
+ .action-fact:first-of-type { margin-top: 0; }
135
+ .action-label { color: var(--muted); font-size: 12px; }
136
+ .action-items { margin: 0; padding-left: 18px; font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre-wrap; overflow-wrap: anywhere; }
97
137
  .kv { display: flex; gap: 16px; flex-wrap: wrap; padding: 8px 12px; font-size: 12px; color: var(--muted); }
98
138
  .kv b { color: var(--ink); font-weight: 600; }
99
139
  .err { color: var(--bad); }
100
140
  .struct-block { padding: 10px 12px; border-top: 1px solid var(--line); }
101
141
  .struct-block:first-child { border-top: none; }
102
- .struct-block .struct-title { font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted); margin-bottom: 6px; }
142
+ .struct-block .struct-title { font-size: 12px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted); margin-bottom: 6px; }
103
143
  .struct-table { width: 100%; border-collapse: collapse; font-size: 12px; }
104
144
  .struct-table th, .struct-table td {
105
145
  text-align: left; padding: 4px 8px; border-bottom: 1px solid var(--line);