deftai

deft-directive-release

Cut a v0.X.Y release of the deft framework safely. Use when the user says "release", "cut release", "v0.X.Y", or "publish release" -- to walk an 8-phase workflow that pre-flights, runs an end-to-end rehearsal against a temp repo, lands a draft GitHub release (npm ships irrevocably at tag push), optionally QA's draft assets, then publishes the GitHub release or rolls back. Re-uses the deft-directive-swarm Phase 6 Step 5 Slack announcement template.

deftai 31 14 Updated 1w ago
GitHub

Install

npx skillscat add deftai/directive/deft-directive-release

Install via the SkillsCat registry.

SKILL.md

Deft Directive Release

Structured 8-phase workflow for cutting a v0.X.Y release of the deft framework. Operationalizes the task release / task release:publish / task release:rollback / task release:e2e surface introduced in #716 (safety hardening of #74).

Legend (from RFC2119): !=MUST, ~=SHOULD, ≉=SHOULD NOT, ⊗=MUST NOT, ?=MAY.

See also: deft-directive-swarm Phase 6 Step 5 (Slack announcement template re-used by Phase 8 below) | deft-directive-review-cycle (user-gate pattern) | deft-directive-refinement (conversational phased flow).

Platform Requirements

! GitHub as the SCM platform; the GitHub CLI (gh) must be installed and authenticated. The full pipeline plus the rehearsal target (task release:e2e) all dispatch through gh.

Branch-Protection Policy Guard

! Before any Phase 1 state mutation, run the skill-level branch-policy guard documented in task policy:show / task verify:branch (#746 / #747). Releases run on the configured base branch (default master), so the operator MUST be on the explicit-opt-in side of the policy before the pipeline starts writing files.

Preferred path — typed direct-commit policy opt-out (#1553). For a release session on the default branch, prefer the audited typed flag over the emergency env-var bypass:

task policy:allow-direct-commits -- --confirm

This writes plan.policy.allowDirectCommitsToMaster = true on xbrief/PROJECT-DEFINITION.xbrief.json with a capability-cost disclosure. After the release completes (or if the session aborts), restore enforcement and commit the restore in the same closeout (#2623):

task policy:enforce-branches
# enforce flips the typed flag to false locally — the commit that lands that
# flip cannot use the typed opt-in anymore. Scope the emergency env bypass to
# ONLY this closeout commit+push (do NOT export it for the whole session):
DEFT_ALLOW_DEFAULT_BRANCH_COMMIT=1 git add xbrief/PROJECT-DEFINITION.xbrief.json meta/policy-changes.log
DEFT_ALLOW_DEFAULT_BRANCH_COMMIT=1 git commit -m "chore(policy): restore branch protection after vX.Y.Z"
DEFT_ALLOW_DEFAULT_BRANCH_COMMIT=1 git push origin HEAD

⊗ Leave allowDirectCommitsToMaster=true on origin after publish. ⊗ Run policy:enforce-branches and leave the dirty restore under protection ON without committing (forces a follow-up PR — the v0.79.0 / #2619 failure mode).

Branch-guard probe (either path). Regardless of which opt-out path you chose, confirm the guard passes before Phase 1 mutates state:

task verify:branch

or invoke task verify:branch. This is the canonical surface that surfaces the policy state to the operator before the pipeline starts writing files. The release pipeline's other safety surfaces (the dirty-tree guard, base-branch check, task ci:local gate) remain independent of this check.

Emergency env-var bypass — narrow scope only (#1553). DEFT_ALLOW_DEFAULT_BRANCH_COMMIT=1 is process-wide: every child process, nested test, and temporary repository spawned from the same shell inherits it. During the v0.43.0 release attempt, wrapping the entire task release invocation in this env var let the bypass leak into the Step 5 task ci:local preflight, which caused TestWriteConsumerGitHooks_VendoredCommitBlocked_RealGit to fail because the vendored test repo allowed a direct master commit the test expected the hook to block.

  • ! Prefer task policy:allow-direct-commits -- --confirm for release sessions instead of exporting DEFT_ALLOW_DEFAULT_BRANCH_COMMIT=1 for the whole shell.
  • ⊗ Wrap task release, task ci:local, or task check in DEFT_ALLOW_DEFAULT_BRANCH_COMMIT=1 -- the env var is inherited by every subprocess and can produce false preflight failures before any release mutation.
  • ? If the env-var path is unavoidable, scope it to a single branch-guard probe only (e.g. DEFT_ALLOW_DEFAULT_BRANCH_COMMIT=1 task verify:branch) and do NOT export it for the release session. The release pipeline itself passes the bypass only in scoped subprocess env= for its authorised commit/tag/push mutations (#867); operators MUST NOT mirror that pattern at the shell level.

The release pipeline's Step 9/10/11 git mutations carry the bypass in subprocess env= only (the release pipeline subprocess env, #867) so the parent shell stays clean. Operator-side env-var exports defeat that isolation.

Deterministic Questions Contract

! Every numbered-menu prompt rendered in this skill (Phase 1 version-bump magnitude check, Phase 2 dry-run review yes/back/quit, Phase 5 optional defer/rollback/Discuss (happy path auto-publishes after draft QA)) MUST follow `../../contracts/deterministic-questions.md`: the final two numbered options MUST be Discuss and Back, in that order. Existing back/quit options remain valid; this contract simply adds Discuss as a peer alongside Back. The Discuss-pause semantic is documented verbatim in the contract -- implicit resumption is forbidden.

When to Use

  • User says "release", "cut release", "v0.X.Y", "publish release", "ship a release"
  • The framework's [Unreleased] CHANGELOG section is non-empty and the operator wants to cut a tagged release
  • A previous release rehearsal succeeded and the operator is ready for the production cut

Phase 1 — Pre-flight

! Validate the local + remote state before any irreversible action.

Parallel prep — #1880 Gap D (#2692)

! Phase 1 long steps (task reconcile:issues -- --apply-lifecycle-fixes, cache refresh when ritual-stale, task ci:local / task check) and Phase 3 task release:e2e MUST be backgrounded or subagent-dispatched when the host supports it (Cursor: Task tool run_in_background: true), with progress surfaced via DONE/heartbeat — same ownership as review-cycle / merge-ready workers (#1880 Gap D). The operator conversation MUST stay interactive for version magnitude confirmation, --summary, and the Phase 2 dry-run yes/back/quit gate while prep runs.

! Checklist: Phase 1 prep parallelized — long prep started in background before (or while) collecting version magnitude / summary / npm irrevocability disclosure.

! On Windows PowerShell, do NOT wrap long task output in Select-Object -Last (it buffers until the process exits); stream to the terminal or log to a file and read incrementally. See scm/github.md § #2646 / Windows encoding guidance for related PS pitfalls.

⊗ Foreground-block the operator chat on reconcile / ci:local / release:e2e when background dispatch is available (#1880 Gap D / #2692).

Fixable check failure — file-and-merge before resume (#2859)

! When Step 4 (task ci:local or task check) fails on a fixable product or test defect (hang, failing test, validation bug — not operator env misconfiguration), the release cut MUST pause and route the blocker through normal issue → xBRIEF → feature branch → PR → merge before resuming Phase 1.

? Step 5 branch-coverage threshold misses during task release (Vitest branch coverage below 85% with no other failure mode) are carved out to § Step 5 branch-coverage threshold — open-issue ledger hatch (#2866) below — not this file-and-merge path.

Required path:

  1. File a GitHub issue with root cause, recurrence signature, and acceptance criteria.
  2. Ingest / promote / activate scope xBRIEF; implement on a feature branch with drive-to: merge-ready.
  3. Merge; confirm task check / ci:local is green for the failure mode.
  4. Resume the release cut from Phase 1 (re-run Step 4).

⊗ Lead with an inline-only hotfix on the release branch / default branch without a tracked issue and merged PR.
⊗ Suggest untracked --skip-ci or --allow-skip-ci without a tracked issue number as the first recovery when the failure is a clear, shippable bug.
⊗ Continue the cut with "raise timeouts," "fix it here," or other shortcut theater before file-and-merge completes.

? --skip-ci / --allow-skip-ci=#N remains valid only under explicit operator incident review with a tracked issue cited on the flag (Phase 4 contract). It is NOT a substitute for filing and merging a fixable defect discovered in Phase 1.

AGENTS.md bulk rejected (#2859): Expanding AGENTS.md / content/templates/agents-entry.md with an always-on pin for this release-phase reminder was considered and rejected. This rule lives in the release skill (and optional lesson); do NOT add AGENTS.md / agents-entry bulk for it.

See `docs/RELEASING.md` § Fixable check failure during release for the operator runbook and the existing --allow-skip-ci=#N incident contract.

Step 5 branch-coverage threshold — open-issue ledger hatch (#2866 / #3187)

! When task release Step 5 fails on Vitest coverage below the 85% goal, apply this hatch only when branches is the sole metric below 85% (hairline branch miss — lines, functions, and statements all ≥ 85%). Confirm via the Step 5 failure output, coverage/coverage-final.json, or task coverage:hotspots before hatching. If any other metric is also below 85%, or the failure is a hang / failing test / non-coverage defect, STOP — use § Fixable check failure — file-and-merge before resume (#2859) instead.

Runtime disclosure (#2573): --allow-coverage-debt=#N sets vitest coverage thresholds to zero for the release Step 5 run (vitest.config.ts). The hatch is justified only for branch-only hairlines; the filed debt issue MUST require restoring all four metrics (lines, functions, branches, statements) to ≥ 85% before close.

Auto-hatch in task release Step 5 (#3187): the pipeline classifies a non-zero Step 5 as REAL_FAILURE | BRANCH_HAIRLINE | OTHER_COVERAGE | UNKNOWN after one suite run.

  1. REAL_FAILURE | OTHER_COVERAGE | UNKNOWNfail closed (no auto-issue). Route product defects via #2859.
  2. BRANCH_HAIRLINE + any open coverage-debt ledger entry → fail closed ("restore coverage; close #N first") — consecutive soft-pass ban (#2866).
  3. BRANCH_HAIRLINE + empty open debt ledger → auto-file #N with markers coverage-debt and --allow-coverage-debt, measured metrics, cut version, and acceptance (all four metrics ≥ 85%); mark Step 5 PASS_WITH_DEBT(#N); continue the cut without re-running vitest. Loud stderr banner + cite #N in CHANGELOG / release notes.

! File before continue — never soft-pass without a durable issue number.

? Operators MAY still pass --allow-coverage-debt=#N manually (PowerShell-safe: --allow-coverage-debt=N or --allow-coverage-debt="#N" — #2621) when re-running after a pre-filed debt issue.

SHA suite stamp (#3187, coordinates with #3188 check ordering): after suite green or PASS_WITH_DEBT, write a local SHA-bound stamp under .deft/release-suite-stamp.json (gitignored). Re-entry at the same clean HEAD skips the suite; dirty tree or different HEAD invalidates the stamp. ⊗ CI never trusts the stamp (GHA has no committed stamp; CI/GITHUB_ACTIONS force miss).

Open-issue ledger (release-scoped only):

  1. Check for an open coverage-debt tracking issue (unpaid hatch from a prior cut) — union all three probes and dedupe by issue number:
    • Marker search (new-format debt issues):
      gh issue list --repo <owner>/<repo> --state open --search "coverage-debt in:title,body" --limit 20
      gh issue list --repo <owner>/<repo> --state open --search "allow-coverage-debt in:body" --limit 20
    • CHANGELOG citation scan (legacy hatch issues filed before markers were mandatory): parse CHANGELOG.md [Unreleased] plus the last three ## [version] sections for --allow-coverage-debt=#N / allow-coverage-debt=#N citations; for each #N, probe issue state via REST — OPEN (or unknown) counts as unpaid debt even when title/body lack the new markers (#2866).
  2. If no open coverage-debt issue exists → auto-hatch files #N (or operator files manually) with title prefix coverage-debt: and body containing both markers. The open #N remains WIP until coverage is restored and the issue is closed.
  3. If an open coverage-debt issue from a prior hatch still exists → ⊗ soft-pass again; restore real coverage (all four metrics ≥ 85%) and close the debt issue before the cut proceeds.

Framework-release-first (#3187): auto-hatch applies to framework task release Step 5 (e.g. deftai/directive). Consumer expansion of auto-hatch via plan.policy.coverageDebt is reserved; consumer expansion is not implemented (#3314). Refuse when unset or off. Live hatch is --allow-coverage-debt=#N (#2866). Do not auto-file debt on the framework repo from consumer trees; consumer ledger is always the consumer repo.

⊗ Auto-pass on a near-miss band without #N (#2573).
⊗ Silent soft-pass with no tracked issue.
⊗ File a debt issue without coverage-debt / allow-coverage-debt markers in title or body — the ledger query will miss it and permit a consecutive soft-pass (#2866).
⊗ Use this carve-out when lines, functions, or statements are also below 85%, or for hangs, failing tests, or non-coverage Step 5 failures — those stay under #2859 file-and-merge.
⊗ Treat file-debt-then-hatch as the default for ordinary PR / task check work outside a release cut — this hatch is release-scoped only.
⊗ Trust a laptop suite stamp in CI or after HEAD/tree drift (#3187).

See `docs/RELEASING.md` § Coverage debt hatch during release.

Routine vs hard cut for Step 5 (#2953)

! Treat hard cut as the default: full Step 5 (task check + Vitest coverage) with no --skip-ci.

~ Treat routine cut as the same full Step 5, sped up by default-excluding .deft-scratch/ (and legacy swarm-worktrees/) from content/link/path/build-dist walks, plus pre-cut hygiene (green required checks on the tip SHA; prune stale .deft-scratch/worktrees).

! Confirm required CI is green on the tip you will tag when aiming for a calm routine cut — green tip CI is a precondition, not a replacement for Step 5.

⊗ Invent a silent lighter Step 5 that skips or soft-passes coverage without an explicit hatch (--allow-coverage-debt=#N for branch-only hairlines, or --skip-ci + --allow-skip-ci=#N for incidents). Silent soft-pass is forbidden in every mode.

~ Prefer pruning abandoned worktrees / cutting from a clean clone over re-scanning scratch noise.

See `docs/RELEASING.md` § Routine vs hard cut for Step 5 (#2953).

~ Frozen Go-installer bridge (#1912 / #1972 / #1987): by default a release tag above the frozen line (the LAST_GO_INSTALLER constant in packages/core/src/legacy-bridge/sot.ts) will NOT rebuild the 6 Go binaries -- the CI freeze-gate job in .github/workflows/release.yml skips the build (the run stays green; npm still ships from the separate npm-publish.yml). If this release must rebuild the Go installer, follow the runbook in `docs/RELEASING.md` § Frozen Go-installer bridge: roll LAST_GO_INSTALLER forward to the cut tag BEFORE tagging (pinning to the exact cut tag both releases the gate AND re-freezes at the new line), then see that section's "After the release" step for the re-pin.

  1. ! Verify the operator is on the configured base branch (default master) and the working tree is clean
  2. ! Confirm the next version number (X.Y.Z) with the user. Major / minor / patch decision flows from the [Unreleased] content (breaking change → major; new feature → minor; fix-only → patch)
  3. ! Inspect [Unreleased] content vs the proposed version bump. If a breaking change appears in ### Changed / ### Removed but only a patch is proposed, surface the mismatch and ask the user to choose
  4. ! Verify task ci:local passes locally (or task check as the graceful-degradation fallback per tasks/release.yml line 9-10). The task release script will refuse to proceed otherwise -- but Phase 1 catches it earlier — on failure from a fixable defect, STOP and follow § Fixable check failure below (#2859); do NOT proceed to step 5
  5. ! Verify gh auth status reports authenticated (task release will refuse otherwise)
  6. ! Run task reconcile:issues -- --apply-lifecycle-fixes to clear any closed-issue / non-completed-folder xBRIEFs before invoking task release (#734). The release pipeline carries the deterministic gate at Step 3 (task reconcile:issues -- --apply-lifecycle-fixes, refuses with EXIT_VIOLATION on any Section (c) mismatch), but Phase 1 is the operator's first-line defence -- running the apply-mode flag here is the canonical clean path; --allow-vbrief-drift on the pipeline exists only as the explicit-acknowledgment escape hatch (analogous to --allow-dirty). The recurrence record is the v0.21.0 cut, which surfaced 13 stranded xBRIEFs (8 cycle-relevant + 5 historical residue) post-publish; the gate now blocks that drift before any irreversible action
  7. ! Verify the proposed v<version> tag is not already in use locally, on origin, or as a published GitHub release (#784). The release pipeline carries the deterministic gate at Step 4 (the release tag-availability gate, refuses with EXIT_VIOLATION before any state mutation -- CHANGELOG promotion, ROADMAP refresh, build, commit), but Phase 1 is the operator's first-line defence. Quickly probe with git tag -l v<version> (local), git ls-remote --tags origin refs/tags/v<version> (remote), and gh release view v<version> --repo <owner>/<repo> (release-only, where gh release view exits 0 only when the release exists). The recurrence record is the v0.22.0 → v0.23.0 release attempt on 2026-05-01: the operator typed 0.22.0 (the prior release from 12 hours earlier) and the legacy pipeline ran 8 steps before failing at git tag -- leaving a wrong-version local commit + dist/deft-0.22.0.zip orphan + manual git reset --hard recovery. The new pre-flight gate blocks that mode before any irreversible action
  8. ! Verify the npm credential path is configured before cutting the tag (#1910, #1909). A v* tag now auto-triggers .github/workflows/npm-publish.yml, which publishes the four @deftai/directive* packages with npm publish --provenance. Confirm the publish path can authenticate: either the NPM_TOKEN repo secret is present (gh secret list --repo <owner>/<repo> shows NPM_TOKEN) OR an npm OIDC trusted publisher is configured for the @deftai/directive* packages. If neither is in place, WARN loudly that the tag will fire a publish job that fails (red X on the tag, no packages) -- the operator may still proceed for a GitHub-only release, but the npm channel will not land until #1909's credential is provisioned. Cross-reference #1909.
  9. ! Disclose npm irrevocability before any tag push (#1972, #2002, #3527). A v<version> tag push is the real npm publish gate -- NOT Phase 5 or task release:publish. Tag push fires .github/workflows/npm-publish.yml in a separate workflow that is NOT draft-gated; npm packages ship immediately and cannot be retracted (npm unpublish is forbidden). Recovery is forward-only: deprecate, dist-tag, or ship a patch. The last human gates before npm goes live are: (a) Phase 2 dry-run yes, (b) a human-origin closed-verb grant (deft authz:grant -- --template release-publish --target <version> or DEFT_ALLOW_RELEASE_PUBLISH=1). task release fails closed at the Step 10–11 tag-push boundary without that grant. Phase 5 only controls GitHub release visibility (draft → public); it does NOT gate npm. The draft-flip release:publish closed-verb check remains (#1095).
  10. ~ Ask the operator for an optional one-line release summary (recommended 80-160 chars; can be skipped). The summary is the canonical narrative for THIS release across three audiences: (a) injected as a Markdown blockquote at the top of the promoted CHANGELOG.md [<version>] section, (b) auto-flowed into the GitHub release body via the existing _section_for_version pickup, and (c) populated VERBATIM into the Phase 8 Slack *Summary*: slot. Capture the wording once here; do NOT regenerate per-audience downstream

⊗ Skip the version-bump magnitude check -- a patch release that ships breaking changes is the kind of regression that Repair Authority [AXIOM] (#709) is designed to prevent.

⊗ Skip the xBRIEF-lifecycle-sync check (#734); the gate exists because operators consistently forget the manual task scope:complete move step. The v0.21.0 cut surfaced 13 stranded xBRIEFs (8 cycle-relevant + 5 historical residue) post-publish as the recurrence record this gate prevents. If task release reports [3/13] Pre-flight xBRIEF lifecycle sync... FAIL (<count> mismatches; run task reconcile:issues -- --apply-lifecycle-fixes to fix), the canonical recovery is the apply-mode invocation -- --allow-vbrief-drift is reserved for cases where the operator has explicitly reviewed the drift and chosen to defer the lifecycle reconcile to the next refinement pass (e.g. an emergency hot-fix release).

⊗ Skip the tag-availability check (#784); the gate exists because the legacy 12-step pipeline only invoked git tag at Step 9, after Steps 1-8 had already mutated state (CHANGELOG promoted, ROADMAP refreshed, dist built, release commit made locally). A duplicate-tag failure at Step 9 stranded the operator with an unpushed wrong-version commit + orphaned dist/deft-<wrong>.zip artifact + manual git reset --hard recovery (forbidden by AGENTS.md SCM rules without explicit permission). The recurrence record is the v0.22.0 → v0.23.0 release attempt on 2026-05-01. If task release reports [4/13] Pre-flight tag availability... FAIL (<surface> tag v<version> already exists ...), the canonical recovery is to choose a different version (the most likely cause is operator typo of a prior release).

⊗ Hand-write a different one-line narrative for each of the three downstream surfaces (CHANGELOG / GitHub release / Slack) -- that drift is exactly the gap the --summary flag is designed to close. If the operator insists on per-audience tone, populate the canonical --summary ONCE here and document the deviation in the Phase 8 anti-pattern.

Phase 2 — Dry-run review

! Invoke task release -- <version> --dry-run --skip-tag --skip-release and present the plan to the user. If Phase 1 collected an operator summary, also pass --summary "<text>" so the dry-run preview reflects the canonical narrative the operator just authored.

task release -- <version> --dry-run --skip-tag --skip-release --summary "<text>"

The dry-run prints [N/13] <step>... DRYRUN (would <action>) for every pipeline step (Step 13 is the post-create verify-isDraft gate added by #724; Step 4 is the tag-availability pre-flight gate added by #784). Step 6 (CHANGELOG promotion) surfaces whether a summary was supplied (truncated to ~60 chars in the preview) so the operator can validate the wording before any file is written. Capture the output and present it to the user, then wait for explicit confirmation before continuing.

! Wait for explicit user confirmation: yes / back / quit. Remind the operator that Phase 4's tag push will irrevocably publish npm (#1972) and requires a release-publish grant (or DEFT_ALLOW_RELEASE_PUBLISH=1) -- this yes is the last safe abort before that channel opens (#3527).

  • yes (or confirmed / approve) → proceed to Phase 3
  • back → return to Phase 1 for re-validation (e.g. user wants to amend the version or [Unreleased] content)
  • quit → abort the workflow cleanly; no state changes

⊗ Skip the dry-run preview. The dry-run is the operator's last opportunity to catch a bad version number, malformed CHANGELOG, or wrong base branch before the pipeline starts writing files.

Phase 3 — E2E sanity

! Invoke task release:e2e against an auto-created temp repo to verify the full pipeline shape works end-to-end before touching the real repo.

! #1880 Gap D (#2692): task release:e2e is a long-running step — MUST background / subagent-dispatch it when the host supports it so Phase 2 confirmation and other human gates stay interactive. Do not wrap its output in PowerShell Select-Object -Last.

task release:e2e

The harness provisions deftai/deftai-release-test-<ts>-<uuid6>, runs the smoke-test rehearsal, and by default keeps the temp repo (#2572). Stderr always includes the full owner/slug and a copy-pasteable manual-cleanup command (gh repo delete <owner>/<slug> --yes). Privileged environments (CI or an operator with delete_repo) MAY pass task release:e2e -- --destroy-repo to attempt auto-delete; destroy failure emits a WARN and does not block Phase 4 when the rehearsal succeeded.

! After Phase 3, the agent MUST NOT retry or escalate temp-repo deletion. Include any leftover temp repo(s) in the phase summary for the operator to clean up manually.

! Treat a non-zero exit from task release:e2e as a hard refusal to proceed to Phase 4. Surface the diagnostic and ask whether to debug (return to Phase 1) or abort (quit).

? Skip allowed when the operator has just run task release:e2e successfully against the same branch in the past 30 minutes. Note the prior run timestamp in the user-facing summary.

! task release:e2e now also rehearses the npm publish (#1910). Unless --skip-npm is passed (or npm is absent from PATH, which soft-skips), the rehearsal runs npm publish --dry-run --access public for all four @deftai/directive* packages against the throwaway clone in dependency order (types -> core -> content -> cli), after pnpm install + pnpm -w run build and a version-alignment pass. This catches a broken files allowlist, a version-drift bug, or a dependency-order error BEFORE the real v* tag fires the publish workflow -- without touching the real registry. The install+build exceeds the <90s fast budget, so pass task release:e2e -- --skip-npm when you only need the GitHub-pipeline shape check.

! Tag -> npm coupling + irrevocability (#1910, #1972, #2002). A v<version> tag is a TWO-channel action: the GitHub release (this skill's pipeline) AND .github/workflows/npm-publish.yml, which runs in a SEPARATE workflow that does NOT block the GitHub release and is NOT draft-gated. The npm workflow derives the published version from the tag (${GITHUB_REF_NAME#v}); this skill owns the version chosen in Phase 1. These MUST stay consistent -- the tag you cut IS the npm version that ships; there is no separate npm version bump. npm publish is irrevocable (#1972): once the tag fires, packages are live on npm and cannot be unpulled; task release:rollback does NOT retract npm (forward-only recovery). A red npm job on a green GitHub release means the npm channel did not ship (verify in Phase 5/7).

Phase 4 — Production draft

! Last human gate before npm (#1972, #2002, #3527). Immediately before invoking task release, re-state that the tag push in this step will irrevocably publish all four @deftai/directive* packages to npm via .github/workflows/npm-publish.yml. There is no undo on npm; only forward recovery (deprecate / dist-tag / patch). Proceed only when the operator explicitly confirms and a human-origin grant covers release-publish for this version (deft authz:grant -- --template release-publish --target <version> or DEFT_ALLOW_RELEASE_PUBLISH=1). The pipeline fails closed at Step 10–11 without that grant -- that is the npm-distributing boundary. ⊗ Rely on Phase 5 release:publish as the npm gate; it only flips the GitHub draft. ⊗ Delete the draft-flip check as a substitute for the tag-push gate.

! Invoke task release -- <version> (NO --dry-run, NO --skip-tag, NO --skip-release, NO --skip-ci). If Phase 1 collected an operator summary, pass --summary "<text>" so the production cut writes the same blockquote the dry-run previewed.

⊗ Use --skip-ci on a production cut except under explicit operator incident review — it skips Step 5 vitest coverage and ships untested npm builds (#2652). When unavoidable, pass --allow-skip-ci=#N citing the tracked issue; Step 5 emits a loud WARN. See `docs/RELEASING.md` § Vitest coverage hang recovery. The next patch after the hang fix must cut without --skip-ci.

task release -- <version> --summary "<text>"

Per #716 default-draft hardening, this lands the release as a --draft on the real repo. Binaries upload via release.yml CI, but the artifact is NOT yet visible to consumers. The operator-authored summary becomes part of the promoted CHANGELOG.md [<version>] section AND the GitHub release body (auto-pickup via _section_for_version). The same wording is the canonical source for the Phase 8 Slack *Summary*: slot.

! Maintainer-mode release notes auto-lead with an "Upgrading from an older version?" banner (#1413). When the cut targets the canonical framework repo (deftai/directive), task release Step 12 prepends the banner from the editable template at .github/release-notes/upgrade-banner.md to the notes passed to gh release create (via _prepend_upgrade_banner). The banner points consumers at the canonical deft-install --yes --upgrade --repo-root . --json upgrade command and #1411. This is GitHub-release-body-only -- it is NEVER injected into CHANGELOG.md, so the CHANGELOG section and the release body intentionally differ by this leading block. To change the wording, edit the template file; do not hand-edit the published release body. Consumer-mode releases (any non-deftai/directive repo) are unaffected -- a downstream project that vendors the release pipeline never inherits deft's upgrade guidance. A missing/unreadable template degrades gracefully (notes ship without the banner; the cut is never blocked).

! Verify isDraft within 5 seconds; flip immediately if not (#724). Immediately after gh release create --draft returns success, task release Step 11 polls gh release view v<version> --json isDraft up to 5 times at 1-second intervals. If the release exists with isDraft=false, the pipeline auto-flips it via gh release edit v<version> --draft=true and emits a WARNING: release landed as public; flipping to draft (defense-in-depth, see #724) line. This closes the ~90-second public-exposure window observed during the v0.21.0 cut where a manual recovery created a public release before the operator noticed and flipped it. The verify gate is defense in depth even when --draft was passed correctly: it catches the case where gh release create partially succeeded (release record written, error returned) AND the operator-error variant where an alternate code path sent the release without --draft. A release-not-found-within-budget result emits a WARN and does NOT fail the pipeline (release.yml CI may still be processing).

! Wait for task release to exit 0 before continuing. A non-zero exit means the pipeline halted partway through; consult Phase 7's task release:rollback recovery before retrying.

⊗ Pass --no-draft here unless the operator has explicitly opted into direct-publish (e.g. automated security patch). The default-draft contract is the foundation of the safety hardening surface.

⊗ Skip the post-create verify-isDraft gate -- the gate is the only reliable safety net against "create call exited 0 but the release somehow landed as public" variants (#724). If task release is invoked manually outside the canonical task release flow, the operator MUST run gh release view v<version> --json isDraft followed by gh release edit --draft=true on isDraft=false BEFORE handing off to Phase 5.

Phase 5 — GitHub draft QA (optional; NOT the npm authority gate)

! After task release exits 0, QA the GitHub draft release only. npm packages typically already shipped when the tag push in Phase 4 fired .github/workflows/npm-publish.yml (#1972, #2002). Phase 5 is NOT a "user-only authority before going live" gate for the release as a whole -- it is optional draft QA for GitHub assets, notes, and binaries.

  1. ! Verify npm publish status FIRST (in parallel with draft inspection). Run gh run list --workflow=npm-publish.yml --repo <owner>/<repo> --limit 5 and confirm the tag run for v<version> is completed/success. If npm failed, surface immediately -- the GitHub draft QA is secondary to a red npm channel.
  2. ! Run gh release view v<version> --json url,name,body,assets,isDraft --repo <owner>/<repo> and present the output to the user
  3. ! Surface the asset list (size + filename) so the user can verify binaries uploaded correctly
  4. ! Surface the auto-generated release notes (or the CHANGELOG section that was promoted into the release body)

Happy path (default when npm succeeded and draft assets look correct)

! When the npm workflow succeeded AND draft assets/notes pass inspection, auto-proceed to Phase 6 Publish branch -- run task release:publish -- <version> without a redundant human publish prompt (#2002). npm already shipped at tag push; waiting for a separate publish confirmation does not protect the npm channel.

? Operator override: if the operator wants to hold the GitHub release in draft (e.g. embargo, last-minute notes edit), they MAY say defer before auto-publish runs.

Exception paths (operator-initiated)

  • rollback → proceed to Phase 6 (Rollback branch). Reminder: rollback unwinds the GitHub release only; npm packages already published at tag push are NOT retracted (#1972).
  • defer → halt and exit. Surface the draft URL so the operator can return later with task release:publish -- <version> or task release:rollback -- <version>.

⊗ Treat Phase 5 as the npm publish-authority gate -- npm ships at tag push (Phase 4), not at task release:publish. A human publish prompt here is redundant when npm already succeeded and only delays flipping the GitHub draft to public.
⊗ Skip npm workflow verification in Phase 5 and defer it entirely to post-publish Phase 7 -- npm status MUST be checked before or in parallel with the GitHub publish flip.

Phase 6 — Publish or rollback

! Branch on the Phase 5 outcome. The happy path auto-enters the Publish branch when npm succeeded and draft QA passed (#2002).

Publish branch (happy path auto-run, or resumed after defer)

task release:publish -- <version>

The companion script flips --draft=false, then re-reads the release to verify isDraft == false actually flipped. State machine:

  • draft found → flip to public; verify; exit 0
  • already published → exit 0 no-op (idempotent re-runs are safe)
  • not-found → exit 1 (cannot publish a missing release)
  • gh-error → exit 1 with diagnostic

! Wait for task release:publish to exit 0 before continuing. On the happy path this runs immediately after Phase 5 draft QA without a separate human publish prompt.

Rollback branch (user said rollback)

task release:rollback -- <version>

The state-aware unwind detects the post-release state and applies the matching tiered recovery. Time-windowed download-count guard:

  • release age < 5 min → threshold = 0 (rollback safe; nobody noticed yet)
  • release age 5-30 min → threshold = max(--allow-low-downloads, 10) (filters bot fetches)
  • release age > 30 min → refuse without --allow-data-loss

Three escape hatches (escalating warnings):

  • --allow-low-downloads N -- accept up to N downloads
  • --allow-data-loss -- accept any count (consumer impact)
  • --force-strict-0 -- require exactly 0 regardless of release age

Race-condition mitigation: download_count is double-read with a 5s sleep between reads; rollback only proceeds if both reads agree below threshold.

! When the guard refuses, surface the recommendation to the user: rollback is risky on a released artifact with non-zero downloads. Prefer the hot-fix path (cut the next patch with a withdrawal note in [Unreleased]/Changed rather than deleting the broken release).

! task release:rollback does NOT retract npm (#1972, #2002). Rollback unwinds GitHub release state (draft/public, tag, assets) only. npm packages published at tag push remain on the registry irrevocably. Recovery is forward-only: deprecate the bad version, move a dist-tag, or ship a patch release.

Phase 7 — Post-publish verification

! Only enter Phase 7 if Phase 6 took the Publish branch (rollback branch ends here with the unwind log).

  1. ! Re-verify npm publish landed (#1910, #1909, #2002). Phase 5 checked workflow status before the GitHub publish flip; Phase 7 confirms registry truth AFTER task release:publish. For each of @deftai/directive-types, @deftai/directive-core, @deftai/directive-content, and @deftai/directive, run npm view <pkg>@<version> version --prefer-online (expect <version>) and confirm provenance on the npm page. A green GitHub release with missing npm packages means consumers cannot npm i -g @deftai/directive@<version> -- escalate immediately. (Real-registry verification depends on #1909's credential; until then verify workflow-run status and flag credential gaps.) Reading one package is not sufficient — sibling publish is not atomic.
  2. ! Report local-vs-released CLI drift (#3753). After the all-four registry check, print the local global CLI version, the released version, whether they match, and the exact remediation npm i -g @deftai/directive@<version> --prefer-online. Compare versions with checkActiveCliAgainstTarget (not bare deft --version) so a PATH-shadowed install cannot report success (#3233). If some siblings are visible and others are not, report still propagating. If none are visible after this wait, report publish incomplete. task release already emitted a non-blocking snapshot of the same report at pipeline completion (single probe, no wait — Step 13 returns before npm-publish.yml is green). Phase 7 is the bounded wait. ⊗ Run npm i -g. The report must not fail the release.
  3. ! Verify GitHub auto-closed the discrete-task issue(s) referenced via Closes #N in the release notes (mirrors skills/deft-directive-swarm/SKILL.md Phase 6 Step 2)
  4. ! Run gh issue view <N> --json state --jq .state for each closed issue. If any didn't auto-close, manually close with gh issue close <N> --comment "Closed by release v<version> (squash auto-close did not trigger)" (Layer 1, #167)
  5. ! Verify ROADMAP.md correctness via task roadmap:render (the release pipeline already invoked this; Phase 7 is the second-pass sanity check)
  6. ! Verify binaries are downloadable from the public release URL: gh release view v<version> --json assets --jq '.assets[].url' and curl one to confirm 200 OK
  7. ! For any umbrella / staying-OPEN issue (Refs #N) referenced in the release notes, run the Layer 3 reopen sweep from skills/deft-directive-swarm/SKILL.md Phase 6 Step 1: any protected issue that auto-closed MUST be reopened with a comment citing #701

⊗ Skip the post-publish verification. The closing-keyword false-positive (Layer 1 / Layer 2 / Layer 3) and the incremental-renderer-drift (#641, #614) are exactly the kind of issues that surface only AFTER a release is public.

Phase 8 — Slack announcement

! Generate the canonical Slack release announcement and present it to the user for copy-paste, re-using the template from skills/deft-directive-swarm/SKILL.md Phase 6 Step 5.

The announcement block MUST include:

:rocket: *deft v<version>* -- <release title>

*Summary*: <one-sentence description of the release scope>

*Key Changes*:
- <bullet per significant change, 3-5 items max>

*Stats*: 1 release | ~<duration> elapsed | <N> commits since v<previous>
*Release*: <GitHub release URL>

! Populate version from the freshly-published gh release view v<version> output. Populate release title from the CHANGELOG section heading (or the GitHub release title). Summarize key changes from the promoted [Unreleased] -> [<version>] CHANGELOG section (NOT raw commit messages). Populate stats from git log v<previous>..v<version> --oneline | wc -l.

! Populate the *Summary*: slot VERBATIM from the operator-authored blockquote at the top of the CHANGELOG [<version>] section (the line beginning with > immediately after the ## [<version>] - <date> heading). The Phase 1 prompt + Phase 4 --summary flag exist precisely so this populate step is mechanical -- one canonical narrative authored once at Phase 1, propagated through Phase 4 promotion, and copy-pasted here without re-authoring. If the CHANGELOG section has no blockquote (operator skipped the Phase 1 prompt), generate a one-sentence summary from the ### Added / ### Changed bullets and surface to the operator that this is a regenerated narrative (NOT canonical) so they can decide whether to amend the CHANGELOG before publishing.

! Present the block as a code-fenced snippet the user can copy directly. Do NOT post to Slack from inside this skill -- the user owns the actual broadcast.

Skill Completion

! When Phase 8 completes (or when Phase 5 took the defer / quit path, or when Phase 6 completed the rollback branch), explicitly confirm skill exit:

deft-directive-release complete -- exiting skill.
Next: <one-line guidance>

Where <one-line guidance> is one of:

  • "release v live -- monitor consumer reports for ~24h before cutting v"
  • "release v rolled back -- the underlying defect needs a hot-fix in the next CHANGELOG entry"
  • "release deferred -- resume by running task release:publish -- <version> (GitHub only; npm already shipped at tag push) or task release:rollback -- <version> (GitHub unwind only; npm is forward-recovery) when ready"

⊗ Exit silently without confirming completion or providing next-step guidance.

Anti-Patterns

  • ⊗ Foreground-block the operator chat on Phase 1 long prep (reconcile:issues, cache refresh, ci:local / check) or Phase 3 release:e2e when background / subagent dispatch is available (#1880 Gap D / #2692) — the interactive channel must stay free for version confirmation, --summary, and the Phase 2 dry-run gate
  • ⊗ Wrap long release-prep task output in PowerShell Select-Object -Last — it buffers until exit and makes the session look hung (#2692)
  • ⊗ Run task release without a Phase 2 dry-run preview -- the dry-run is the only safe place to catch a bad version, malformed CHANGELOG, or wrong base branch
  • ⊗ Skip Phase 3 (e2e rehearsal) on the assumption that "the dry-run is enough" -- the e2e harness catches gh-CLI auth issues, repo permission gaps, and pipeline-shape regressions that the dry-run cannot detect
  • ⊗ Pass --no-draft to task release without explicit operator opt-in -- the default-draft contract is the foundation of the safety hardening surface
  • ⊗ Invoke task release without --skip-tag when no release-publish grant or DEFT_ALLOW_RELEASE_PUBLISH is present -- the v0.105.0 cut published npm with no authz because the closed-verb check sat only on the later draft flip (#3527)
  • ⊗ Delete the release:publish draft-flip closed-verb check as a substitute for the tag-push gate -- placement, not strength; both stay (#3527 / #1095)
  • ⊗ Treat Phase 5 as the npm authority gate or require a redundant human publish prompt when npm already succeeded -- npm ships at tag push (#1972); Phase 5 is GitHub draft QA only
  • ⊗ Expect task release:rollback to retract npm packages -- rollback is GitHub-only; npm recovery is forward-only (deprecate / dist-tag / patch)
  • ⊗ Run task release:rollback against a release that has > 30 minutes of consumer-driven downloads without first weighing the hot-fix path -- a withdrawal note in the next patch is almost always less disruptive than deleting a public artifact
  • ⊗ Use --allow-data-loss without first reading the script docstring's hot-fix-path recommendation -- the flag is an explicit acknowledgment of consumer impact, not a default
  • ⊗ Skip the Phase 7 Layer 3 reopen sweep -- protected umbrellas can auto-close on a release-merge squash even when the release notes use Refs #N only
  • ⊗ Run npm i -g from the release pipeline or Phase 7 (#3753) -- report local-vs-released drift only; no global mutation
  • ⊗ Trust bare deft --version after a cut -- use checkActiveCliAgainstTarget so a PATH-shadowed install cannot report success (#3233 / #3753)
  • ⊗ Treat a missing sibling package as a failed publish without distinguishing still-propagating from publish-incomplete (#3753)
  • ⊗ Post the Phase 8 Slack announcement directly from this skill -- the user owns the broadcast; the skill only generates the template
  • ⊗ Hardcode master as the base branch -- delegate to the configured base branch from task release --base-branch <branch>
  • ⊗ Skip the post-create verify-isDraft gate (#724) -- a successful gh release create exit code does NOT prove the release actually landed in draft state; the 5-second poll-and-flip gate in task release Step 11 is the only safety net against operator-error variants and partial-success races, and any manual recovery path that bypasses task release MUST run gh release view --json isDraft followed by gh release edit --draft=true on isDraft=false before handing off to Phase 5
  • ⊗ Manually rewrite the Phase 8 Slack *Summary*: line to deviate from the CHANGELOG [<version>] blockquote -- the canonical narrative is authored ONCE at Phase 1 via --summary and propagates verbatim across all three audiences (CHANGELOG / GitHub release body / Slack). Per-audience hand-edits create documentation drift that the deterministic --summary flow is designed to prevent. If the operator wants Slack-specific tone, fold it into the canonical Phase 1 wording before passing --summary, OR amend the CHANGELOG blockquote BEFORE Phase 8 so all three surfaces stay aligned
  • ⊗ Export DEFT_ALLOW_DEFAULT_BRANCH_COMMIT=1 for the entire release session or wrap task release / task ci:local in it (#1553) -- the env var is process-wide and leaks into nested tests and temporary repos, producing false preflight failures. Prefer task policy:allow-direct-commits -- --confirm and restore with task policy:enforce-branches after the cut (closeout commit+push may use a scoped env prefix on those three git commands only — see Branch-Protection Policy Guard, #2623)
  • ⊗ Pass --allow-coverage-debt=#N unquoted on Windows PowerShell (#2621) -- # starts a comment and silently drops the issue number. Use --allow-coverage-debt=N or --allow-coverage-debt="#N"
  • ⊗ Soft-pass coverage debt while an open coverage-debt issue from a prior hatch still exists (#2866 / #2573 / #3187) -- restore real branch coverage >= 85% and close the debt issue before reusing --allow-coverage-debt or expecting auto-hatch; the ledger is open GitHub issues, not prior CHANGELOG citations (#2618 superseded by open-issue ledger)
  • ⊗ Re-run the full Step 5 suite after a legal branch-only hairline when auto-hatch already filed #N and continued (PASS_WITH_DEBT) — that is the ceremony tax #3187 removes
  • ⊗ Trust .deft/release-suite-stamp.json in CI or after HEAD/tree drift (#3187)