From 14020840fed5a2e4918c4d1bd6feb93aeb23dc64 Mon Sep 17 00:00:00 2001 From: jepegit Date: Sun, 19 Apr 2026 16:56:27 +0200 Subject: [PATCH] Add branch and folder hygiene to issue-flow commands (#31) Issue #31 reported two recurring pain points: local issue branches end up "several commits ahead of main" after squash-merged PRs, and .issueflows/01-current-issues/ accumulates stale issue files. Extend the three existing slash commands (and their matching skills) so the assistant helps with both without introducing new commands: - /issue-start now runs a non-destructive branch-status preflight (fetch --prune, current branch, ahead/behind vs default, stale-branch warning) and then auto-moves every issueNN_* group in 01-current-issues that is NOT the focus issue to 02-partly-solved-issues or 03-solved-issues based on whether a status file contains - [x] Done. - /issue-close replaces the old "branch reminder" with an explicit post-merge cleanup: detect merge via gh pr view, offer git switch && git pull --ff-only && git fetch --prune, then ask once (one consolidated yes/no) before git branch -d on every local branch already reachable from the default (squash-merged ones included). -D is never used automatically. Step 4 clarifies syncing via git pull --ff-only so unrelated history cannot sneak in silently. - /issue-init gains a branch-status preflight paragraph and an archived-issue guard: re-opening an issue already sitting in 02-/03- requires an extra explicit confirmation. Also update the workflow doc with a new "Branch and folder hygiene" section, remove a duplicated "Agent Skills (optional)" block, and extend the workspace rules with "Branch hygiene" and "Folder hygiene" subsections. Re-rendered the project's own .cursor/ and docs/ scaffold from the updated templates. Added regression assertions in tests/test_templating.py for the new headings. Full suite: 35 passed. Closes #31 Made-with: Cursor --- .cursor/commands/issue-close.md | 14 ++++-- .cursor/commands/issue-init.md | 6 +++ .cursor/commands/issue-start.md | 17 +++++++ .cursor/rules/issueflow-rules.mdc | 14 ++++++ .cursor/skills/issueflow-issue-close/SKILL.md | 8 ++-- .cursor/skills/issueflow-issue-init/SKILL.md | 5 +- .cursor/skills/issueflow-issue-start/SKILL.md | 17 ++++--- .../03-solved-issues/issue31_original.md | 9 ++++ .../03-solved-issues/issue31_status.md | 47 +++++++++++++++++++ docs/cursor-issue-workflow.md | 28 +++++------ .../templates/commands/issue-close.md.j2 | 14 ++++-- .../templates/commands/issue-init.md.j2 | 6 +++ .../templates/commands/issue-start.md.j2 | 17 +++++++ .../docs/cursor-issue-workflow.md.j2 | 28 +++++------ .../templates/rules/issueflow-rules.mdc.j2 | 14 ++++++ .../skills/issueflow_issue_close/SKILL.md.j2 | 8 ++-- .../skills/issueflow_issue_init/SKILL.md.j2 | 5 +- .../skills/issueflow_issue_start/SKILL.md.j2 | 17 ++++--- tests/test_templating.py | 45 ++++++++++++++++++ 19 files changed, 263 insertions(+), 56 deletions(-) create mode 100644 .issueflows/03-solved-issues/issue31_original.md create mode 100644 .issueflows/03-solved-issues/issue31_status.md diff --git a/.cursor/commands/issue-close.md b/.cursor/commands/issue-close.md index d6acf39..d134e41 100644 --- a/.cursor/commands/issue-close.md +++ b/.cursor/commands/issue-close.md @@ -34,7 +34,7 @@ Other optional notes still apply: branch name, PR title, draft PR, skip issue do - 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. - Write a commit message that states what changed and why in normal sentences. - - Make sure you have pulled the last changes from the default branch (e.g. `main`) and check for and fix merge conflicts. + - 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** - Push your branch to `origin` (or the remote you use). @@ -43,12 +43,18 @@ Other optional notes still apply: branch name, PR title, draft PR, skip issue do - 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. **Branch reminder** - - After the PR is created, remind the user that the working copy is still on the issue branch, not the default branch (e.g. `main`). - - Suggest they run `git switch main` (or the repo's default) before starting any unrelated work, so new changes don't accidentally land on the issue branch. +7. **Post-merge branch cleanup** + - Detect the default branch (prefer `gh repo view --json defaultBranchRef -q .defaultBranchRef.name`; fall back to `git symbolic-ref --quiet --short refs/remotes/origin/HEAD | sed 's|^origin/||'`, else `main`). + - Detect merge status of this PR with `gh pr view --json state,mergedAt,mergeCommit,headRefName`. If `gh` is unavailable, approximate with `git fetch --prune` followed by `git rev-list ..origin/` and `git cherry origin/ ` (all commits marked `-` means squash-merged). + - **If the PR is merged:** + - Ask once whether to run the standard post-merge cleanup. On yes, run: `git switch && git pull --ff-only && git fetch --prune`. These are non-destructive because the issue branch is already merged. + - List local branches whose tip is already reachable from `origin/` (including squash-merged ones detected via `git cherry`). Present them as a single group and ask **once** (one consolidated yes/no listing every branch) before running `git branch -d ` for each. Never use `-D` automatically; if `-d` refuses, report the branch and stop touching it. + - **If the PR is not yet merged:** + - Remind the user that the working copy is still on the issue branch, not the default branch. Suggest `git switch ` before starting any unrelated work so new changes don't accidentally land on the issue branch. Tell them to re-run `/issue-close` after the PR merges so the post-merge cleanup actually runs. 8. **After review** - Address feedback, push updates, and merge when approved and CI is green. + - Re-run `/issue-close` after the merge to pick up the post-merge cleanup in step 7. ## Output diff --git a/.cursor/commands/issue-init.md b/.cursor/commands/issue-init.md index 9bde2bc..50a4a08 100644 --- a/.cursor/commands/issue-init.md +++ b/.cursor/commands/issue-init.md @@ -21,6 +21,7 @@ The text after this slash command is the **issue reference**. It may also be **e - Ask: "You have not provided an issue reference. Should I use issue #NN from the current branch ``?" (use the real branch name). - If the user **confirms**, treat `NN` as the issue number and continue with the bullets under **B** as if the user had typed only `NN`. - If the user **declines** or is **unclear**, ask for an issue number, full URL, or `owner/repo`, and do not proceed until you have a clear reference. + - **Archived-issue guard:** if files for issue `NN` are already present under `.issueflows/02-partly-solved-issues/` or `.issueflows/03-solved-issues/`, warn the user that the issue is archived and require a second, explicit confirmation before re-opening it in `.issueflows/01-current-issues/`. - Else (branch does not match that pattern): **do not guess.** Ask the user for an issue number, full GitHub issue URL, or `owner/repo/#number`, and do not proceed until they provide it. - **B. You have a concrete issue reference** (from user input or from **A** after confirmation): - If the reference is a full URL, extract `owner`, `repo`, and `issue number`. @@ -36,6 +37,11 @@ The text after this slash command is the **issue reference**. It may also be **e - number - and confirm resolved `owner/repo` +2.5. **Branch status preflight** (report only; do not block and do not delete anything). + - Run `git fetch --prune` so tracking info is fresh. + - Report: current branch, clean/dirty working tree, and ahead/behind counts vs `origin/` (detect default via `gh repo view --json defaultBranchRef -q .defaultBranchRef.name`, else `git symbolic-ref --quiet --short refs/remotes/origin/HEAD`, else `main`). + - If the current branch matches `^(\d+)-.+` and files for that issue now live under `.issueflows/02-partly-solved-issues/` or `.issueflows/03-solved-issues/`, note that the branch looks stale. Do not delete or move anything at this step. + 3. Archive existing issue files already in `.issueflows/01-current-issues` (except the current issue number). - Inspect issue groups by issue number (for example `issue121_*` belongs to issue 121). - Consider all files for that issue in `.issueflows/01-current-issues` (original + status/supplementary files) as one group to move together. diff --git a/.cursor/commands/issue-start.md b/.cursor/commands/issue-start.md index adaf74f..d452b17 100644 --- a/.cursor/commands/issue-start.md +++ b/.cursor/commands/issue-start.md @@ -9,6 +9,23 @@ If additional input is added, use that for further detailed guidance 0. If the issue markdown file is not present, or it is ambiguous which one to select, ask. Could it be that the user has not run the /issue-init command? +0.5 **Branch status preflight** (non-destructive — report, do not delete). + - Detect the default branch: `gh repo view --json defaultBranchRef -q .defaultBranchRef.name`. If `gh` is unavailable, use `git symbolic-ref --quiet --short refs/remotes/origin/HEAD | sed 's|^origin/||'`, else fall back to `main`. + - Run `git fetch --prune` so tracking info is fresh. + - Report: current branch, clean/dirty working tree (`git status --porcelain`), and ahead/behind counts vs `origin/` (`git rev-list --left-right --count origin/...HEAD`). + - Classify the current branch: + - On default (`main`/`master`/etc.): propose switching to or creating an issue branch before implementing, e.g. `git switch -c -` where `N` is the focus issue number. Ask before running. + - Matches `^(\d+)-.+`: treat the leading digits as issue number `N`. Cross-check `.issueflows/01-current-issues/`, `.issueflows/02-partly-solved-issues/`, and `.issueflows/03-solved-issues/`. If `issueN_*` is already under `02-partly-solved-issues/` or `03-solved-issues/`, warn that the branch looks stale and ask whether to switch back to default before continuing. Never delete a branch from `/issue-start`. + - Any other branch name: warn that the branch does not look like an issue branch and ask whether to continue on it. + +0.6 **Sweep stale current issues** (auto-safe file moves — no destructive git). + - Group files in `.issueflows/01-current-issues/` by issue number (`issueNN_*`). + - For every group **other than the focus issue**: + - If any status markdown for that group contains `- [x] Done` (case-insensitive on `done`), move the whole group to `.issueflows/03-solved-issues/`. + - Otherwise, move the whole group to `.issueflows/02-partly-solved-issues/`. + - Never move the focus issue's own files. + - Report every move (source -> destination, grouped by issue number) in the opening summary. + 1. Plan. If not in plan mode, stop and ask for a confirmation. 2. Check that the plan is not too broad. If too broad, ask if it should be split into several parts. diff --git a/.cursor/rules/issueflow-rules.mdc b/.cursor/rules/issueflow-rules.mdc index 1994715..9b50f15 100644 --- a/.cursor/rules/issueflow-rules.mdc +++ b/.cursor/rules/issueflow-rules.mdc @@ -84,3 +84,17 @@ If the issue is fully resolved (no additional subtasks present), move the origin ### Scripts that can help us when working on issues If you want, you can put small scripts etc. that you have made and think could be useful in the future in our llm tools folder: `.issueflows/00-tools`. Also, feel free to use the tools in our llm tools folder if you find someone that could be useful. + + +### Branch hygiene + +- Do issue work on an **issue branch** named like `-`, not on the default branch. +- Before starting or continuing work on an issue branch, run `git fetch --prune` and check where the branch sits relative to `origin/` (ahead/behind). A branch that is "several commits ahead" after a merged PR usually means the PR was squash-merged and the local branch is stale. +- **Assume squash-merges on GitHub.** After a PR merges: switch to the default branch, `git pull --ff-only`, `git fetch --prune`, and delete the local issue branch with `git branch -d ` (never `-D` automatically). `/issue-close` does this with a single consolidated confirm. +- If an issue is already archived under `.issueflows/02-partly-solved-issues` or `.issueflows/03-solved-issues`, the matching local branch is stale; don't resume work on it silently — switch back to the default branch and, if the issue really needs re-opening, do it deliberately through `/issue-init` (which will ask for a second confirmation). + + +### Folder hygiene for `.issueflows/01-current-issues` + +- Only the **focus issue** (the one currently being worked on) should live in `.issueflows/01-current-issues`. +- `/issue-init` and `/issue-start` both sweep that folder automatically: every `issue_*` group **other than the focus issue** is moved to `.issueflows/03-solved-issues` if a status file contains `- [x] Done`, otherwise to `.issueflows/02-partly-solved-issues`. Keep status files accurate so the sweep routes them correctly. diff --git a/.cursor/skills/issueflow-issue-close/SKILL.md b/.cursor/skills/issueflow-issue-close/SKILL.md index 83c2827..a3c2eff 100644 --- a/.cursor/skills/issueflow-issue-close/SKILL.md +++ b/.cursor/skills/issueflow-issue-close/SKILL.md @@ -35,15 +35,17 @@ When a bump applies: read `.cursor/skills/issueflow-version-bump/SKILL.md`, run 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. **Branch hygiene** — Ensure the branch is up to date with the default branch where appropriate; resolve merge conflicts before pushing. +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. **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. **Branch reminder** — After opening the PR, tell the user the working copy is still on the issue branch (not the default branch like `main`). Suggest switching back with `git switch main` before starting unrelated work so new changes don't land on the issue branch. +8. **Post-merge branch cleanup** — Detect the default branch (prefer `gh repo view --json defaultBranchRef -q .defaultBranchRef.name`; fall back to `git symbolic-ref --quiet --short refs/remotes/origin/HEAD`, else `main`). Detect merge status with `gh pr view --json state,mergedAt,mergeCommit,headRefName`. If `gh` is unavailable, approximate with `git fetch --prune` + `git cherry origin/ ` (all commits marked `-` means squash-merged). + - **If merged:** ask once whether to run the standard cleanup. On yes: `git switch && git pull --ff-only && git fetch --prune`. Then list every local branch whose tip is already reachable from `origin/` (including squash-merged ones) and ask **once** (one consolidated yes/no listing every branch) before running `git branch -d ` for each. Never use `-D` automatically; if `-d` refuses, report the branch and leave it alone. + - **If not yet merged:** remind the user the working copy is still on the issue branch (not the default). Suggest `git switch ` before starting unrelated work, and tell them to re-run `/issue-close` after the PR merges so the post-merge cleanup runs. -9. **Output** — Summarize commit, push result, and PR URL, or the next blocker. +9. **Output** — Summarize commit, push result, PR URL, and (when applicable) which local branches were deleted during post-merge cleanup. If blocked, report the next step. ## Constraints diff --git a/.cursor/skills/issueflow-issue-init/SKILL.md b/.cursor/skills/issueflow-issue-init/SKILL.md index 2948110..8ba2c42 100644 --- a/.cursor/skills/issueflow-issue-init/SKILL.md +++ b/.cursor/skills/issueflow-issue-init/SKILL.md @@ -24,10 +24,13 @@ Follow this skill when the user wants to **capture a GitHub issue locally** usin - **URL** — Parse `owner`, `repo`, issue number. - **Number only** — Use `git remote get-url origin` (HTTPS or SSH) to derive `owner/repo`. If parsing fails, ask for a full URL or `owner/repo`. - **Empty / whitespace** — Run `git branch --show-current`. If empty or `main`/`master` (case-insensitive), **stop** and ask for a number, URL, or `owner/repo/#n`. If the branch matches `^\d+-.+`, ask once whether to use that leading issue number; do not proceed without a clear yes/no. + - **Archived-issue guard** — Before writing, check `.issueflows/02-partly-solved-issues/` and `.issueflows/03-solved-issues/` for existing `issue_*` files. If the issue is already archived, warn and require a second explicit confirmation before re-opening it in `.issueflows/01-current-issues/`. 3. **Fetch** — `gh issue view --repo owner/repo --json title,body,url,number`. On failure, report the error and suggest `gh auth login`. -4. **Archive** — In `.issueflows/01-current-issues/`, group files by issue number (`issue121_*`). For each group **other than** the issue being created: move the whole group to `.issueflows/03-solved-issues/` only if a status file for that issue contains a checked **Done** line matching `- [x] Done` (case-insensitive on “done”). Otherwise move to `.issueflows/02-partly-solved-issues/`. If no status file or checkbox is unclear, treat as **not done**. +3.5 **Branch status preflight** (report only) — Run `git fetch --prune`. Report current branch, clean/dirty working tree, and ahead/behind counts vs `origin/` (detect default via `gh repo view --json defaultBranchRef -q .defaultBranchRef.name`, else `git symbolic-ref --quiet --short refs/remotes/origin/HEAD`, else `main`). If the current branch matches `^(\d+)-.+` and files for that issue already live in `.issueflows/02-partly-solved-issues/` or `.issueflows/03-solved-issues/`, note that the branch looks stale. Never delete or move anything at this step. + +4. **Archive** — In `.issueflows/01-current-issues/`, group files by issue number (`issue121_*`). For each group **other than** the issue being created: move the whole group to `.issueflows/03-solved-issues/` only if a status file for that issue contains a checked **Done** line matching `- [x] Done` (case-insensitive on "done"). Otherwise move to `.issueflows/02-partly-solved-issues/`. If no status file or checkbox is unclear, treat as **not done**. 5. **Write** — Create `.issueflows/01-current-issues/issue_original.md` with: diff --git a/.cursor/skills/issueflow-issue-start/SKILL.md b/.cursor/skills/issueflow-issue-start/SKILL.md index bdef541..96b4287 100644 --- a/.cursor/skills/issueflow-issue-start/SKILL.md +++ b/.cursor/skills/issueflow-issue-start/SKILL.md @@ -20,20 +20,25 @@ Follow this skill when the user wants to **begin implementation** from issue not 1. **Select the issue** — Read `.issueflows/01-current-issues/`. If there is no `*_original.md` (or multiple ambiguous groups), **stop** and ask which issue to use. -2. **Plan first** — Produce a concrete plan (steps, files touched, tests). If you are **not** in plan mode, **stop and ask for explicit confirmation** before implementing, per the command definition. +2. **Branch status preflight** (non-destructive) — Detect the default branch (prefer `gh repo view --json defaultBranchRef -q .defaultBranchRef.name`, else `git symbolic-ref --quiet --short refs/remotes/origin/HEAD`, else `main`). Run `git fetch --prune`. Report current branch, clean/dirty working tree, and ahead/behind counts vs `origin/`. If on the default branch, propose creating an issue branch (`git switch -c -`); ask before running. If the current branch matches `^(\d+)-.+` and files for that issue now live in `.issueflows/02-partly-solved-issues/` or `.issueflows/03-solved-issues/`, warn the branch looks stale and ask whether to switch back before continuing. If the branch is neither default nor an issue-style branch, warn and ask whether to continue. Never delete a branch from `/issue-start`. -3. **Scope check** — If the plan is broad, propose splitting into phases and ask whether to narrow scope before coding. +3. **Sweep stale current issues** (auto-safe) — Group files in `.issueflows/01-current-issues/` by `issueNN_` prefix. For every group **other than the focus issue**, move the whole group to `.issueflows/03-solved-issues/` if any of its status files contains `- [x] Done` (case-insensitive on `done`), otherwise move it to `.issueflows/02-partly-solved-issues/`. Never move the focus issue's files. Report every move. -4. **Implement** — Execute the confirmed plan. Prefer minimal, focused diffs. Match existing code style and tooling. +4. **Plan first** — Produce a concrete plan (steps, files touched, tests). If you are **not** in plan mode, **stop and ask for explicit confirmation** before implementing, per the command definition. -5. **Project conventions** +5. **Scope check** — If the plan is broad, propose splitting into phases and ask whether to narrow scope before coding. + +6. **Implement** — Execute the confirmed plan. Prefer minimal, focused diffs. Match existing code style and tooling. + +7. **Project conventions** - Run Python via **`uv run`** (scripts, pytest, tools), not bare `python`, unless the user overrides. - Manage dependencies with **`uv add` / `uv remove` / `uv sync`** only. - After meaningful progress, update or create a status markdown file under `.issueflows/01-current-issues/` (e.g. `issue_status.md`) with an explicit **Done** checkbox: `- [ ] Done` until fully resolved, then `- [x] Done`. -6. **Reporting** — Summarize what changed, what remains, and where the issue docs live. +8. **Reporting** — Summarize what changed, what remains, and where the issue docs live. Include any branch warnings from step 2 and any issue-group moves from step 3. ## Constraints - Do not invent issue text; treat `*_original.md` as read-only source of requirements unless the user asks to edit it. -- Do not move issue files between `01-` / `02-` / `03-` folders during `/issue-start` unless the user explicitly asked for that housekeeping. +- The stale sweep in step 3 is the **only** automatic move `/issue-start` performs, and it never touches the focus issue's own files. Do not move the focus issue's files between `01-` / `02-` / `03-` folders during `/issue-start`. +- Never delete or force-update git branches from `/issue-start`. diff --git a/.issueflows/03-solved-issues/issue31_original.md b/.issueflows/03-solved-issues/issue31_original.md new file mode 100644 index 0000000..1de5a6b --- /dev/null +++ b/.issueflows/03-solved-issues/issue31_original.md @@ -0,0 +1,9 @@ +# Issue #31: problems with branches and merging + +Source: https://github.com/jepegit/issue-flow/issues/31 + +## Original issue text + +When using issue-flow I experience many times (probably due to mistakes I do) that issue branches ends up being marked as several commits ahead of main, though the code changes are actually implemented. What can we implement to help users prevent experiencing the same as me? + +Also, many times there are left-overs in the .issueflows folder. I guess mostly within the 01-current-issues folder. Are there any things we can do to help that from happening that often? diff --git a/.issueflows/03-solved-issues/issue31_status.md b/.issueflows/03-solved-issues/issue31_status.md new file mode 100644 index 0000000..9ec4340 --- /dev/null +++ b/.issueflows/03-solved-issues/issue31_status.md @@ -0,0 +1,47 @@ +# Issue #31 status — problems with branches and merging + +Source issue: [.issueflows/01-current-issues/issue31_original.md](issue31_original.md) + +## Summary of decision + +Extend the existing three slash commands (`/issue-init`, `/issue-start`, `/issue-close`) and their matching skills with: + +- A **branch status preflight** (fetch --prune, current branch, ahead/behind vs default, stale-branch warning) in all three commands. +- A **stale-current-issues sweep** in `/issue-start` (same auto-safe rule already used by `/issue-init`): any `issue_*` group other than the focus issue is moved to `03-solved-issues/` if a status file contains `- [x] Done`, otherwise to `02-partly-solved-issues/`. +- A **post-merge branch cleanup** in `/issue-close`: detect merge via `gh pr view`, offer `git switch && git pull --ff-only && git fetch --prune`, then ask once (one consolidated yes/no) before `git branch -d` on every local branch whose commits are already in the default branch (including squash-merged ones). `-D` is never used automatically. +- An **archived-issue guard** in `/issue-init` so re-opening an issue that already lives in `02-`/`03-` requires an extra confirmation. +- A short **Branch hygiene** and **Folder hygiene** section in the workspace rules so the assistant keeps these habits outside the three commands. +- Cleanup of the duplicated "Agent Skills (optional)" block in the workflow doc. + +No new slash commands were added. + +## Checklist + +- [x] Add branch-preflight + stale-sweep steps to `src/issue_flow/templates/commands/issue-start.md.j2`. +- [x] Replace the post-PR reminder in `src/issue_flow/templates/commands/issue-close.md.j2` with explicit post-merge cleanup (detect merge, switch, pull --ff-only, fetch --prune, consolidated confirm before `git branch -d`). +- [x] Add branch-preflight + archived-issue guard to `src/issue_flow/templates/commands/issue-init.md.j2`. +- [x] Mirror the same additions in the three skill files under `src/issue_flow/templates/skills/`. +- [x] Update `src/issue_flow/templates/docs/cursor-issue-workflow.md.j2`: new "Branch and folder hygiene" section, per-command bullets updated, duplicated Agent Skills block removed. +- [x] Append "Branch hygiene" and "Folder hygiene" subsections to `src/issue_flow/templates/rules/issueflow-rules.mdc.j2`. +- [x] Re-render the project scaffold via `uv run scripts/update_issueflow_setup.py` so the live `.cursor/commands/`, `.cursor/skills/`, `.cursor/rules/`, and `docs/cursor-issue-workflow.md` pick up the new behavior. +- [x] Add regression assertions in `tests/test_templating.py` for the new headings; run full `uv run pytest` (35 passed). +- [x] Commit the template + doc + test changes on branch `31-problems-with-branches-and-merging`, push, and open a PR linking `Closes #31`. +- [x] Done + +## Remaining work before closing + +None. The implementation, tests, and documentation updates are complete. Once the PR is merged, re-running `/issue-close` will exercise the new post-merge cleanup end-to-end (switch to default, pull --ff-only, fetch --prune, single confirm before `git branch -d`). + +## Files touched + +- `src/issue_flow/templates/commands/issue-init.md.j2` +- `src/issue_flow/templates/commands/issue-start.md.j2` +- `src/issue_flow/templates/commands/issue-close.md.j2` +- `src/issue_flow/templates/skills/issueflow_issue_init/SKILL.md.j2` +- `src/issue_flow/templates/skills/issueflow_issue_start/SKILL.md.j2` +- `src/issue_flow/templates/skills/issueflow_issue_close/SKILL.md.j2` +- `src/issue_flow/templates/docs/cursor-issue-workflow.md.j2` +- `src/issue_flow/templates/rules/issueflow-rules.mdc.j2` +- `tests/test_templating.py` (added regression assertions) + +Re-rendered (not manually edited): `.cursor/commands/*.md`, `.cursor/skills/**/SKILL.md`, `.cursor/rules/issueflow-rules.mdc`, `docs/cursor-issue-workflow.md`. diff --git a/docs/cursor-issue-workflow.md b/docs/cursor-issue-workflow.md index 91eb8e7..53b8205 100644 --- a/docs/cursor-issue-workflow.md +++ b/docs/cursor-issue-workflow.md @@ -25,17 +25,14 @@ Each skill sets `disable-model-invocation: true` so it is included when you **ex --- -## Agent Skills (optional) +## Branch and folder hygiene -`issue-flow init` / `issue-flow update` also install **Cursor Agent Skills** under `.cursor/skills/` — longer, on-demand playbooks that mirror the three commands: +Two recurring pain points the commands actively help with: -| Skill folder | Invoke (examples) | Role | -|--------------|-------------------|------| -| `issueflow-issue-init` | `/issueflow-issue-init` or attach `@issueflow-issue-init` | Same flow as `/issue-init` (resolve reference, `gh`, archive, write `*_original.md`). | -| `issueflow-issue-start` | `/issueflow-issue-start` | Plan + confirmation + scope + implement from `.issueflows/01-current-issues/`. | -| `issueflow-issue-close` | `/issueflow-issue-close` | Tests, status checkboxes, move issue docs, commit, push, PR. | +- **Stale local branches that look "several commits ahead of main" after a squash-merged PR.** `/issue-close` detects merge status via `gh pr view`, and once the PR is merged it offers (with one consolidated confirm) to switch back to the default branch, `git pull --ff-only`, `git fetch --prune`, and run `git branch -d` on every local branch whose commits are already in the default branch (including squash-merged ones). Destructive flags like `-D` are never used automatically. +- **Left-overs in `.issueflows/01-current-issues/`.** Both `/issue-init` (when a new issue is captured) and `/issue-start` (before implementation begins) sweep that folder: every `issue_*` group **other than the focus issue** is moved automatically to `.issueflows/03-solved-issues/` if a status file contains `- [x] Done`, otherwise to `.issueflows/02-partly-solved-issues/`. -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. +All three commands also run a short **branch-status preflight**: `git fetch --prune`, current branch, ahead/behind vs the default branch, and a warning when the current branch's leading digits refer to an issue already archived in `02-`/`03-`. --- @@ -65,9 +62,11 @@ Each skill sets `disable-model-invocation: true` so it is included when you **ex **What the assistant does:** 1. Confirms **which** issue file applies if several exist or things are ambiguous. -2. **Plans** the work. If you are not in plan mode, it should stop and ask you to confirm before large changes. -3. Checks the plan is **not too broad**; may suggest splitting into smaller chunks. -4. **Implements** the plan (code, tests, and updates to issue status docs as appropriate for the task). +2. **Branch status preflight** — `git fetch --prune`, report current branch and ahead/behind vs the default branch, warn if the current branch looks stale (leading digits point at an issue already in `02-`/`03-`) or if you are still on the default branch. +3. **Sweeps stale current issues** — moves every `issue_*` group **other than the focus issue** to `.issueflows/03-solved-issues/` (if a status file contains `- [x] Done`) or `.issueflows/02-partly-solved-issues/`. +4. **Plans** the work. If you are not in plan mode, it should stop and ask you to confirm before large changes. +5. Checks the plan is **not too broad**; may suggest splitting into smaller chunks. +6. **Implements** the plan (code, tests, and updates to issue status docs as appropriate for the task). **Result:** Implementation aligned with the markdown in `.issueflows/01-current-issues/` and project rules (tests with `uv run`, dependency management with `uv`, etc.). @@ -91,12 +90,13 @@ 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/` (see project rules). -4. **Commit** — focused staging and a clear message (include `pyproject.toml` / `uv.lock` if the bump changed them). +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` so unrelated history never sneaks in silently. 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** — address comments, merge when ready. +7. **Post-merge branch cleanup** — once the PR is merged, re-run `/issue-close` (or simply ask the assistant to do the cleanup). It will detect the merge, offer to `git switch && git pull --ff-only && git fetch --prune`, and ask once before running `git branch -d` on every local branch already reachable from the default (including squash-merged ones). Nothing destructive happens without your yes. +8. **After review** — address comments, merge when ready; then come back for step 7. -**Result:** Short summary of commit, push, and PR link (or what is blocked). +**Result:** Short summary of commit, push, PR link, and (after merge) which local branches were deleted — or what is blocked. --- diff --git a/src/issue_flow/templates/commands/issue-close.md.j2 b/src/issue_flow/templates/commands/issue-close.md.j2 index b36200a..088b438 100644 --- a/src/issue_flow/templates/commands/issue-close.md.j2 +++ b/src/issue_flow/templates/commands/issue-close.md.j2 @@ -34,7 +34,7 @@ Other optional notes still apply: branch name, PR title, draft PR, skip issue do - 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. - Write a commit message that states what changed and why in normal sentences. - - Make sure you have pulled the last changes from the default branch (e.g. `main`) and check for and fix merge conflicts. + - 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** - Push your branch to `origin` (or the remote you use). @@ -43,12 +43,18 @@ Other optional notes still apply: branch name, PR title, draft PR, skip issue do - 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. **Branch reminder** - - After the PR is created, remind the user that the working copy is still on the issue branch, not the default branch (e.g. `main`). - - Suggest they run `git switch main` (or the repo's default) before starting any unrelated work, so new changes don't accidentally land on the issue branch. +7. **Post-merge branch cleanup** + - Detect the default branch (prefer `gh repo view --json defaultBranchRef -q .defaultBranchRef.name`; fall back to `git symbolic-ref --quiet --short refs/remotes/origin/HEAD | sed 's|^origin/||'`, else `main`). + - Detect merge status of this PR with `gh pr view --json state,mergedAt,mergeCommit,headRefName`. If `gh` is unavailable, approximate with `git fetch --prune` followed by `git rev-list ..origin/` and `git cherry origin/ ` (all commits marked `-` means squash-merged). + - **If the PR is merged:** + - Ask once whether to run the standard post-merge cleanup. On yes, run: `git switch && git pull --ff-only && git fetch --prune`. These are non-destructive because the issue branch is already merged. + - List local branches whose tip is already reachable from `origin/` (including squash-merged ones detected via `git cherry`). Present them as a single group and ask **once** (one consolidated yes/no listing every branch) before running `git branch -d ` for each. Never use `-D` automatically; if `-d` refuses, report the branch and stop touching it. + - **If the PR is not yet merged:** + - Remind the user that the working copy is still on the issue branch, not the default branch. Suggest `git switch ` before starting any unrelated work so new changes don't accidentally land on the issue branch. Tell them to re-run `/issue-close` after the PR merges so the post-merge cleanup actually runs. 8. **After review** - Address feedback, push updates, and merge when approved and CI is green. + - Re-run `/issue-close` after the merge to pick up the post-merge cleanup in step 7. ## Output diff --git a/src/issue_flow/templates/commands/issue-init.md.j2 b/src/issue_flow/templates/commands/issue-init.md.j2 index 42e278e..d002a83 100644 --- a/src/issue_flow/templates/commands/issue-init.md.j2 +++ b/src/issue_flow/templates/commands/issue-init.md.j2 @@ -21,6 +21,7 @@ The text after this slash command is the **issue reference**. It may also be **e - Ask: "You have not provided an issue reference. Should I use issue #NN from the current branch ``?" (use the real branch name). - If the user **confirms**, treat `NN` as the issue number and continue with the bullets under **B** as if the user had typed only `NN`. - If the user **declines** or is **unclear**, ask for an issue number, full URL, or `owner/repo`, and do not proceed until you have a clear reference. + - **Archived-issue guard:** if files for issue `NN` are already present under `{{ issueflows_dir }}/{{ partly_solved_folder }}/` or `{{ issueflows_dir }}/{{ solved_folder }}/`, warn the user that the issue is archived and require a second, explicit confirmation before re-opening it in `{{ issueflows_dir }}/{{ current_issues_folder }}/`. - Else (branch does not match that pattern): **do not guess.** Ask the user for an issue number, full GitHub issue URL, or `owner/repo/#number`, and do not proceed until they provide it. - **B. You have a concrete issue reference** (from user input or from **A** after confirmation): - If the reference is a full URL, extract `owner`, `repo`, and `issue number`. @@ -36,6 +37,11 @@ The text after this slash command is the **issue reference**. It may also be **e - number - and confirm resolved `owner/repo` +2.5. **Branch status preflight** (report only; do not block and do not delete anything). + - Run `git fetch --prune` so tracking info is fresh. + - Report: current branch, clean/dirty working tree, and ahead/behind counts vs `origin/` (detect default via `gh repo view --json defaultBranchRef -q .defaultBranchRef.name`, else `git symbolic-ref --quiet --short refs/remotes/origin/HEAD`, else `main`). + - If the current branch matches `^(\d+)-.+` and files for that issue now live under `{{ issueflows_dir }}/{{ partly_solved_folder }}/` or `{{ issueflows_dir }}/{{ solved_folder }}/`, note that the branch looks stale. Do not delete or move anything at this step. + 3. Archive existing issue files already in `{{ issueflows_dir }}/{{ current_issues_folder }}` (except the current issue number). - Inspect issue groups by issue number (for example `issue121_*` belongs to issue 121). - Consider all files for that issue in `{{ issueflows_dir }}/{{ current_issues_folder }}` (original + status/supplementary files) as one group to move together. diff --git a/src/issue_flow/templates/commands/issue-start.md.j2 b/src/issue_flow/templates/commands/issue-start.md.j2 index ecdeaa9..8468c0a 100644 --- a/src/issue_flow/templates/commands/issue-start.md.j2 +++ b/src/issue_flow/templates/commands/issue-start.md.j2 @@ -9,6 +9,23 @@ If additional input is added, use that for further detailed guidance 0. If the issue markdown file is not present, or it is ambiguous which one to select, ask. Could it be that the user has not run the /issue-init command? +0.5 **Branch status preflight** (non-destructive — report, do not delete). + - Detect the default branch: `gh repo view --json defaultBranchRef -q .defaultBranchRef.name`. If `gh` is unavailable, use `git symbolic-ref --quiet --short refs/remotes/origin/HEAD | sed 's|^origin/||'`, else fall back to `main`. + - Run `git fetch --prune` so tracking info is fresh. + - Report: current branch, clean/dirty working tree (`git status --porcelain`), and ahead/behind counts vs `origin/` (`git rev-list --left-right --count origin/...HEAD`). + - Classify the current branch: + - On default (`main`/`master`/etc.): propose switching to or creating an issue branch before implementing, e.g. `git switch -c -` where `N` is the focus issue number. Ask before running. + - Matches `^(\d+)-.+`: treat the leading digits as issue number `N`. Cross-check `{{ issueflows_dir }}/{{ current_issues_folder }}/`, `{{ issueflows_dir }}/{{ partly_solved_folder }}/`, and `{{ issueflows_dir }}/{{ solved_folder }}/`. If `issueN_*` is already under `{{ partly_solved_folder }}/` or `{{ solved_folder }}/`, warn that the branch looks stale and ask whether to switch back to default before continuing. Never delete a branch from `/issue-start`. + - Any other branch name: warn that the branch does not look like an issue branch and ask whether to continue on it. + +0.6 **Sweep stale current issues** (auto-safe file moves — no destructive git). + - Group files in `{{ issueflows_dir }}/{{ current_issues_folder }}/` by issue number (`issueNN_*`). + - For every group **other than the focus issue**: + - If any status markdown for that group contains `- [x] Done` (case-insensitive on `done`), move the whole group to `{{ issueflows_dir }}/{{ solved_folder }}/`. + - Otherwise, move the whole group to `{{ issueflows_dir }}/{{ partly_solved_folder }}/`. + - Never move the focus issue's own files. + - Report every move (source -> destination, grouped by issue number) in the opening summary. + 1. Plan. If not in plan mode, stop and ask for a confirmation. 2. Check that the plan is not too broad. If too broad, ask if it should be split into several parts. 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 74b2956..13ace50 100644 --- a/src/issue_flow/templates/docs/cursor-issue-workflow.md.j2 +++ b/src/issue_flow/templates/docs/cursor-issue-workflow.md.j2 @@ -25,17 +25,14 @@ Each skill sets `disable-model-invocation: true` so it is included when you **ex --- -## Agent Skills (optional) +## Branch and folder hygiene -`issue-flow init` / `issue-flow update` also install **Cursor Agent Skills** under `{{ agent_dir }}/skills/` — longer, on-demand playbooks that mirror the three commands: +Two recurring pain points the commands actively help with: -| Skill folder | Invoke (examples) | Role | -|--------------|-------------------|------| -| `issueflow-issue-init` | `/issueflow-issue-init` or attach `@issueflow-issue-init` | Same flow as `/issue-init` (resolve reference, `gh`, archive, write `*_original.md`). | -| `issueflow-issue-start` | `/issueflow-issue-start` | Plan + confirmation + scope + implement from `{{ issueflows_dir }}/{{ current_issues_folder }}/`. | -| `issueflow-issue-close` | `/issueflow-issue-close` | Tests, status checkboxes, move issue docs, commit, push, PR. | +- **Stale local branches that look "several commits ahead of main" after a squash-merged PR.** `/issue-close` detects merge status via `gh pr view`, and once the PR is merged it offers (with one consolidated confirm) to switch back to the default branch, `git pull --ff-only`, `git fetch --prune`, and run `git branch -d` on every local branch whose commits are already in the default branch (including squash-merged ones). Destructive flags like `-D` are never used automatically. +- **Left-overs in `{{ issueflows_dir }}/{{ current_issues_folder }}/`.** Both `/issue-init` (when a new issue is captured) and `/issue-start` (before implementation begins) sweep that folder: every `issue_*` group **other than the focus issue** is moved automatically to `{{ issueflows_dir }}/{{ solved_folder }}/` if a status file contains `- [x] Done`, otherwise to `{{ issueflows_dir }}/{{ partly_solved_folder }}/`. -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. +All three commands also run a short **branch-status preflight**: `git fetch --prune`, current branch, ahead/behind vs the default branch, and a warning when the current branch's leading digits refer to an issue already archived in `02-`/`03-`. --- @@ -65,9 +62,11 @@ Each skill sets `disable-model-invocation: true` so it is included when you **ex **What the assistant does:** 1. Confirms **which** issue file applies if several exist or things are ambiguous. -2. **Plans** the work. If you are not in plan mode, it should stop and ask you to confirm before large changes. -3. Checks the plan is **not too broad**; may suggest splitting into smaller chunks. -4. **Implements** the plan (code, tests, and updates to issue status docs as appropriate for the task). +2. **Branch status preflight** — `git fetch --prune`, report current branch and ahead/behind vs the default branch, warn if the current branch looks stale (leading digits point at an issue already in `02-`/`03-`) or if you are still on the default branch. +3. **Sweeps stale current issues** — moves every `issue_*` group **other than the focus issue** to `{{ issueflows_dir }}/{{ solved_folder }}/` (if a status file contains `- [x] Done`) or `{{ issueflows_dir }}/{{ partly_solved_folder }}/`. +4. **Plans** the work. If you are not in plan mode, it should stop and ask you to confirm before large changes. +5. Checks the plan is **not too broad**; may suggest splitting into smaller chunks. +6. **Implements** the plan (code, tests, and updates to issue status docs as appropriate for the task). **Result:** Implementation aligned with the markdown in `{{ issueflows_dir }}/{{ current_issues_folder }}/` and project rules (tests with `uv run`, dependency management with `uv`, etc.). @@ -91,12 +90,13 @@ 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 }}/` (see project rules). -4. **Commit** — focused staging and a clear message (include `pyproject.toml` / `uv.lock` if the bump changed them). +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` so unrelated history never sneaks in silently. 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** — address comments, merge when ready. +7. **Post-merge branch cleanup** — once the PR is merged, re-run `/issue-close` (or simply ask the assistant to do the cleanup). It will detect the merge, offer to `git switch && git pull --ff-only && git fetch --prune`, and ask once before running `git branch -d` on every local branch already reachable from the default (including squash-merged ones). Nothing destructive happens without your yes. +8. **After review** — address comments, merge when ready; then come back for step 7. -**Result:** Short summary of commit, push, and PR link (or what is blocked). +**Result:** Short summary of commit, push, PR link, and (after merge) which local branches were deleted — or what is blocked. --- diff --git a/src/issue_flow/templates/rules/issueflow-rules.mdc.j2 b/src/issue_flow/templates/rules/issueflow-rules.mdc.j2 index 9777e92..da93772 100644 --- a/src/issue_flow/templates/rules/issueflow-rules.mdc.j2 +++ b/src/issue_flow/templates/rules/issueflow-rules.mdc.j2 @@ -84,3 +84,17 @@ If the issue is fully resolved (no additional subtasks present), move the origin ### Scripts that can help us when working on issues If you want, you can put small scripts etc. that you have made and think could be useful in the future in our llm tools folder: `{{ issueflows_dir }}/{{ tools_folder }}`. Also, feel free to use the tools in our llm tools folder if you find someone that could be useful. + + +### Branch hygiene + +- Do issue work on an **issue branch** named like `-`, not on the default branch. +- Before starting or continuing work on an issue branch, run `git fetch --prune` and check where the branch sits relative to `origin/` (ahead/behind). A branch that is "several commits ahead" after a merged PR usually means the PR was squash-merged and the local branch is stale. +- **Assume squash-merges on GitHub.** After a PR merges: switch to the default branch, `git pull --ff-only`, `git fetch --prune`, and delete the local issue branch with `git branch -d ` (never `-D` automatically). `/issue-close` does this with a single consolidated confirm. +- If an issue is already archived under `{{ issueflows_dir }}/{{ partly_solved_folder }}` or `{{ issueflows_dir }}/{{ solved_folder }}`, the matching local branch is stale; don't resume work on it silently — switch back to the default branch and, if the issue really needs re-opening, do it deliberately through `/issue-init` (which will ask for a second confirmation). + + +### Folder hygiene for `{{ issueflows_dir }}/{{ current_issues_folder }}` + +- Only the **focus issue** (the one currently being worked on) should live in `{{ issueflows_dir }}/{{ current_issues_folder }}`. +- `/issue-init` and `/issue-start` both sweep that folder automatically: every `issue_*` group **other than the focus issue** is moved to `{{ issueflows_dir }}/{{ solved_folder }}` if a status file contains `- [x] Done`, otherwise to `{{ issueflows_dir }}/{{ partly_solved_folder }}`. Keep status files accurate so the sweep routes them correctly. 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 68dfc94..70a9c79 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 @@ -35,15 +35,17 @@ When a bump applies: read `{{ agent_dir }}/skills/issueflow-version-bump/SKILL.m 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. **Branch hygiene** — Ensure the branch is up to date with the default branch where appropriate; resolve merge conflicts before pushing. +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. **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. **Branch reminder** — After opening the PR, tell the user the working copy is still on the issue branch (not the default branch like `main`). Suggest switching back with `git switch main` before starting unrelated work so new changes don't land on the issue branch. +8. **Post-merge branch cleanup** — Detect the default branch (prefer `gh repo view --json defaultBranchRef -q .defaultBranchRef.name`; fall back to `git symbolic-ref --quiet --short refs/remotes/origin/HEAD`, else `main`). Detect merge status with `gh pr view --json state,mergedAt,mergeCommit,headRefName`. If `gh` is unavailable, approximate with `git fetch --prune` + `git cherry origin/ ` (all commits marked `-` means squash-merged). + - **If merged:** ask once whether to run the standard cleanup. On yes: `git switch && git pull --ff-only && git fetch --prune`. Then list every local branch whose tip is already reachable from `origin/` (including squash-merged ones) and ask **once** (one consolidated yes/no listing every branch) before running `git branch -d ` for each. Never use `-D` automatically; if `-d` refuses, report the branch and leave it alone. + - **If not yet merged:** remind the user the working copy is still on the issue branch (not the default). Suggest `git switch ` before starting unrelated work, and tell them to re-run `/issue-close` after the PR merges so the post-merge cleanup runs. -9. **Output** — Summarize commit, push result, and PR URL, or the next blocker. +9. **Output** — Summarize commit, push result, PR URL, and (when applicable) which local branches were deleted during post-merge cleanup. If blocked, report the next step. ## Constraints diff --git a/src/issue_flow/templates/skills/issueflow_issue_init/SKILL.md.j2 b/src/issue_flow/templates/skills/issueflow_issue_init/SKILL.md.j2 index 93d514e..b792f6c 100644 --- a/src/issue_flow/templates/skills/issueflow_issue_init/SKILL.md.j2 +++ b/src/issue_flow/templates/skills/issueflow_issue_init/SKILL.md.j2 @@ -24,10 +24,13 @@ Follow this skill when the user wants to **capture a GitHub issue locally** usin - **URL** — Parse `owner`, `repo`, issue number. - **Number only** — Use `git remote get-url origin` (HTTPS or SSH) to derive `owner/repo`. If parsing fails, ask for a full URL or `owner/repo`. - **Empty / whitespace** — Run `git branch --show-current`. If empty or `main`/`master` (case-insensitive), **stop** and ask for a number, URL, or `owner/repo/#n`. If the branch matches `^\d+-.+`, ask once whether to use that leading issue number; do not proceed without a clear yes/no. + - **Archived-issue guard** — Before writing, check `{{ issueflows_dir }}/{{ partly_solved_folder }}/` and `{{ issueflows_dir }}/{{ solved_folder }}/` for existing `issue_*` files. If the issue is already archived, warn and require a second explicit confirmation before re-opening it in `{{ issueflows_dir }}/{{ current_issues_folder }}/`. 3. **Fetch** — `gh issue view --repo owner/repo --json title,body,url,number`. On failure, report the error and suggest `gh auth login`. -4. **Archive** — In `{{ issueflows_dir }}/{{ current_issues_folder }}/`, group files by issue number (`issue121_*`). For each group **other than** the issue being created: move the whole group to `{{ issueflows_dir }}/{{ solved_folder }}/` only if a status file for that issue contains a checked **Done** line matching `- [x] Done` (case-insensitive on “done”). Otherwise move to `{{ issueflows_dir }}/{{ partly_solved_folder }}/`. If no status file or checkbox is unclear, treat as **not done**. +3.5 **Branch status preflight** (report only) — Run `git fetch --prune`. Report current branch, clean/dirty working tree, and ahead/behind counts vs `origin/` (detect default via `gh repo view --json defaultBranchRef -q .defaultBranchRef.name`, else `git symbolic-ref --quiet --short refs/remotes/origin/HEAD`, else `main`). If the current branch matches `^(\d+)-.+` and files for that issue already live in `{{ issueflows_dir }}/{{ partly_solved_folder }}/` or `{{ issueflows_dir }}/{{ solved_folder }}/`, note that the branch looks stale. Never delete or move anything at this step. + +4. **Archive** — In `{{ issueflows_dir }}/{{ current_issues_folder }}/`, group files by issue number (`issue121_*`). For each group **other than** the issue being created: move the whole group to `{{ issueflows_dir }}/{{ solved_folder }}/` only if a status file for that issue contains a checked **Done** line matching `- [x] Done` (case-insensitive on "done"). Otherwise move to `{{ issueflows_dir }}/{{ partly_solved_folder }}/`. If no status file or checkbox is unclear, treat as **not done**. 5. **Write** — Create `{{ issueflows_dir }}/{{ current_issues_folder }}/issue_original.md` with: diff --git a/src/issue_flow/templates/skills/issueflow_issue_start/SKILL.md.j2 b/src/issue_flow/templates/skills/issueflow_issue_start/SKILL.md.j2 index 506d5bd..11cd003 100644 --- a/src/issue_flow/templates/skills/issueflow_issue_start/SKILL.md.j2 +++ b/src/issue_flow/templates/skills/issueflow_issue_start/SKILL.md.j2 @@ -20,20 +20,25 @@ Follow this skill when the user wants to **begin implementation** from issue not 1. **Select the issue** — Read `{{ issueflows_dir }}/{{ current_issues_folder }}/`. If there is no `*_original.md` (or multiple ambiguous groups), **stop** and ask which issue to use. -2. **Plan first** — Produce a concrete plan (steps, files touched, tests). If you are **not** in plan mode, **stop and ask for explicit confirmation** before implementing, per the command definition. +2. **Branch status preflight** (non-destructive) — Detect the default branch (prefer `gh repo view --json defaultBranchRef -q .defaultBranchRef.name`, else `git symbolic-ref --quiet --short refs/remotes/origin/HEAD`, else `main`). Run `git fetch --prune`. Report current branch, clean/dirty working tree, and ahead/behind counts vs `origin/`. If on the default branch, propose creating an issue branch (`git switch -c -`); ask before running. If the current branch matches `^(\d+)-.+` and files for that issue now live in `{{ issueflows_dir }}/{{ partly_solved_folder }}/` or `{{ issueflows_dir }}/{{ solved_folder }}/`, warn the branch looks stale and ask whether to switch back before continuing. If the branch is neither default nor an issue-style branch, warn and ask whether to continue. Never delete a branch from `/issue-start`. -3. **Scope check** — If the plan is broad, propose splitting into phases and ask whether to narrow scope before coding. +3. **Sweep stale current issues** (auto-safe) — Group files in `{{ issueflows_dir }}/{{ current_issues_folder }}/` by `issueNN_` prefix. For every group **other than the focus issue**, move the whole group to `{{ issueflows_dir }}/{{ solved_folder }}/` if any of its status files contains `- [x] Done` (case-insensitive on `done`), otherwise move it to `{{ issueflows_dir }}/{{ partly_solved_folder }}/`. Never move the focus issue's files. Report every move. -4. **Implement** — Execute the confirmed plan. Prefer minimal, focused diffs. Match existing code style and tooling. +4. **Plan first** — Produce a concrete plan (steps, files touched, tests). If you are **not** in plan mode, **stop and ask for explicit confirmation** before implementing, per the command definition. -5. **Project conventions** +5. **Scope check** — If the plan is broad, propose splitting into phases and ask whether to narrow scope before coding. + +6. **Implement** — Execute the confirmed plan. Prefer minimal, focused diffs. Match existing code style and tooling. + +7. **Project conventions** - Run Python via **`uv run`** (scripts, pytest, tools), not bare `python`, unless the user overrides. - Manage dependencies with **`uv add` / `uv remove` / `uv sync`** only. - After meaningful progress, update or create a status markdown file under `{{ issueflows_dir }}/{{ current_issues_folder }}/` (e.g. `issue_status.md`) with an explicit **Done** checkbox: `- [ ] Done` until fully resolved, then `- [x] Done`. -6. **Reporting** — Summarize what changed, what remains, and where the issue docs live. +8. **Reporting** — Summarize what changed, what remains, and where the issue docs live. Include any branch warnings from step 2 and any issue-group moves from step 3. ## Constraints - Do not invent issue text; treat `*_original.md` as read-only source of requirements unless the user asks to edit it. -- Do not move issue files between `01-` / `02-` / `03-` folders during `/issue-start` unless the user explicitly asked for that housekeeping. +- The stale sweep in step 3 is the **only** automatic move `/issue-start` performs, and it never touches the focus issue's own files. Do not move the focus issue's files between `01-` / `02-` / `03-` folders during `/issue-start`. +- Never delete or force-update git branches from `/issue-start`. diff --git a/tests/test_templating.py b/tests/test_templating.py index 6b35bff..c2ff7c3 100644 --- a/tests/test_templating.py +++ b/tests/test_templating.py @@ -54,3 +54,48 @@ def test_resolve_output_path() -> None: def test_manifest_entry_count() -> None: assert len(TEMPLATE_MANIFEST) == 9 + + +def _default_context() -> dict[str, str]: + return { + "issueflows_dir": ".issueflows", + "agent_dir": ".cursor", + "docs_dir": "docs", + "tools_folder": "00-tools", + "current_issues_folder": "01-current-issues", + "partly_solved_folder": "02-partly-solved-issues", + "solved_folder": "03-solved-issues", + "project_name": "test-project", + } + + +def test_issue_start_mentions_branch_and_sweep_preflight() -> None: + """The /issue-start command must include the new preflight and sweep steps.""" + rendered = render_template("commands/issue-start.md.j2", _default_context()) + assert "Branch status preflight" in rendered + assert "Sweep stale current issues" in rendered + assert "git fetch --prune" in rendered + + +def test_issue_close_mentions_post_merge_cleanup() -> None: + """The /issue-close command must describe post-merge branch cleanup.""" + rendered = render_template("commands/issue-close.md.j2", _default_context()) + assert "Post-merge branch cleanup" in rendered + assert "git branch -d" in rendered + assert "git pull --ff-only" in rendered + assert "gh pr view" in rendered + + +def test_issue_init_mentions_branch_preflight_and_archive_guard() -> None: + """The /issue-init command must include the preflight and archived-issue guard.""" + rendered = render_template("commands/issue-init.md.j2", _default_context()) + assert "Branch status preflight" in rendered + assert "Archived-issue guard" in rendered or "archived" in rendered.lower() + + +def test_issueflow_rules_has_branch_hygiene_section() -> None: + """The workspace rules must describe branch and folder hygiene expectations.""" + rendered = render_template("rules/issueflow-rules.mdc.j2", _default_context()) + assert "Branch hygiene" in rendered + assert "git branch -d" in rendered + assert "Folder hygiene" in rendered