cool-workflow 0.2.3 → 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 (65) 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 +2 -1
  10. package/dist/cli/io.js +6 -20
  11. package/dist/core/capability-data.js +2 -2
  12. package/dist/core/format/help.js +7 -1
  13. package/dist/core/util/cli-args.js +33 -0
  14. package/dist/core/version.js +1 -1
  15. package/dist/shell/drive.js +14 -6
  16. package/dist/shell/execution-backend/agent.js +10 -1
  17. package/dist/shell/execution-backend/local.js +15 -10
  18. package/dist/shell/pipeline-cli.js +20 -2
  19. package/dist/shell/reclamation-io.js +74 -6
  20. package/dist/shell/registry-cli.js +4 -0
  21. package/dist/shell/run-registry-io.js +1 -0
  22. package/dist/shell/workflow-app-loader.js +67 -1
  23. package/dist/wiring/capability-table/basics.js +2 -2
  24. package/dist/wiring/capability-table/exec-backend.js +9 -9
  25. package/dist/wiring/capability-table/multi-agent.js +69 -69
  26. package/dist/wiring/capability-table/pipeline.js +28 -28
  27. package/dist/wiring/capability-table/registry-core.js +3 -3
  28. package/dist/wiring/capability-table/reporting.js +38 -38
  29. package/dist/wiring/capability-table/scheduling-registry.js +58 -58
  30. package/dist/wiring/capability-table/state.js +34 -34
  31. package/dist/wiring/capability-table/trust-ledger.js +12 -12
  32. package/dist/wiring/capability-table/workflow-apps.js +15 -15
  33. package/docs/agent-delegation-drive.7.md +2 -0
  34. package/docs/cli-mcp-parity.7.md +18 -13
  35. package/docs/contract-migration-tooling.7.md +2 -0
  36. package/docs/control-plane-scheduling.7.md +2 -0
  37. package/docs/durable-state-and-locking.7.md +2 -0
  38. package/docs/evidence-adoption-reasoning-chain.7.md +2 -0
  39. package/docs/execution-backends.7.md +2 -0
  40. package/docs/multi-agent-cli-mcp-surface.7.md +2 -0
  41. package/docs/multi-agent-eval-replay-harness.7.md +2 -0
  42. package/docs/multi-agent-operator-ux.7.md +2 -0
  43. package/docs/node-snapshot-diff-replay.7.md +2 -0
  44. package/docs/observability-cost-accounting.7.md +2 -0
  45. package/docs/project-index.md +8 -4
  46. package/docs/real-execution-backends.7.md +2 -0
  47. package/docs/release-and-migration.7.md +2 -0
  48. package/docs/release-tooling.7.md +20 -0
  49. package/docs/run-registry-control-plane.7.md +2 -0
  50. package/docs/run-retention-reclamation.7.md +22 -3
  51. package/docs/state-explosion-management.7.md +2 -0
  52. package/docs/team-collaboration.7.md +2 -0
  53. package/docs/trust-audit-anchor.7.md +2 -0
  54. package/docs/web-desktop-workbench.7.md +2 -0
  55. package/manifest/plugin.manifest.json +1 -1
  56. package/package.json +2 -1
  57. package/scripts/block-unapproved-tag.sh +19 -4
  58. package/scripts/bump-version.js +27 -10
  59. package/scripts/canonical-apps.js +4 -4
  60. package/scripts/dogfood-release.js +1 -1
  61. package/scripts/golden-path.js +4 -4
  62. package/scripts/purity-baseline.json +0 -30
  63. package/scripts/release-flow.js +137 -12
  64. package/scripts/release-oneclick.js +407 -0
  65. package/scripts/version-sync-check.js +39 -22
@@ -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.2.3",
86
+ "Cool Workflow v0.2.4",
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.2.3");
120
+ assert.equal(summary.version, "0.2.4");
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.2.3");
127
+ assert.equal(shown.app.version, "0.2.4");
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.2.3");
138
+ assert.equal(state.workflow.app.version, "0.2.4");
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/shell/orchestrator.js");
8
8
 
9
- const TARGET_VERSION = "0.2.3";
9
+ const TARGET_VERSION = "0.2.4";
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.2.3");
36
+ assert.equal(appValidation.summary.version, "0.2.4");
37
37
 
38
38
  const plan = runJson(
39
39
  [
@@ -42,7 +42,7 @@ function main() {
42
42
  "--repo",
43
43
  tmp,
44
44
  "--question",
45
- "Prove the deterministic v0.2.3 end-to-end golden path."
45
+ "Prove the deterministic v0.2.4 end-to-end golden path."
46
46
  ],
47
47
  pluginRoot
48
48
  );
@@ -52,7 +52,7 @@ function main() {
52
52
 
53
53
  let state = readJson(plan.statePath);
54
54
  assert.equal(state.workflow.app.id, "end-to-end-golden-path");
55
- assert.equal(state.workflow.app.version, "0.2.3");
55
+ assert.equal(state.workflow.app.version, "0.2.4");
56
56
  assert.equal(state.loopStage, "interpret");
57
57
 
58
58
  const dispatch = runJson(["dispatch", plan.runId, "--limit", "1", "--sandbox", "readonly"], tmp);
@@ -195,7 +195,7 @@ function main() {
195
195
  assert.equal(reportPath, plan.reportPath);
196
196
  assert.ok(fs.existsSync(reportPath));
197
197
  const report = fs.readFileSync(reportPath, "utf8");
198
- assert.match(report, /Workflow App: end-to-end-golden-path@0\.2\.3/);
198
+ assert.match(report, /Workflow App: end-to-end-golden-path@0\.2\.4/);
199
199
  assert.match(report, /## Candidates/);
200
200
  assert.match(report, /## Trust Audit/);
201
201
  assert.match(report, /## Acceptance Rationale/);
@@ -36,36 +36,6 @@
36
36
  "layerViolations": {
37
37
  "core/capability-table.ts": [
38
38
  "../wiring/capability-table"
39
- ],
40
- "wiring/capability-table/basics.ts": [
41
- "../../cli/io"
42
- ],
43
- "wiring/capability-table/exec-backend.ts": [
44
- "../../cli/io"
45
- ],
46
- "wiring/capability-table/multi-agent.ts": [
47
- "../../cli/io"
48
- ],
49
- "wiring/capability-table/pipeline.ts": [
50
- "../../cli/io"
51
- ],
52
- "wiring/capability-table/registry-core.ts": [
53
- "../../cli/io"
54
- ],
55
- "wiring/capability-table/reporting.ts": [
56
- "../../cli/io"
57
- ],
58
- "wiring/capability-table/scheduling-registry.ts": [
59
- "../../cli/io"
60
- ],
61
- "wiring/capability-table/state.ts": [
62
- "../../cli/io"
63
- ],
64
- "wiring/capability-table/trust-ledger.ts": [
65
- "../../cli/io"
66
- ],
67
- "wiring/capability-table/workflow-apps.ts": [
68
- "../../cli/io"
69
39
  ]
70
40
  }
71
41
  }
@@ -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
@@ -450,13 +457,22 @@ function prevTagOf(version) {
450
457
  return git(["describe", "--tags", "--abbrev=0", `v${version}^`]).out || "";
451
458
  }
452
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
+
453
469
  // Extract the `## <version>` section body from the CHANGELOG AS SHIPPED AT THE
454
470
  // TAG (git show), so the notes reflect what that tag actually carried.
455
471
  function changelogSection(version) {
456
472
  const show = git(["show", `v${version}:CHANGELOG.md`]);
457
473
  if (show.code !== 0 || !show.out) return "";
458
474
  const lines = show.out.split(/\r?\n/);
459
- const esc = version.replace(/\./g, "\\.");
475
+ const esc = escapeRegExp(version);
460
476
  const startRe = new RegExp(`^## \\[?${esc}\\b`);
461
477
  let start = -1;
462
478
  for (let i = 0; i < lines.length; i++) if (startRe.test(lines[i])) { start = i; break; }
@@ -558,10 +574,102 @@ function releaseGitHub(version, { required = false } = {}) {
558
574
  return true;
559
575
  }
560
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
+
561
669
  // ---- 3. optional cut (bump + commit verdict + tag + push) ------------------
562
670
  function cut(resultPath, capability) {
563
671
  if (!cutVersion || !/^\d+\.\d+\.\d+$/.test(cutVersion)) die("--cut requires --version x.y.z");
564
- 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; }
565
673
  const bump = spawnSync("npm", ["run", "bump:version", "--", cutVersion], { cwd: pluginRoot, encoding: "utf8", stdio: "inherit" });
566
674
  if (bump.status !== 0) die("bump:version failed");
567
675
  // Regenerate the gated project index after the version bump (PR #87 gate).
@@ -585,11 +693,16 @@ function cut(resultPath, capability) {
585
693
  const tag = git(["tag", "-a", `v${cutVersion}`, "-m", `v${cutVersion}: ${capability || "release"}`]);
586
694
  if (tag.code !== 0) die("git tag failed", tag.err);
587
695
  if (PUSH) {
588
- // Atomic: the verdict commit on HEAD and the tag land together or not at all.
589
- // A non-atomic two-push could leave main advanced with no tag, so CI's
590
- // release-gate (which fires on the tag) never runs and the release silently stalls.
591
- const push = git(["push", "--atomic", "origin", "HEAD", `v${cutVersion}`]);
592
- 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);
593
706
  }
594
707
  say(`tagged v${cutVersion}${PUSH ? " and pushed" : " (local only; push when ready)"}`);
595
708
  // Finishing step: create the GitHub Release for the just-pushed tag. Only when
@@ -614,6 +727,18 @@ function main() {
614
727
  return;
615
728
  }
616
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
+
617
742
  const markerDir = path.join(repoRoot, ".cw-release");
618
743
  fs.mkdirSync(markerDir, { recursive: true });
619
744
  const resultPath = path.join(markerDir, `review-${HEAD}.verdict`);