diff --git a/.github/workflows/release-manual.yml b/.github/workflows/release-manual.yml
new file mode 100644
index 00000000..ffb93ec7
--- /dev/null
+++ b/.github/workflows/release-manual.yml
@@ -0,0 +1,263 @@
+name: Release (one-click)
+
+on:
+ workflow_dispatch:
+ inputs:
+ version:
+ description: "Version to publish. Blank = auto next patch over npm latest."
+ required: false
+ default: ""
+
+concurrency:
+ group: one-click-release
+ cancel-in-progress: false
+
+jobs:
+ release:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ pull-requests: write
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: npm
+ registry-url: https://registry.npmjs.org
+
+ - name: Resolve version
+ id: version
+ run: |
+ REQ="${{ inputs.version }}"
+ if [ -n "$REQ" ]; then
+ if ! echo "$REQ" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'; then
+ echo "::error::version '$REQ' does not look like semver (x.y.z or x.y.z-prerelease)"
+ exit 1
+ fi
+ echo "version=$REQ" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+ PKG="$(node -p "require('./package.json').name")"
+ CUR="$(npm view "$PKG" version)"
+ if [ -z "$CUR" ]; then
+ echo "::error::cannot read the npm latest version of $PKG"
+ exit 1
+ fi
+ V="$(node -p 'const p = process.argv[1].split("-")[0].split("."); p[2] = String(Number(p[2]) + 1); p.join(".")' "$CUR")"
+ echo "auto-bumped $CUR -> $V"
+ echo "version=$V" >> "$GITHUB_OUTPUT"
+
+ - name: Drift guard
+ run: |
+ PKG="$(node -p "require('./package.json').name")"
+ MASTER_V="$(node -p "require('./package.json').version")"
+ NPM_V="$(npm view "$PKG" version)"
+ if [ "$MASTER_V" != "$NPM_V" ]; then
+ echo "::error::$PKG: master package.json ($MASTER_V) != npm latest ($NPM_V). Sync master with the last published release before one-click releasing."
+ exit 1
+ fi
+ if [ "${{ steps.version.outputs.version }}" = "$NPM_V" ]; then
+ echo "::error::$NPM_V is already published; pick a newer version"
+ exit 1
+ fi
+
+ - name: Bump version
+ run: |
+ node -e '
+ const fs = require("fs");
+ const [oldV, newV] = process.argv.slice(1);
+ for (const f of ["package.json", "package-lock.json"]) {
+ const s = fs.readFileSync(f, "utf8");
+ const needle = `"version": "${oldV}"`;
+ const n = s.split(needle).length - 1;
+ if (!n) { console.error(`version field not found in ${f}`); process.exit(1); }
+ fs.writeFileSync(f, s.split(needle).join(`"version": "${newV}"`));
+ console.log(`${f}: replaced ${n} occurrence(s)`);
+ }
+ ' "$(node -p "require('./package.json').version")" "${{ steps.version.outputs.version }}"
+
+ - name: Verify bump diff
+ run: |
+ git add package.json package-lock.json
+ CHANGED="$(git diff --cached --name-only)"
+ EXPECT="package-lock.json
+ package.json"
+ if [ "$CHANGED" != "$EXPECT" ]; then
+ echo "::error::bump touched unexpected files: $CHANGED"
+ exit 1
+ fi
+
+ - name: Pre-flight gate
+ run: |
+ npm ci
+ npm run check:package
+ npm test
+
+ - name: Commit and push to master
+ id: push
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+ VER="${{ steps.version.outputs.version }}"
+ git commit -q -m "release v$VER"
+ if git push -q origin HEAD:master; then
+ echo "pushed release commit to master"
+ echo "direct=true" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+ echo "::warning::direct push to master was blocked (branch protection). Fallback: release prepared on a branch; a PR is needed to publish."
+ BASE_BR="$(date -u +%Y-%m-%d)_release-v${VER}"
+ BR="$BASE_BR"
+ if git ls-remote --exit-code --heads origin "$BR" >/dev/null 2>&1; then
+ BR="${BASE_BR}-$(date -u +%H%M%S)"
+ echo "::warning::fallback branch ${BASE_BR} already exists on origin (stale earlier run of the same version+date); using ${BR} instead."
+ fi
+ if ! git push -q origin "HEAD:$BR"; then
+ echo "::error::fallback branch push to ${BR} failed — nothing published, aborting."
+ exit 1
+ fi
+ echo "fallback release branch pushed: ${BR}"
+ echo "needs_pr=true" >> "$GITHUB_OUTPUT"
+ echo "pr_branch=${BR}" >> "$GITHUB_OUTPUT"
+ exit 0
+
+ - name: Open fallback PR (best effort)
+ if: steps.push.outputs.needs_pr == 'true'
+ continue-on-error: true
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ PR_BRANCH: ${{ steps.push.outputs.pr_branch }}
+ run: |
+ VER="${{ steps.version.outputs.version }}"
+ LINK="https://github.com/${{ github.repository }}/pull/new/${PR_BRANCH}"
+ LAST_TAG="$(git describe --match 'v[0-9]*' --tags --abbrev=0 HEAD~1 2>/dev/null || true)"
+ if [ -n "$LAST_TAG" ]; then
+ NOTES="$(git log --no-merges --pretty='- %s' "${LAST_TAG}..HEAD~1")"
+ else
+ NOTES="(no prior release tag found)"
+ fi
+ PAYLOAD_FILE="$(mktemp)"
+ BODY_FILE="$(mktemp)"
+ VER="$VER" PR_BRANCH="$PR_BRANCH" LAST_TAG="$LAST_TAG" NOTES="$NOTES" node -e '
+ const fs = require("fs");
+ const body = [
+ "Auto-fallback from the one-click release workflow: direct push to master was blocked by branch protection. Merging this PR publishes via the standard release.yml flow.",
+ "",
+ "## Changes since " + (process.env.LAST_TAG || "n/a"),
+ "",
+ process.env.NOTES || "(no changes recorded)",
+ "",
+ "",
+ ].join("\n");
+ fs.writeFileSync(process.argv[1], JSON.stringify({ title: "release v" + process.env.VER, head: process.env.PR_BRANCH, base: "master", body }));
+ fs.writeFileSync(process.argv[2], body);
+ ' "$PAYLOAD_FILE" "$BODY_FILE"
+ RESP_FILE="$(mktemp)"
+ HTTP_CODE=$(curl -sS -o "$RESP_FILE" -w '%{http_code}' -X POST \
+ -H "Authorization: Bearer $GH_TOKEN" \
+ -H "Accept: application/vnd.github+json" \
+ -H "Content-Type: application/json" \
+ "https://api.github.com/repos/${{ github.repository }}/pulls" \
+ -d @"$PAYLOAD_FILE")
+ PR_URL=$(node -e 'try{console.log(JSON.parse(require("fs").readFileSync(process.argv[1],"utf8")).html_url||"")}catch(e){console.log("")}' "$RESP_FILE")
+ echo "POST /pulls HTTP: $HTTP_CODE"
+ write_notes() {
+ {
+ echo ""
+ echo "Release notes — paste into the PR description
"
+ echo ""
+ echo '```markdown'
+ cat "$BODY_FILE"
+ echo '```'
+ echo " "
+ } >> "$GITHUB_STEP_SUMMARY"
+ }
+ if [ -n "$PR_URL" ]; then
+ {
+ echo "### :rotating_light: Release v${VER} prepared — NOT PUBLISHED yet"
+ echo ""
+ echo "Direct push to master was blocked (branch protection); the release commit is on branch \`${PR_BRANCH}\`."
+ echo "Release PR opened automatically **with the generated changelog**: **${PR_URL}** — merge it to publish via CI."
+ } >> "$GITHUB_STEP_SUMMARY"
+ write_notes
+ echo "::notice::Release PR opened automatically: ${PR_URL} — merge it to publish v${VER}"
+ exit 0
+ fi
+ {
+ echo "### :rotating_light: Release v${VER} prepared — NOTHING PUBLISHED YET"
+ echo ""
+ echo "The release commit passed the full pre-flight gate and is on branch \`${PR_BRANCH}\`."
+ echo "Auto-opening the release PR failed (HTTP ${HTTP_CODE}): $(cat "$RESP_FILE")"
+ echo ""
+ echo "**Finish with one click:** [open the release PR](${LINK}), copy the release notes below into the description, then merge to publish."
+ } >> "$GITHUB_STEP_SUMMARY"
+ write_notes
+ echo "::warning::NOTHING WAS PUBLISHED YET — auto-PR failed (HTTP ${HTTP_CODE}): $(cat "$RESP_FILE")"
+ echo "::warning::Finish the release with one click: ${LINK} (then merge to publish v${VER})"
+ exit 0
+
+ - name: Check if prerelease
+ id: pre
+ if: steps.push.outputs.direct == 'true'
+ run: |
+ if echo "${{ steps.version.outputs.version }}" | grep -q '-'; then
+ echo "tag=dev" >> "$GITHUB_OUTPUT"
+ echo "prerelease=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "tag=latest" >> "$GITHUB_OUTPUT"
+ echo "prerelease=false" >> "$GITHUB_OUTPUT"
+ fi
+
+ - name: Create tag
+ if: steps.push.outputs.direct == 'true'
+ run: |
+ git tag "v${{ steps.version.outputs.version }}" || true
+ git push origin "v${{ steps.version.outputs.version }}" || true
+
+ - name: Publish
+ if: steps.push.outputs.direct == 'true'
+ run: npm publish --tag ${{ steps.pre.outputs.tag }} --access public
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+
+ - name: Create GitHub Release
+ if: steps.push.outputs.direct == 'true'
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ VER="${{ steps.version.outputs.version }}"
+ LAST="$(git describe --tags --abbrev=0 2>/dev/null || echo "")"
+ NOTES="$RUNNER_TEMP/release-notes.md"
+ if [ -n "$LAST" ] && [ "$LAST" != "v$VER" ]; then
+ {
+ echo "## Changes since $LAST"
+ echo ""
+ git log --no-merges --pretty='- %s' "$LAST..HEAD~1"
+ } > "$NOTES"
+ else
+ echo "- initial release" > "$NOTES"
+ fi
+ node -e '
+ const fs = require("fs");
+ const [notesFile, ver, prerelease, out] = process.argv.slice(1);
+ const body = {
+ tag_name: "v" + ver,
+ name: "v" + ver,
+ prerelease: prerelease === "true",
+ body: fs.readFileSync(notesFile, "utf8").trim()
+ };
+ fs.writeFileSync(out, JSON.stringify(body));
+ ' "$NOTES" "$VER" "${{ steps.pre.outputs.prerelease }}" "$RUNNER_TEMP/release.json"
+ curl -sS -f -X POST \
+ -H "Authorization: Bearer $GH_TOKEN" \
+ -H "Accept: application/vnd.github+json" \
+ -H "X-GitHub-Api-Version: 2022-11-28" \
+ -H "Content-Type: application/json" \
+ "https://api.github.com/repos/${{ github.repository }}/releases" \
+ -d @"$RUNNER_TEMP/release.json"
diff --git a/devlog/2026-09-20_one-click-release/REQ.md b/devlog/2026-09-20_one-click-release/REQ.md
new file mode 100644
index 00000000..bae7bbf2
--- /dev/null
+++ b/devlog/2026-09-20_one-click-release/REQ.md
@@ -0,0 +1,63 @@
+# REQ - One-click release workflow (workflow_dispatch)
+
+- Task ID: `2026-09-20_one-click-release`
+- Home Repo: `opencode-acp`
+- Created: 2026-09-20
+- Status: InProgress
+- Priority: P2
+- Owner: ranxianglei
+- References: https://github.com/ranxianglei/opencode-acp/pull/443 ; port of the one-click release workflow from ranxianglei/billion-context#772 (built there as #944 core + #978 green-on-prepared fallback + #994 generated changelog body + #1017 stale-branch collision fix)
+
+## 1. Background & Problem Statement
+
+- **Context**: Releases in this repo are already CI-automated end-to-end — merging a date-prefixed release branch (`YYYY-MM-DD_release-v{V}`) triggers `release.yml`, which tags, gates, publishes to npm, and creates the GitHub Release. What is still manual is *preparing* that release: bumping `package.json` + `package-lock.json`, pushing a branch, opening the PR, writing the changelog body. Every release therefore costs a round of manual bookkeeping before CI takes over.
+- **Current behavior**: A human must locally edit two files, commit as `release v{V}`, push a correctly named branch, open a PR with a changelog body, then merge. Error-prone naming (release.yml only detects the branch name in the merge commit message) and duplicated bookkeeping are the recurring pain points.
+- **Goal**: An Actions button ("Release (one-click)") that does all preparation automatically: resolve version → guard against drift → minimal bump → full pre-flight gate → land on master directly if allowed (publish in the same run) or prepare a correctly-named release branch + best-effort auto-PR otherwise (human merges; existing `release.yml` publishes).
+- **Impact**: One click per release instead of manual file edits + branch + PR; version and branch naming can no longer be mistyped; the pre-flight gate runs in every path (direct or fallback), so nothing untested is ever published.
+
+## 2. Usage / Reproduction
+
+- Actions tab → **Release (one-click)** → Run workflow.
+- `version` input blank = auto next-patch over npm `latest` (e.g. `1.18.2` → `1.18.3`). Type an explicit semver (`x.y.z` or `x.y.z-prerelease`) for minor/major/prerelease releases.
+- Expected outcomes:
+ - Direct push allowed → run publishes to npm + tag + GitHub Release and stays green.
+ - Direct push blocked by branch protection (this repo's normal case) → run stays **green** ("prepared" state), release lives on branch `YYYY-MM-DD_release-v{V}` (+ `-HHMMSS` suffix on name collision), job summary carries the auto-opened release PR link (or a one-click `/pull/new` link if auto-PR fails). Human merges with a **standard merge commit** → `release.yml` detects the merge and publishes via CI.
+
+## 3. Constraints & Non-Goals
+
+- **Constraints**:
+ - New file only: `.github/workflows/release-manual.yml`. No product code touched.
+ - Pre-flight gate MUST mirror this repo's own `release.yml` gate exactly: `npm ci` + `npm run check:package` + `npm test` (no separate build step), `actions/setup-node@v4` with node 22, `cache: npm`, registry npmjs.
+ - No hardcoded references to another repo's package name — the npm target is read dynamically from `package.json` (`node -p "require('./package.json').name"`).
+ - Auto-bump starts from `npm latest` (the stable lane), matching this repo's dev/stable tag model.
+ - Drift guard: master's `package.json` version must equal npm `latest`, else abort loudly (prevents publishing over an unpublished manual change).
+ - The fallback branch name MUST match `release.yml`'s detection regex (`Merge pull request #[0-9]+ from .*[0-9]{4}-[0-9]{2}-[0-9]{2}_release-v`) — hence the generated release PR must be merged with a standard merge commit (squash/rebase drops the branch name from the commit message and `release.yml` skips publishing).
+ - Existing `release.yml` (including its `workflow_dispatch` force input) stays untouched; the new workflow is separately named "Release (one-click)" so both appear distinctly in the Actions dropdown.
+- **Non-Goals**:
+ - No changes to product code, config schema, state format, or persistence.
+ - No changes to `pr-checks.yml` / `ci.yml` / `release.yml`.
+ - Not automating the final merge (human-only operation per AGENTS.md §5.1.1.2).
+
+## 4. Acceptance Criteria
+
+- **Correctness**:
+ - [x] Workflow YAML parses; every inline `run:` script passes `bash -n`.
+ - [x] Zero hardcoded references to another repo's package name (npm target read dynamically from `package.json`).
+ - [x] Version resolution: explicit semver validated by regex; blank input auto-bumps patch over `npm view version`.
+ - [x] Drift guard aborts when master `package.json` != npm `latest`, and when the target version is already published.
+ - [x] Bump touches ONLY `package.json` + `package-lock.json`; verify step asserts the staged diff contains exactly those two files.
+ - [x] Pre-flight gate identical to `release.yml`'s: `npm ci` / `npm run check:package` / `npm test`.
+ - [x] Direct-push path: commit `release v{V}` pushed to master continues in-run to tag + `npm publish --tag {latest|dev}` + GitHub Release (prerelease when version contains `-`).
+ - [x] Fallback path: branch `YYYY-MM-DD_release-v{V}` (timestamp-suffixed on stale collision), best-effort auto-PR with generated changelog (`git log --no-merges LAST_TAG..HEAD~1`), run stays GREEN, PR link + release notes in job summary; one-click `/pull/new` link if auto-PR is blocked.
+ - [ ] First real use: drift guard passes (master currently matches npm `latest`, so it will not trip).
+- **Performance / Stability**:
+ - [x] Single job, single concurrency group (`one-click-release`, no cancel-in-progress); no new dependencies, no runners other than `ubuntu-latest`.
+- **Regression**: N/A — CI-only change; existing test suite untouched (green on the PR: test(22)/test(24)).
+
+## 5. Proposed Approach
+
+- **Affected files**: `.github/workflows/release-manual.yml` (NEW, ~263 lines, 13 steps) + this devlog entry.
+- **Flow** (13 steps): checkout (fetch-depth 0) → setup-node (node 22, cache npm, registry npmjs) → Resolve version → Drift guard → Bump version (string-replace `"version": ""` in package.json + package-lock.json) → Verify bump diff (staged diff == exactly those two files) → Pre-flight gate (`npm ci && npm run check:package && npm test`) → Commit and push to master (id `push`; sets `direct=true` or `needs_pr=true` + `pr_branch`) → Open fallback PR (best effort, `continue-on-error`; generated changelog body from last `v*` tag to `HEAD~1`, i.e. excluding the release commit itself; writes job summary in all outcomes) → Check if prerelease (`direct` only) → Create tag (`direct` only) → Publish (`direct` only, `NPM_TOKEN` secret) → Create GitHub Release (`direct` only, REST API, prerelease flag honored).
+- **Why "green-on-prepared"**: in this repo branch protection blocks direct pushes, so the common outcome is "release prepared on a branch". Failing the run in that case would make the happy path look like an error; instead the run succeeds and surfaces the PR link (and a one-click `/pull/new` escape hatch) in the job summary. Nothing is published until a human merges, preserving the human-in-the-loop rule.
+- **Risks**: the coupling to `release.yml`'s merge-title regex means squash/rebase merges of the generated PR silently skip publishing — documented in the PR description Note.
+- **Rollback strategy**: delete the single new file; no state, persistence, or API impact.
diff --git a/devlog/2026-09-20_one-click-release/WORKLOG.md b/devlog/2026-09-20_one-click-release/WORKLOG.md
new file mode 100644
index 00000000..6862f657
--- /dev/null
+++ b/devlog/2026-09-20_one-click-release/WORKLOG.md
@@ -0,0 +1,81 @@
+# WORKLOG - One-click release workflow (workflow_dispatch)
+
+- Task ID: `2026-09-20_one-click-release`
+- Home Repo: `opencode-acp`
+- Status: InProgress
+- Updated: 2026-09-20 13:15
+
+## 1. Summary
+
+- **What was done**: Added `.github/workflows/release-manual.yml` — a `workflow_dispatch` workflow ("Release (one-click)") that automates release preparation: version resolution (auto next-patch over npm `latest`, or explicit semver), drift guard, minimal two-file bump, full pre-flight gate, then either a direct push to master (publishing in-run) or a prepared `YYYY-MM-DD_release-v{V}` branch with a best-effort auto-PR carrying a generated changelog. Ported from ranxianglei/billion-context#772 (#944 + #978 + #994 + #1017).
+- **Why**: Manual release bookkeeping (editing package.json/lockfile, naming the branch correctly, writing the PR body) is error-prone and duplicated on every release; the existing `release.yml` already publishes from a correctly-named merge, so only the *preparation* needed automation.
+- **Behavior / compatibility changes**: CI-only. No product code, config schema, state format, persistence, or public API changes. Existing `release.yml` untouched.
+- **Risk level**: Low
+
+## 2. Change Log
+
+### Commits
+
+| Commit | Description |
+|--------|-------------|
+| `1510f154` | feat(ci): one-click release workflow (workflow_dispatch) |
+| `(this commit)` | docs: add devlog entry (REQ.md + WORKLOG.md) required by AGENTS.md §5.1.2 / pr-validation |
+
+### Key Files
+
+- `.github/workflows/release-manual.yml` — NEW. Single job `release`, 13 steps, concurrency group `one-click-release` (no cancel-in-progress), permissions `contents: write` + `pull-requests: write`.
+- `devlog/2026-09-20_one-click-release/REQ.md`, `WORKLOG.md` — this entry.
+
+## 3. Design & Implementation Notes
+
+- **Version resolution** (`Resolve version` step): explicit input validated against `^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$`; blank input reads the package name dynamically from `package.json` and auto-bumps the patch over `npm view version`.
+- **Drift guard**: aborts if master `package.json` version != npm `latest` (master must equal the last published release before one-clicking), and if the target version is already published.
+- **Bump**: string-replace `"version": ""` → `"version": ""` in exactly `package.json` + `package-lock.json`; the following step stages those two files and asserts `git diff --cached --name-only` equals exactly that pair.
+- **Pre-flight gate**: `npm ci && npm run check:package && npm test` — mirrors `release.yml`'s gate exactly (verified line-by-line), with `actions/setup-node@v4` node 22 + `cache: npm` + registry npmjs like the existing workflows.
+- **Landing strategy** (`Commit and push to master` step, id `push`): commits `release v{V}`; tries `git push origin HEAD:master` first. On success sets `direct=true` and the run continues to tag → publish → GitHub Release. On failure (branch protection — this repo's normal case) pushes to branch `YYYY-MM-DD_release-v{V}`, appending `-HHMMSS` when that name already exists on origin (stale same-day earlier run of the same version, ported from billion-context #1017), sets `needs_pr=true` + `pr_branch`, and the run continues green.
+- **Fallback PR** (`Open fallback PR`, `continue-on-error`): builds a generated changelog body from `git log --no-merges --pretty='- %s' LAST_TAG..HEAD~1` (last `v*` tag → parent of the release commit, so the release commit itself is excluded), opens the PR via REST API with GITHUB_TOKEN, and writes the job summary in every outcome: PR URL when auto-opened, or a one-click `/pull/new/` link plus paste-ready notes when the account-level Actions restriction blocks auto-PR.
+- **Direct-path publish**: prerelease detection (version contains `-` → npm tag `dev` + GitHub Release `prerelease: true`, mirroring `release.yml`); tag push is idempotent (`|| true`); publish uses the repo's `NPM_TOKEN` secret.
+- **Coupling note**: the fallback branch name is chosen specifically to match `release.yml`'s Pattern-1 regex (`Merge pull request #[0-9]+ from .*[0-9]{4}-[0-9]{2}-[0-9]{2}_release-v`) — the generated release PR must be merged with a **standard merge commit**; squash/rebase drops the branch name from the merge title and `release.yml` skips publishing. Documented in the PR description Note.
+
+## 4. Testing & Verification
+
+### Build & Test Commands
+
+```sh
+# Validate workflow YAML + inline scripts (done locally before PR)
+python3 -c "import yaml; yaml.safe_load(open('.github/workflows/release-manual.yml'))" # 13 steps parse OK
+bash -n # all pass
+# Product suite (unchanged by this PR; runs in CI)
+npm ci && npm run check:package && npm test
+```
+
+### Test Coverage
+
+- N/A for new unit tests — CI-only change, no product code touched. The existing suite runs unchanged in CI (test(22)/test(24) green on this PR).
+- Key scenarios verified locally:
+ - YAML parses (pyyaml); every `run:` script passes `bash -n`.
+ - Zero hardcoded references to another repo's package name — npm target read dynamically from `package.json`.
+ - Pre-flight gate matches `release.yml` exactly (`npm ci` / `npm run check:package` / `npm test`; setup-node node 22 cache npm registry npmjs).
+ - Fallback branch-name format matches `release.yml`'s detection regex.
+ - master currently matches npm `latest`, so the drift guard will not trip on first use.
+
+### Results
+
+- **PASS/FAIL**: PASS — local static verification clean; CI `PR Checks` initially failed ONLY on the missing devlog entry (fixed by this commit); product test/build jobs green.
+
+## 5. Risk Assessment & Rollback
+
+- **Risk points**:
+ - Squash/rebase merge of the generated release PR silently skips publishing (regex coupling) — mitigated by the explicit Note in the PR description.
+ - If both direct push and fallback branch push fail, the run fails loudly with "nothing published, aborting" — no partial state.
+- **Rollback method**: delete `.github/workflows/release-manual.yml` (single-file revert); no state/persistence/API impact.
+- **Compatibility notes**: none — no data format or config schema changes.
+
+## 6. Lessons Learned
+
+- What went well: porting an already-proven workflow (billion-context has run it through #944→#1017 hardening) kept the adaptation surface small — only the gate commands and branch-name convention needed re-checking against this repo's `release.yml`.
+- Reusable conclusion: "green-on-prepared" (succeed when the release is safely staged behind a human merge) keeps the common path out of the red while preserving human-in-the-loop publishing.
+
+## 7. Follow-ups
+
+- [ ] First real one-click run: confirm drift guard passes and the fallback path lands a correctly-named branch + PR end-to-end (will happen on the next actual release).