diff --git a/.cursor/commands/issue-close.md b/.cursor/commands/issue-close.md index db40e84..076a0ab 100644 --- a/.cursor/commands/issue-close.md +++ b/.cursor/commands/issue-close.md @@ -11,6 +11,8 @@ Optional text after the command (same line). Examples: - **`bump minor`** or **`minor`** — bump **minor**. - **`bump major`** or **`major`** — bump **major**. - **Free text** — if it clearly asks to release or bump the version, infer `patch`, `minor`, or `major` from wording (e.g. "bugfix release" → patch); if unclear, ask once. +- **`nohistory`** or **`skip history`** — skip the `HISTORY.md` update step (step 3) for this run. +- **`log "..."`** or **`note "..."`** — use the quoted text verbatim as the `HISTORY.md` bullet summary (otherwise the GitHub issue title is used). Other optional notes still apply: branch name, PR title, draft PR, skip issue doc update, commit all changes, etc. @@ -26,24 +28,32 @@ Other optional notes still apply: branch name, PR title, draft PR, skip issue do - Do this **before** updating issue markdown and **before** commit / push / PR so the new version is in the tree that gets merged. - If the project has no bumpable `pyproject.toml` version, skip and say so; continue with the remaining steps. -3. **Issue tracking in the repo** (see project rules under `.issueflows/01-current-issues`) +3. **Update `HISTORY.md`** (opt-out via `nohistory`) + - Read `.cursor/skills/issueflow-history-update/SKILL.md` and follow it. + - Default summary for the new bullet is the GitHub issue title; override with `log "..."` / `note "..."` from the command input. + - If step 2 did **not** bump the version, append a bullet to the existing `## [Unreleased]` section: `- . (#)`. + - If step 2 **did** bump the version, promote `## [Unreleased]` to `## [] - `, prepend a fresh empty `## [Unreleased]` above it, and put the new bullet inside the just-closed release section. + - If `HISTORY.md` does not exist at the project root, skip this step with a short note and continue. Never create it here. + - Show the proposed diff and confirm once before writing. If declined, leave `HISTORY.md` untouched and move on. + +4. **Issue tracking in the repo** (see project rules under `.issueflows/01-current-issues`) - Update the status file for this issue: clear checklist, remaining work, and use `- [x] Done` only when fully resolved. - If the issue is fully resolved, move its markdown files from `.issueflows/01-current-issues` to `.issueflows/03-solved-issues`. If partially resolved, move to `.issueflows/02-partly-solved-issues`. -4. **Commit and fix merge conflicts** +5. **Commit and fix merge conflicts** - Before staging, run `git status` to list all modified/untracked files. If any changes are **not relevant** to this issue, tell the user which ones and ask whether to include them in this commit or leave them for later. Do not silently drop or include unrelated changes. - - Unless told to commit all, stage the right files (avoid unrelated changes). Include `pyproject.toml` (and `uv.lock` if it changed) when a version bump ran. + - Unless told to commit all, stage the right files (avoid unrelated changes). Include `pyproject.toml` (and `uv.lock` if it changed) when a version bump ran. Include `HISTORY.md` when step 3 updated it. - Write a commit message that states what changed and why in normal sentences. - Sync with the default branch before pushing: run `git fetch --prune` then `git pull --ff-only` from the default branch (e.g. `main`) merged into the issue branch (or rebase, per project preference). Use `--ff-only` so unrelated work never gets merged in silently; if it refuses, stop and ask how to reconcile. Check for and fix merge conflicts. -5. **Push** +6. **Push** - Push your branch to `origin` (or the remote you use). -6. **Pull request** +7. **Pull request** - Open a PR against the default branch (e.g. `main`). - Describe the change, how to test it, and link the GitHub issue (e.g. `Closes #123` or `Refs #123` in the PR body). -7. **After review** +8. **After review** - Address feedback, push updates, and merge when approved and CI is green. - Remind the user that the working copy is still on the issue branch, not the default. Suggest `git switch ` before starting unrelated work so new changes don't accidentally land on the issue branch. - Once the PR is merged, run **`/issue-cleanup`** to switch back to the default branch, `git pull --ff-only`, `git fetch --prune`, and delete local branches whose commits are already in the default branch (single consolidated confirm). `/issue-close` no longer does post-merge cleanup itself. diff --git a/.cursor/commands/issue-yolo.md b/.cursor/commands/issue-yolo.md index 12825a1..c5c50e5 100644 --- a/.cursor/commands/issue-yolo.md +++ b/.cursor/commands/issue-yolo.md @@ -9,6 +9,8 @@ For anything non-trivial, use the individual commands so you get confirmation ch Same as `/issue-init` (issue number, URL, or empty to infer from the branch). Optional extra tokens are forwarded to the downstream commands: - `bump` / `patch` / `minor` / `major` — forwarded to `/issue-close` for the version bump. +- `nohistory` / `skip history` — forwarded to `/issue-close` to skip the `HISTORY.md` update step. +- `log "..."` / `note "..."` — forwarded to `/issue-close` as the `HISTORY.md` bullet summary. - `draft` — open a draft PR in `/issue-close`. - Free-form notes — used as the plan/commit context. diff --git a/.cursor/skills/issueflow-history-update/SKILL.md b/.cursor/skills/issueflow-history-update/SKILL.md new file mode 100644 index 0000000..bc4763c --- /dev/null +++ b/.cursor/skills/issueflow-history-update/SKILL.md @@ -0,0 +1,84 @@ +--- +name: issueflow-history-update +description: >- + Keep HISTORY.md (or equivalent changelog) up to date when landing an issue: + append a bullet to [Unreleased], or promote [Unreleased] to a new [x.y.z] + release section when a version bump happened. Invoked from /issue-close. +disable-model-invocation: true +--- + +# issue-flow — history update + +Use this skill to update the project's changelog file (default **`HISTORY.md`**, overridable via `ISSUEFLOW_HISTORY_FILE` in `.env`) as part of `/issue-close`. It never runs on its own schedule; it is driven by the "update HISTORY" step in `.cursor/commands/issue-close.md`. + +## When to use + +- `/issue-close` is landing an issue and the project has a changelog file in the repo root. +- The user did **not** pass `nohistory` / `skip history` on the command line. + +## Preconditions + +1. The changelog file (`HISTORY.md`) exists at the **project root**. If it does not, **skip** this step, print "no `HISTORY.md` — skipping changelog update" and continue the rest of `/issue-close`. Never create the file from this skill. +2. The file is in **Keep a Changelog** shape: a top-level `## [Unreleased]` heading, with released versions below as `## [x.y.z] - YYYY-MM-DD` headings. If the shape does not match, **stop and report the mismatch** instead of guessing — let the user fix the file or pass `nohistory`. +3. UTF-8 read/write with explicit encoding. + +## Inputs from `/issue-close` + +| From | Used for | +|---|---| +| Issue number `N` | Reference suffix on the new bullet, e.g. `(#42)`. | +| Issue title (from `.issueflows/01-current-issues/issue_original.md`) | Default bullet summary. | +| `log "..."` / `note "..."` input token | Override the bullet summary verbatim. | +| Version-bump outcome (from step 2 of `/issue-close`) | Decides **append** vs **promote** (see below). | + +## Operation modes + +### A. No version bump — append to `[Unreleased]` + +1. Read `HISTORY.md`. Locate the first `## [Unreleased]` heading. The block ends at the next `## [` heading (or EOF). +2. Compose the new bullet: + + ``` + - . (#) + ``` + + Summary = `log "..."` override if provided, else the issue title with sentence case, trailing period trimmed before the `.` we add. +3. Append the bullet to the end of the Unreleased bullet list. Preserve existing formatting (blank lines, list markers). Do not reorder existing entries. +4. Show the user the proposed diff of `HISTORY.md` and confirm once before writing. + +### B. Version bump happened — promote `[Unreleased]` to a new release section + +Only runs when step 2 of `/issue-close` actually changed `pyproject.toml` to a new version `NEW_VERSION`. + +1. Determine `NEW_VERSION` (e.g. read from `pyproject.toml`, or from the `uv version` command output). Determine `TODAY` as `YYYY-MM-DD` in the user's local timezone. +2. Read `HISTORY.md`. Find `## [Unreleased]`. +3. Compose the new bullet (same shape as mode A). If `[Unreleased]` was empty when the bump happened, still create the new release section with this bullet inside it — a version bump implies a release, and the focus issue's bullet is always meaningful. +4. Rename the existing heading from `## [Unreleased]` to `## [] - ` and add the new bullet at the end of that section's bullet list. +5. Prepend a fresh, empty `## [Unreleased]` section above the just-closed release, with one blank line separating them: + + ```markdown + ## [Unreleased] + + ## [NEW_VERSION] - TODAY + + - …existing bullets from before the promote… + - (#N) + ``` + +6. Show the user the proposed diff and confirm once before writing. + +## Staging + +When `/issue-close` reaches its commit step: + +- Stage `HISTORY.md` alongside the issue's other changes. +- If a version bump also ran, `HISTORY.md` is staged in the same commit as `pyproject.toml` (and `uv.lock` if it changed). + +## Constraints + +- Read/write only `HISTORY.md` at the project root. Do not touch any other file from this skill. +- Never create `HISTORY.md` from scratch — scaffolding a starter changelog is out of scope for `issue-flow init` / `update`. +- If the user passed `nohistory` (or `skip history`) to `/issue-close`, don't run this skill at all. +- If the confirm prompt in mode A or mode B is declined, leave `HISTORY.md` untouched and print a short "skipped changelog update" note. The rest of `/issue-close` continues normally. +- Preserve existing formatting conventions (bullet style, sentence case, trailing punctuation). Match the style of the nearest existing entries when in doubt. +- The new bullet's `(#)` suffix is always GitHub issue `#N`, matching the focus issue's number in `.issueflows/01-current-issues/issue_original.md`. diff --git a/.cursor/skills/issueflow-issue-close/SKILL.md b/.cursor/skills/issueflow-issue-close/SKILL.md index c8ed4bd..f73233b 100644 --- a/.cursor/skills/issueflow-issue-close/SKILL.md +++ b/.cursor/skills/issueflow-issue-close/SKILL.md @@ -28,25 +28,32 @@ If the user included text after `/issue-close` that requests a version bump: When a bump applies: read `.cursor/skills/issueflow-version-bump/SKILL.md`, run the bump from the **project root** **after** the sanity check and **before** issue-folder updates and **before** commit / push / PR. +## Changelog update tokens (command input) + +- **`nohistory`** or **`skip history`** → skip step 3 entirely. +- **`log "..."`** or **`note "..."`** → override the bullet summary verbatim. Otherwise the GitHub issue title is used. + ## Instructions 1. **Sanity check** — Run the project test suite (e.g. `uv run pytest`) and any checks the repo relies on. Skim the diff; avoid bundling unrelated changes. 2. **Optional version bump** — If the user asked for a bump (see above), follow `.cursor/skills/issueflow-version-bump/SKILL.md` and run `uv version --bump `. If there is no bumpable `pyproject.toml`, skip and continue. -3. **Issue tracking** — Under `.issueflows/01-current-issues/`, update the status file: remaining work, checklists, and **`- [x] Done`** only when the issue is fully resolved. If fully resolved, move that issue's markdown files (`issue_*`) to `.issueflows/03-solved-issues/`. If partially resolved, move to `.issueflows/02-partly-solved-issues/`. Follow any stricter rules in `.cursor/rules/issueflow-rules.mdc` if present. +3. **Update `HISTORY.md`** — Unless the user passed `nohistory`, follow `.cursor/skills/issueflow-history-update/SKILL.md`. If step 2 did not bump the version, append a bullet to the `## [Unreleased]` section. If step 2 bumped the version, promote `## [Unreleased]` to `## [] - ` and open a fresh empty `## [Unreleased]` above it. Show the diff and confirm once before writing. Skip with a note if `HISTORY.md` does not exist at the project root. + +4. **Issue tracking** — Under `.issueflows/01-current-issues/`, update the status file: remaining work, checklists, and **`- [x] Done`** only when the issue is fully resolved. If fully resolved, move that issue's markdown files (`issue_*`) to `.issueflows/03-solved-issues/`. If partially resolved, move to `.issueflows/02-partly-solved-issues/`. Follow any stricter rules in `.cursor/rules/issueflow-rules.mdc` if present. -4. **Commit** — First check `git status`; if there are unrelated uncommitted changes, surface them and ask the user whether to include them — do not auto-include or drop silently. Then stage intentionally (include `pyproject.toml` and `uv.lock` if changed after a bump); write a commit message in full sentences describing what changed and why. +5. **Commit** — First check `git status`; if there are unrelated uncommitted changes, surface them and ask the user whether to include them — do not auto-include or drop silently. Then stage intentionally (include `pyproject.toml` and `uv.lock` if changed after a bump, and `HISTORY.md` if step 3 updated it); write a commit message in full sentences describing what changed and why. -5. **Branch hygiene before push** — Run `git fetch --prune`, then sync with the default branch using `git pull --ff-only` (rebase or merge per project preference). Use `--ff-only` so unrelated history never gets pulled in silently; if it refuses, stop and ask how to reconcile. Resolve merge conflicts before pushing. +6. **Branch hygiene before push** — Run `git fetch --prune`, then sync with the default branch using `git pull --ff-only` (rebase or merge per project preference). Use `--ff-only` so unrelated history never gets pulled in silently; if it refuses, stop and ask how to reconcile. Resolve merge conflicts before pushing. -6. **Push** — Push to the remote the project uses (typically `origin`). +7. **Push** — Push to the remote the project uses (typically `origin`). -7. **Pull request** — Open (or update) a PR against the default branch. Body should explain the change, how to test, and link the GitHub issue (`Closes #n` / `Refs #n`). +8. **Pull request** — Open (or update) a PR against the default branch. Body should explain the change, how to test, and link the GitHub issue (`Closes #n` / `Refs #n`). -8. **After review** — Remind the user the working copy is still on the issue branch (not the default). Suggest `git switch ` before starting unrelated work. Tell them to run **`/issue-cleanup`** once the PR is merged so the standard post-merge cleanup runs (switch to default, `git pull --ff-only`, `git fetch --prune`, `git branch -d` on merged local branches under a single consolidated confirm). +9. **After review** — Remind the user the working copy is still on the issue branch (not the default). Suggest `git switch ` before starting unrelated work. Tell them to run **`/issue-cleanup`** once the PR is merged so the standard post-merge cleanup runs (switch to default, `git pull --ff-only`, `git fetch --prune`, `git branch -d` on merged local branches under a single consolidated confirm). -9. **Output** — Summarize commit, push result, PR URL, and next step (`/issue-cleanup` after merge, or "blocked on …" if stuck). +10. **Output** — Summarize commit, push result, PR URL, and next step (`/issue-cleanup` after merge, or "blocked on …" if stuck). ## Constraints diff --git a/.issueflows/03-solved-issues/issue15_original.md b/.issueflows/03-solved-issues/issue15_original.md new file mode 100644 index 0000000..1ca799a --- /dev/null +++ b/.issueflows/03-solved-issues/issue15_original.md @@ -0,0 +1,7 @@ +# Issue #15: update history when making changes worthy of mentioning + +Source: https://github.com/jepegit/issue-flow/issues/15 + +## Original issue text + +It would be nice if issue-flow could help in updating HISTORY.md or similar file. It could be either as a part of `/issue-close` or a new slash command. diff --git a/.issueflows/03-solved-issues/issue15_plan.md b/.issueflows/03-solved-issues/issue15_plan.md new file mode 100644 index 0000000..868df80 --- /dev/null +++ b/.issueflows/03-solved-issues/issue15_plan.md @@ -0,0 +1,134 @@ +# Plan for issue #15: update HISTORY.md when making changes worthy of mentioning + +Source: [.issueflows/01-current-issues/issue15_original.md](./issue15_original.md) · GitHub: + +## Goal + +Let issue-flow help users keep `HISTORY.md` (or an equivalent changelog file) up to date as part of landing an issue, so the **Unreleased** section and per-version entries stay accurate without a manual round-trip. Works in two shapes: + +1. **Without a version bump:** append a short bullet to the `[Unreleased]` section describing what this PR does, with a `(#N)` issue reference. +2. **With a version bump (`/issue-close bump …`):** "promote" the current `[Unreleased]` block into a new `[x.y.z] - YYYY-MM-DD` section that matches the bumped version, and leave a fresh empty `[Unreleased]` above it. + +This mirrors how `HISTORY.md` is already maintained in this repo (Keep a Changelog style, `[Unreleased]` plus dated releases, entries end with `(#N)`). + +## Recommendation (design call) + +**Extend `/issue-close` with a new step**, and factor the mechanical bits into a dedicated skill (`issueflow-history-update`) — same pattern as `issueflow-version-bump`. Do **not** add a separate `/issue-history` slash command. + +Rationale: + +- **Timing is natural.** Changelog entries belong to "this change is going out", which is exactly when `/issue-close` runs. A standalone command is easy to forget. +- **Version bump integration is free.** The "promote `[Unreleased]` → `[x.y.z]`" action only makes sense alongside `uv version --bump`, which `/issue-close` already orchestrates. +- **`/issue-yolo` gets it for free** because it calls `/issue-close` at the end. +- **The logic is small.** A skill file describes the rules; the slash command just names the new step and forwards to the skill. + +If a later use-case wants "log without closing", that can be added as a tiny `@issueflow-history-update` invocation without a new slash command. + +## Constraints + +- **Template-first.** Every user-facing behavior must live in `src/issue_flow/templates/**/*.j2`. Plain `.cursor/commands/*.md` and `.cursor/skills/**/SKILL.md` in this repo are generated by `uv run python scripts/update_issueflow_setup.py` (or `issue-flow update`) and must be regenerated, not hand-edited for final shape. +- **Back-compat.** Projects scaffolded before this change must still work after running `issue-flow update`. In particular: + - If the project has **no** `HISTORY.md`, the new step **auto-skips** with a short note (same shape as the "no bumpable `pyproject.toml` version" fallback in the version-bump step). Never creates one silently. + - Default input behavior for `/issue-close` (with no extra tokens) should **not** change hostility-wise — i.e. no silent mutation of `HISTORY.md`. See open question O2. +- **No new hard dependencies.** Skill uses plain file edits (Jinja templates describe the edit; the agent performs it). No `towncrier`, no `git-cliff`, no extra Python packages. +- **Config via `.env` only.** New optional env var `ISSUEFLOW_HISTORY_FILE` (default `HISTORY.md`) surfaced through `Settings` and threaded into the template context. +- **Windows-safe file handling.** Keep UTF-8 read/write with explicit encoding (as elsewhere in the codebase). +- **Keep-a-Changelog shape.** The skill assumes a top-level `## [Unreleased]` heading and `## [x.y.z] - YYYY-MM-DD` headings below it. If the file doesn't match, the skill reports the mismatch and stops instead of guessing. + +## Approach + +High-level flow slotted into `/issue-close`: + +``` +1. Sanity check (tests, diff) +2. Optional version bump ← existing +3. Update HISTORY.md ← NEW (opt-out via `nohistory`; auto-skip if file missing) +4. Issue tracking / status file +5. Commit +6. Push +7. Pull request +8. After review (point at /issue-cleanup) +``` + +### Step 3 details + +- **Detect the history file.** Resolve `settings.history_file` (default `HISTORY.md`). If it doesn't exist at the project root, print "no `HISTORY.md` — skipping changelog update" and continue. +- **Parse the top.** Find the first `## [Unreleased]` heading and the next `## [` heading below it. The region between them is the Unreleased block. +- **Compose the new bullet.** Shape: + + ``` + - (#). + ``` + + Summary is derived from the issue title / plan by default; the user can override via the `log "..."` input token (see O4). +- **Decide the operation**: + - If **no version bump** happened in step 2 → **append** the new bullet to the existing Unreleased list. + - If a version bump happened → **promote**: rename the existing Unreleased heading to `## [] - `, **prepend** a fresh empty `## [Unreleased]` section above it, and append the new bullet inside the just-closed version block (or leave it empty if Unreleased had nothing new — decide per O5). +- **Confirm before writing.** Show the proposed diff of `HISTORY.md` to the user and ask once (unless the user passed `yolo`-style trust, which is not in scope here). This matches how `/issue-close` treats unrelated-changes staging. +- **Stage `HISTORY.md`** in the same commit as the rest of the issue's changes (and alongside `pyproject.toml` / `uv.lock` when the version bumped). + +### Input tokens added to `/issue-close` + +Extending the existing input grammar (`bump`, `patch`, `minor`, `major`, free text): + +| Token | Effect | +|---|---| +| `nohistory` / `skip history` | Skip step 3 entirely for this run. | +| `log "..."` / `note "..."` | Use the quoted text verbatim as the bullet summary. | +| `history from issue` | Use the issue title (current default). | + +`/issue-yolo` forwards these tokens too, same as it forwards `bump` / `patch` / etc. + +## Files to touch + +Templates (source of truth): + +- [src/issue_flow/templates/commands/issue-close.md.j2](../../src/issue_flow/templates/commands/issue-close.md.j2) — add step 3, extend the Input section with the new tokens. +- [src/issue_flow/templates/commands/issue-yolo.md.j2](../../src/issue_flow/templates/commands/issue-yolo.md.j2) — mention that history tokens are forwarded. +- [src/issue_flow/templates/skills/issueflow_issue_close/SKILL.md.j2](../../src/issue_flow/templates/skills/issueflow_issue_close/SKILL.md.j2) — mirror step 3 wording and point at the new skill. +- **New:** `src/issue_flow/templates/skills/issueflow_history_update/SKILL.md.j2` — the mechanical "parse Unreleased / promote / append" playbook (the heavy lifting). +- [src/issue_flow/templates/docs/cursor-issue-workflow.md.j2](../../src/issue_flow/templates/docs/cursor-issue-workflow.md.j2) — update `/issue-close` section with the new step and the `nohistory` / `log` tokens; add the new skill to the Agent Skills table. + +Python (pipe the new knobs through): + +- [src/issue_flow/config.py](../../src/issue_flow/config.py) — add `history_file: str` (env `ISSUEFLOW_HISTORY_FILE`, default `HISTORY.md`); expose in `template_context`. +- [src/issue_flow/init.py](../../src/issue_flow/init.py) — add `ISSUEFLOW_HISTORY_FILE` to `_DOTENV_KEYS` so `init` scaffolds a commented hint. +- [src/issue_flow/templating.py](../../src/issue_flow/templating.py) — register the new skill in `TEMPLATE_MANIFEST`. + +Project docs (this repo): + +- [README.md](../../README.md) — add the new skill row and the `ISSUEFLOW_HISTORY_FILE` config row; extend the `/issue-close` bullet with "updates `HISTORY.md`". +- [HISTORY.md](../../HISTORY.md) — add the Unreleased bullet for #15 itself (dog-fooding the feature, added in the `/issue-close` step for this very issue). + +Tests: + +- [tests/test_config.py](../../tests/test_config.py) — cover `history_file` default + override via env. +- [tests/test_templating.py](../../tests/test_templating.py) — assert the new skill path appears in `TEMPLATE_MANIFEST` and that the template renders with a `{{ history_file }}` reference. +- [tests/test_init.py](../../tests/test_init.py) — assert the new `.env` key is scaffolded. + +## Test strategy + +- `uv run pytest` — existing suite plus the three new cases above. +- `uv run ruff check src/ tests/` — lint stays clean. +- Manual smoke: + 1. `uv run python scripts/update_issueflow_setup.py` in this repo, confirm the generated `.cursor/commands/issue-close.md`, `.cursor/skills/issueflow-issue-close/SKILL.md`, and new `.cursor/skills/issueflow-history-update/SKILL.md` match expectations. + 2. In a scratch project without `HISTORY.md`: run the updated `/issue-close` flow manually against these skill files and confirm it prints "no HISTORY.md — skipping" and still commits/pushes. + 3. In this repo: run `/issue-close` (no bump) for issue #15 itself and verify a bullet like `- /issue-close now updates HISTORY.md … (#15).` lands in `[Unreleased]`. + +## Open questions + +Please answer these before `/issue-start`; a couple have straightforward defaults if you don't care. + +- **O1. Extend `/issue-close` vs new `/issue-log` command.** I'm recommending **extend `/issue-close`** (see rationale above). Any reason to prefer a separate command? (Default: extend.) +- **O2. Default behavior without explicit opt-in.** Two options: + - (a) **Opt-out by default** — `/issue-close` always proposes a HISTORY entry (with a confirm prompt), and `nohistory` skips it. Simpler, ensures coverage. + - (b) **Opt-in by default** — only run when the user passes `log` / `history` / `bump …`. Less intrusive, but easier to forget. + - Default if you don't say: (a) opt-out with a confirm prompt (same prompt style as the "unrelated changes" check already in `/issue-close`). +- **O3. Configurable filename.** Add `ISSUEFLOW_HISTORY_FILE` env (default `HISTORY.md`)? I recommend **yes** — some projects use `CHANGELOG.md`. (Default: yes, add it.) +- **O4. Summary source.** For the bullet text, default to **the GitHub issue title** (e.g. "`- update HISTORY.md when making changes worthy of mentioning (#15).`"), overridable via `log "..."`. Alternative: derive from the commit subject. I recommend **issue title + manual override**. (Default: issue title.) +- **O5. Empty Unreleased at bump time.** If `[Unreleased]` is empty when the user runs `/issue-close bump …`, should the step still create a `[x.y.z]` section (with the new bullet inside it) or warn and stop? I recommend **create the version section anyway** with the new bullet; a version bump implies a release, and we always know at least this issue's bullet. (Default: create it.) +- **O6. Scaffold a starter `HISTORY.md`?** When `issue-flow init` runs in a project without one, should we generate a small Keep-a-Changelog starter file? I recommend **no** (out of scope, keeps `init` boring and non-destructive — matches how we treat `.env`). Opt-out stays simple: no HISTORY.md = no history step. (Default: no, do not scaffold.) + +## Scope check + +This fits comfortably in a single PR: one new skill, one new config knob, additions to one slash command, and doc/test updates. No refactor, no cross-cutting change. No need to split. diff --git a/.issueflows/03-solved-issues/issue15_status.md b/.issueflows/03-solved-issues/issue15_status.md new file mode 100644 index 0000000..9d6bfd4 --- /dev/null +++ b/.issueflows/03-solved-issues/issue15_status.md @@ -0,0 +1,43 @@ +# Status for issue #15: update HISTORY.md when making changes worthy of mentioning + +- [x] Done + +Plan: [issue15_plan.md](./issue15_plan.md) · Original: [issue15_original.md](./issue15_original.md) · GitHub: + +## Done so far + +- **Design decision:** extend `/issue-close` with a new "update `HISTORY.md`" step (plan recommendation O1). No new slash command added. +- **New skill** `issueflow-history-update` describing both operation modes: + - append a bullet to `## [Unreleased]` when no version bump happened + - promote `## [Unreleased]` to `## [] - ` (with a fresh empty `## [Unreleased]` above) when a bump happened + - graceful skip when `HISTORY.md` is missing (never auto-created) + - single confirm-before-write using a diff preview; `nohistory` opts out entirely +- **New config knob** `ISSUEFLOW_HISTORY_FILE` (default `HISTORY.md`) wired through `Settings.history_file`, `Settings.template_context`, and the `.env` scaffold in `run_init`. +- **Templates updated** (all source-of-truth): + - [`commands/issue-close.md.j2`](../../src/issue_flow/templates/commands/issue-close.md.j2) — new step 3, renumbered remaining steps, and new `nohistory` / `log "..."` / `note "..."` input tokens. Commit step now stages `HISTORY.md` too. + - [`commands/issue-yolo.md.j2`](../../src/issue_flow/templates/commands/issue-yolo.md.j2) — forwards the new tokens. + - [`skills/issueflow_issue_close/SKILL.md.j2`](../../src/issue_flow/templates/skills/issueflow_issue_close/SKILL.md.j2) — mirrors step 3 and the new input tokens. + - [`skills/issueflow_history_update/SKILL.md.j2`](../../src/issue_flow/templates/skills/issueflow_history_update/SKILL.md.j2) — new skill. + - [`docs/cursor-issue-workflow.md.j2`](../../src/issue_flow/templates/docs/cursor-issue-workflow.md.j2) — `/issue-close` role row, skills table (new skill row), and `/issue-close` section. +- **Python updated:** + - [`src/issue_flow/config.py`](../../src/issue_flow/config.py) — added `history_file: str` via `ISSUEFLOW_HISTORY_FILE`; exposed in `template_context`. + - [`src/issue_flow/init.py`](../../src/issue_flow/init.py) — added `("ISSUEFLOW_HISTORY_FILE", "HISTORY.md")` to `_DOTENV_KEYS`. + - [`src/issue_flow/templating.py`](../../src/issue_flow/templating.py) — registered the new skill template in `TEMPLATE_MANIFEST` (now 20 entries). +- **Regenerated** `.cursor/commands/*`, `.cursor/skills/**`, and `docs/cursor-issue-workflow.md` via `uv run python scripts/update_issueflow_setup.py`. +- **README.md** — added the new skill row, new config-variable row for `ISSUEFLOW_HISTORY_FILE`, and a richer `/issue-close` bullet mentioning the changelog update. +- **Tests added / updated** (49 total now pass, up from 45): + - `test_config.py` — assert `history_file == "HISTORY.md"` default, `history_file` in the template-context keys, and env override via `ISSUEFLOW_HISTORY_FILE=CHANGELOG.md`. + - `test_templating.py` — added `history_file` to shared contexts, bumped manifest count from 19 → 20, and added tests for: the `/issue-close` history step (tokens + skill reference), the history-update skill's two modes + missing-file fallback + never-create invariant, `{{ history_file }}` substitution with a custom filename, and `/issue-yolo` token forwarding. + - `test_init.py` — assert `# ISSUEFLOW_HISTORY_FILE=HISTORY.md` lands in both the fresh `.env` and the append path, include it in the custom-`.env`/force scenario, add `issueflow-history-update` to the skills scaffold check, and add a new test covering `issue-close.md` documenting the HISTORY step. +- **Lint clean:** `uv run ruff check src/ tests/` — all checks passed. + +## Remaining work + +- **`HISTORY.md` bullet for #15 itself** is deliberately deferred to `/issue-close`, which will dog-food the new step (per the plan's Files-to-touch note). Running `/issue-close` should append something like `- /issue-close now updates HISTORY.md when landing an issue, with promote-on-bump support (#15).` to the `[Unreleased]` section. + +No open blockers. Ready for `/issue-close`. + +## Notes on plan deltas + +- None. All defaults (O1–O6) from the plan were accepted and implemented as described. +- Scope ended up matching the plan exactly: one new skill, one new config knob, one new step in `/issue-close`, matching doc / test / readme updates. diff --git a/HISTORY.md b/HISTORY.md index 04c4242..5a96f81 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -19,6 +19,7 @@ than the GitHub release notes they link to. - `/issue-cleanup` now owns post-merge branch hygiene (detect merge via `gh pr view`, consolidated single confirm, `git branch -d` on every local branch reachable from `origin/` — never `-D`). This logic was removed from `/issue-close` step 7 (**breaking**); `/issue-close` now points users at `/issue-cleanup` after the PR merges. - `/issue-yolo` chains `init → plan → start → close` for small, low-risk issues with up-front safeguards (refuses on default branch, refuses with dirty unrelated changes, requires `uv run pytest` to pass, single consolidated confirm). Never chains `/issue-cleanup`. - **Quick start `/iflow` smart dispatcher.** Inspects the focus issue (a branch-derived `N` from an `-` branch is authoritative — it wins even when `issue_*` files don't exist yet or unrelated groups sit in `.issueflows/01-current-issues/`; otherwise falls back to the single group in `01-`, else asks) and dispatches to `/issue-init`, `/issue-plan`, `/issue-start`, or `/issue-close` based on which files exist and whether the status file is marked `- [x] Done`. Warns up front when the focus issue is archived under `02-partly-solved-issues/` or `03-solved-issues/` so the user knows `/issue-init`'s archived-issue guard will ask for an explicit re-open confirmation. Forwards trailing args verbatim. Never auto-dispatches to `/issue-pause`, `/issue-cleanup`, or `/issue-yolo` — those stay explicit. +- **`/issue-close` now updates `HISTORY.md` (#15).** New step between the version bump and issue-folder housekeeping, driven by a new `issueflow-history-update` Agent Skill. Appends a bullet to `## [Unreleased]` on a regular close, and on `/issue-close bump ` promotes `## [Unreleased]` to `## [] - ` with a fresh empty `## [Unreleased]` above it. Opt-out via `nohistory`; override the bullet summary with `log "..."`. New config knob `ISSUEFLOW_HISTORY_FILE` (default `HISTORY.md`) lets projects point at `CHANGELOG.md` or similar. ## [0.2.2] - 2026-04-17 diff --git a/README.md b/README.md index 1330ef7..505619f 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ your-project/ issueflow-issue-cleanup/SKILL.md issueflow-issue-yolo/SKILL.md issueflow-version-bump/SKILL.md + issueflow-history-update/SKILL.md rules/ issueflow-rules.mdc # Always-on Cursor rule for the workflow docs/ @@ -46,7 +47,7 @@ The Cursor slash commands give agents a repeatable flow. The linear path is: 1. `/issue-init 42` — pulls GitHub issue #42 into `.issueflows/01-current-issues/` and archives older issues. 2. `/issue-plan` — drafts `issue_plan.md` (Goal / Constraints / Approach / Files to touch / Test strategy / Open questions) and stops for your confirmation. 3. `/issue-start` — reads the confirmed plan and implements it. If no plan file exists, it offers to run `/issue-plan` first, proceed without a plan, or abort. -4. `/issue-close` — runs tests, optionally bumps version with `uv version --bump`, updates status files, commits, pushes, and opens a PR. +4. `/issue-close` — runs tests, optionally bumps version with `uv version --bump`, appends a `HISTORY.md` entry (or promotes `[Unreleased]` to a new release section on a bump), updates status files, commits, pushes, and opens a PR. 5. `/issue-cleanup` — after the PR merges, switches to the default branch, fast-forwards, prunes, and deletes the merged local branch. Plus a few off-path commands: @@ -55,7 +56,7 @@ Plus a few off-path commands: - `/issue-pause` — park the current issue in `02-partly-solved-issues/` with a **Remaining work** note; optional WIP commit + switch back to the default branch. - `/issue-yolo` — all-in-one chain (`init → plan → start → close`) for small, low-risk issues, with up-front safeguards (refuses on the default branch, refuses with dirty unrelated changes, requires passing tests, single consolidated confirm). -The matching **Agent Skills** (under `.cursor/skills/`) carry the same workflows for on-demand use with `/issueflow-iflow`, `/issueflow-issue-init`, `/issueflow-issue-plan`, `/issueflow-issue-start`, `/issueflow-issue-pause`, `/issueflow-issue-close`, `/issueflow-issue-cleanup`, `/issueflow-issue-yolo`, or `@issueflow-version-bump` when you need only the bump steps (see [Cursor Agent Skills](https://cursor.com/docs/context/skills)). +The matching **Agent Skills** (under `.cursor/skills/`) carry the same workflows for on-demand use with `/issueflow-iflow`, `/issueflow-issue-init`, `/issueflow-issue-plan`, `/issueflow-issue-start`, `/issueflow-issue-pause`, `/issueflow-issue-close`, `/issueflow-issue-cleanup`, `/issueflow-issue-yolo`, `@issueflow-version-bump` when you need only the bump steps, or `@issueflow-history-update` when you need only the changelog update (see [Cursor Agent Skills](https://cursor.com/docs/context/skills)). ## Installation @@ -121,6 +122,7 @@ issue-flow reads a `.env` file from the project root (via python-dotenv). The fo | `ISSUEFLOW_DIR` | `.issueflows` | Name of the issue-tracking directory. | | `ISSUEFLOW_AGENT_DIR` | `.cursor` | Name of the agent/IDE config directory (currently `.cursor`). | | `ISSUEFLOW_DOCS_DIR` | `docs` | Where to write the workflow documentation file. | +| `ISSUEFLOW_HISTORY_FILE` | `HISTORY.md` | Changelog file that `/issue-close` updates (set to e.g. `CHANGELOG.md` for different conventions). | ## Development diff --git a/docs/cursor-issue-workflow.md b/docs/cursor-issue-workflow.md index fee02f8..f4cc643 100644 --- a/docs/cursor-issue-workflow.md +++ b/docs/cursor-issue-workflow.md @@ -11,7 +11,7 @@ This repo uses eight Cursor **slash commands** under `.cursor/commands/` that li | `/issue-plan` | `issue-plan.md` | Write a structured `issue_plan.md` and get explicit user confirmation before any code is touched. | | `/issue-start` | `issue-start.md` | Implement the confirmed plan (no planning step of its own any more). | | `/issue-pause` | `issue-pause.md` | Park work safely: update status, move the issue group to `02-partly-solved-issues/`, optional WIP commit and branch switch. | -| `/issue-close` | `issue-close.md` | Finish: tests, optional semver bump (`uv version --bump …`), issue-folder housekeeping, commit, push, PR. | +| `/issue-close` | `issue-close.md` | Finish: tests, optional semver bump (`uv version --bump …`), `HISTORY.md` update, issue-folder housekeeping, commit, push, PR. | | `/issue-cleanup` | `issue-cleanup.md` | Post-merge hygiene: switch to default, `git pull --ff-only`, `git fetch --prune`, delete merged local branches (single consolidated confirm). | | `/issue-yolo` | `issue-yolo.md` | All-in-one for small, low-risk issues: chains `init → plan → start → close` with up-front safeguards and a single confirmation. | @@ -32,6 +32,7 @@ This repo uses eight Cursor **slash commands** under `.cursor/commands/` that li | `issueflow-issue-cleanup` | `/issueflow-issue-cleanup` | Post-merge cleanup (single consolidated confirm, never `-D`). | | `issueflow-issue-yolo` | `/issueflow-issue-yolo` | Chain `init → plan → start → close` with safeguards. | | `issueflow-version-bump` | `@issueflow-version-bump` (often used from `/issue-close`) | Bump `[project]` version in `pyproject.toml` via `uv version --bump patch|minor|major`. | +| `issueflow-history-update` | `@issueflow-history-update` (used from `/issue-close`) | Append an entry to `## [Unreleased]` in `HISTORY.md`, or promote it to a new `## [x.y.z] - YYYY-MM-DD` release section when a version bump happened. | Each skill sets `disable-model-invocation: true` so it is included when you **explicitly** invoke it, not on every chat. See [Agent Skills](https://cursor.com/docs/context/skills) in the Cursor docs. @@ -151,6 +152,8 @@ All the commands that touch git also run a short **branch-status preflight**: `g - `/issue-close bump minor` — bump **minor**. - `/issue-close bump major` or `/issue-close major` — bump **major**. - Free text that clearly describes the bump level — the assistant infers patch vs minor vs major. +- `/issue-close nohistory` (or `skip history`) — skip the `HISTORY.md` update step for this run. +- `/issue-close log "one-line summary"` (or `note "..."`) — override the `HISTORY.md` bullet summary instead of using the GitHub issue title. The bump runs **after** tests and **before** issue-folder moves and **before** commit / push / PR so the PR includes the new version. If `pyproject.toml` has no bumpable version, the assistant skips the bump and continues. @@ -158,11 +161,12 @@ The bump runs **after** tests and **before** issue-folder moves and **before** c 1. **Sanity check** — e.g. `uv run pytest`, review the diff. 2. **Optional version bump** — if requested, follow `.cursor/skills/issueflow-version-bump/SKILL.md` and run `uv version --bump …` from the project root. -3. **Issue folders** — update status markdown; use `- [x] Done` only when fully resolved. Move completed issue files from `.issueflows/01-current-issues/` to `.issueflows/03-solved-issues/`, or partly done work to `.issueflows/02-partly-solved-issues/`. -4. **Commit** — focused staging and a clear message (include `pyproject.toml` / `uv.lock` if the bump changed them). Sync with the default branch using `git pull --ff-only`. -5. **Push** — to your usual remote (e.g. `origin`). -6. **Pull request** — open against the default branch; link the GitHub issue (`Closes #n` / `Refs #n`). -7. **After review** — remind you the working copy is still on the issue branch; once the PR merges, run `/issue-cleanup` for the post-merge tidy-up. +3. **Update `HISTORY.md`** — unless `nohistory` was passed, follow `.cursor/skills/issueflow-history-update/SKILL.md`. Append a bullet to `## [Unreleased]` (no bump) or promote `## [Unreleased]` to `## [] - ` and open a fresh empty `## [Unreleased]` above it (with bump). The assistant shows the diff and asks for a single confirm before writing. If `HISTORY.md` is missing at the project root, the step is skipped with a note — never auto-created. +4. **Issue folders** — update status markdown; use `- [x] Done` only when fully resolved. Move completed issue files from `.issueflows/01-current-issues/` to `.issueflows/03-solved-issues/`, or partly done work to `.issueflows/02-partly-solved-issues/`. +5. **Commit** — focused staging and a clear message (include `pyproject.toml` / `uv.lock` if the bump changed them, and `HISTORY.md` when step 3 updated it). Sync with the default branch using `git pull --ff-only`. +6. **Push** — to your usual remote (e.g. `origin`). +7. **Pull request** — open against the default branch; link the GitHub issue (`Closes #n` / `Refs #n`). +8. **After review** — remind you the working copy is still on the issue branch; once the PR merges, run `/issue-cleanup` for the post-merge tidy-up. **Result:** Commit, push, PR link. No branches are deleted from `/issue-close` itself. diff --git a/src/issue_flow/config.py b/src/issue_flow/config.py index 458060b..d804d5f 100644 --- a/src/issue_flow/config.py +++ b/src/issue_flow/config.py @@ -31,6 +31,9 @@ class Settings: docs_dir: str = field( default_factory=lambda: os.getenv("ISSUEFLOW_DOCS_DIR", "docs") ) + history_file: str = field( + default_factory=lambda: os.getenv("ISSUEFLOW_HISTORY_FILE", "HISTORY.md") + ) # Give a deprecation warning if the user is using the old ISSUEFLOW_CURSOR_DIR environment variable if os.getenv("ISSUEFLOW_CURSOR_DIR"): @@ -58,6 +61,7 @@ def template_context(self, project_root: Path) -> dict[str, str]: "issueflows_dir": self.issueflows_dir, "agent_dir": self.agent_dir, "docs_dir": self.docs_dir, + "history_file": self.history_file, "tools_folder": self.tools_folder, "current_issues_folder": self.current_issues_folder, "partly_solved_folder": self.partly_solved_folder, diff --git a/src/issue_flow/init.py b/src/issue_flow/init.py index 4e58d7e..c2f5f28 100644 --- a/src/issue_flow/init.py +++ b/src/issue_flow/init.py @@ -21,6 +21,7 @@ ("ISSUEFLOW_DIR", ".issueflows"), ("ISSUEFLOW_AGENT_DIR", ".cursor"), ("ISSUEFLOW_DOCS_DIR", "docs"), + ("ISSUEFLOW_HISTORY_FILE", "HISTORY.md"), ) _DOTENV_SECTION_HEADER = "# --- issue-flow: optional environment variables ---\n" diff --git a/src/issue_flow/templates/commands/issue-close.md.j2 b/src/issue_flow/templates/commands/issue-close.md.j2 index 8d19dc4..4b1f342 100644 --- a/src/issue_flow/templates/commands/issue-close.md.j2 +++ b/src/issue_flow/templates/commands/issue-close.md.j2 @@ -11,6 +11,8 @@ Optional text after the command (same line). Examples: - **`bump minor`** or **`minor`** — bump **minor**. - **`bump major`** or **`major`** — bump **major**. - **Free text** — if it clearly asks to release or bump the version, infer `patch`, `minor`, or `major` from wording (e.g. "bugfix release" → patch); if unclear, ask once. +- **`nohistory`** or **`skip history`** — skip the `{{ history_file }}` update step (step 3) for this run. +- **`log "..."`** or **`note "..."`** — use the quoted text verbatim as the `{{ history_file }}` bullet summary (otherwise the GitHub issue title is used). Other optional notes still apply: branch name, PR title, draft PR, skip issue doc update, commit all changes, etc. @@ -26,24 +28,32 @@ Other optional notes still apply: branch name, PR title, draft PR, skip issue do - Do this **before** updating issue markdown and **before** commit / push / PR so the new version is in the tree that gets merged. - If the project has no bumpable `pyproject.toml` version, skip and say so; continue with the remaining steps. -3. **Issue tracking in the repo** (see project rules under `{{ issueflows_dir }}/{{ current_issues_folder }}`) +3. **Update `{{ history_file }}`** (opt-out via `nohistory`) + - Read `{{ agent_dir }}/skills/issueflow-history-update/SKILL.md` and follow it. + - Default summary for the new bullet is the GitHub issue title; override with `log "..."` / `note "..."` from the command input. + - If step 2 did **not** bump the version, append a bullet to the existing `## [Unreleased]` section: `- . (#)`. + - If step 2 **did** bump the version, promote `## [Unreleased]` to `## [] - `, prepend a fresh empty `## [Unreleased]` above it, and put the new bullet inside the just-closed release section. + - If `{{ history_file }}` does not exist at the project root, skip this step with a short note and continue. Never create it here. + - Show the proposed diff and confirm once before writing. If declined, leave `{{ history_file }}` untouched and move on. + +4. **Issue tracking in the repo** (see project rules under `{{ issueflows_dir }}/{{ current_issues_folder }}`) - Update the status file for this issue: clear checklist, remaining work, and use `- [x] Done` only when fully resolved. - If the issue is fully resolved, move its markdown files from `{{ issueflows_dir }}/{{ current_issues_folder }}` to `{{ issueflows_dir }}/{{ solved_folder }}`. If partially resolved, move to `{{ issueflows_dir }}/{{ partly_solved_folder }}`. -4. **Commit and fix merge conflicts** +5. **Commit and fix merge conflicts** - Before staging, run `git status` to list all modified/untracked files. If any changes are **not relevant** to this issue, tell the user which ones and ask whether to include them in this commit or leave them for later. Do not silently drop or include unrelated changes. - - Unless told to commit all, stage the right files (avoid unrelated changes). Include `pyproject.toml` (and `uv.lock` if it changed) when a version bump ran. + - Unless told to commit all, stage the right files (avoid unrelated changes). Include `pyproject.toml` (and `uv.lock` if it changed) when a version bump ran. Include `{{ history_file }}` when step 3 updated it. - Write a commit message that states what changed and why in normal sentences. - Sync with the default branch before pushing: run `git fetch --prune` then `git pull --ff-only` from the default branch (e.g. `main`) merged into the issue branch (or rebase, per project preference). Use `--ff-only` so unrelated work never gets merged in silently; if it refuses, stop and ask how to reconcile. Check for and fix merge conflicts. -5. **Push** +6. **Push** - Push your branch to `origin` (or the remote you use). -6. **Pull request** +7. **Pull request** - Open a PR against the default branch (e.g. `main`). - Describe the change, how to test it, and link the GitHub issue (e.g. `Closes #123` or `Refs #123` in the PR body). -7. **After review** +8. **After review** - Address feedback, push updates, and merge when approved and CI is green. - Remind the user that the working copy is still on the issue branch, not the default. Suggest `git switch ` before starting unrelated work so new changes don't accidentally land on the issue branch. - Once the PR is merged, run **`/issue-cleanup`** to switch back to the default branch, `git pull --ff-only`, `git fetch --prune`, and delete local branches whose commits are already in the default branch (single consolidated confirm). `/issue-close` no longer does post-merge cleanup itself. diff --git a/src/issue_flow/templates/commands/issue-yolo.md.j2 b/src/issue_flow/templates/commands/issue-yolo.md.j2 index 12825a1..cd41d04 100644 --- a/src/issue_flow/templates/commands/issue-yolo.md.j2 +++ b/src/issue_flow/templates/commands/issue-yolo.md.j2 @@ -9,6 +9,8 @@ For anything non-trivial, use the individual commands so you get confirmation ch Same as `/issue-init` (issue number, URL, or empty to infer from the branch). Optional extra tokens are forwarded to the downstream commands: - `bump` / `patch` / `minor` / `major` — forwarded to `/issue-close` for the version bump. +- `nohistory` / `skip history` — forwarded to `/issue-close` to skip the `{{ history_file }}` update step. +- `log "..."` / `note "..."` — forwarded to `/issue-close` as the `{{ history_file }}` bullet summary. - `draft` — open a draft PR in `/issue-close`. - Free-form notes — used as the plan/commit context. diff --git a/src/issue_flow/templates/docs/cursor-issue-workflow.md.j2 b/src/issue_flow/templates/docs/cursor-issue-workflow.md.j2 index 020a477..fc358f0 100644 --- a/src/issue_flow/templates/docs/cursor-issue-workflow.md.j2 +++ b/src/issue_flow/templates/docs/cursor-issue-workflow.md.j2 @@ -11,7 +11,7 @@ This repo uses eight Cursor **slash commands** under `{{ agent_dir }}/commands/` | `/issue-plan` | `issue-plan.md` | Write a structured `issue_plan.md` and get explicit user confirmation before any code is touched. | | `/issue-start` | `issue-start.md` | Implement the confirmed plan (no planning step of its own any more). | | `/issue-pause` | `issue-pause.md` | Park work safely: update status, move the issue group to `{{ partly_solved_folder }}/`, optional WIP commit and branch switch. | -| `/issue-close` | `issue-close.md` | Finish: tests, optional semver bump (`uv version --bump …`), issue-folder housekeeping, commit, push, PR. | +| `/issue-close` | `issue-close.md` | Finish: tests, optional semver bump (`uv version --bump …`), `{{ history_file }}` update, issue-folder housekeeping, commit, push, PR. | | `/issue-cleanup` | `issue-cleanup.md` | Post-merge hygiene: switch to default, `git pull --ff-only`, `git fetch --prune`, delete merged local branches (single consolidated confirm). | | `/issue-yolo` | `issue-yolo.md` | All-in-one for small, low-risk issues: chains `init → plan → start → close` with up-front safeguards and a single confirmation. | @@ -32,6 +32,7 @@ This repo uses eight Cursor **slash commands** under `{{ agent_dir }}/commands/` | `issueflow-issue-cleanup` | `/issueflow-issue-cleanup` | Post-merge cleanup (single consolidated confirm, never `-D`). | | `issueflow-issue-yolo` | `/issueflow-issue-yolo` | Chain `init → plan → start → close` with safeguards. | | `issueflow-version-bump` | `@issueflow-version-bump` (often used from `/issue-close`) | Bump `[project]` version in `pyproject.toml` via `uv version --bump patch|minor|major`. | +| `issueflow-history-update` | `@issueflow-history-update` (used from `/issue-close`) | Append an entry to `## [Unreleased]` in `{{ history_file }}`, or promote it to a new `## [x.y.z] - YYYY-MM-DD` release section when a version bump happened. | Each skill sets `disable-model-invocation: true` so it is included when you **explicitly** invoke it, not on every chat. See [Agent Skills](https://cursor.com/docs/context/skills) in the Cursor docs. @@ -151,6 +152,8 @@ All the commands that touch git also run a short **branch-status preflight**: `g - `/issue-close bump minor` — bump **minor**. - `/issue-close bump major` or `/issue-close major` — bump **major**. - Free text that clearly describes the bump level — the assistant infers patch vs minor vs major. +- `/issue-close nohistory` (or `skip history`) — skip the `{{ history_file }}` update step for this run. +- `/issue-close log "one-line summary"` (or `note "..."`) — override the `{{ history_file }}` bullet summary instead of using the GitHub issue title. The bump runs **after** tests and **before** issue-folder moves and **before** commit / push / PR so the PR includes the new version. If `pyproject.toml` has no bumpable version, the assistant skips the bump and continues. @@ -158,11 +161,12 @@ The bump runs **after** tests and **before** issue-folder moves and **before** c 1. **Sanity check** — e.g. `uv run pytest`, review the diff. 2. **Optional version bump** — if requested, follow `{{ agent_dir }}/skills/issueflow-version-bump/SKILL.md` and run `uv version --bump …` from the project root. -3. **Issue folders** — update status markdown; use `- [x] Done` only when fully resolved. Move completed issue files from `{{ issueflows_dir }}/{{ current_issues_folder }}/` to `{{ issueflows_dir }}/{{ solved_folder }}/`, or partly done work to `{{ issueflows_dir }}/{{ partly_solved_folder }}/`. -4. **Commit** — focused staging and a clear message (include `pyproject.toml` / `uv.lock` if the bump changed them). Sync with the default branch using `git pull --ff-only`. -5. **Push** — to your usual remote (e.g. `origin`). -6. **Pull request** — open against the default branch; link the GitHub issue (`Closes #n` / `Refs #n`). -7. **After review** — remind you the working copy is still on the issue branch; once the PR merges, run `/issue-cleanup` for the post-merge tidy-up. +3. **Update `{{ history_file }}`** — unless `nohistory` was passed, follow `{{ agent_dir }}/skills/issueflow-history-update/SKILL.md`. Append a bullet to `## [Unreleased]` (no bump) or promote `## [Unreleased]` to `## [] - ` and open a fresh empty `## [Unreleased]` above it (with bump). The assistant shows the diff and asks for a single confirm before writing. If `{{ history_file }}` is missing at the project root, the step is skipped with a note — never auto-created. +4. **Issue folders** — update status markdown; use `- [x] Done` only when fully resolved. Move completed issue files from `{{ issueflows_dir }}/{{ current_issues_folder }}/` to `{{ issueflows_dir }}/{{ solved_folder }}/`, or partly done work to `{{ issueflows_dir }}/{{ partly_solved_folder }}/`. +5. **Commit** — focused staging and a clear message (include `pyproject.toml` / `uv.lock` if the bump changed them, and `{{ history_file }}` when step 3 updated it). Sync with the default branch using `git pull --ff-only`. +6. **Push** — to your usual remote (e.g. `origin`). +7. **Pull request** — open against the default branch; link the GitHub issue (`Closes #n` / `Refs #n`). +8. **After review** — remind you the working copy is still on the issue branch; once the PR merges, run `/issue-cleanup` for the post-merge tidy-up. **Result:** Commit, push, PR link. No branches are deleted from `/issue-close` itself. diff --git a/src/issue_flow/templates/skills/issueflow_history_update/SKILL.md.j2 b/src/issue_flow/templates/skills/issueflow_history_update/SKILL.md.j2 new file mode 100644 index 0000000..16c6f30 --- /dev/null +++ b/src/issue_flow/templates/skills/issueflow_history_update/SKILL.md.j2 @@ -0,0 +1,84 @@ +--- +name: issueflow-history-update +description: >- + Keep HISTORY.md (or equivalent changelog) up to date when landing an issue: + append a bullet to [Unreleased], or promote [Unreleased] to a new [x.y.z] + release section when a version bump happened. Invoked from /issue-close. +disable-model-invocation: true +--- + +# issue-flow — history update + +Use this skill to update the project's changelog file (default **`{{ history_file }}`**, overridable via `ISSUEFLOW_HISTORY_FILE` in `.env`) as part of `/issue-close`. It never runs on its own schedule; it is driven by the "update HISTORY" step in `{{ agent_dir }}/commands/issue-close.md`. + +## When to use + +- `/issue-close` is landing an issue and the project has a changelog file in the repo root. +- The user did **not** pass `nohistory` / `skip history` on the command line. + +## Preconditions + +1. The changelog file (`{{ history_file }}`) exists at the **project root**. If it does not, **skip** this step, print "no `{{ history_file }}` — skipping changelog update" and continue the rest of `/issue-close`. Never create the file from this skill. +2. The file is in **Keep a Changelog** shape: a top-level `## [Unreleased]` heading, with released versions below as `## [x.y.z] - YYYY-MM-DD` headings. If the shape does not match, **stop and report the mismatch** instead of guessing — let the user fix the file or pass `nohistory`. +3. UTF-8 read/write with explicit encoding. + +## Inputs from `/issue-close` + +| From | Used for | +|---|---| +| Issue number `N` | Reference suffix on the new bullet, e.g. `(#42)`. | +| Issue title (from `{{ issueflows_dir }}/{{ current_issues_folder }}/issue_original.md`) | Default bullet summary. | +| `log "..."` / `note "..."` input token | Override the bullet summary verbatim. | +| Version-bump outcome (from step 2 of `/issue-close`) | Decides **append** vs **promote** (see below). | + +## Operation modes + +### A. No version bump — append to `[Unreleased]` + +1. Read `{{ history_file }}`. Locate the first `## [Unreleased]` heading. The block ends at the next `## [` heading (or EOF). +2. Compose the new bullet: + + ``` + - . (#) + ``` + + Summary = `log "..."` override if provided, else the issue title with sentence case, trailing period trimmed before the `.` we add. +3. Append the bullet to the end of the Unreleased bullet list. Preserve existing formatting (blank lines, list markers). Do not reorder existing entries. +4. Show the user the proposed diff of `{{ history_file }}` and confirm once before writing. + +### B. Version bump happened — promote `[Unreleased]` to a new release section + +Only runs when step 2 of `/issue-close` actually changed `pyproject.toml` to a new version `NEW_VERSION`. + +1. Determine `NEW_VERSION` (e.g. read from `pyproject.toml`, or from the `uv version` command output). Determine `TODAY` as `YYYY-MM-DD` in the user's local timezone. +2. Read `{{ history_file }}`. Find `## [Unreleased]`. +3. Compose the new bullet (same shape as mode A). If `[Unreleased]` was empty when the bump happened, still create the new release section with this bullet inside it — a version bump implies a release, and the focus issue's bullet is always meaningful. +4. Rename the existing heading from `## [Unreleased]` to `## [] - ` and add the new bullet at the end of that section's bullet list. +5. Prepend a fresh, empty `## [Unreleased]` section above the just-closed release, with one blank line separating them: + + ```markdown + ## [Unreleased] + + ## [NEW_VERSION] - TODAY + + - …existing bullets from before the promote… + - (#N) + ``` + +6. Show the user the proposed diff and confirm once before writing. + +## Staging + +When `/issue-close` reaches its commit step: + +- Stage `{{ history_file }}` alongside the issue's other changes. +- If a version bump also ran, `{{ history_file }}` is staged in the same commit as `pyproject.toml` (and `uv.lock` if it changed). + +## Constraints + +- Read/write only `{{ history_file }}` at the project root. Do not touch any other file from this skill. +- Never create `{{ history_file }}` from scratch — scaffolding a starter changelog is out of scope for `issue-flow init` / `update`. +- If the user passed `nohistory` (or `skip history`) to `/issue-close`, don't run this skill at all. +- If the confirm prompt in mode A or mode B is declined, leave `{{ history_file }}` untouched and print a short "skipped changelog update" note. The rest of `/issue-close` continues normally. +- Preserve existing formatting conventions (bullet style, sentence case, trailing punctuation). Match the style of the nearest existing entries when in doubt. +- The new bullet's `(#)` suffix is always GitHub issue `#N`, matching the focus issue's number in `{{ issueflows_dir }}/{{ current_issues_folder }}/issue_original.md`. diff --git a/src/issue_flow/templates/skills/issueflow_issue_close/SKILL.md.j2 b/src/issue_flow/templates/skills/issueflow_issue_close/SKILL.md.j2 index d46f1c2..e2ebee6 100644 --- a/src/issue_flow/templates/skills/issueflow_issue_close/SKILL.md.j2 +++ b/src/issue_flow/templates/skills/issueflow_issue_close/SKILL.md.j2 @@ -28,25 +28,32 @@ If the user included text after `/issue-close` that requests a version bump: When a bump applies: read `{{ agent_dir }}/skills/issueflow-version-bump/SKILL.md`, run the bump from the **project root** **after** the sanity check and **before** issue-folder updates and **before** commit / push / PR. +## Changelog update tokens (command input) + +- **`nohistory`** or **`skip history`** → skip step 3 entirely. +- **`log "..."`** or **`note "..."`** → override the bullet summary verbatim. Otherwise the GitHub issue title is used. + ## Instructions 1. **Sanity check** — Run the project test suite (e.g. `uv run pytest`) and any checks the repo relies on. Skim the diff; avoid bundling unrelated changes. 2. **Optional version bump** — If the user asked for a bump (see above), follow `{{ agent_dir }}/skills/issueflow-version-bump/SKILL.md` and run `uv version --bump `. If there is no bumpable `pyproject.toml`, skip and continue. -3. **Issue tracking** — Under `{{ issueflows_dir }}/{{ current_issues_folder }}/`, update the status file: remaining work, checklists, and **`- [x] Done`** only when the issue is fully resolved. If fully resolved, move that issue's markdown files (`issue_*`) to `{{ issueflows_dir }}/{{ solved_folder }}/`. If partially resolved, move to `{{ issueflows_dir }}/{{ partly_solved_folder }}/`. Follow any stricter rules in `{{ agent_dir }}/rules/issueflow-rules.mdc` if present. +3. **Update `{{ history_file }}`** — Unless the user passed `nohistory`, follow `{{ agent_dir }}/skills/issueflow-history-update/SKILL.md`. If step 2 did not bump the version, append a bullet to the `## [Unreleased]` section. If step 2 bumped the version, promote `## [Unreleased]` to `## [] - ` and open a fresh empty `## [Unreleased]` above it. Show the diff and confirm once before writing. Skip with a note if `{{ history_file }}` does not exist at the project root. + +4. **Issue tracking** — Under `{{ issueflows_dir }}/{{ current_issues_folder }}/`, update the status file: remaining work, checklists, and **`- [x] Done`** only when the issue is fully resolved. If fully resolved, move that issue's markdown files (`issue_*`) to `{{ issueflows_dir }}/{{ solved_folder }}/`. If partially resolved, move to `{{ issueflows_dir }}/{{ partly_solved_folder }}/`. Follow any stricter rules in `{{ agent_dir }}/rules/issueflow-rules.mdc` if present. -4. **Commit** — First check `git status`; if there are unrelated uncommitted changes, surface them and ask the user whether to include them — do not auto-include or drop silently. Then stage intentionally (include `pyproject.toml` and `uv.lock` if changed after a bump); write a commit message in full sentences describing what changed and why. +5. **Commit** — First check `git status`; if there are unrelated uncommitted changes, surface them and ask the user whether to include them — do not auto-include or drop silently. Then stage intentionally (include `pyproject.toml` and `uv.lock` if changed after a bump, and `{{ history_file }}` if step 3 updated it); write a commit message in full sentences describing what changed and why. -5. **Branch hygiene before push** — Run `git fetch --prune`, then sync with the default branch using `git pull --ff-only` (rebase or merge per project preference). Use `--ff-only` so unrelated history never gets pulled in silently; if it refuses, stop and ask how to reconcile. Resolve merge conflicts before pushing. +6. **Branch hygiene before push** — Run `git fetch --prune`, then sync with the default branch using `git pull --ff-only` (rebase or merge per project preference). Use `--ff-only` so unrelated history never gets pulled in silently; if it refuses, stop and ask how to reconcile. Resolve merge conflicts before pushing. -6. **Push** — Push to the remote the project uses (typically `origin`). +7. **Push** — Push to the remote the project uses (typically `origin`). -7. **Pull request** — Open (or update) a PR against the default branch. Body should explain the change, how to test, and link the GitHub issue (`Closes #n` / `Refs #n`). +8. **Pull request** — Open (or update) a PR against the default branch. Body should explain the change, how to test, and link the GitHub issue (`Closes #n` / `Refs #n`). -8. **After review** — Remind the user the working copy is still on the issue branch (not the default). Suggest `git switch ` before starting unrelated work. Tell them to run **`/issue-cleanup`** once the PR is merged so the standard post-merge cleanup runs (switch to default, `git pull --ff-only`, `git fetch --prune`, `git branch -d` on merged local branches under a single consolidated confirm). +9. **After review** — Remind the user the working copy is still on the issue branch (not the default). Suggest `git switch ` before starting unrelated work. Tell them to run **`/issue-cleanup`** once the PR is merged so the standard post-merge cleanup runs (switch to default, `git pull --ff-only`, `git fetch --prune`, `git branch -d` on merged local branches under a single consolidated confirm). -9. **Output** — Summarize commit, push result, PR URL, and next step (`/issue-cleanup` after merge, or "blocked on …" if stuck). +10. **Output** — Summarize commit, push result, PR URL, and next step (`/issue-cleanup` after merge, or "blocked on …" if stuck). ## Constraints diff --git a/src/issue_flow/templating.py b/src/issue_flow/templating.py index 08dd0b7..d9b4407 100644 --- a/src/issue_flow/templating.py +++ b/src/issue_flow/templating.py @@ -116,6 +116,10 @@ def render_template(template_name: str, context: dict[str, str]) -> str: "skills/issueflow_version_bump/SKILL.md.j2", "{agent_dir}/skills/issueflow-version-bump/SKILL.md", ), + ( + "skills/issueflow_history_update/SKILL.md.j2", + "{agent_dir}/skills/issueflow-history-update/SKILL.md", + ), ] diff --git a/tests/test_config.py b/tests/test_config.py index 7b44a17..c1dfffa 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -12,6 +12,7 @@ def test_default_settings() -> None: assert settings.issueflows_dir == ".issueflows" assert settings.agent_dir == ".cursor" assert settings.docs_dir == "docs" + assert settings.history_file == "HISTORY.md" def test_issueflows_subdirs() -> None: @@ -31,6 +32,7 @@ def test_template_context_keys(tmp_path: Path) -> None: "issueflows_dir", "agent_dir", "docs_dir", + "history_file", "tools_folder", "current_issues_folder", "partly_solved_folder", @@ -56,3 +58,10 @@ def test_settings_from_env(tmp_path: Path, monkeypatch: "pytest.MonkeyPatch") -> monkeypatch.setenv("ISSUEFLOW_DIR", "custom-dir") settings = Settings() assert settings.issueflows_dir == "custom-dir" + + +def test_history_file_override_from_env(monkeypatch: "pytest.MonkeyPatch") -> None: # noqa: F821 + """ISSUEFLOW_HISTORY_FILE should override the default changelog filename.""" + monkeypatch.setenv("ISSUEFLOW_HISTORY_FILE", "CHANGELOG.md") + settings = Settings() + assert settings.history_file == "CHANGELOG.md" diff --git a/tests/test_init.py b/tests/test_init.py index af1e49c..62b1020 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -17,6 +17,7 @@ def test_init_creates_dotenv_with_commented_keys(tmp_path: Path) -> None: assert "# ISSUEFLOW_DIR=.issueflows" in text assert "# ISSUEFLOW_AGENT_DIR=.cursor" in text assert "# ISSUEFLOW_DOCS_DIR=docs" in text + assert "# ISSUEFLOW_HISTORY_FILE=HISTORY.md" in text def test_init_second_run_skips_dotenv_when_keys_documented(tmp_path: Path) -> None: @@ -42,13 +43,20 @@ def test_init_appends_missing_dotenv_keys(tmp_path: Path) -> None: assert "# ISSUEFLOW_DIR=.issueflows" in text assert "# ISSUEFLOW_AGENT_DIR=.cursor" in text assert "# ISSUEFLOW_DOCS_DIR=docs" in text + assert "# ISSUEFLOW_HISTORY_FILE=HISTORY.md" in text def test_init_force_does_not_wipe_custom_dotenv(tmp_path: Path) -> None: """init --force must not replace an existing .env wholesale.""" run_init(tmp_path) env_file = tmp_path / ".env" - custom = "MY_SECRET=keep-me\n# ISSUEFLOW_DIR=.issueflows\n# ISSUEFLOW_AGENT_DIR=.cursor\n# ISSUEFLOW_DOCS_DIR=docs\n" + custom = ( + "MY_SECRET=keep-me\n" + "# ISSUEFLOW_DIR=.issueflows\n" + "# ISSUEFLOW_AGENT_DIR=.cursor\n" + "# ISSUEFLOW_DOCS_DIR=docs\n" + "# ISSUEFLOW_HISTORY_FILE=HISTORY.md\n" + ) env_file.write_text(custom, encoding="utf-8") run_init(tmp_path, force=True) @@ -103,6 +111,7 @@ def test_init_creates_cursor_skills(tmp_path: Path) -> None: "issueflow-issue-start", "issueflow-issue-close", "issueflow-version-bump", + "issueflow-history-update", ): skill_file = skills / name / "SKILL.md" assert skill_file.is_file(), f"expected {skill_file}" @@ -177,6 +186,18 @@ def test_init_issue_close_documents_version_bump(tmp_path: Path) -> None: assert "issueflow-version-bump" in content +def test_init_issue_close_documents_history_update_step(tmp_path: Path) -> None: + """issue-close.md should describe the HISTORY.md update step and opt-out token.""" + run_init(tmp_path) + content = (tmp_path / ".cursor" / "commands" / "issue-close.md").read_text( + encoding="utf-8" + ) + assert "HISTORY.md" in content + assert "issueflow-history-update" in content + assert "[Unreleased]" in content + assert "nohistory" in content + + def test_init_issue_close_documents_uncommitted_and_branch_reminder( tmp_path: Path, ) -> None: diff --git a/tests/test_templating.py b/tests/test_templating.py index d7ee297..b1f2316 100644 --- a/tests/test_templating.py +++ b/tests/test_templating.py @@ -17,6 +17,7 @@ def test_all_templates_render_without_error() -> None: "issueflows_dir": ".issueflows", "agent_dir": ".cursor", "docs_dir": "docs", + "history_file": "HISTORY.md", "tools_folder": "00-tools", "current_issues_folder": "01-current-issues", "partly_solved_folder": "02-partly-solved-issues", @@ -35,6 +36,7 @@ def test_template_substitution() -> None: "issueflows_dir": "CUSTOM_DIR", "agent_dir": ".cursor", "docs_dir": "docs", + "history_file": "HISTORY.md", "tools_folder": "00-tools", "current_issues_folder": "01-current-issues", "partly_solved_folder": "02-partly-solved-issues", @@ -53,8 +55,8 @@ def test_resolve_output_path() -> None: def test_manifest_entry_count() -> None: - # 8 commands + 1 rule + 1 doc + 9 skills = 19 - assert len(TEMPLATE_MANIFEST) == 19 + # 8 commands + 1 rule + 1 doc + 10 skills = 20 + assert len(TEMPLATE_MANIFEST) == 20 def test_manifest_has_expected_commands_and_skills() -> None: @@ -81,6 +83,7 @@ def test_manifest_has_expected_commands_and_skills() -> None: "issueflow_issue_cleanup", "issueflow_issue_yolo", "issueflow_version_bump", + "issueflow_history_update", ): assert f"skills/{skill}/SKILL.md.j2" in template_names @@ -90,6 +93,7 @@ def _default_context() -> dict[str, str]: "issueflows_dir": ".issueflows", "agent_dir": ".cursor", "docs_dir": "docs", + "history_file": "HISTORY.md", "tools_folder": "00-tools", "current_issues_folder": "01-current-issues", "partly_solved_folder": "02-partly-solved-issues", @@ -208,3 +212,49 @@ def test_issueflow_rules_has_branch_hygiene_section() -> None: assert "Branch hygiene" in rendered assert "git branch -d" in rendered assert "Folder hygiene" in rendered + + +def test_issue_close_describes_history_update_step() -> None: + """/issue-close must describe the HISTORY.md update step and its input tokens.""" + rendered = render_template("commands/issue-close.md.j2", _default_context()) + assert "HISTORY.md" in rendered + assert "[Unreleased]" in rendered + assert "issueflow-history-update" in rendered + # Opt-out token is documented. + assert "nohistory" in rendered + # Override token for the bullet summary is documented. + assert 'log "..."' in rendered or "log " in rendered + + +def test_history_update_skill_documents_both_modes() -> None: + """The history-update skill must describe append-and-promote, plus the missing-file fallback.""" + rendered = render_template( + "skills/issueflow_history_update/SKILL.md.j2", _default_context() + ) + assert "HISTORY.md" in rendered + assert "[Unreleased]" in rendered + # Append mode (no bump) and promote mode (with bump) are both described. + assert "append" in rendered.lower() + assert "promote" in rendered.lower() + # Gracefully skip when the file is missing; never auto-create. + assert "skipping changelog" in rendered.lower() or "skip" in rendered.lower() + assert "Never create" in rendered or "never create" in rendered.lower() + + +def test_history_update_skill_respects_history_file_override() -> None: + """The history-update skill should reference {{ history_file }} so custom filenames work.""" + context = _default_context() + context["history_file"] = "CHANGELOG.md" + rendered = render_template( + "skills/issueflow_history_update/SKILL.md.j2", context + ) + assert "CHANGELOG.md" in rendered + # No leftover Jinja placeholder. + assert "{{ history_file }}" not in rendered + + +def test_issue_yolo_forwards_history_tokens() -> None: + """/issue-yolo must forward the new history-related tokens to /issue-close.""" + rendered = render_template("commands/issue-yolo.md.j2", _default_context()) + assert "nohistory" in rendered + assert "log " in rendered # `log "..."` bullet-summary override