diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ecd9fafda..84d12245d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,10 +7,10 @@ name: Release # check:release-package-contents` (Layer 2 — fresh-surface contract per # ADR-0021 / SPEC-V05-010), `actions/attest-build-provenance` for the # workflow-built tarball, `gh release create` for the canonical `vX.Y.Z` tag -# on `main` per ADR-0020, `npm publish` to npmjs.com via classic NPM_TOKEN -# (ADR-0041 — interim fallback to ADR-0040 Trusted Publishing, tracked in -# issue #411), and `gh release upload` to attach the package tarball as a -# release asset. +# on `main` per ADR-0020, `npm publish --provenance` to npmjs.com via +# Trusted Publishing (ADR-0044 restoring ADR-0040 after the ADR-0041 +# NPM_TOKEN fallback used for v0.7.0 / v0.7.1), and `gh release upload` to +# attach the package tarball as a release asset. # # Inputs and the confirm gate satisfy SPEC-V05-002 (explicit publish # authorisation), SPEC-V05-003 (GitHub Release publication), SPEC-V05-004 @@ -50,16 +50,16 @@ on: required: false default: false -# Least-privilege workflow permissions per ADR-0020 / ADR-0040 / ADR-0041 / +# Least-privilege workflow permissions per ADR-0020 / ADR-0040 / ADR-0044 / # SPEC-V05-002 / NFR-V05-001. `REQUIRED_WORKFLOW_PERMISSIONS` # (scripts/lib/release-readiness.ts) enforces the top-level block as exactly # { contents: write, attestations: write, id-token: write }; job-level # overrides may only narrow, never widen. `contents: write` for # `gh release create` + upload, `attestations: write` to persist GitHub -# Release tarball attestations, `id-token: write` for the -# `actions/attest-build-provenance` step that signs the GitHub Release -# tarball asset (kept across the ADR-0041 fallback so the GitHub Release -# asset retains its provenance even while npm provenance is paused). +# Release tarball attestations, `id-token: write` is now load-bearing for +# both attestation paths: it mints the OIDC token consumed by `npm publish +# --provenance` (ADR-0044) and the OIDC token consumed by +# `actions/attest-build-provenance` (Release tarball asset). # zizmor: suppressed inline. permissions: # zizmor: ignore[excessive-permissions,undocumented-permissions] contents: write # zizmor: ignore[excessive-permissions] @@ -74,16 +74,25 @@ jobs: smoke: name: Smoke test (release gate) uses: ./.github/workflows/smoke-test.yml - permissions: - contents: read + # No job-level `permissions:` block — `scripts/lib/release-readiness.ts` + # `diagnosticsForPermissions` enforces strict equality between job-level + # and top-level permission values (line ~852: "is `` but must be + # ``"). A `contents: read` override here failed Layer 1 + # readiness on the v0.8.0-rc.1 dispatch (run 25639883562). The smoke job + # therefore inherits the top-level `{ contents: write, attestations: + # write, id-token: write }` block. The reusable smoke-test workflow is + # read-only in practice (npm pack + install + CLI smoke); the inherited + # write scopes are unused. release: name: Manual GitHub Release needs: smoke runs-on: ubuntu-latest - # Deployment environment scopes `NPM_TOKEN` to release dispatches per - # GitHub's `secrets-without-environment` advisory and lets the operator - # add required-reviewer / wait-timer protection rules without changing + # Deployment environment matches the npmjs.com Trusted Publisher + # configuration (workflow=`release.yml`, environment=`release`) so the + # OIDC token minted by GitHub for this job authenticates the npm + # publish step. The environment also lets the operator add + # required-reviewer / wait-timer protection rules without changing # the workflow. URL points at the just-published GitHub Release page. environment: name: release @@ -97,13 +106,9 @@ jobs: # `registry-url` writes `~/.npmrc` so `npm publish` resolves the # registry to npmjs.com. The `scope` field is unnecessary because the - # package is unscoped (`specorator`), per ADR-0040. - # - # `NODE_AUTH_TOKEN` is supplied at the publish step (not here) via the - # `NPM_TOKEN` repo secret — a classic Automation token, per ADR-0041 - # (interim fallback). When Trusted Publishing is unblocked (#411) the - # publish step will switch back to OIDC + `--provenance` and the - # `NPM_TOKEN` env will be removed. + # package is unscoped (`specorator`), per ADR-0040. No + # `NODE_AUTH_TOKEN` is set — Trusted Publishing (ADR-0044) supplies + # the auth token via OIDC at the publish step. - name: Setup Node uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: @@ -343,18 +348,19 @@ jobs: --jq '.body' \ || echo '(generate-notes preview unavailable; readiness diagnostics above remain authoritative)' - # Step 10 — publish the package to npmjs.com via classic NPM_TOKEN - # (ADR-0041, interim fallback to ADR-0040). The v0.7.0 release - # dispatch failed at this step with `404 PUT` because npmjs.com - # Trusted Publishing requires the publisher to be pre-registered - # against an existing package, and `specorator` did not exist at - # the time of the dispatch (chicken-and-egg). Subsequent attempts - # to configure Trusted Publisher on the now-existing package are - # currently blocked. `actions/setup-node` wrote `~/.npmrc` pointing - # at `https://registry.npmjs.org`; `NODE_AUTH_TOKEN` (set via the - # `NPM_TOKEN` repo secret — a classic Automation token) authenticates - # the publish. Tracking issue: #411. Re-enable OIDC + `--provenance` - # once Trusted Publisher is configured and verified. + # Step 10 — publish the package to npmjs.com via npmjs.com Trusted + # Publishing (ADR-0044, restoring ADR-0040 after the ADR-0041 + # NPM_TOKEN fallback used for v0.7.0 / v0.7.1). The publisher is + # pre-registered on npmjs.com against this workflow file + # (`release.yml`) on the `release` deployment environment; the + # OIDC token minted via `id-token: write` authenticates the + # publish. `actions/setup-node` wrote `~/.npmrc` pointing at + # `https://registry.npmjs.org`; no `NODE_AUTH_TOKEN` is read. + # `npm publish --provenance` mints a sigstore provenance + # statement that ships with the tarball and is visible on the + # npmjs.com package page under `Provenance`. Tracking issue + # #411 closed when Trusted Publishing was activated on + # 2026-05-10. # # The pre-flight check short-circuits before `npm publish` if # `package.json` drifted from `INPUT_VERSION` between Layer 1 @@ -376,29 +382,28 @@ jobs: # - npm E404 → version genuinely not published, proceed. # - any other failure (transient registry / auth / DNS) → fail # closed. - # - # zizmor's `use-trusted-publishing` audit suggests OIDC trusted - # publishing instead of a token. Trusted Publishing is the - # documented target (ADR-0040) and intended return path; ADR-0041 - # records the deferral. The audit is suppressed at the step - # boundary; revisit when issue #411 closes. - - name: Publish to npmjs.com # zizmor: ignore[use-trusted-publishing] + - name: Publish to npmjs.com if: ${{ ! inputs.dry_run && inputs.publish_package }} env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} INPUT_VERSION: ${{ inputs.version }} + INPUT_PRERELEASE: ${{ inputs.prerelease }} TARBALL: ${{ steps.pack.outputs.tarball }} run: | - if [ -z "${NODE_AUTH_TOKEN}" ]; then - echo "::error::NPM_TOKEN repo secret is not set — refusing to publish (ADR-0041 fallback requires a classic Automation token until issue #411 closes)" >&2 - exit 1 - fi actual="$(node -p "require('./package.json').name + '@' + require('./package.json').version")" expected="specorator@${INPUT_VERSION}" if [ "$actual" != "$expected" ]; then echo "::error::package.json identity (${actual}) does not match expected (${expected}) — refusing to publish (ADR-0040)" >&2 exit 1 fi + # Pre-release versions must publish under a non-`latest` dist-tag. + # `npm publish` refuses to default a prerelease to `latest` and + # exits with "You must specify a tag using --tag when publishing + # a prerelease version." `inputs.prerelease == true` → publish + # under `next`; stable releases → default `latest` (no `--tag`). + publish_args=("--provenance") + if [ "${INPUT_PRERELEASE}" = "true" ]; then + publish_args+=("--tag" "next") + fi set +e view_output="$(npm view "specorator@${INPUT_VERSION}" version --json 2>&1)" view_exit=$? @@ -408,9 +413,9 @@ jobs: elif echo "$view_output" | grep -qE "\"code\": *\"E404\"|E404|code E404|404 Not Found"; then # Publish the byte-identical tarball produced in step 5 so the # published archive equals the GitHub Release asset uploaded in - # step 11 (T-V05-013). No `--provenance` flag — Trusted Publishing - # is deferred per ADR-0041 (#411). - npm publish "${TARBALL}" + # step 11 (T-V05-013). `--provenance` mints a sigstore provenance + # statement via the OIDC token (ADR-0044, restoring ADR-0040). + npm publish "${publish_args[@]}" "${TARBALL}" else echo "::error::npm view failed with a non-404 error — refusing to publish so EPUBLISHCONFLICT cannot mask a real failure" >&2 echo "$view_output" >&2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 0426d9176..8320dbf8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,58 @@ All notable changes to Specorator are documented here. Format follows [Keep a Ch --- +## [v0.8.0-rc.1] — 2026-05-10 + +Release candidate for the v0.8.0 cycle. Smoke-tests npmjs.com Trusted Publishing on the `specorator` package after [ADR-0044](docs/adr/0044-restore-npmjs-trusted-publishing.md) restored the OIDC + `--provenance` path (supersedes ADR-0041). The first successful RC dispatch confirms `release.yml` mints an OIDC token, `npmjs.com` accepts the publish, and the package page surfaces a sigstore provenance attestation. Surface content is identical to the v0.8.0 final entry below. + +--- + +## [v0.8.0] — 2026-05-10 + +### Changed + +- **Plugin distribution moves to an orphan dist branch.** The Claude Code plugin bundle (`claude-plugin/specorator/{agents,skills,commands,.claude-plugin}/` + `.mcp.json`) is now gitignored on `develop` and `main` and published to the long-lived orphan branch `dist/claude-plugin` by `.github/workflows/publish-claude-plugin.yml` on every push to `main`. The marketplace entry in `.claude-plugin/marketplace.json` switched from a relative-path source to a `git-subdir` source pinned to that ref. Marketplace consumers transparently pick up the new bundle on `/plugin marketplace update`. ADR-0043 records the rationale and considered alternatives. Closes #461 (gitignore-the-bundle) and #474 (install-flow redesign). +- **npmjs.com Trusted Publishing restored** — `release.yml` step 10 reverts to OIDC + `npm publish --provenance`; `NODE_AUTH_TOKEN` env removed; `NPM_TOKEN` repo secret decommissioned; `# zizmor: ignore[use-trusted-publishing]` suppression dropped. Every v0.8.x release ships with a sigstore provenance attestation visible on the npmjs.com package page. ADR-0044 supersedes ADR-0041; closes #411. Operator-guide §1 (prereqs), §5 (publish step), §5.1 (provenance posture), and §7.1 (manual recovery) refreshed for the OIDC happy path. +- `npm run check:claude-plugin` is now structural-only on a clean `develop`/`main` checkout — generated-output checks (manifest, `.mcp.json`, agents/skills/commands directories) are conditional on the file being present. The `build:claude-plugin --check` drift check is no longer part of the verify gate. +- `.github/workflows/release.yml` now runs `npm run build:claude-plugin` before the readiness gate so the npm tarball still ships the bundle in `claude-plugin/specorator/`. +- ADR-0030 (repo-adoption track) withdrawn — superseded by the plugin-packaging path. + +### Added + +- **`/issue:tackle` conductor skill** — triage-first workflow for resolving a GitHub issue or PR end-to-end: classifies type/priority, scans for open tasks, proposes a resolution path, creates an isolated worktree, guides execution, and opens a PR (#443). +- **`/specorator:onboard`** — guided 5-step onboarding series scaffolded on first install. Idempotent; falls back to local Markdown when `gh` auth is missing (#460). +- **GitHub remote MCP server** wired into the project `.mcp.json` and the plugin bundle. Issue, PR, branch, and review-comment operations are now first-class for any agent in a Specorator project (#471). +- **`specorator --version` / `-v`** CLI flag — version reported from `package.json#version`, no more drift (#424). Bare `--version` and `-V` now exit `0` (#419). +- **Conductor-driven model-tier injection** for subagents — orchestrator skills can specify the model tier per dispatch (#440). +- **Plugin install smoke test** in CI — `.github/workflows/smoke-test.yml` packs the tarball, installs `specorator` globally, and asserts CLI subcommand exit codes (#427). +- **ADR-0042** — adopt typed-artifact reader seam to keep agent IO contracts auditable (#442). +- **ADR-0043** — distribute Claude Code plugin bundle from an orphan dist branch via `git-subdir`. +- **Plugin user manual** at `docs/how-to/install-claude-plugin.md` rewritten for the orphan-branch flow + plugin-first install path (#410, #431, #467). +- **Operational bot dry-run + drift checks** — `tests/scripts/operational-bots.test.ts` validates every PROMPT.md surface (#438). +- **Agentic control-plane threat model** at `docs/security/control-plane-threats.md` (#437). +- **Specorator product ladder** at `docs/specorator-product/product-ladder.md` and supporting proof-loop asset (#435, #436). +- **Automation contract** entry point at `docs/automation-contract.md` cross-referencing every automation surface (#434). + +### Fixed + +- `scripts/build-claude-plugin.ts` strips the `specorator/` prefix from canonical command paths so `/specorator:specorator:init` collapses back to `/specorator:init` in the published bundle (#420). +- `specorator init` warns when the target directory has no `.git` so adopters see the missing-VCS state before running through onboarding (#421). +- `/quality:status` and quality-related agents/skills now invoke the `specorator` CLI rather than raw `npm run` (#426, #428). +- Hardcoded `model: opus` removed from `architect`, `dev`, and `reviewer` subagents — the conductor sets the tier (#429). +- Quality status loader normalises historical status values (`done`, `complete`) to the canonical `completed` to silence loader warnings on legacy specs (#439). +- Product page deployment moved to a dedicated `gh-pages` branch — broken images on the public site fixed (#456). +- `.github/workflows/sync-github-archive.yml` sets the bot git identity before the merge step so scheduled syncs no longer fail at `nothing to commit` (#468). +- Feature-tracker test guards `readFile` against the ENOENT race when the page-data fixture is regenerated mid-run (#408). + +### Internal + +- `actions/setup-node` bumped to v6.4.0 across remaining workflows (#473). +- v0.7 spec stub backfilled at `specs/v07-npm-publish/` for traceability (#432). +- `docs/specorator-product/site-vision.md` defines the Astro product-page roadmap. +- 30+ merged PRs since v0.7.0 — see the GitHub Release notes for the full enumeration. + +--- + ## [v0.7.0] — 2026-05-09 ### Breaking diff --git a/README.md b/README.md index c84a422dd..1615f481a 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # Specorator — Agentic Development Workflow -![Version](https://img.shields.io/badge/version-v0.7.0-blue) ![License](https://img.shields.io/badge/license-MIT-green) +![Version](https://img.shields.io/badge/version-v0.8.0-blue) ![License](https://img.shields.io/badge/license-MIT-green) [![Verify](https://github.com/Luis85/agentic-workflow/actions/workflows/verify.yml/badge.svg?branch=main)](https://github.com/Luis85/agentic-workflow/actions/workflows/verify.yml) [![gitleaks](https://github.com/Luis85/agentic-workflow/actions/workflows/gitleaks.yml/badge.svg?branch=main)](https://github.com/Luis85/agentic-workflow/actions/workflows/gitleaks.yml) [![typos](https://github.com/Luis85/agentic-workflow/actions/workflows/typos.yml/badge.svg?branch=main)](https://github.com/Luis85/agentic-workflow/actions/workflows/typos.yml) [![zizmor](https://github.com/Luis85/agentic-workflow/actions/workflows/zizmor.yml/badge.svg?branch=main)](https://github.com/Luis85/agentic-workflow/actions/workflows/zizmor.yml) **Build software the right way with AI.** Specorator is a spec-driven workflow template: humans decide what to build, specialist agents handle how, and every requirement, decision, task, test, and release note stays traceable. -> **Status:** v0.7.0 — npm publication migrated from GitHub Packages (`@luis85/agentic-workflow`) to npmjs.com as `specorator` (unscoped, public). Adopters now run `npm install -g specorator` with no `.npmrc` configuration or PAT. Trusted publishing via OIDC is deferred per [ADR-0041](docs/adr/0041-defer-npmjs-trusted-publishing.md) (tracking [#411](https://github.com/Luis85/agentic-workflow/issues/411)) — v0.7.x publishes via classic `NPM_TOKEN` and ships without npm provenance attestations until the trusted publisher path is unblocked. The GitHub Release tarball asset still carries a sigstore attestation. v0.6.2 patched the Layer 1 readiness `refs/heads/main` lookup. v0.6.1 shipped Specorator as a Claude Code plugin. v0.6.0 introduced the Astro 6 product page. Claude Code is first-class; Codex, Cursor, Aider, Copilot, and Gemini have Markdown-based walkthroughs. **Breaking:** the GitHub Packages package `@luis85/agentic-workflow` is deprecated; existing versions still install but new releases land only on npmjs.com. +> **Status:** v0.8.0 — Claude Code plugin bundle moves to a long-lived orphan branch (`dist/claude-plugin`) rebuilt by CI on every push to `main`; the marketplace entry now uses a `git-subdir` source pinned to that ref ([ADR-0043](docs/adr/0043-distribute-claude-plugin-bundle-from-orphan-dist-branch.md)). Bundle is gitignored on `develop`/`main`, so PR diffs stop carrying generated-artifact churn. New `/issue:tackle` conductor skill, `/specorator:onboard` guided issue series, GitHub remote MCP server in the project default, conductor-driven model-tier injection for subagents, and `specorator --version` flag. npmjs.com Trusted Publishing restored ([ADR-0044](docs/adr/0044-restore-npmjs-trusted-publishing.md), supersedes ADR-0041); every v0.8.x release ships with a sigstore provenance attestation visible on the npmjs.com package page. v0.7.0 migrated the npm CLI from GitHub Packages to npmjs.com as `specorator` (unscoped, public, no `.npmrc`). Claude Code is first-class; Codex, Cursor, Aider, Copilot, and Gemini have Markdown-based walkthroughs. Product page: diff --git a/docs/adr/0041-defer-npmjs-trusted-publishing.md b/docs/adr/0041-defer-npmjs-trusted-publishing.md index caa6bb3d2..3c730b772 100644 --- a/docs/adr/0041-defer-npmjs-trusted-publishing.md +++ b/docs/adr/0041-defer-npmjs-trusted-publishing.md @@ -1,7 +1,7 @@ --- id: ADR-0041 title: Defer npmjs.com Trusted Publishing — fall back to NPM_TOKEN -status: accepted +status: superseded date: 2026-05-09 deciders: - maintainer @@ -10,7 +10,7 @@ consulted: informed: - template adopters supersedes: [] -superseded-by: [] +superseded-by: [ADR-0044] tags: [release, distribution, security] --- @@ -18,7 +18,7 @@ tags: [release, distribution, security] ## Status -Accepted +Superseded by [ADR-0044](0044-restore-npmjs-trusted-publishing.md) on 2026-05-10. npmjs.com Trusted Publisher was activated against `release.yml`; the OIDC + `--provenance` path is restored beginning with the v0.8.0 release dispatch. Tracking issue #411 closed. ## Context diff --git a/docs/adr/0044-restore-npmjs-trusted-publishing.md b/docs/adr/0044-restore-npmjs-trusted-publishing.md new file mode 100644 index 000000000..61a1d1fa0 --- /dev/null +++ b/docs/adr/0044-restore-npmjs-trusted-publishing.md @@ -0,0 +1,106 @@ +--- +id: ADR-0044 +title: Restore npmjs.com Trusted Publishing — re-enable OIDC + provenance +status: accepted +date: 2026-05-10 +deciders: + - Luis Mendez +consulted: + - docs/adr/0040-migrate-npm-publication-to-npmjs-com.md + - docs/adr/0041-defer-npmjs-trusted-publishing.md +informed: + - template adopters +supersedes: [ADR-0041] +superseded-by: [] +tags: [release, distribution, security, supply-chain] +--- + +# ADR-0044 — Restore npmjs.com Trusted Publishing — re-enable OIDC + provenance + +## Status + +Accepted + +## Context + +[ADR-0040](0040-migrate-npm-publication-to-npmjs-com.md) committed Specorator to publishing the `specorator` npm package to npmjs.com via OIDC Trusted Publishing, with `npm publish --provenance` minting a sigstore provenance attestation. The v0.7.0 release dispatch failed at the publish step with `404 PUT` because npmjs.com Trusted Publishing requires the publisher to be pre-registered against an existing package, and `specorator` did not exist on npmjs.com when the dispatch ran. [ADR-0041](0041-defer-npmjs-trusted-publishing.md) recorded the fallback to a classic `NPM_TOKEN` Automation token and tracked the diagnose-and-fix work in [issue #411](https://github.com/Luis85/agentic-workflow/issues/411). + +The fallback shipped v0.7.0, v0.7.1, and the package now exists on npmjs.com with three published versions. The operator subsequently activated the npmjs.com Trusted Publisher configuration on the `specorator` package against this repository's `release.yml` workflow on 2026-05-10, closing #411. The OIDC path is now functional end-to-end. + +This ADR restores ADR-0040's published auth shape (OIDC-only, `--provenance` on every publish) and supersedes ADR-0041's interim fallback. The ADR-0041 NPM_TOKEN secret is decommissioned: removed from the publish step's environment, no longer required by `release.yml`, and revoked from the maintainer's npmjs.com account. The `id-token: write` workflow permission stays — it was kept across the ADR-0041 fallback for the `actions/attest-build-provenance` step that signs the GitHub Release tarball asset, and it is now also required for OIDC token minting on the npm publish step. + +## Decision + +Re-enable npmjs.com Trusted Publishing for the `specorator` package, beginning with the v0.8.0 release dispatch. + +`release.yml` step 10 (`Publish to npmjs.com`) is updated to: + +- Drop the `NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}` environment variable. The OIDC token minted via `id-token: write` authenticates the publish; no long-lived secret is read. +- Drop the leading `if [ -z "${NODE_AUTH_TOKEN}" ]` guard. The token-not-set failure mode no longer applies because the auth path is OIDC. +- Add `--provenance` to `npm publish` so every published version ships with a sigstore provenance attestation visible on the npmjs.com package page (`Provenance` badge under `Provenance` tab). +- Remove the inline `# zizmor: ignore[use-trusted-publishing]` suppression on the publish step. The step is now compliant with zizmor's `use-trusted-publishing` audit by construction. + +`package.json#publishConfig.provenance` stays implicit — the `--provenance` flag on the `npm publish` invocation is sufficient and matches the ADR-0040 §Compliance shape ("`npm publish --provenance` is invoked from the workflow"). The publishConfig override is not added back to keep `npm publish` from a contributor's local checkout (without `--provenance`) from accidentally minting a useless provenance statement. + +The pre-flight `package.json` identity check, `npm view` idempotency probe, and tarball-publish flow from ADR-0041 are preserved unchanged — this ADR changes only the auth mechanism. + +The `NPM_TOKEN` repo secret is removed from `Luis85/agentic-workflow` after the first successful OIDC publish on the `release` environment. Tracking issue #411 is closed. + +## Considered options + +### Option A — Re-enable OIDC + `--provenance` on this PR — chosen + +- Pros: Restores ADR-0040's published shape immediately. Every v0.8.x and onward release ships with npm-side provenance attestations, closing the gap from v0.7.x. Removes the long-lived `NPM_TOKEN` credential and shrinks the auth surface area. +- Cons: First Trusted-Publishing dispatch is the v0.8.0 release. If the npmjs.com Trusted Publisher configuration is misaligned (wrong workflow file name, wrong environment, wrong workflow ref), the publish step fails closed and the operator either fixes the configuration on npmjs.com or re-dispatches from the `NPM_TOKEN` fallback shape. Mitigation: validate first against a `0.8.0-rc.1` pre-release dispatch; only proceed to `0.8.0` if RC publish succeeds. + +### Option B — Stay on NPM_TOKEN through v0.8.x; restore in v0.9.0 + +- Pros: Decouples the workflow change from the v0.8.0 promotion; lets the operator dry-run other auth paths first. +- Cons: Indefinitely defers ADR-0040's compliance footprint. Long-lived `NPM_TOKEN` keeps living in repo secrets. Every additional release without provenance widens the attestation gap on npmjs.com. + +### Option C — Re-enable OIDC, keep `NPM_TOKEN` as a parallel fallback + +- Pros: Belt-and-braces — if OIDC fails, the workflow falls through to NPM_TOKEN. +- Cons: Doubled auth surface. Workflow logic becomes branchy and hard to reason about. zizmor's `use-trusted-publishing` audit still fires on the fallback path. Defeats the simplification ADR-0044 buys. + +## Consequences + +### Positive + +- Every v0.8.x and onward release ships with sigstore provenance attestations on npmjs.com (visible under the package page's `Provenance` tab and via `npm view specorator --json | jq '.dist.attestations'`). +- The `NPM_TOKEN` long-lived credential is removed from the repo secrets surface. +- zizmor's `use-trusted-publishing` audit passes on `release.yml` without an inline suppression on the publish step. +- ADR-0040's §Consequences/Neutral line "NPM_TOKEN repo secret stays as an emergency fallback for one cycle" is honored — the fallback was used for one cycle (v0.7.0 + v0.7.1) and is now removed. + +### Negative + +- First Trusted Publishing dispatch is a real production publish; misconfiguration on npmjs.com would block the publish step. + - Mitigation 1: Validate via a `0.8.0-rc.1` pre-release dispatch before the `0.8.0` final dispatch (`gh workflow run release.yml -f version=0.8.0-rc.1 -f dry_run=false -f prerelease=true -f publish_package=true -f confirm=0.8.0-rc.1`). + - Mitigation 2: If the publish step fails, the operator can revert ADR-0044 with a one-line PR re-adding `NODE_AUTH_TOKEN` + `--provenance=false` and re-dispatch. +- A future Trusted Publisher misalignment on npmjs.com (wrong workflow ref, account change, GitHub repo rename) would block all publishes until the configuration is repaired on the npmjs.com side. Operator-guide §5 documents the configuration shape. + +### Neutral + +- ADR-0041 transitions from `accepted` to `superseded`, with `superseded-by: [ADR-0044]`. ADR-0040 is unaffected — its core decision (npmjs.com as the registry, `specorator` as the package name) was preserved across the deferral. +- `actions/attest-build-provenance` continues to sign the GitHub Release tarball asset. The `id-token: write` workflow permission is now load-bearing for both attestation paths (npm publish + Release asset). +- The bootstrap `specorator-bootstrap` Automation token used to publish v0.7.0 was already revoked in ADR-0041's compliance step. The `NPM_TOKEN` repo secret is decommissioned by ADR-0044. + +## Compliance + +- `.github/workflows/release.yml` step 10 reads no `NODE_AUTH_TOKEN`, runs `npm publish --provenance`, and removes the `# zizmor: ignore[use-trusted-publishing]` suppression. +- `scripts/lib/release-readiness.ts` `REQUIRED_WORKFLOW_PERMISSIONS` continues to enforce `id-token: write` (now load-bearing for both the npm publish OIDC and the Release tarball attestation). +- The first v0.8.0 release dispatch is preceded by a `0.8.0-rc.1` pre-release dispatch as a Trusted Publishing smoke test. +- The `NPM_TOKEN` repo secret on `Luis85/agentic-workflow` is removed after the first successful OIDC publish. +- Tracking issue [#411](https://github.com/Luis85/agentic-workflow/issues/411) is closed. + +## References + +- Supersedes: [ADR-0041](0041-defer-npmjs-trusted-publishing.md) (interim fallback to NPM_TOKEN). +- Restores: [ADR-0040](0040-migrate-npm-publication-to-npmjs-com.md) (npmjs.com Trusted Publishing + `specorator` package name). +- Tracking issue: [#411](https://github.com/Luis85/agentic-workflow/issues/411) — npmjs.com Trusted Publisher configuration unblocked. +- npmjs.com Trusted Publishing docs: . +- sigstore provenance attestations: . + +--- + +> **ADR bodies are immutable.** To change a decision, supersede it with a new ADR; only the predecessor's `status` and `superseded-by` pointer fields may be updated. diff --git a/docs/adr/README.md b/docs/adr/README.md index 20997684c..e5e6d6f19 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -53,9 +53,10 @@ Records of architecturally significant decisions. Format follows Michael Nygard' | [0038](0038-adopt-tailwind-v4-with-vite-plugin.md) | Adopt Tailwind v4 with @tailwindcss/vite and bridge brand tokens via @theme inline | Accepted | | [0039](0039-adopt-hybrid-feature-tracker-data-source.md) | Adopt hybrid feature-tracker data source — issues/ folder primary, GitHub-API snapshot fallback | Accepted | | [0040](0040-migrate-npm-publication-to-npmjs-com.md) | Migrate npm publication from GitHub Packages to npmjs.com | Accepted | -| [0041](0041-defer-npmjs-trusted-publishing.md) | Defer npmjs.com Trusted Publishing — fall back to NPM_TOKEN | Accepted | +| [0041](0041-defer-npmjs-trusted-publishing.md) | Defer npmjs.com Trusted Publishing — fall back to NPM_TOKEN | Superseded | | [0042](0042-adopt-typed-artifact-reader-seam.md) | Adopt a typed-artifact reader seam for frontmatter parsing | Accepted | | [0043](0043-distribute-claude-plugin-bundle-from-orphan-dist-branch.md) | Distribute Claude Code plugin bundle from an orphan dist branch via git-subdir | Accepted | +| [0044](0044-restore-npmjs-trusted-publishing.md) | Restore npmjs.com Trusted Publishing — re-enable OIDC + provenance | Accepted | ## ADR Dispositions diff --git a/docs/release-operator-guide.md b/docs/release-operator-guide.md index 5d1f17ff9..d7587f591 100644 --- a/docs/release-operator-guide.md +++ b/docs/release-operator-guide.md @@ -23,7 +23,7 @@ You should already have: - `specs/version-X-Y-plan/release-notes.md` is finalised. 2. The canonical tag `vX.Y.Z` cut on `main` after the merge (never on the release branch). The workflow uses `gh release create … --verify-tag` and refuses to fall back to auto-tagging. 3. Green v0.4 quality signals available to the readiness check, surfaced through the `RELEASE_*` repository variables (or an explicit operator waiver via `RELEASE_QUALITY_WAIVER`). See `scripts/lib/release-readiness.ts` (`QualitySignals`) for the contract. -4. **`NPM_TOKEN` repo secret set** (a classic Automation token on npmjs.com with publish permission for `specorator`). [ADR-0041](adr/0041-defer-npmjs-trusted-publishing.md) defers the ADR-0040 Trusted Publishing path until [#411](https://github.com/Luis85/agentic-workflow/issues/411) closes. Without `NPM_TOKEN`, the `Publish to npmjs.com` step fails closed before reaching `npm publish`. The workflow keeps `id-token: write` so the GitHub Release tarball asset still gets a sigstore attestation via `actions/attest-build-provenance`; npm-side provenance is paused under the fallback. When Trusted Publishing returns, this prereq reverts to the ADR-0040 OIDC happy path. +4. **npmjs.com Trusted Publisher configured** for the `specorator` package against this repository's `release.yml` workflow on the `release` deployment environment ([ADR-0044](adr/0044-restore-npmjs-trusted-publishing.md), restoring ADR-0040 after the ADR-0041 NPM_TOKEN fallback used for v0.7.0 / v0.7.1; tracking [#411](https://github.com/Luis85/agentic-workflow/issues/411) closed). The `id-token: write` workflow permission mints the OIDC token consumed by `npm publish --provenance`; no long-lived `NPM_TOKEN` secret is required. The `actions/attest-build-provenance` step still uses the same OIDC permission to sign the GitHub Release tarball asset. 5. **Repo Settings → General → Releases → "Immutable releases" is DISABLED.** When the setting is on, GitHub auto-flags every new Release immutable. If asset upload then fails — or the operator deletes the Release — the tag is **permanently burned**: the GitHub API returns HTTP 422 `tag_name was used by an immutable release` to every later attempt to host a Release on that tag, including a fresh draft. The v0.5.0 publish dispatch hit exactly this and forced the v0.5.1 recovery release ([#233](https://github.com/Luis85/agentic-workflow/issues/233); incident timeline in `specs/version-0-5-plan/retrospective.md` §Incident). Verify with `gh api repos/{owner}/{repo}/immutable-releases`. Per the GitHub REST contract the endpoint returns HTTP 404 (`Not Found`) when the setting is **disabled** — that is the safe state. HTTP 200 means the setting is enabled; the JSON body's `enforced_by_owner` field tells you whether the toggle came from this repo or an org-level default. Disable before dispatching, or accept the failure mode knowingly. If any of items 1–4 is missing, **stop**. The readiness check fails closed on those (preferred). Item 5 is owned by the operator: the v0.5.0 retrospective showed the setting is not always operator-controlled (org-level defaults can propagate via `enforced_by_owner: true`), so the readiness check cannot always fail closed on it without blocking legitimate dispatches against repos the operator does not own. Verify by hand before every dispatch. @@ -98,7 +98,7 @@ Only after at least one fully green dry run, request a stable publish. - Confirm gate — refuses to continue unless `confirm == version`. - `actions/attest-build-provenance` — emits a GitHub artifact attestation for the workflow-built release tarball. This happens before the Release is created or the npmjs.com publish step runs, and it does not change the npm registry path. - `gh release create vX.Y.Z --target main --verify-tag --generate-notes ${TARBALL}` — creates the GitHub Release with the candidate tarball attached **in one call** when no Release for the tag exists. When a Release already exists (the two-step CLAR-V05-003 path), the workflow runs `gh release edit … --draft= --prerelease=` to flip flags in place and uploads the asset only if it is not already attached, so a single Release per tag is preserved (#233 prevention B + C). The promote branch refuses to demote an already-published stable Release back to draft or prerelease — that flip would unpublish a consumer-visible release; cut a new `vX.Y.(Z+1)` instead. - - `npm publish` — only when `publish_package: true`; authenticates via the `NPM_TOKEN` repo secret (classic Automation token, ADR-0041 fallback while [#411](https://github.com/Luis85/agentic-workflow/issues/411) is open); idempotent (see §7.1). No `--provenance` flag — npm-side provenance is paused under the fallback. The GitHub Release tarball asset still carries a sigstore attestation from `actions/attest-build-provenance` above. + - `npm publish --provenance` — only when `publish_package: true`; authenticates via npmjs.com Trusted Publishing ([ADR-0044](adr/0044-restore-npmjs-trusted-publishing.md), restoring ADR-0040 after the ADR-0041 NPM_TOKEN fallback used for v0.7.0 / v0.7.1; #411 closed); idempotent (see §7.1). The `--provenance` flag mints a sigstore provenance statement that ships with the tarball and is visible on the npmjs.com package page under `Provenance`. The GitHub Release tarball asset also carries its own sigstore attestation from `actions/attest-build-provenance` above. 3. Verify on `https://github.com/Luis85/agentic-workflow/releases/tag/vX.Y.Z`: - Release notes body matches the dry-run preview. @@ -142,11 +142,11 @@ Release provenance has two surfaces, both produced by the release workflow: | Surface | Posture | Mechanism | |---|---|---| | GitHub Release tarball | Required for non-dry-run releases. | `actions/attest-build-provenance` runs against the packed `.tgz` after the fresh-surface check and confirm gate, before the GitHub Release is created. Verify with `gh attestation verify`. | -| npmjs.com package | **Paused under [ADR-0041](adr/0041-defer-npmjs-trusted-publishing.md)** until [#411](https://github.com/Luis85/agentic-workflow/issues/411) closes. | The publish path runs `npm publish` (no `--provenance`) authenticated by `NPM_TOKEN`. No provenance statement is emitted on the npmjs.com package page. When Trusted Publishing is unblocked, this row reverts to `npm publish --provenance` via OIDC and the statement becomes verifiable with `npm view specorator@X.Y.Z --registry https://registry.npmjs.org --json`. | +| npmjs.com package | Required for non-dry-run releases that publish the package ([ADR-0044](adr/0044-restore-npmjs-trusted-publishing.md), restoring ADR-0040 after the ADR-0041 NPM_TOKEN fallback used for v0.7.0 / v0.7.1; #411 closed). | The publish path runs `npm publish --provenance` authenticated via npmjs.com Trusted Publishing (OIDC, no long-lived token). The sigstore provenance statement is visible on the npmjs.com package page under `Provenance`, and verifiable with `npm view specorator@X.Y.Z --registry https://registry.npmjs.org --json` (look at `dist.attestations`). | -The GitHub Release tarball attestation always binds the artifact to this repository's `.github/workflows/release.yml` workflow run, even under the ADR-0041 fallback. Consumers can verify the GitHub Release tarball chain without trusting maintainer signatures. +Both surfaces bind the artifact to this repository's `.github/workflows/release.yml` workflow run via OIDC. Consumers can verify either chain without trusting maintainer signatures. -Trusted publishing on npmjs.com is the documented target (ADR-0040) and intended return path. The intended one-time setup is: Repo `Luis85/agentic-workflow`, Workflow `release.yml` (bare filename — entering `.github/workflows/release.yml` would cause OIDC authentication to fail), Environment none. Configure once via the npmjs.com web UI when the per-package or account-level Trusted Publisher option is reachable for `specorator`; close [#411](https://github.com/Luis85/agentic-workflow/issues/411) and supersede ADR-0041 with an ADR that re-locks OIDC. +The npmjs.com Trusted Publisher configuration is: Repo `Luis85/agentic-workflow`, Workflow `release.yml` (bare filename — entering `.github/workflows/release.yml` would cause OIDC authentication to fail), Environment `release`. The deployment environment on the workflow's `release` job must match the Trusted Publisher's `Environment` field. If the npmjs.com configuration ever drifts (workflow rename, repo rename, environment removed), the publish step fails closed; repair on the npmjs.com side and re-dispatch. ## 5.2 SBOM posture @@ -194,7 +194,7 @@ Symptom: the GitHub Release exists (the tarball may or may not be attached), but Recovery — rerun the release workflow with the same inputs. As of ADR-0040 the workflow's publish step is idempotent (NFR-V05-005): it queries `npm view` first and skips publish when the version already exists. Trusted publishing handles auth automatically. -If the rerun fails again, fall back to a manual publish from a local checkout of `vX.Y.Z`. This requires the `NPM_TOKEN` repo secret (kept as emergency fallback per ADR-0040): +If the rerun fails again, fall back to a manual publish from a local checkout of `vX.Y.Z`. ADR-0044 removed the long-lived `NPM_TOKEN` repo secret, so this path requires minting a fresh classic Automation token on the npmjs.com web UI just for the recovery, and revoking it immediately after the publish completes: ```bash # Run the whole block as a paste-once unit. @@ -206,8 +206,11 @@ If the rerun fails again, fall back to a manual publish from a local checkout of git fetch --tags origin git checkout vX.Y.Z - # 1. Authenticate npm against npmjs.com using the emergency NPM_TOKEN. - export NPM_TOKEN= + # 1. Authenticate npm against npmjs.com using a freshly-minted classic + # Automation token. Generate at npmjs.com → Account → Access Tokens → + # Generate New Token → Classic Token → Automation. Revoke after the + # publish completes. + export NPM_TOKEN= cat > .npmrc <<'EOF' //registry.npmjs.org/:_authToken=${NPM_TOKEN} EOF diff --git a/docs/scripts/lib/release-readiness/variables/REQUIRED_WORKFLOW_PERMISSIONS.md b/docs/scripts/lib/release-readiness/variables/REQUIRED_WORKFLOW_PERMISSIONS.md index b3bd5753c..c802c13cf 100644 --- a/docs/scripts/lib/release-readiness/variables/REQUIRED_WORKFLOW_PERMISSIONS.md +++ b/docs/scripts/lib/release-readiness/variables/REQUIRED_WORKFLOW_PERMISSIONS.md @@ -19,10 +19,8 @@ check applies to both the workflow-level `permissions:` block and any by a job override (Codex round-3 P1 on PR #158). `attestations: write` and `id-token: write` are required so the release workflow can sign and persist a build-provenance attestation for the packed GitHub Release tarball asset -(#387). The same `id-token: write` permission also unlocks npmjs.com Trusted -Publishing (ADR-0040) when that path is enabled; ADR-0041 currently defers -Trusted Publishing in favour of an `NPM_TOKEN`-authenticated `npm publish` -(tracked in #411), but the GitHub Release tarball still gets a sigstore -attestation, so `id-token: write` stays required. `packages: write` is no -longer required because GitHub Packages publication has been removed -(ADR-0040). +(#387). The same `id-token: write` permission also mints the OIDC token +consumed by `npm publish --provenance` for npmjs.com Trusted Publishing +(ADR-0044, restoring ADR-0040 after the ADR-0041 NPM_TOKEN fallback used +for v0.7.0 / v0.7.1; #411 closed). `packages: write` is no longer required +because GitHub Packages publication has been removed (ADR-0040). diff --git a/docs/specorator.md b/docs/specorator.md index 11cedcc15..6880e3066 100644 --- a/docs/specorator.md +++ b/docs/specorator.md @@ -1,8 +1,8 @@ # Specorator — Quality-Driven, Agentic Development Workflow -**Version:** 0.7.0 · **Status:** Minor release — npm CLI migrated from GitHub Packages to npmjs.com (public, no auth, OIDC trusted publishing) · **Purpose:** Spec-driven, agentic workflow template +**Version:** 0.8.0 · **Status:** Minor release — Claude Code plugin bundle moves to orphan dist branch + `git-subdir` marketplace source (ADR-0043); npmjs.com Trusted Publishing restored with sigstore provenance (ADR-0044, supersedes ADR-0041); plus `/issue:tackle`, `/specorator:onboard`, GitHub MCP, conductor-driven model tiers, `--version` flag · **Purpose:** Spec-driven, agentic workflow template -v0.7.0 migrates the Specorator npm CLI from GitHub Packages (`@luis85/agentic-workflow`, auth-walled) to npmjs.com as `specorator` (unscoped, public, no `.npmrc` configuration required). Published via OIDC Trusted Publishing with `--provenance` attestation; `NODE_AUTH_TOKEN` is no longer required. See [ADR-0040](adr/0040-migrate-npm-publication-to-npmjs-com.md). v0.6.2 was a patch release unblocking the v0.6.x dispatch path. v0.6.1 shipped Specorator as a Claude Code plugin (`claude-plugin/specorator/`) with auto-derived plugin manifest version and verify/release-readiness gates. v0.6.0 introduced the Astro 6 product page replacing `sites/index.html`. v0.5.0 introduced the release workflow, GitHub Release / Package distribution, and fresh-surface package contract; v0.5.1 was the recovery release for the [Immutable Releases incident](https://github.com/Luis85/agentic-workflow/issues/233). +v0.8.0 moves the Claude Code plugin bundle to a long-lived orphan branch `dist/claude-plugin` rebuilt by CI on every push to `main`. The marketplace entry in `.claude-plugin/marketplace.json` switches to a `git-subdir` source pinned to that ref, so the bundle becomes gitignored on `develop`/`main` and PR diffs stop carrying generated-artifact churn. See [ADR-0043](adr/0043-distribute-claude-plugin-bundle-from-orphan-dist-branch.md). v0.8.0 also adds the `/issue:tackle` conductor skill (triage-first issue/PR resolution), `/specorator:onboard` (guided 5-step onboarding series), the GitHub remote MCP server in the project `.mcp.json` default, conductor-driven model-tier injection for subagents, and `specorator --version` / `-v`. v0.7.0 migrated the Specorator npm CLI from GitHub Packages (`@luis85/agentic-workflow`) to npmjs.com as `specorator` (unscoped, public). See [ADR-0040](adr/0040-migrate-npm-publication-to-npmjs-com.md). v0.6.2 was a patch release unblocking the v0.6.x dispatch path. v0.6.1 shipped Specorator as a Claude Code plugin. v0.6.0 introduced the Astro 6 product page. v0.5.0 introduced the release workflow, GitHub Release / Package distribution, and fresh-surface package contract; v0.5.1 was the recovery release for the [Immutable Releases incident](https://github.com/Luis85/agentic-workflow/issues/233). A solution-agnostic, **spec-driven** workflow for building software with humans and AI agents. Treats specifications as the source of truth and code as their artifact. Covers the full SDLC: Product → UX → UI → Engineering → Testing → Quality → Delivery → Operations. diff --git a/package.json b/package.json index 0888637bd..bb20693cf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "specorator", - "version": "0.7.0", + "version": "0.8.0", "description": "Specorator — template for spec-driven, agentic software development. The workflow is the deliverable.", "keywords": [ "agents", diff --git a/scripts/check-claude-plugin.ts b/scripts/check-claude-plugin.ts index fbd4ef0d0..a388740e3 100644 --- a/scripts/check-claude-plugin.ts +++ b/scripts/check-claude-plugin.ts @@ -1,7 +1,11 @@ import fs from "node:fs"; import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { spawnSync } from "node:child_process"; import { failIfErrors, parseSimpleYaml, readText, repoRoot } from "./lib/repo.js"; +const scriptsDir = path.dirname(fileURLToPath(import.meta.url)); + type PluginManifest = { name?: unknown; version?: unknown; @@ -31,21 +35,77 @@ const errors: string[] = []; checkMarketplace(); checkPluginReadme(); -// Generated-output checks (per ADR-0043) are conditional on the file being -// present. The bundle is gitignored on develop/main and produced by the CI +// Generated-output checks (per ADR-0043) gate as a single all-or-nothing +// group. The bundle is gitignored on develop/main and produced by the CI // publish workflow; a clean checkout validates without a prior local // `npm run build:claude-plugin`. When a contributor has built the bundle -// locally (e.g. for `claude --plugin-dir` smoke-test), the same checks run -// and catch shape regressions. -checkPluginManifest(); -checkPluginMcpJson(); -checkGeneratedDirectories(); +// locally (e.g. for `claude --plugin-dir` smoke-test), the full shape is +// required — partial bundles (e.g. someone deleted `agents/` after a +// build) fail closed instead of silently passing (Codex P2 on PR #478). +const generatedPaths = collectGeneratedPaths(); +const presentCount = generatedPaths.filter((p) => fs.existsSync(p.path)).length; +if (presentCount > 0 && presentCount < generatedPaths.length) { + for (const p of generatedPaths) { + if (!fs.existsSync(p.path)) { + errors.push(`${p.label} missing — partial bundle detected; run npm run build:claude-plugin to refresh, or delete the bundle entirely`); + } + } +} else if (presentCount === generatedPaths.length) { + checkPluginManifest(); + checkPluginMcpJson(); + checkGeneratedDirectories(); + // Bundle is fully built — also verify it has not drifted from canonical + // .claude/{agents,skills,commands}/ + .mcp.json sources. Per ADR-0043 the + // verify gate does not require a built bundle, so the drift check no + // longer runs unconditionally from the package.json `check:claude-plugin` + // script. But when a contributor HAS built the bundle locally (e.g. for + // `claude --plugin-dir` smoke-test), shape-only validation lets stale or + // hand-edited bundles pass even though they will diverge from what the CI + // publish workflow rebuilds (Codex P2 on PR #478). Run the byte-level + // comparison here only when the full bundle is present. + checkBundleDrift(); +} failIfErrors(errors, "check:claude-plugin"); -function checkPluginManifest(): void { - if (!fs.existsSync(manifestPath)) return; +function checkBundleDrift(): void { + // Resolve the build script via the same `repoRoot` the structural checks + // use (honours `SPECORATOR_ROOT` for the test harness). Leave `cwd` + // unset so node + tsx resolve against the parent process's cwd — that + // is the actual install root in both production and the test harness, + // which is where `node_modules/tsx` lives. `SPECORATOR_ROOT` propagates + // through `env` so the spawned build-claude-plugin reads sources from + // the fixture rather than the real repo. + // Resolve the build script via this file's on-disk location, not via + // `repoRoot`. `repoRoot` honours `SPECORATOR_ROOT` and points at the + // test fixture; the actual `build-claude-plugin.ts` always sits next to + // this file in the real install. + const buildScript = path.join(scriptsDir, "build-claude-plugin.ts"); + const result = spawnSync(process.execPath, ["--import", "tsx", buildScript, "--check"], { + encoding: "utf8", + env: process.env, + windowsHide: true, + }); + if (result.status === 0) return; + const message = (result.stderr || result.stdout || "").trim(); + if (message) { + errors.push(`claude-plugin/specorator/ has drifted from canonical .claude sources; run npm run build:claude-plugin to refresh:\n${message}`); + } else { + errors.push("claude-plugin/specorator/ has drifted from canonical .claude sources; run npm run build:claude-plugin to refresh"); + } +} +function collectGeneratedPaths(): Array<{ path: string; label: string }> { + return [ + { path: manifestPath, label: "claude-plugin/specorator/.claude-plugin/plugin.json" }, + { path: path.join(pluginRoot, ".mcp.json"), label: "claude-plugin/specorator/.mcp.json" }, + { path: path.join(pluginRoot, "agents"), label: "claude-plugin/specorator/agents/" }, + { path: path.join(pluginRoot, "skills"), label: "claude-plugin/specorator/skills/" }, + { path: path.join(pluginRoot, "commands"), label: "claude-plugin/specorator/commands/" }, + ]; +} + +function checkPluginManifest(): void { const manifest = readJson(manifestPath, "plugin manifest"); if (!manifest) return; @@ -126,7 +186,6 @@ function checkPluginReadme(): void { function checkPluginMcpJson(): void { const mcpPath = path.join(pluginRoot, ".mcp.json"); - if (!fs.existsSync(mcpPath)) return; const parsed = readJson<{ mcpServers?: unknown }>(mcpPath, "plugin .mcp.json"); if (!parsed) return; if (!parsed.mcpServers || typeof parsed.mcpServers !== "object") { @@ -137,7 +196,6 @@ function checkPluginMcpJson(): void { function checkGeneratedDirectories(): void { for (const rel of ["agents", "skills", "commands"]) { const dir = path.join(pluginRoot, rel); - if (!fs.existsSync(dir)) continue; if (!fs.statSync(dir).isDirectory()) { errors.push(`claude-plugin/specorator/${rel} exists but is not a directory`); continue; diff --git a/scripts/lib/release-readiness.ts b/scripts/lib/release-readiness.ts index 909617d5a..5840598ee 100644 --- a/scripts/lib/release-readiness.ts +++ b/scripts/lib/release-readiness.ts @@ -61,13 +61,11 @@ export const EXPECTED_PACKAGE_REPOSITORY = "https://github.com/Luis85/agentic-wo * by a job override (Codex round-3 P1 on PR #158). `attestations: write` and * `id-token: write` are required so the release workflow can sign and persist * a build-provenance attestation for the packed GitHub Release tarball asset - * (#387). The same `id-token: write` permission also unlocks npmjs.com Trusted - * Publishing (ADR-0040) when that path is enabled; ADR-0041 currently defers - * Trusted Publishing in favour of an `NPM_TOKEN`-authenticated `npm publish` - * (tracked in #411), but the GitHub Release tarball still gets a sigstore - * attestation, so `id-token: write` stays required. `packages: write` is no - * longer required because GitHub Packages publication has been removed - * (ADR-0040). + * (#387). The same `id-token: write` permission also mints the OIDC token + * consumed by `npm publish --provenance` for npmjs.com Trusted Publishing + * (ADR-0044, restoring ADR-0040 after the ADR-0041 NPM_TOKEN fallback used + * for v0.7.0 / v0.7.1; #411 closed). `packages: write` is no longer required + * because GitHub Packages publication has been removed (ADR-0040). */ export const REQUIRED_WORKFLOW_PERMISSIONS: Readonly> = { contents: "write", diff --git a/sites/src/content/schemas.ts b/sites/src/content/schemas.ts index 6acb572a4..77ba5ad94 100644 --- a/sites/src/content/schemas.ts +++ b/sites/src/content/schemas.ts @@ -42,8 +42,13 @@ export const StageEnum = z.enum([ "done", ]); -// "done" and "paused" are valid per scripts/lib/workflow-schema.ts workflowStatuses; -// included here so the site loader accepts all values the script validator allows. +// "done", "paused", and "superseded" are valid per +// scripts/lib/workflow-schema.ts `workflowStatuses`; included here so the +// site loader accepts every value the script validator allows. Without +// `superseded`, workflow-state files set to that status (e.g. +// `specs/repo-adoption-track/workflow-state.md` after ADR-0030 was +// withdrawn in v0.8.0) are silently dropped from the feature tracker +// (Codex P2 on PR #478). export const StateStatusEnum = z.enum([ "pending", "active", @@ -53,6 +58,7 @@ export const StateStatusEnum = z.enum([ "blocked", "done", "paused", + "superseded", ]); export const FeatureTypeEnum = z.enum(["feature", "chore", "bug", "spike", "docs"]); diff --git a/tests/scripts/claude-plugin.test.ts b/tests/scripts/claude-plugin.test.ts index 998ac0146..c6942deba 100644 --- a/tests/scripts/claude-plugin.test.ts +++ b/tests/scripts/claude-plugin.test.ts @@ -104,29 +104,47 @@ test("build:claude-plugin --check reports plugin manifest drift when version is }); test("check:claude-plugin validates manifest, marketplace, and generated directories", () => { + const root = makeFixtureRoot(); + try { + seedCheckFixture(root); + expect(runScript(buildScript, root).status).toBe(0); + + const result = runScript(checkScript, root); + expect(result.status, `stdout:\n${result.stdout}\nstderr:\n${result.stderr}`).toBe(0); + expect(result.stdout).toContain("check:claude-plugin: ok"); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("check:claude-plugin rejects a drifted bundle (Codex P2 on PR #478)", () => { + const root = makeFixtureRoot(); + try { + seedCheckFixture(root); + expect(runScript(buildScript, root).status).toBe(0); + // Hand-edit one generated file so the bundle drifts from canonical + // sources. The all-or-nothing presence gate accepts this (all 5 + // generated paths still present), but the byte-level drift check + // re-runs build:claude-plugin --check and surfaces the divergence. + write(root, "claude-plugin/specorator/agents/dev.md", "# Dev (hand-edited)\n"); + + const result = runScript(checkScript, root); + expect(result.status).toBe(1); + expect(result.stderr).toContain("drifted from canonical .claude sources"); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("check:claude-plugin passes on a clean checkout with no generated bundle (ADR-0043)", () => { const root = makeFixtureRoot(); try { write(root, ".claude-plugin/marketplace.json", JSON.stringify(marketplaceFixture())); - write( - root, - "claude-plugin/specorator/.claude-plugin/plugin.json", - JSON.stringify({ - name: "specorator", - version: "0.6.0", - description: "Spec-driven workflow.", - repository: "https://github.com/Luis85/agentic-workflow", - license: "MIT", - }), - ); write( root, "claude-plugin/specorator/README.md", ["---", 'title: "Plugin"', 'folder: "claude-plugin/specorator"', 'description: "Plugin package."', "entry_point: true", "---", "", "# Plugin"].join("\n"), ); - write(root, "claude-plugin/specorator/agents/dev.md", "# Dev\n"); - write(root, "claude-plugin/specorator/skills/verify/SKILL.md", "# Verify\n"); - write(root, "claude-plugin/specorator/commands/spec/start.md", "# Start\n"); - write(root, "claude-plugin/specorator/.mcp.json", mcpFixture); const result = runScript(checkScript, root); expect(result.status).toBe(0); @@ -136,7 +154,7 @@ test("check:claude-plugin validates manifest, marketplace, and generated directo } }); -test("check:claude-plugin passes on a clean checkout with no generated bundle (ADR-0043)", () => { +test("check:claude-plugin rejects a partial bundle (ADR-0043)", () => { const root = makeFixtureRoot(); try { write(root, ".claude-plugin/marketplace.json", JSON.stringify(marketplaceFixture())); @@ -145,10 +163,29 @@ test("check:claude-plugin passes on a clean checkout with no generated bundle (A "claude-plugin/specorator/README.md", ["---", 'title: "Plugin"', 'folder: "claude-plugin/specorator"', 'description: "Plugin package."', "entry_point: true", "---", "", "# Plugin"].join("\n"), ); + // Three of five generated artifacts present, two missing — a partial + // bundle that the conditional-presence gate (Codex P2 on PR #478) must + // reject rather than silently pass. + write( + root, + "claude-plugin/specorator/.claude-plugin/plugin.json", + JSON.stringify({ + name: "specorator", + version: "0.6.0", + description: "Spec-driven workflow.", + repository: "https://github.com/Luis85/agentic-workflow", + license: "MIT", + }), + ); + write(root, "claude-plugin/specorator/.mcp.json", mcpFixture); + write(root, "claude-plugin/specorator/agents/dev.md", "# Dev\n"); + // skills/ and commands/ deliberately omitted. const result = runScript(checkScript, root); - expect(result.status).toBe(0); - expect(result.stdout).toContain("check:claude-plugin: ok"); + expect(result.status).toBe(1); + expect(result.stderr).toContain("partial bundle detected"); + expect(result.stderr).toContain("skills"); + expect(result.stderr).toContain("commands"); } finally { fs.rmSync(root, { recursive: true, force: true }); } @@ -179,6 +216,25 @@ test("check:claude-plugin rejects a marketplace source that is not git-subdir (A } }); +function seedCheckFixture(root: string): void { + // Canonical sources — what build:claude-plugin reads. + write(root, "package.json", JSON.stringify({ name: "fixture", version: "9.9.9" })); + write(root, ".claude/agents/dev.md", "# Dev\n"); + write(root, ".claude/skills/verify/SKILL.md", "# Verify\n"); + write(root, ".claude/commands/spec/start.md", "# Start\n"); + write(root, ".mcp.json", mcpFixture); + // Plugin-shape committed files that build:claude-plugin does NOT touch. + write(root, ".claude-plugin/marketplace.json", JSON.stringify(marketplaceFixture())); + write( + root, + "claude-plugin/specorator/README.md", + ["---", 'title: "Plugin"', 'folder: "claude-plugin/specorator"', 'description: "Plugin package."', "entry_point: true", "---", "", "# Plugin"].join("\n"), + ); + // Placeholder plugin.json so build:claude-plugin's compareManifest sees the + // generated tree's parent dir already exists. + write(root, "claude-plugin/specorator/.claude-plugin/plugin.json", "{}\n"); +} + function marketplaceFixture() { return { name: "specorator-marketplace",