(e.g., `0.2.5`)\n- Contains `enqueue(` (eforge queue entries)\n- Contains `cleanup(` (eforge cleanup commits)\n- Contains `plan(` (eforge planning artifacts)\n- Contains `Merge ` (merge commits)\n- Contains `bump plugin version`\n\n**Clean up commit messages:**\n- Strip the leading commit hash\n- Strip `plan-NN-` and `hardening-NN-` prefixes from conventional commit scopes (e.g., `feat(plan-01-foo): bar` becomes `feat(foo): bar`, `feat(hardening-03-foo): bar` becomes `feat(foo): bar`)\n- Extract the scope (text inside parentheses) and the description (text after `: `)\n- Normalize the scope: if it maps to a known package/module, use that; if no scope, use `core`\n\nKnown scope → package mappings:\n- `engine` → **engine**\n- `client` → **client**\n- `monitor` → **monitor**\n- `monitor-ui` → **monitor-ui**\n- `eforge` → **eforge** (CLI)\n- `pi-eforge` → **pi-eforge**\n- `plugin` → **plugin** (Claude Code plugin)\n- `mcp` → **mcp**\n- `backends` → **backends**\n- `deps`, `dependencies` → **deps**\n- `queue` → **queue**\n- `revert-*` or `revert` → **core**\n- `gap-close` → **core**\n- `cleanup` → skip these commits entirely (they're PRD cleanup)\n- Any other scope → use as-is (e.g., `backend-new` → **backend-new**)\n- No scope → **core**\n\n**Deduplicate** by description text - keep only the first occurrence of each description.\n\n**Group by conventional commit type** into markdown sections:\n- `feat` - `### Features`\n- `fix` - `### Bug Fixes`\n- `refactor` - `### Refactoring`\n- `perf` - `### Performance`\n- `docs` - `### Documentation`\n- `chore`, `ci`, `build`, `test` - `### Maintenance`\n- Anything else - `### Other`\n\nWithin each section, format entries as:\n```markdown\n- **\u003cpackage\u003e**: \u003cdescription\u003e\n```\n\nFor example:\n```markdown\n- **pi-eforge**: add searchable overlays for provider and model selection\n- **engine**: subprocess-per-build with crash-safe reconciler\n- **core**: Parent scheduler owns sessionId and emits session:start at spawn\n```\n\nSort entries alphabetically by package name within each section.\n\nOmit empty sections. If no meaningful commits remain after filtering, use \"Maintenance release\" as the release notes.\n\nStore the generated markdown for use in Steps 5 and 7.\n\n### Step 5: Update CHANGELOG.md\n\n**Compute the new version** before bumping by reading the source-of-truth version from `packages/eforge/package.json` and incrementing the appropriate component:\n\n```bash\nnode -e \"\nconst v = require('./packages/eforge/package.json').version.split('.');\nconst bump = '$BUMP_TYPE';\nif (bump === 'major') { v[0]++; v[1]=0; v[2]=0; }\nelse if (bump === 'minor') { v[1]++; v[2]=0; }\nelse { v[2]++; }\nconsole.log(v.join('.'));\n\"\n```\n\n(Where `$BUMP_TYPE` is the resolved bump type from Step 1.)\n\nNote: the root `package.json` has no `version` field - this is a pnpm workspace and `packages/eforge/package.json` is the lockstep source of truth.\n\n**Create or update CHANGELOG.md:**\n\n1. If `CHANGELOG.md` does not exist, create it with a `# Changelog` heading\n2. Prepend a new entry immediately after the `# Changelog` heading line:\n\n```markdown\n## [X.Y.Z] - YYYY-MM-DD\n\n\u003crelease notes from Step 4\u003e\n```\n\n3. Trim to a maximum of 20 `## [` sections. If entries are removed, ensure this footer exists at the bottom of the file:\n\n```markdown\n---\nFor older releases, see [GitHub Releases](https://github.com/eforge-build/eforge/releases).\n```\n\n**Commit the changelog:**\n\n```bash\ngit add CHANGELOG.md\ngit commit -m \"docs: update CHANGELOG.md for vX.Y.Z\"\n```\n\n### Step 6: Bump Version, Open PR, and Tag Merged Main\n\nUse the protected-main flow. Create a release branch, bump without tagging, open a PR, and enable auto-merge:\n\n```bash\ngit checkout -b release/v\u003cversion\u003e\npnpm release \u003cbump-type\u003e --no-tag\ngit push -u origin release/v\u003cversion\u003e\ngh pr create --base main --head release/v\u003cversion\u003e --title \"release: v\u003cversion\u003e\" --body-file \u003cnotes-file\u003e\ngh pr merge release/v\u003cversion\u003e --auto --merge --delete-branch\n```\n\n(Where `\u003cbump-type\u003e` is the resolved bump type from Step 1.)\n\n`pnpm release --no-tag` (scripts/bump-version.mjs) bumps `packages/eforge/package.json` (source of truth), propagates the version to the other lockstep packages (`client`, `engine`, `monitor`, `pi-eforge`), and commits the lockstep package/version surfaces with message `X.Y.Z`, but does not create a tag.\n\nAfter the PR merges, update `main`, verify the version, create the annotated tag on the merged main commit, and push only that tag:\n\n```bash\ngit checkout main\ngit pull --ff-only origin main\ngit tag -a v\u003cversion\u003e -m \"v\u003cversion\u003e\"\ngit push origin refs/tags/v\u003cversion\u003e\n```\n\n### Step 7: Create GitHub Release and Summary\n\nCreate a GitHub Release from the generated release notes:\n\n```bash\ngh release create v\u003cversion\u003e --title \"v\u003cversion\u003e\" --notes-file \u003cnotes-file\u003e\n```\n\nReport:\n- The new version number\n- The release type (patch, minor, or major)\n- Link to the GitHub Release\n- Remind the user that npm publish is handled by the tag-triggered GitHub Action\n","repo_fullName":"eforge-build/eforge","repo_stars":66,"repo_language":"TypeScript","repo_license":"Apache-2.0","repo_pushedAt":"2026-06-02T04:14:24Z","owner_login":"eforge-build","owner_type":"Organization","owner_name":null,"owner_avatarUrl":"https://avatars.githubusercontent.com/u/268386632?v=4"}};