Add release approval gate

This commit is contained in:
Affaan Mustafa
2026-05-19 18:05:46 -04:00
committed by Affaan Mustafa
parent 2c0d226439
commit 9819626459
14 changed files with 928 additions and 7 deletions

View File

@@ -85,7 +85,7 @@ As of 2026-05-19:
current May 19 queue-zero state, canonical ECC identity merge, release video current May 19 queue-zero state, canonical ECC identity merge, release video
suite gate, partner/sponsor/talk outreach pack, owner approval packet suite gate, partner/sponsor/talk outreach pack, owner approval packet
(`owner-approval-packet-2026-05-19.md`), preview-pack smoke digest (`owner-approval-packet-2026-05-19.md`), preview-pack smoke digest
`790430aef4a8`, local 2560-test suite, PR #2001 merge and GitHub Actions run `531328aaaa53`, local 2560-test suite, PR #2001 merge and GitHub Actions run
`26102500291` success, PR #2002's owner-approval dashboard gate refresh and `26102500291` success, PR #2002's owner-approval dashboard gate refresh and
GitHub Actions run `26103853507`, PR #2004's Linear readiness evidence sync GitHub Actions run `26103853507`, PR #2004's Linear readiness evidence sync
and GitHub Actions run `26105012698`, plus PR #2005's post-PR #2004 and GitHub Actions run `26105012698`, plus PR #2005's post-PR #2004
@@ -755,7 +755,7 @@ is not complete unless the evidence column exists and has been freshly verified.
| Manage repository discussions | Repo-family discussion recheck plus response playbook | Platform audit reports 0 discussion maintainer-touch gaps and 0 answerable Q&A missing accepted answers; trunk has 59 total discussions after #2003 was routed with a maintainer response; `docs/architecture/discussion-response-playbook.md` distinguishes support, maintainer coordination, stale/concluded, release, informational, and security-sensitive response paths | Complete | | Manage repository discussions | Repo-family discussion recheck plus response playbook | Platform audit reports 0 discussion maintainer-touch gaps and 0 answerable Q&A missing accepted answers; trunk has 59 total discussions after #2003 was routed with a maintainer response; `docs/architecture/discussion-response-playbook.md` distinguishes support, maintainer coordination, stale/concluded, release, informational, and security-sensitive response paths | Complete |
| Manage PR discussions | PR review/comment closure plus merge/close state | ECC #1990-#2011 merged through the harness audit, canonical identity, release video suite, growth outreach, evidence refresh, visual QA, suite-count, owner-approval packet, owner-approval dashboard gate, Linear readiness evidence, supply-chain evidence gate, per-project Claude Code adapter, continuous-learning project-registry hygiene, and GateGuard quoted git introspection batch; no open tracked PRs remain | Complete | | Manage PR discussions | PR review/comment closure plus merge/close state | ECC #1990-#2011 merged through the harness audit, canonical identity, release video suite, growth outreach, evidence refresh, visual QA, suite-count, owner-approval packet, owner-approval dashboard gate, Linear readiness evidence, supply-chain evidence gate, per-project Claude Code adapter, continuous-learning project-registry hygiene, and GateGuard quoted git introspection batch; no open tracked PRs remain | Complete |
| Salvage useful stale work | `docs/stale-pr-salvage-ledger.md` plus `docs/legacy-artifact-inventory.md` | Ledger records salvaged, superseded, skipped, and manual-review tails; #1815-#1818 added cost tracking, skill scout, frontend design guidance, code-reviewer false-positive guardrails, and the May 12 gap pass; #1687, #1609, #1563, #1564, and #1565 localization tails are attached to Linear ITO-55 for language-owner review and no automatic import remains release-blocking | Complete; repeat legacy scan before release | | Salvage useful stale work | `docs/stale-pr-salvage-ledger.md` plus `docs/legacy-artifact-inventory.md` | Ledger records salvaged, superseded, skipped, and manual-review tails; #1815-#1818 added cost tracking, skill scout, frontend design guidance, code-reviewer false-positive guardrails, and the May 12 gap pass; #1687, #1609, #1563, #1564, and #1565 localization tails are attached to Linear ITO-55 for language-owner review and no automatic import remains release-blocking | Complete; repeat legacy scan before release |
| ECC 2.0 preview pack ready | Release docs, quickstart, publication readiness, release notes | `docs/releases/2.0.0-rc.1/` and readiness docs are in-tree; May 19 evidence records queue-zero state, canonical ECC identity, release video suite, growth outreach pack, owner approval packet, local 2560-test suite, PR #2001 merge and GitHub Actions run `26102500291`, PR #2002 owner-approval dashboard gate refresh and GitHub Actions run `26103853507`, PR #2004 Linear readiness evidence sync and GitHub Actions run `26105012698`, PR #2008 supply-chain evidence gate CI run `26108473648`, post-PR #2006 main CI run `26109953093`, PR #2009 project-registry hygiene GitHub Actions run `26111313938`, post-PR #2009 main CI run `26111946778`, post-PR #2011 GateGuard main CI run `26113695068`, May 19 operator dashboard, `owner-approval-packet-2026-05-19.md`, and preview-pack smoke digest `790430aef4a8` | Needs final release approval | | ECC 2.0 preview pack ready | Release docs, quickstart, publication readiness, release notes | `docs/releases/2.0.0-rc.1/` and readiness docs are in-tree; May 19 evidence records queue-zero state, canonical ECC identity, release video suite, growth outreach pack, owner approval packet, local 2560-test suite, PR #2001 merge and GitHub Actions run `26102500291`, PR #2002 owner-approval dashboard gate refresh and GitHub Actions run `26103853507`, PR #2004 Linear readiness evidence sync and GitHub Actions run `26105012698`, PR #2008 supply-chain evidence gate CI run `26108473648`, post-PR #2006 main CI run `26109953093`, PR #2009 project-registry hygiene GitHub Actions run `26111313938`, post-PR #2009 main CI run `26111946778`, post-PR #2011 GateGuard main CI run `26113695068`, May 19 operator dashboard, `owner-approval-packet-2026-05-19.md`, `release-approval-gate.js`, and preview-pack smoke digest `531328aaaa53` | Needs final release approval |
| Hermes specialized skills included safely | Hermes setup/import docs and sanitized skill surface | Hermes setup and import playbook are public; secrets stay local | Needs final release review | | Hermes specialized skills included safely | Hermes setup/import docs and sanitized skill surface | Hermes setup and import playbook are public; secrets stay local | Needs final release review |
| Naming and rename readiness | Naming matrix across package/plugin/docs/social surfaces | `docs/releases/2.0.0-rc.1/naming-and-publication-matrix.md` records current package, repo, Claude plugin, Codex plugin, OpenCode, and npm availability evidence | Complete for rc.1; post-rc rename remains future work | | Naming and rename readiness | Naming matrix across package/plugin/docs/social surfaces | `docs/releases/2.0.0-rc.1/naming-and-publication-matrix.md` records current package, repo, Claude plugin, Codex plugin, OpenCode, and npm availability evidence | Complete for rc.1; post-rc rename remains future work |
| Claude and Codex plugin publication | Contact/submission path with required artifacts and status | Publication readiness, naming matrix, and May 12 dry-run evidence document plugin validation, clean-checkout Claude tag/install smoke, and Codex marketplace CLI shape | Needs explicit approval for real tag/push and marketplace submission | | Claude and Codex plugin publication | Contact/submission path with required artifacts and status | Publication readiness, naming matrix, and May 12 dry-run evidence document plugin validation, clean-checkout Claude tag/install smoke, and Codex marketplace CLI shape | Needs explicit approval for real tag/push and marketplace submission |

View File

@@ -21,6 +21,9 @@
- verify package, plugin, marketplace, OpenCode, and agent metadata stays at `2.0.0-rc.1` - verify package, plugin, marketplace, OpenCode, and agent metadata stays at `2.0.0-rc.1`
- verify `ecc2/Cargo.toml` stays at `0.1.0` for rc.1; `ecc2/` remains an alpha control-plane scaffold - verify `ecc2/Cargo.toml` stays at `0.1.0` for rc.1; `ecc2/` remains an alpha control-plane scaffold
- complete `publication-readiness.md` with fresh evidence before any GitHub release, npm publish, plugin submission, or announcement post - complete `publication-readiness.md` with fresh evidence before any GitHub release, npm publish, plugin submission, or announcement post
- run `npm run release:approval-gate -- --format json` after owner approvals
and live URL readbacks are recorded; it must return ready true before any
publish, upload, social, or outbound action
- rerun the release name/plugin publication checklist before creating a - rerun the release name/plugin publication checklist before creating a
GitHub prerelease, publishing npm, pushing Claude plugin tags, recording the GitHub prerelease, publishing npm, pushing Claude plugin tags, recording the
Codex marketplace path, or posting public copy Codex marketplace path, or posting public copy

View File

@@ -15,7 +15,8 @@ Source commit for the clean evidence baseline this packet extends:
| Evidence | Current recorded state | Repeat before approval | | Evidence | Current recorded state | Repeat before approval |
| --- | --- | --- | | --- | --- | --- |
| Platform audit | ready true, 0 open PRs, 0 open issues, 0 discussion gaps, 0 dirty files | yes | | Platform audit | ready true, 0 open PRs, 0 open issues, 0 discussion gaps, 0 dirty files | yes |
| Preview pack smoke | ready true, digest `790430aef4a8`, 5/5 checks | yes | | Preview pack smoke | ready true, digest `531328aaaa53`, 5/5 checks | yes |
| Release approval gate | ready false, digest `ef8f49f727b7`, 4/6 checks pass; owner decisions and live URL readbacks pending | yes |
| Video suite | ready true, 15/15 source assets, 13/13 suite artifacts, 12/12 publish candidates | yes | | Video suite | ready true, 15/15 source assets, 13/13 suite artifacts, 12/12 publish candidates | yes |
| Release surface tests | 27/27 passed after this packet was added | yes | | Release surface tests | 27/27 passed after this packet was added | yes |
| Full local suite | 2560/2560 passed after PR #2011 was prepared; focused GateGuard regression passed 91/91 again on current `main` | yes | | Full local suite | 2560/2560 passed after PR #2011 was prepared; focused GateGuard regression passed 91/91 again on current `main` | yes |
@@ -56,6 +57,7 @@ Run these from the exact release commit before approving publication:
git status --short --branch git status --short --branch
node scripts/platform-audit.js --json node scripts/platform-audit.js --json
npm run preview-pack:smoke -- --format json npm run preview-pack:smoke -- --format json
npm run release:approval-gate -- --format json
npm run release:video-suite -- --format json npm run release:video-suite -- --format json
npm run harness:adapters -- --check npm run harness:adapters -- --check
npm run harness:audit -- --format json npm run harness:audit -- --format json

View File

@@ -17,6 +17,7 @@ surfaces, or posting announcements.
| `docs/architecture/observability-readiness.md` | Local operator-readiness gate | Verified by `npm run observability:ready` | | `docs/architecture/observability-readiness.md` | Local operator-readiness gate | Verified by `npm run observability:ready` |
| `docs/architecture/progress-sync-contract.md` | GitHub, Linear, handoff, roadmap, and work-item sync boundary | Checked by `node scripts/platform-audit.js --json` | | `docs/architecture/progress-sync-contract.md` | GitHub, Linear, handoff, roadmap, and work-item sync boundary | Checked by `node scripts/platform-audit.js --json` |
| `scripts/preview-pack-smoke.js` | Deterministic preview-pack smoke gate | Verified by `npm run preview-pack:smoke` | | `scripts/preview-pack-smoke.js` | Deterministic preview-pack smoke gate | Verified by `npm run preview-pack:smoke` |
| `scripts/release-approval-gate.js` | Final owner-decision, live-URL, and launch-copy gate | Must return ready true before any release publish, package publish, plugin tag, video upload, announcement, or outbound batch |
| `docs/releases/2.0.0-rc.1/release-notes.md` | GitHub release copy source | Must be refreshed with final live release/package/plugin URLs before publication | | `docs/releases/2.0.0-rc.1/release-notes.md` | GitHub release copy source | Must be refreshed with final live release/package/plugin URLs before publication |
| `docs/releases/2.0.0-rc.1/quickstart.md` | Clone-to-first-workflow path | Covers clone, install, verify, first skill, and harness switch | | `docs/releases/2.0.0-rc.1/quickstart.md` | Clone-to-first-workflow path | Covers clone, install, verify, first skill, and harness switch |
| `docs/releases/2.0.0-rc.1/launch-checklist.md` | Operator launch checklist | Must remain approval-gated for release, package, plugin, and announcement actions | | `docs/releases/2.0.0-rc.1/launch-checklist.md` | Operator launch checklist | Must remain approval-gated for release, package, plugin, and announcement actions |
@@ -25,7 +26,7 @@ surfaces, or posting announcements.
| `docs/releases/2.0.0-rc.1/publication-evidence-2026-05-16.md` | Current May 16/17 queue cleanup, recsys skill merge, GateGuard triage, PR #1947 supply-chain protection, AgentShield #87 plugin-cache confidence evidence, AgentShield #88 evidence-pack inspect/readback, AgentShield #89 evidence-pack fleet routing, AgentShield #90 fleet review items, AgentShield #91 policy export, AgentShield #92 policy promotion, ECC-Tools #76 fleet-summary consumption, ECC-Tools #77 hosted finding evidence paths, ECC-Tools #78 harness policy-route linking, dashboard refresh, and combined Node/Rust/release-surface gate evidence through the May 16 mirror | Must still be repeated from a strict clean checkout before real publication | | `docs/releases/2.0.0-rc.1/publication-evidence-2026-05-16.md` | Current May 16/17 queue cleanup, recsys skill merge, GateGuard triage, PR #1947 supply-chain protection, AgentShield #87 plugin-cache confidence evidence, AgentShield #88 evidence-pack inspect/readback, AgentShield #89 evidence-pack fleet routing, AgentShield #90 fleet review items, AgentShield #91 policy export, AgentShield #92 policy promotion, ECC-Tools #76 fleet-summary consumption, ECC-Tools #77 hosted finding evidence paths, ECC-Tools #78 harness policy-route linking, dashboard refresh, and combined Node/Rust/release-surface gate evidence through the May 16 mirror | Must still be repeated from a strict clean checkout before real publication |
| `docs/releases/2.0.0-rc.1/publication-evidence-2026-05-17.md` | May 17 queue-zero state, Japanese localization merge, Dependabot TypeScript and Node type merges, post-merge ja-JP lint repair, Mini Shai-Hulud/TanStack protection recheck, npm audit/signature checks, legacy and Linear progress routing, deterministic preview-pack smoke, operator dashboard refresh, Linear sync, and GitHub CI evidence for `27dc2918` | Superseded by the May 18 evidence snapshot; repeat from a strict clean checkout before real publication | | `docs/releases/2.0.0-rc.1/publication-evidence-2026-05-17.md` | May 17 queue-zero state, Japanese localization merge, Dependabot TypeScript and Node type merges, post-merge ja-JP lint repair, Mini Shai-Hulud/TanStack protection recheck, npm audit/signature checks, legacy and Linear progress routing, deterministic preview-pack smoke, operator dashboard refresh, Linear sync, and GitHub CI evidence for `27dc2918` | Superseded by the May 18 evidence snapshot; repeat from a strict clean checkout before real publication |
| `docs/releases/2.0.0-rc.1/publication-evidence-2026-05-18.md` | May 18 queue-zero state, #1970/#1971/#1972 merge batch, #1978 review/closure, supply-chain recheck, AgentShield evidence mirror, Linear sync, current-head CI/security scan success for `4470e2e6`, and ITO-46 naming/plugin publication closure | Superseded by the May 19 ECC identity, video, and growth evidence snapshot | | `docs/releases/2.0.0-rc.1/publication-evidence-2026-05-18.md` | May 18 queue-zero state, #1970/#1971/#1972 merge batch, #1978 review/closure, supply-chain recheck, AgentShield evidence mirror, Linear sync, current-head CI/security scan success for `4470e2e6`, and ITO-46 naming/plugin publication closure | Superseded by the May 19 ECC identity, video, and growth evidence snapshot |
| `docs/releases/2.0.0-rc.1/publication-evidence-2026-05-19.md` | Current May 19 evidence for canonical ECC identity, release video suite, partner/sponsor/talk outreach pack, owner approval packet, May 19 operator dashboard, preview-pack smoke digest `790430aef4a8`, 2560-test local suite, PR #1998 visual QA CI success, PR #1999 dashboard evidence CI success, PR #2000 suite-count evidence success, PR #2001 owner approval packet CI success, PR #2002 owner-approval dashboard gate CI success, PR #2004 Linear readiness evidence sync CI success, PR #2008 supply-chain evidence gate CI success, post-PR #2006 main CI success, PR #2009 project-registry hygiene CI success, post-PR #2009 main CI success, post-PR #2011 GateGuard CI success, and the May 19 Linear sync document | Current strongest readiness snapshot; must still be repeated from a strict clean checkout before real publication | | `docs/releases/2.0.0-rc.1/publication-evidence-2026-05-19.md` | Current May 19 evidence for canonical ECC identity, release video suite, partner/sponsor/talk outreach pack, owner approval packet, release approval gate, May 19 operator dashboard, preview-pack smoke digest `531328aaaa53`, 2560-test local suite, PR #1998 visual QA CI success, PR #1999 dashboard evidence CI success, PR #2000 suite-count evidence success, PR #2001 owner approval packet CI success, PR #2002 owner-approval dashboard gate CI success, PR #2004 Linear readiness evidence sync CI success, PR #2008 supply-chain evidence gate CI success, post-PR #2006 main CI success, PR #2009 project-registry hygiene CI success, post-PR #2009 main CI success, post-PR #2011 GateGuard CI success, and the May 19 Linear sync document | Current strongest readiness snapshot; must still be repeated from a strict clean checkout before real publication |
| `docs/releases/2.0.0-rc.1/operator-readiness-dashboard-2026-05-17.md` | Previous prompt-to-artifact operator dashboard | Superseded by the May 18 generated dashboard | | `docs/releases/2.0.0-rc.1/operator-readiness-dashboard-2026-05-17.md` | Previous prompt-to-artifact operator dashboard | Superseded by the May 18 generated dashboard |
| `docs/releases/2.0.0-rc.1/operator-readiness-dashboard-2026-05-18.md` | Previous prompt-to-artifact operator dashboard | Superseded by the May 19 generated dashboard | | `docs/releases/2.0.0-rc.1/operator-readiness-dashboard-2026-05-18.md` | Previous prompt-to-artifact operator dashboard | Superseded by the May 19 generated dashboard |
| `docs/releases/2.0.0-rc.1/operator-readiness-dashboard-2026-05-19.md` | Current prompt-to-artifact operator dashboard | Shows PR/issue/discussion/platform/supply-chain gates current and adds the current `$1,728/mo` to `$10,000/mo` hypergrowth, video owner-approval, and outbound-pack operating lanes | | `docs/releases/2.0.0-rc.1/operator-readiness-dashboard-2026-05-19.md` | Current prompt-to-artifact operator dashboard | Shows PR/issue/discussion/platform/supply-chain gates current and adds the current `$1,728/mo` to `$10,000/mo` hypergrowth, video owner-approval, and outbound-pack operating lanes |
@@ -80,6 +81,7 @@ Run these from the exact release commit before publication:
git status --short --branch git status --short --branch
node scripts/platform-audit.js --json node scripts/platform-audit.js --json
npm run preview-pack:smoke npm run preview-pack:smoke
npm run release:approval-gate -- --format json
npm run release:video-suite -- --format json npm run release:video-suite -- --format json
npm run harness:adapters -- --check npm run harness:adapters -- --check
npm run harness:audit -- --format json npm run harness:audit -- --format json
@@ -98,6 +100,8 @@ The preview pack is assembled, but publication is still blocked until these live
surfaces exist and are recorded in a final evidence file: surfaces exist and are recorded in a final evidence file:
- final release URL ledger regenerated from the intended release commit; - final release URL ledger regenerated from the intended release commit;
- `npm run release:approval-gate -- --format json` returning ready true after
owner approvals and live URL readbacks are recorded;
- final release name/plugin publication checklist rerun from the intended - final release name/plugin publication checklist rerun from the intended
release commit; release commit;
- GitHub prerelease `v2.0.0-rc.1`; - GitHub prerelease `v2.0.0-rc.1`;

View File

@@ -63,7 +63,8 @@ Tracked repositories in the platform audit were:
| Gate | Command | Result | | Gate | Command | Result |
| --- | --- | --- | | --- | --- | --- |
| Release-surface tests | `node tests/docs/ecc2-release-surface.test.js` | 27 passed, 0 failed | | Release-surface tests | `node tests/docs/ecc2-release-surface.test.js` | 27 passed, 0 failed |
| Preview-pack smoke | `npm run preview-pack:smoke -- --format json` | Ready true; digest `790430aef4a8`; 31 required artifacts; 5 passed, 0 failed | | Preview-pack smoke | `npm run preview-pack:smoke -- --format json` | Ready true; digest `531328aaaa53`; 32 required artifacts; 5 passed, 0 failed |
| Release approval gate | `npm run release:approval-gate -- --format json` | Expected blocked; digest `ef8f49f727b7`; 4 passed, 2 failed; owner decisions and live URL readbacks remain approval-gated |
| Operator dashboard | `npm run operator:dashboard -- --write docs/releases/2.0.0-rc.1/operator-readiness-dashboard-2026-05-19.md` | Regenerated from the May 19 `main` baseline with platform audit ready true, 0 tracked PRs, 0 tracked issues, 0 discussion gaps, `$1,728/mo` current MRR, `$10,000/mo` target MRR, the release video suite marked current, and top actions for plugin publication, notifications, outbound approval, AgentShield, and ECC Tools billing | | Operator dashboard | `npm run operator:dashboard -- --write docs/releases/2.0.0-rc.1/operator-readiness-dashboard-2026-05-19.md` | Regenerated from the May 19 `main` baseline with platform audit ready true, 0 tracked PRs, 0 tracked issues, 0 discussion gaps, `$1,728/mo` current MRR, `$10,000/mo` target MRR, the release video suite marked current, and top actions for plugin publication, notifications, outbound approval, AgentShield, and ECC Tools billing |
| Supply-chain verification | `npm audit --audit-level=moderate`; `npm audit signatures`; `yarn install --immutable --mode=skip-build` | Current supply-chain refresh found 0 npm vulnerabilities, verified 254 registry signatures and 30 attestations, and accepted the Yarn lock after pinning `@types/node@25.7.0` plus refreshing `brace-expansion` to `5.0.6` / `1.1.14` | | Supply-chain verification | `npm audit --audit-level=moderate`; `npm audit signatures`; `yarn install --immutable --mode=skip-build` | Current supply-chain refresh found 0 npm vulnerabilities, verified 254 registry signatures and 30 attestations, and accepted the Yarn lock after pinning `@types/node@25.7.0` plus refreshing `brace-expansion` to `5.0.6` / `1.1.14` |
| Release video suite | `npm run release:video-suite -- --format json --summary` with `ECC_VIDEO_SOURCE_ROOT` and `ECC_VIDEO_RELEASE_SUITE_ROOT` | Ready true; 15/15 source assets present; 13/13 render, timeline, caption, EDL, and segment artifacts present; 12/12 publish-candidate outputs present with zero detected black-frame segments; primary rough render self-eval passed at 144.759 seconds, 1920x1080, 1 audio stream, and 106.78 MB | | Release video suite | `npm run release:video-suite -- --format json --summary` with `ECC_VIDEO_SOURCE_ROOT` and `ECC_VIDEO_RELEASE_SUITE_ROOT` | Ready true; 15/15 source assets present; 13/13 render, timeline, caption, EDL, and segment artifacts present; 12/12 publish-candidate outputs present with zero detected black-frame segments; primary rough render self-eval passed at 144.759 seconds, 1920x1080, 1 audio stream, and 106.78 MB |

View File

@@ -102,7 +102,8 @@ Record the exact commit SHA and command output before any publication action:
| Evidence | Command | Required result | Recorded output | | Evidence | Command | Required result | Recorded output |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| Clean release branch | `git status --short --branch` | On intended release commit; no unrelated files | Current May 19 baseline `bc519e5b8ed42f26c0a5a611756e04351c323f21`: `## main...origin/main`; repeat from the exact final publication commit before release | | Clean release branch | `git status --short --branch` | On intended release commit; no unrelated files | Current May 19 baseline `bc519e5b8ed42f26c0a5a611756e04351c323f21`: `## main...origin/main`; repeat from the exact final publication commit before release |
| Preview-pack smoke | `npm run preview-pack:smoke` | Preview pack artifacts, Hermes boundary, final verification command list, and publication blockers pass | `publication-evidence-2026-05-19.md`: ready yes, digest `790430aef4a8`, 31 artifacts, 5 passed, 0 failed; repeat in the final strict clean-checkout release pass | | Preview-pack smoke | `npm run preview-pack:smoke` | Preview pack artifacts, Hermes boundary, final verification command list, and publication blockers pass | `publication-evidence-2026-05-19.md`: ready yes, digest `531328aaaa53`, 32 artifacts, 5 passed, 0 failed; repeat in the final strict clean-checkout release pass |
| Release approval gate | `npm run release:approval-gate -- --format json` | Ready true only after owner decision rows are approved, live release/package/plugin/video/billing URLs are recorded, and launch/outbound copy has no placeholders or private paths | Current May 19 state is intentionally blocked because owner decisions and live URL readbacks remain approval-gated |
| Harness audit | `npm run harness:audit -- --format json` | 80/80 passing | Current release gate: 80/80 across 8 applicable categories, 0 top actions | | Harness audit | `npm run harness:audit -- --format json` | 80/80 passing | Current release gate: 80/80 across 8 applicable categories, 0 top actions |
| Adapter scorecard | `npm run harness:adapters -- --check` | PASS | Current release gate: PASS, 11 adapters | | Adapter scorecard | `npm run harness:adapters -- --check` | PASS | Current release gate: PASS, 11 adapters |
| Observability readiness | `npm run observability:ready` | 21/21 passing | Current release gate: 21/21, ready true | | Observability readiness | `npm run observability:ready` | 21/21 passing | Current release gate: 21/21, ready true |

View File

@@ -66,6 +66,7 @@ npm pack --dry-run --json
npm publish --tag next --dry-run npm publish --tag next --dry-run
npm run build:opencode npm run build:opencode
npm run preview-pack:smoke npm run preview-pack:smoke
npm run release:approval-gate -- --format json
``` ```
If a command is unavailable on the release machine, record the exact error and If a command is unavailable on the release machine, record the exact error and

View File

@@ -47,6 +47,7 @@ npm view ecc-universal name version dist-tags --json
codex plugin marketplace add --help codex plugin marketplace add --help
rg -n "TODO|TBD|PLACEHOLDER" docs/releases/2.0.0-rc.1 rg -n "TODO|TBD|PLACEHOLDER" docs/releases/2.0.0-rc.1
npm run preview-pack:smoke npm run preview-pack:smoke
npm run release:approval-gate -- --format json
``` ```
Do not post the social or notification copy until the approval-gated URLs above Do not post the social or notification copy until the approval-gated URLs above

View File

@@ -89,6 +89,7 @@
"scripts/operator-readiness-dashboard.js", "scripts/operator-readiness-dashboard.js",
"scripts/platform-audit.js", "scripts/platform-audit.js",
"scripts/preview-pack-smoke.js", "scripts/preview-pack-smoke.js",
"scripts/release-approval-gate.js",
"scripts/release-video-suite.js", "scripts/release-video-suite.js",
"scripts/hooks/", "scripts/hooks/",
"scripts/install-apply.js", "scripts/install-apply.js",
@@ -312,6 +313,7 @@
"observability:ready": "node scripts/observability-readiness.js", "observability:ready": "node scripts/observability-readiness.js",
"operator:dashboard": "node scripts/operator-readiness-dashboard.js", "operator:dashboard": "node scripts/operator-readiness-dashboard.js",
"preview-pack:smoke": "node scripts/preview-pack-smoke.js", "preview-pack:smoke": "node scripts/preview-pack-smoke.js",
"release:approval-gate": "node scripts/release-approval-gate.js",
"release:video-suite": "node scripts/release-video-suite.js", "release:video-suite": "node scripts/release-video-suite.js",
"platform:audit": "node scripts/platform-audit.js", "platform:audit": "node scripts/platform-audit.js",
"discussion:audit": "node scripts/discussion-audit.js", "discussion:audit": "node scripts/discussion-audit.js",

View File

@@ -18,6 +18,7 @@ const REQUIRED_ARTIFACTS = [
'docs/architecture/observability-readiness.md', 'docs/architecture/observability-readiness.md',
'docs/architecture/progress-sync-contract.md', 'docs/architecture/progress-sync-contract.md',
'scripts/preview-pack-smoke.js', 'scripts/preview-pack-smoke.js',
'scripts/release-approval-gate.js',
`${RELEASE_DIR}/release-notes.md`, `${RELEASE_DIR}/release-notes.md`,
`${RELEASE_DIR}/quickstart.md`, `${RELEASE_DIR}/quickstart.md`,
`${RELEASE_DIR}/launch-checklist.md`, `${RELEASE_DIR}/launch-checklist.md`,
@@ -47,6 +48,7 @@ const REQUIRED_VERIFICATION_COMMANDS = [
'git status --short --branch', 'git status --short --branch',
'node scripts/platform-audit.js --json', 'node scripts/platform-audit.js --json',
'npm run preview-pack:smoke', 'npm run preview-pack:smoke',
'npm run release:approval-gate -- --format json',
'npm run release:video-suite -- --format json', 'npm run release:video-suite -- --format json',
'npm run harness:adapters -- --check', 'npm run harness:adapters -- --check',
'npm run harness:audit -- --format json', 'npm run harness:audit -- --format json',

View File

@@ -0,0 +1,553 @@
#!/usr/bin/env node
'use strict';
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const RELEASE = '2.0.0-rc.1';
const RELEASE_DIR = `docs/releases/${RELEASE}`;
const SCHEMA_VERSION = 'ecc.release-approval-gate.v1';
const SCRIPT_PATH = 'scripts/release-approval-gate.js';
const OWNER_PACKET_PATH = `${RELEASE_DIR}/owner-approval-packet-2026-05-19.md`;
const URL_LEDGER_PATH = `${RELEASE_DIR}/release-url-ledger-2026-05-19.md`;
const PREVIEW_MANIFEST_PATH = `${RELEASE_DIR}/preview-pack-manifest.md`;
const REQUIRED_COMMAND = 'npm run release:approval-gate -- --format json';
const REQUIRED_DECISIONS = [
{
id: 'github-prerelease',
label: 'GitHub prerelease',
},
{
id: 'npm-next-publish',
label: 'npm `next` publish',
},
{
id: 'claude-plugin-tag',
label: 'Claude plugin tag',
},
{
id: 'codex-repo-marketplace',
label: 'Codex repo marketplace',
},
{
id: 'ecc-tools-billing-language',
label: 'ECC Tools billing language',
},
{
id: 'video-upload',
label: 'Video upload',
},
{
id: 'social-and-longform',
label: 'X, LinkedIn, GitHub Discussion, longform',
},
{
id: 'outbound-growth',
label: 'Sponsor, partner, consulting, conference, podcast outreach',
},
];
const REQUIRED_URL_SURFACES = [
{
id: 'github-prerelease-url',
label: 'GitHub prerelease URL',
exampleUrl: 'https://github.com/affaan-m/ECC/releases/tag/v2.0.0-rc.1',
},
{
id: 'npm-rc-package-url',
label: 'npm rc package URL',
exampleUrl: 'https://www.npmjs.com/package/ecc-universal/v/2.0.0-rc.1',
},
{
id: 'claude-plugin-tag-url',
label: 'Claude plugin tag URL',
exampleUrl: 'https://github.com/affaan-m/ECC/releases/tag/ecc--v2.0.0-rc.1',
},
{
id: 'codex-repo-marketplace-evidence',
label: 'Codex repo-marketplace evidence',
exampleUrl: 'https://github.com/affaan-m/ECC/tree/v2.0.0-rc.1/.codex-plugin',
},
{
id: 'primary-launch-video-url',
label: 'Primary launch video URL',
exampleUrl: 'https://x.com/affaanmustafa/status/0000000000000000000',
},
{
id: 'short-clip-urls',
label: 'Short clip URLs',
exampleUrl: 'https://x.com/affaanmustafa/status/0000000000000000001',
},
{
id: 'ecc-tools-billing-readiness-url',
label: 'ECC Tools billing/readiness URL',
exampleUrl: 'https://github.com/ECC-Tools',
},
];
const ANNOUNCEMENT_FILES = [
`${RELEASE_DIR}/release-notes.md`,
`${RELEASE_DIR}/x-thread.md`,
`${RELEASE_DIR}/linkedin-post.md`,
`${RELEASE_DIR}/article-outline.md`,
`${RELEASE_DIR}/partner-sponsor-talks-pack.md`,
'docs/business/social-launch-copy.md',
];
function usage() {
console.log([
'Usage: node scripts/release-approval-gate.js [--format <text|json>] [--root <dir>]',
'',
'Final approval gate for ECC 2.0 rc.1 publication and outbound actions.',
'',
'Options:',
' --format <text|json> Output format (default: text)',
' --json Alias for --format json',
' --root <dir> Repository root to inspect (default: cwd)',
' --help, -h Show this help',
].join('\n'));
}
function readArgValue(args, index, flagName) {
const value = args[index + 1];
if (!value || value.startsWith('--')) {
throw new Error(`${flagName} requires a value`);
}
return value;
}
function parseArgs(argv) {
const args = argv.slice(2);
const parsed = {
format: 'text',
help: false,
root: path.resolve(process.cwd()),
};
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === '--help' || arg === '-h') {
parsed.help = true;
continue;
}
if (arg === '--json') {
parsed.format = 'json';
continue;
}
if (arg === '--format') {
parsed.format = readArgValue(args, index, arg).toLowerCase();
index += 1;
continue;
}
if (arg.startsWith('--format=')) {
parsed.format = arg.slice('--format='.length).toLowerCase();
continue;
}
if (arg === '--root') {
parsed.root = path.resolve(readArgValue(args, index, arg));
index += 1;
continue;
}
if (arg.startsWith('--root=')) {
parsed.root = path.resolve(arg.slice('--root='.length));
continue;
}
throw new Error(`Unknown argument: ${arg}`);
}
if (!['text', 'json'].includes(parsed.format)) {
throw new Error(`Invalid format: ${parsed.format}. Use text or json.`);
}
return parsed;
}
function readText(rootDir, relativePath) {
try {
return fs.readFileSync(path.join(rootDir, relativePath), 'utf8');
} catch (_error) {
return '';
}
}
function fileExists(rootDir, relativePath) {
return fs.existsSync(path.join(rootDir, relativePath));
}
function safeParseJson(text) {
if (!text.trim()) {
return null;
}
try {
return JSON.parse(text);
} catch (_error) {
return null;
}
}
function normalizeLabel(value) {
return String(value)
.replace(/[`*_]/g, '')
.replace(/\s+/g, ' ')
.trim()
.toLowerCase();
}
function normalizeState(value) {
return String(value)
.replace(/[`*_]/g, '')
.replace(/\s+/g, ' ')
.trim()
.toLowerCase();
}
function splitMarkdownRow(row) {
const trimmed = row.trim();
if (!trimmed.startsWith('|') || !trimmed.endsWith('|')) {
return [];
}
return trimmed
.slice(1, -1)
.split('|')
.map(cell => cell.trim());
}
function parseDecisionRegister(packet) {
const decisions = new Map();
for (const line of packet.split('\n')) {
const cells = splitMarkdownRow(line);
if (cells.length < 4) {
continue;
}
const [decision, state] = cells;
const normalizedDecision = normalizeLabel(decision);
if (
!normalizedDecision
|| normalizedDecision === 'decision'
|| /^-+$/.test(normalizedDecision)
) {
continue;
}
decisions.set(normalizedDecision, normalizeState(state));
}
return decisions;
}
function isApproved(state) {
return state === 'approve' || state === 'approved';
}
function lineNumberForIndex(text, index) {
return text.slice(0, index).split('\n').length;
}
function findAnnouncementOffenders(rootDir, relativePaths) {
const offenders = [];
const privatePathPattern = /\/Users\/(?!\.\.\.)[A-Za-z0-9._-]+|\/home\/(?!user|runner)[A-Za-z0-9._-]+/g;
const anglePlaceholderPattern = /<(?!(?:https?:\/\/|mailto:|#))[^>\n]*(?:url|link|todo|tbd|placeholder)[^>\n]*>/gi;
const barePlaceholderPattern = /\bTODO\b|\bTBD\b|\bPLACEHOLDER\b/g;
for (const relativePath of relativePaths) {
const text = readText(rootDir, relativePath);
if (!text) {
offenders.push({
path: relativePath,
line: 1,
marker: 'missing file',
});
continue;
}
for (const match of text.matchAll(privatePathPattern)) {
offenders.push({
path: relativePath,
line: lineNumberForIndex(text, match.index),
marker: match[0],
});
}
for (const match of text.matchAll(anglePlaceholderPattern)) {
offenders.push({
path: relativePath,
line: lineNumberForIndex(text, match.index),
marker: match[0],
});
}
for (const match of text.matchAll(barePlaceholderPattern)) {
offenders.push({
path: relativePath,
line: lineNumberForIndex(text, match.index),
marker: match[0],
});
}
}
return offenders;
}
function ledgerBlockers(ledger) {
const blockers = [];
if (/^##\s+Approval-Gated URLs\s*$/im.test(ledger)) {
blockers.push('approval-gated URL section still present');
}
for (const [pattern, label] of [
[/not published yet/i, 'not-published marker still present'],
[/must return/i, 'must-return readback marker still present'],
[/Gate before use/i, 'gate-before-use column still present'],
[/\bpending\b/i, 'pending marker still present'],
[/\bblocked\b/i, 'blocked marker still present'],
]) {
if (pattern.test(ledger)) {
blockers.push(label);
}
}
return blockers;
}
function makeCheck(id, status, evidence, fix) {
return {
id,
status,
evidence,
fix: status === 'pass' ? '' : fix,
};
}
function topActionsForChecks(checks) {
const actions = [];
const failedIds = new Set(checks.filter(check => check.status !== 'pass').map(check => check.id));
if (failedIds.has('release-approval-script-registered')) {
actions.push('Wire release:approval-gate into package.json, package files, and the preview-pack manifest.');
}
if (failedIds.has('owner-decisions-approved')) {
actions.push('Approve, defer, or block each owner decision row explicitly after final evidence is rerun from the release commit.');
}
if (failedIds.has('release-url-ledger-finalized')) {
actions.push('Replace approval-gated URL ledger rows with live readback URLs from the approved release, package, plugin, video, and billing surfaces.');
}
if (failedIds.has('final-evidence-command-listed')) {
actions.push('Add release:approval-gate to the final evidence command lists before asking for publication approval.');
}
if (failedIds.has('announcement-copy-finalized')) {
actions.push('Remove unresolved placeholders and private local paths from launch, social, and outbound copy.');
}
if (failedIds.has('public-action-guard-present')) {
actions.push('Restore the explicit no-outbound/no-publish authorization boundary in the owner packet.');
}
return actions;
}
function buildReport(options = {}) {
const rootDir = path.resolve(options.root || process.cwd());
const packageJson = safeParseJson(readText(rootDir, 'package.json')) || {};
const packageScripts = packageJson.scripts || {};
const packageFiles = Array.isArray(packageJson.files) ? packageJson.files : [];
const ownerPacket = readText(rootDir, OWNER_PACKET_PATH);
const ledger = readText(rootDir, URL_LEDGER_PATH);
const manifest = readText(rootDir, PREVIEW_MANIFEST_PATH);
const decisions = parseDecisionRegister(ownerPacket);
const missingDecisions = [];
const unapprovedDecisions = [];
for (const decision of REQUIRED_DECISIONS) {
const state = decisions.get(normalizeLabel(decision.label));
if (!state) {
missingDecisions.push(decision.label);
} else if (!isApproved(state)) {
unapprovedDecisions.push(`${decision.label}=${state}`);
}
}
const missingUrlSurfaces = REQUIRED_URL_SURFACES
.filter(surface => !ledger.includes(surface.label))
.map(surface => surface.label);
const urlBlockers = ledgerBlockers(ledger);
const announcementOffenders = findAnnouncementOffenders(rootDir, ANNOUNCEMENT_FILES);
const commandListedIn = [
ownerPacket.includes(REQUIRED_COMMAND) ? OWNER_PACKET_PATH : '',
ledger.includes(REQUIRED_COMMAND) ? URL_LEDGER_PATH : '',
manifest.includes(REQUIRED_COMMAND) ? PREVIEW_MANIFEST_PATH : '',
].filter(Boolean);
const checks = [
makeCheck(
'release-approval-script-registered',
packageScripts['release:approval-gate'] === `node ${SCRIPT_PATH}`
&& packageFiles.includes(SCRIPT_PATH)
&& fileExists(rootDir, SCRIPT_PATH)
&& manifest.includes(`\`${SCRIPT_PATH}\``)
&& manifest.includes(REQUIRED_COMMAND)
? 'pass'
: 'fail',
'package script, npm package file entry, local script, and preview-pack manifest reference',
'Add release:approval-gate to package scripts, package files, and preview-pack-manifest.md.'
),
makeCheck(
'owner-decisions-approved',
missingDecisions.length === 0 && unapprovedDecisions.length === 0 ? 'pass' : 'fail',
missingDecisions.length === 0 && unapprovedDecisions.length === 0
? `${REQUIRED_DECISIONS.length} owner decision rows are approved`
: `missing decisions: ${missingDecisions.join(', ') || 'none'}; pending decisions: ${unapprovedDecisions.join(', ') || 'none'}`,
'Set every required owner decision row to approve only after the final release evidence has been rerun.'
),
makeCheck(
'release-url-ledger-finalized',
ledger
&& missingUrlSurfaces.length === 0
&& urlBlockers.length === 0
? 'pass'
: 'fail',
ledger && missingUrlSurfaces.length === 0 && urlBlockers.length === 0
? `${REQUIRED_URL_SURFACES.length} final URL surfaces are recorded without approval-gated blockers`
: `missing URL surfaces: ${missingUrlSurfaces.join(', ') || 'none'}; blockers: ${urlBlockers.join(', ') || 'none'}`,
'Regenerate the release URL ledger after the approved publication actions and record live readback URLs.'
),
makeCheck(
'final-evidence-command-listed',
commandListedIn.length === 3 ? 'pass' : 'fail',
commandListedIn.length === 3
? `${REQUIRED_COMMAND} is listed in owner packet, URL ledger, and preview manifest`
: `${REQUIRED_COMMAND} listed in: ${commandListedIn.join(', ') || 'none'}`,
'List release:approval-gate in every final evidence command block.'
),
makeCheck(
'announcement-copy-finalized',
announcementOffenders.length === 0 ? 'pass' : 'fail',
announcementOffenders.length === 0
? `${ANNOUNCEMENT_FILES.length} launch/outbound copy files have no placeholders or private paths`
: `offenders: ${announcementOffenders.map(item => `${item.path}:${item.line}`).join(', ')}`,
'Replace placeholders with live URLs and remove private local paths from launch/outbound copy.'
),
makeCheck(
'public-action-guard-present',
ownerPacket.includes(
'No outbound email, personal-account post, package publish, plugin tag, or billing announcement is authorized by this packet alone.'
)
? 'pass'
: 'fail',
'owner packet preserves the explicit no-public-action authorization boundary',
'Restore the owner-packet sentence that blocks outbound, posts, package publish, plugin tags, and billing announcements.'
),
];
const failed = checks.filter(check => check.status !== 'pass');
const digest = crypto
.createHash('sha256')
.update(JSON.stringify(checks.map(check => [check.id, check.status, check.evidence])))
.digest('hex')
.slice(0, 12);
return {
schema_version: SCHEMA_VERSION,
release: RELEASE,
ready: failed.length === 0,
digest,
summary: {
passed: checks.length - failed.length,
failed: failed.length,
total: checks.length,
},
top_actions: topActionsForChecks(checks),
checks,
};
}
function renderText(report) {
const lines = [
'ECC release approval gate',
`Release: ${report.release}`,
`Ready: ${report.ready ? 'yes' : 'no'}`,
`Digest: ${report.digest}`,
'',
'Checks:',
];
for (const check of report.checks) {
lines.push(`- ${check.status} ${check.id}: ${check.evidence}`);
if (check.fix) {
lines.push(` fix: ${check.fix}`);
}
}
if (report.top_actions.length > 0) {
lines.push('');
lines.push('Top actions:');
for (const action of report.top_actions) {
lines.push(`- ${action}`);
}
}
lines.push('');
lines.push(`Passed: ${report.summary.passed}`);
lines.push(`Failed: ${report.summary.failed}`);
return `${lines.join('\n')}\n`;
}
function main() {
let parsed;
try {
parsed = parseArgs(process.argv);
} catch (error) {
console.error(`Error: ${error.message}`);
process.exit(1);
}
if (parsed.help) {
usage();
return;
}
const report = buildReport({ root: parsed.root });
if (parsed.format === 'json') {
console.log(JSON.stringify(report, null, 2));
} else {
process.stdout.write(renderText(report));
}
if (!report.ready) {
process.exit(2);
}
}
if (require.main === module) {
main();
}
module.exports = {
ANNOUNCEMENT_FILES,
REQUIRED_COMMAND,
REQUIRED_DECISIONS,
REQUIRED_URL_SURFACES,
buildReport,
parseArgs,
renderText,
};

View File

@@ -177,6 +177,7 @@ test('preview pack manifest assembles release, Hermes, and publication gates', (
'skills/hermes-imports/SKILL.md', 'skills/hermes-imports/SKILL.md',
'docs/architecture/harness-adapter-compliance.md', 'docs/architecture/harness-adapter-compliance.md',
'scripts/preview-pack-smoke.js', 'scripts/preview-pack-smoke.js',
'scripts/release-approval-gate.js',
'docs/releases/2.0.0-rc.1/publication-readiness.md', 'docs/releases/2.0.0-rc.1/publication-readiness.md',
'docs/releases/2.0.0-rc.1/naming-and-publication-matrix.md', 'docs/releases/2.0.0-rc.1/naming-and-publication-matrix.md',
'docs/releases/2.0.0-rc.1/release-url-ledger-2026-05-19.md', 'docs/releases/2.0.0-rc.1/release-url-ledger-2026-05-19.md',
@@ -201,6 +202,7 @@ test('preview pack manifest assembles release, Hermes, and publication gates', (
assert.ok(manifest.includes('no raw workspace exports')); assert.ok(manifest.includes('no raw workspace exports'));
assert.ok(manifest.includes('Final Verification Commands')); assert.ok(manifest.includes('Final Verification Commands'));
assert.ok(manifest.includes('npm run preview-pack:smoke')); assert.ok(manifest.includes('npm run preview-pack:smoke'));
assert.ok(manifest.includes('npm run release:approval-gate -- --format json'));
assert.ok(manifest.includes('npm run release:video-suite -- --format json')); assert.ok(manifest.includes('npm run release:video-suite -- --format json'));
assert.ok(manifest.includes('Reference-Inspired Adapter Direction')); assert.ok(manifest.includes('Reference-Inspired Adapter Direction'));
}); });
@@ -229,6 +231,7 @@ test('owner approval packet consolidates the final gated decisions', () => {
for (const command of [ for (const command of [
'node scripts/platform-audit.js --json', 'node scripts/platform-audit.js --json',
'npm run preview-pack:smoke -- --format json', 'npm run preview-pack:smoke -- --format json',
'npm run release:approval-gate -- --format json',
'npm run release:video-suite -- --format json', 'npm run release:video-suite -- --format json',
'node tests/run-all.js', 'node tests/run-all.js',
]) { ]) {
@@ -255,7 +258,7 @@ test('GA roadmap mirrors the current May 19 release evidence', () => {
for (const marker of [ for (const marker of [
'owner-approval-packet-2026-05-19.md', 'owner-approval-packet-2026-05-19.md',
'preview-pack smoke digest `790430aef4a8`', 'preview-pack smoke digest `531328aaaa53`',
'local 2560-test suite', 'local 2560-test suite',
'PR #2001', 'PR #2001',
'GitHub Actions run `26102500291`', 'GitHub Actions run `26102500291`',
@@ -344,6 +347,31 @@ test('release video suite manifest gates the content launch lane', () => {
assert.ok(packageJson.files.includes('scripts/release-video-suite.js')); assert.ok(packageJson.files.includes('scripts/release-video-suite.js'));
}); });
test('release approval gate blocks publication until owner decisions and URLs are final', () => {
const manifest = read('docs/releases/2.0.0-rc.1/preview-pack-manifest.md');
const packet = read('docs/releases/2.0.0-rc.1/owner-approval-packet-2026-05-19.md');
const ledger = read('docs/releases/2.0.0-rc.1/release-url-ledger-2026-05-19.md');
const script = read('scripts/release-approval-gate.js');
const packageJson = JSON.parse(read('package.json'));
for (const marker of [
'ecc.release-approval-gate.v1',
'owner-decisions-approved',
'release-url-ledger-finalized',
'announcement-copy-finalized',
'No outbound email, personal-account post, package publish, plugin tag, or billing announcement',
]) {
assert.ok(script.includes(marker), `release approval gate missing ${marker}`);
}
assert.ok(manifest.includes('scripts/release-approval-gate.js'));
assert.ok(manifest.includes('npm run release:approval-gate -- --format json'));
assert.ok(packet.includes('npm run release:approval-gate -- --format json'));
assert.ok(ledger.includes('npm run release:approval-gate -- --format json'));
assert.strictEqual(packageJson.scripts['release:approval-gate'], 'node scripts/release-approval-gate.js');
assert.ok(packageJson.files.includes('scripts/release-approval-gate.js'));
});
test('partner sponsor talks pack gates the hypergrowth outbound lane', () => { test('partner sponsor talks pack gates the hypergrowth outbound lane', () => {
const partnerPack = read('docs/releases/2.0.0-rc.1/partner-sponsor-talks-pack.md'); const partnerPack = read('docs/releases/2.0.0-rc.1/partner-sponsor-talks-pack.md');
const manifest = read('docs/releases/2.0.0-rc.1/preview-pack-manifest.md'); const manifest = read('docs/releases/2.0.0-rc.1/preview-pack-manifest.md');
@@ -510,6 +538,7 @@ test('release name and plugin publication checklist freezes rc.1 surfaces', () =
'codex plugin marketplace add --help', 'codex plugin marketplace add --help',
'npm publish --tag next --dry-run', 'npm publish --tag next --dry-run',
'npm run preview-pack:smoke', 'npm run preview-pack:smoke',
'npm run release:approval-gate -- --format json',
]) { ]) {
assert.ok(checklist.includes(command), `release name/plugin checklist missing command ${command}`); assert.ok(checklist.includes(command), `release name/plugin checklist missing command ${command}`);
} }

View File

@@ -60,6 +60,7 @@ function buildExpectedPublishPaths(repoRoot) {
"scripts/operator-readiness-dashboard.js", "scripts/operator-readiness-dashboard.js",
"scripts/platform-audit.js", "scripts/platform-audit.js",
"scripts/preview-pack-smoke.js", "scripts/preview-pack-smoke.js",
"scripts/release-approval-gate.js",
"scripts/release-video-suite.js", "scripts/release-video-suite.js",
"scripts/skill-create-output.js", "scripts/skill-create-output.js",
"scripts/repair.js", "scripts/repair.js",
@@ -132,6 +133,7 @@ function main() {
"scripts/discussion-audit.js", "scripts/discussion-audit.js",
"scripts/operator-readiness-dashboard.js", "scripts/operator-readiness-dashboard.js",
"scripts/preview-pack-smoke.js", "scripts/preview-pack-smoke.js",
"scripts/release-approval-gate.js",
"scripts/release-video-suite.js", "scripts/release-video-suite.js",
"scripts/work-items.js", "scripts/work-items.js",
"scripts/platform-audit.js", "scripts/platform-audit.js",

View File

@@ -0,0 +1,320 @@
'use strict';
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { execFileSync, spawnSync } = require('child_process');
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'release-approval-gate.js');
const {
REQUIRED_DECISIONS,
REQUIRED_URL_SURFACES,
buildReport,
parseArgs,
renderText,
} = require(SCRIPT);
const RELEASE_DIR = 'docs/releases/2.0.0-rc.1';
function createTempDir(prefix) {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
}
function cleanup(dirPath) {
fs.rmSync(dirPath, { recursive: true, force: true });
}
function writeFile(rootDir, relativePath, content) {
const targetPath = path.join(rootDir, relativePath);
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
fs.writeFileSync(targetPath, content);
}
function approvedPacketContent(overrides = {}) {
const decisions = new Map(REQUIRED_DECISIONS.map(decision => [decision.label, 'approve']));
for (const [label, value] of Object.entries(overrides)) {
decisions.set(label, value);
}
return [
'# ECC v2.0.0-rc.1 Owner Approval Packet',
'',
'## Decision Register',
'',
'| Decision | Approve / defer / block | Evidence required first | Notes |',
'| --- | --- | --- | --- |',
...REQUIRED_DECISIONS.map(decision => (
`| ${decision.label} | ${decisions.get(decision.label)} | final evidence | approved fixture |`
)),
'',
'## Final Evidence Commands',
'',
'```bash',
'npm run release:approval-gate -- --format json',
'```',
'',
'No outbound email, personal-account post, package publish, plugin tag, or billing announcement is authorized by this packet alone.',
].join('\n');
}
function finalLedgerContent(extra = '') {
return [
'# ECC v2.0.0-rc.1 Release URL Ledger',
'',
'## Final Published URLs',
'',
'| Surface | URL | Verification |',
'| --- | --- | --- |',
...REQUIRED_URL_SURFACES.map(surface => (
`| ${surface.label} | ${surface.exampleUrl} | readback from final release commit |`
)),
'',
'## Final Verification Commands',
'',
'```bash',
'npm run release:approval-gate -- --format json',
'```',
'',
extra,
].join('\n');
}
function manifestContent() {
return [
'# ECC v2.0.0-rc.1 Preview Pack Manifest',
'',
'| Artifact | Role | Gate |',
'| --- | --- | --- |',
'| `scripts/release-approval-gate.js` | Final owner approval and live URL gate | Verified by `npm run release:approval-gate -- --format json` |',
'',
'## Final Verification Commands',
'',
'```bash',
'npm run release:approval-gate -- --format json',
'```',
].join('\n');
}
function seedRepo(rootDir, overrides = {}) {
const files = {
'package.json': JSON.stringify({
files: ['scripts/release-approval-gate.js'],
scripts: {
'release:approval-gate': 'node scripts/release-approval-gate.js',
},
}, null, 2),
'scripts/release-approval-gate.js': 'release approval gate script',
[`${RELEASE_DIR}/owner-approval-packet-2026-05-19.md`]: approvedPacketContent(),
[`${RELEASE_DIR}/release-url-ledger-2026-05-19.md`]: finalLedgerContent(),
[`${RELEASE_DIR}/preview-pack-manifest.md`]: manifestContent(),
[`${RELEASE_DIR}/release-notes.md`]: 'Release notes with final URLs.',
[`${RELEASE_DIR}/x-thread.md`]: 'X post with final URLs.',
[`${RELEASE_DIR}/linkedin-post.md`]: 'LinkedIn post with final URLs.',
[`${RELEASE_DIR}/article-outline.md`]: 'Article outline with final URLs.',
[`${RELEASE_DIR}/partner-sponsor-talks-pack.md`]: 'Outbound copy with final URLs.',
'docs/business/social-launch-copy.md': 'Business launch copy with final URLs.',
};
for (const [relativePath, content] of Object.entries({ ...files, ...overrides })) {
if (content === null) {
continue;
}
writeFile(rootDir, relativePath, content);
}
}
function run(args = [], options = {}) {
return execFileSync('node', [SCRIPT, ...args], {
cwd: options.cwd || path.join(__dirname, '..', '..'),
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 10000,
});
}
function runProcess(args = [], options = {}) {
return spawnSync('node', [SCRIPT, ...args], {
cwd: options.cwd || path.join(__dirname, '..', '..'),
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 10000,
});
}
function test(name, fn) {
try {
fn();
console.log(` PASS ${name}`);
return true;
} catch (error) {
console.log(` FAIL ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function runTests() {
console.log('\n=== Testing release-approval-gate.js ===\n');
let passed = 0;
let failed = 0;
if (test('parseArgs accepts approval gate flags and rejects invalid values', () => {
const rootDir = createTempDir('release-approval-args-');
try {
const parsed = parseArgs([
'node',
'script',
'--format=json',
`--root=${rootDir}`,
]);
assert.strictEqual(parsed.format, 'json');
assert.strictEqual(parsed.root, path.resolve(rootDir));
assert.throws(() => parseArgs(['node', 'script', '--format', 'xml']), /Invalid format/);
assert.throws(() => parseArgs(['node', 'script', '--root']), /--root requires a value/);
assert.throws(() => parseArgs(['node', 'script', '--unknown']), /Unknown argument/);
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('seeded approved release passes every publication approval check', () => {
const rootDir = createTempDir('release-approval-pass-');
try {
seedRepo(rootDir);
const report = buildReport({ root: rootDir });
assert.strictEqual(report.schema_version, 'ecc.release-approval-gate.v1');
assert.strictEqual(report.ready, true);
assert.strictEqual(report.summary.failed, 0);
assert.deepStrictEqual(report.top_actions, []);
assert.ok(report.checks.every(check => check.status === 'pass'));
const text = renderText(report);
assert.ok(text.includes('Ready: yes'));
assert.ok(text.includes('Failed: 0'));
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('deferred owner decisions keep the publication gate blocked', () => {
const rootDir = createTempDir('release-approval-deferred-');
try {
seedRepo(rootDir, {
[`${RELEASE_DIR}/owner-approval-packet-2026-05-19.md`]: approvedPacketContent({
'GitHub prerelease': 'defer',
'Sponsor, partner, consulting, conference, podcast outreach': 'block',
}),
});
const report = buildReport({ root: rootDir });
const decisions = report.checks.find(check => check.id === 'owner-decisions-approved');
assert.strictEqual(report.ready, false);
assert.strictEqual(decisions.status, 'fail');
assert.ok(decisions.evidence.includes('GitHub prerelease=defer'));
assert.ok(decisions.evidence.includes('Sponsor, partner, consulting, conference, podcast outreach=block'));
assert.ok(report.top_actions.some(action => action.includes('Approve, defer, or block')));
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('approval-gated URL ledger rows keep the publication gate blocked', () => {
const rootDir = createTempDir('release-approval-ledger-');
try {
seedRepo(rootDir, {
[`${RELEASE_DIR}/release-url-ledger-2026-05-19.md`]: [
'# ECC v2.0.0-rc.1 Release URL Ledger',
'',
'## Approval-Gated URLs',
'',
'| Surface | Intended URL or command | Gate before use |',
'| --- | --- | --- |',
'| GitHub prerelease | https://github.com/affaan-m/ECC/releases/tag/v2.0.0-rc.1 | must return the prerelease |',
].join('\n'),
});
const report = buildReport({ root: rootDir });
const ledger = report.checks.find(check => check.id === 'release-url-ledger-finalized');
assert.strictEqual(report.ready, false);
assert.strictEqual(ledger.status, 'fail');
assert.ok(ledger.evidence.includes('approval-gated URL section still present'));
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('announcement drafts fail on unresolved placeholders and private paths', () => {
const rootDir = createTempDir('release-approval-copy-');
try {
seedRepo(rootDir, {
[`${RELEASE_DIR}/x-thread.md`]: 'Ship copy with <video-url> and /Users/affaan/raw-footage.',
});
const report = buildReport({ root: rootDir });
const copy = report.checks.find(check => check.id === 'announcement-copy-finalized');
assert.strictEqual(report.ready, false);
assert.strictEqual(copy.status, 'fail');
assert.ok(copy.evidence.includes(`${RELEASE_DIR}/x-thread.md:1`));
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('CLI emits json and uses status 2 for blocked approval reports', () => {
const rootDir = createTempDir('release-approval-cli-');
try {
seedRepo(rootDir);
const stdout = run(['--format=json', `--root=${rootDir}`], { cwd: rootDir });
const parsed = JSON.parse(stdout);
assert.strictEqual(parsed.ready, true);
writeFile(
rootDir,
`${RELEASE_DIR}/owner-approval-packet-2026-05-19.md`,
approvedPacketContent({ 'Video upload': 'defer' })
);
const failedRun = runProcess(['--format=json', `--root=${rootDir}`], { cwd: rootDir });
assert.strictEqual(failedRun.status, 2);
assert.strictEqual(failedRun.stderr, '');
assert.ok(failedRun.stdout.includes('"ready": false'));
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('CLI help exits successfully and invalid flags fail before reporting', () => {
const help = runProcess(['--help']);
assert.strictEqual(help.status, 0);
assert.strictEqual(help.stderr, '');
assert.ok(help.stdout.includes('Usage: node scripts/release-approval-gate.js'));
const invalid = runProcess(['--format=xml']);
assert.strictEqual(invalid.status, 1);
assert.strictEqual(invalid.stdout, '');
assert.match(invalid.stderr, /Error: Invalid format/);
})) passed++; else failed++;
console.log(`\nPassed: ${passed}`);
console.log(`Failed: ${failed}`);
if (failed > 0) {
process.exit(1);
}
}
if (require.main === module) {
runTests();
}