From 1063392a374d79374b6833fbc993d74b64eb5f42 Mon Sep 17 00:00:00 2001 From: jepegit Date: Sun, 4 Oct 2026 12:35:29 +0200 Subject: [PATCH] Add a knob that gitignores all of graphify-out and rebuilds the graph before planning. A fresh checkout with nothing committed under graphify-out still needs the report before /iflow-plan and epic draft. The new flag does both, and leaves auto_graphify_on_plan for a refresh when the report stays in git. Co-authored-by: Cursor --- .../03-solved-issues/issue400_original.md | 7 ++ .issueflows/03-solved-issues/issue400_plan.md | 80 +++++++++++++++ .../03-solved-issues/issue400_status.md | 16 +++ .../graphify-integration.md | 12 +++ .../skill-behaviour-knobs.md | 2 + .../04-designs-and-guides/test-registry.md | 4 + HISTORY.md | 1 + docs/configuration.md | 1 + src/issue_flow/agent.py | 3 +- src/issue_flow/cli.py | 3 +- src/issue_flow/config.py | 21 ++++ src/issue_flow/config_ops.py | 1 + src/issue_flow/init.py | 3 + src/issue_flow/modes.py | 25 +++++ src/issue_flow/surfaces.py | 97 +++++++++++++++++++ .../templates/commands/iflow-epic.md.j2 | 4 +- .../templates/commands/iflow-graphify.md.j2 | 2 +- .../templates/commands/iflow-plan.md.j2 | 4 +- .../templates/skills/iflow_epic/SKILL.md.j2 | 3 + .../skills/iflow_graphify/SKILL.md.j2 | 2 + .../templates/skills/iflow_plan/SKILL.md.j2 | 4 +- tests/test_cli.py | 1 + tests/test_config.py | 5 + tests/test_graphify_gitignore.py | 74 ++++++++++++++ tests/test_modes.py | 3 + tests/test_templating.py | 29 +++++- 26 files changed, 398 insertions(+), 9 deletions(-) create mode 100644 .issueflows/03-solved-issues/issue400_original.md create mode 100644 .issueflows/03-solved-issues/issue400_plan.md create mode 100644 .issueflows/03-solved-issues/issue400_status.md create mode 100644 tests/test_graphify_gitignore.py diff --git a/.issueflows/03-solved-issues/issue400_original.md b/.issueflows/03-solved-issues/issue400_original.md new file mode 100644 index 00000000..2a94b1ab --- /dev/null +++ b/.issueflows/03-solved-issues/issue400_original.md @@ -0,0 +1,7 @@ +# Issue #400: graphify totally gitignored + +Source: https://github.com/jepegit/issue-flow/issues/400 + +## Original issue text + +We need to make sure that we have a knob that puts all of graphify on gitignore, and if turned on, the flows have to run graphify before creating plan so that graphify output can be used in planning diff --git a/.issueflows/03-solved-issues/issue400_plan.md b/.issueflows/03-solved-issues/issue400_plan.md new file mode 100644 index 00000000..11705a5d --- /dev/null +++ b/.issueflows/03-solved-issues/issue400_plan.md @@ -0,0 +1,80 @@ +# Plan: graphify totally gitignored (#400) + +## Goal + +Add a config knob that gitignores the whole `graphify-out/` tree, and when that knob is on, make `/iflow-plan` (and epic drafting) run `issue-flow graphify` before planning so a fresh checkout still has graph output to read. + +## Constraints + +- Default stays off. Projects that commit `GRAPH_REPORT.md` keep doing so until they set the key and re-run `issue-flow update`. +- Templates are the source of truth. Skill and command text change in `src/issue_flow/templates/`, then `issue-flow update` bakes them. +- Precedence matches other `[issueflow]` keys: project `config.toml` > user-global > `ISSUEFLOW_*` env > default. +- Do not auto-run `graphify extract` (needs an API key). The refresh is AST `update` only. +- Missing or failing `graphify` does not block planning (same as issue #214). Report the failure and fall back to grep. +- `issue-flow update` must not run `git rm`. Gitignore does not untrack files that are already in the index. +- This plan does not flip the knob in this repo and does not untrack `graphify-out/` here. See Open questions. + +### Prior art + +- `auto_graphify_on_plan` (`Settings.resolve_auto_graphify_on_plan` in `src/issue_flow/config.py`, baked in `templates/skills/iflow_plan/SKILL.md.j2` and `templates/commands/iflow-plan.md.j2`). Opt-in refresh before prior-art. Default false. Missing graphify → note and continue. **Coexist:** the new key also enables that same step; the old key stays for “refresh even when the report is committed.” +- `suggest_graphify` — soft skim/rebuild nudge. Never auto-runs. Leave it. +- This repo’s `.gitignore` already ignores `graphify-out/cache/`, `graphify-out/graph.html`, and `graphify-out/graph.json`. `GRAPH_REPORT.md`, `manifest.json`, and the `.graphify_*` files stay tracked (HISTORY, repo hygiene). The new key ignores the whole directory; it does not rewrite that hand-written partial block. +- Managed gitignore pattern: `ensure_editor_gitignore` in `src/issue_flow/surfaces.py` (`# BEGIN` / `# END` markers). Mirror that for the graphify block. +- Design note in `.issueflows/04-designs-and-guides/graphify-integration.md` (issue #214): projects that gitignore `graphify-out/` can set `auto_graphify_on_plan`. This knob makes that pair one switch. +- Toolbox (`00-tools/`): nothing relevant. Graph query hit `graphify()` / `run_build()` / `Settings` (communities 5, 327, 463); no existing gitignore-all helper. + +## Approach + +New bool `[issueflow].graphify_gitignored`, default `false`. Env `ISSUEFLOW_GRAPHIFY_GITIGNORED`. `issue-flow config show|set` accepts it. Changing it needs `issue-flow update`. + +**Gitignore.** On `init` and `update`, if the resolved value is true, upsert a managed block in the target `.gitignore`: + +``` +# BEGIN issue-flow graphify (generated; do not edit) +graphify-out/ +# END issue-flow graphify +``` + +If the value is false, remove that managed block when present and leave every other line alone. If `graphify-out/` paths are still tracked, print the untrack command and do not run it: + +`git rm -r --cached graphify-out` + +**Plan-time refresh.** Render the existing “refresh knowledge graph” step when `auto_graphify_on_plan` **or** `graphify_gitignored` is true (one step, not two). Wording names which key is on. Same failure policy as #214. Also add that step to epic draft (`iflow_epic` skill + `iflow-epic` command), which writes a plan and today only skims `GRAPH_REPORT.md` when the file already exists. `/iflow-yolo` follows `/iflow-plan`, so it inherits the step. + +**Commit guidance.** In the graphify skill/command, when `graphify_gitignored` is true, say not to commit `graphify-out/`. When false, keep today’s “the graph is fine to commit” line. + +**Docs.** One row in `docs/configuration.md` and in `.issueflows/04-designs-and-guides/skill-behaviour-knobs.md`. Short note on the #214 section of `graphify-integration.md`. + +## Files to touch + +- `src/issue_flow/modes.py` — default, `read_graphify_gitignored`, seed/write into `config.toml` with a comment. +- `src/issue_flow/config.py` — `resolve_graphify_gitignored`, `seed_config_values`, `effective_config`, `template_context`. +- `src/issue_flow/config_ops.py` — `CONFIG_KEYS` entry (`bool`, `needs_update=True`). +- `src/issue_flow/surfaces.py` — managed gitignore upsert/remove, plus a “still tracked” note. +- `src/issue_flow/init.py` — call the gitignore helper from `init` / `update` (same place as `ensure_editor_gitignore`). +- `src/issue_flow/templates/skills/iflow_plan/SKILL.md.j2` and `templates/commands/iflow-plan.md.j2` — gate the refresh step on either flag. +- `src/issue_flow/templates/skills/iflow_epic/SKILL.md.j2` and `templates/commands/iflow-epic.md.j2` — same refresh before epic draft. +- `src/issue_flow/templates/skills/iflow_graphify/SKILL.md.j2` and `templates/commands/iflow-graphify.md.j2` — commit guidance when the key is on. +- `docs/configuration.md` +- `.issueflows/04-designs-and-guides/skill-behaviour-knobs.md` +- `.issueflows/04-designs-and-guides/graphify-integration.md` +- Tests: `tests/test_config.py`, `tests/test_modes.py`, `tests/test_templating.py`, `tests/test_cli.py` (show payload), plus a gitignore helper test next to the existing surfaces/init tests. + +## Test strategy + +`uv run pytest` for the new and neighbouring tests (`test_config`, `test_modes`, `test_templating`, `test_cli`, and the gitignore helper test). `uv run ruff check src/ tests/`. + +Cases: + +- Resolve default `false`; persisted `true` wins over env. +- Plan skill/command: both flags false → no refresh step; only `graphify_gitignored` true → step present; both true → one step. +- Gitignore helper: true writes the managed block; false removes it and keeps unrelated lines; idempotent second write. +- `config show` includes the key. + +## Open questions + +1. **Coexist with `auto_graphify_on_plan`.** Recommended: yes. New key gitignores `graphify-out/` and also enables the plan/epic refresh. The old key stays for refresh without gitignore. +2. **Untrack.** Recommended: print `git rm -r --cached graphify-out` when files are still tracked. Do not run it from `update`. +3. **This repo.** Recommended: leave `.issueflows/config.toml` here at the default (`false`) and keep `GRAPH_REPORT.md` tracked. Turning the knob on in issue-flow itself is a follow-up. + +Accept adopts these three recommendations. diff --git a/.issueflows/03-solved-issues/issue400_status.md b/.issueflows/03-solved-issues/issue400_status.md new file mode 100644 index 00000000..fe2fa658 --- /dev/null +++ b/.issueflows/03-solved-issues/issue400_status.md @@ -0,0 +1,16 @@ +# Issue #400 status + +- [x] Done + +## What's done + +- Captured #400 on branch `400-graphify-gitignore`. +- `graphify_gitignored` (default false) gitignores all of `graphify-out/` on `init` / `update` and makes `/iflow-plan` and epic draft run `issue-flow graphify` first. `auto_graphify_on_plan` still refreshes when the report is committed. Update prints `git rm -r --cached graphify-out` and does not run it. +- This repo's knob stays off, so `GRAPH_REPORT.md` stays tracked. +- Changelog bullet under `## [Unreleased]`. No version bump (no `publish` label). +- Essential review: new tests left unmarked (temp git / text contracts). `test_all_settings_table_lists_every_config_key_once` stays essential and covers the new config row. Registry updated. +- `uv run pytest`: 959 passed. `uv run pytest -m essential`: 24 passed. Ruff clean. + +## Remaining work + +- None. diff --git a/.issueflows/04-designs-and-guides/graphify-integration.md b/.issueflows/04-designs-and-guides/graphify-integration.md index da875e4d..b0e3da1d 100644 --- a/.issueflows/04-designs-and-guides/graphify-integration.md +++ b/.issueflows/04-designs-and-guides/graphify-integration.md @@ -90,6 +90,18 @@ gitignore `graphify-out/` can set `auto_graphify_on_plan = true` under prior-art discovery. Default remains `false`. Missing or failing graphify does not block planning. +## Whole-tree gitignore (issue #400) + +`graphify_gitignored` (default false) makes `init` / `update` write a managed +`.gitignore` block for `graphify-out/`. It does not `git rm` tracked files. +When those paths are still tracked, update prints +`git rm -r --cached graphify-out`. + +When the key is true, `/iflow-plan` and epic draft run `issue-flow graphify` +(AST `update`) before they read the graph, even if `auto_graphify_on_plan` is +false. `auto_graphify_on_plan` stays the switch for that refresh when the +report is still committed. Missing or failing graphify does not block planning. + ## Correction (2026-05-14): graphify is subcommand-based The original implementation assumed `graphify [flags…]` was the canonical "build" invocation, modeled on tools like `ruff` or `pyright`. **It is not.** The `graphify` CLI is dispatch-based — every action is a subcommand (`extract `, `update `, `watch `, `cluster-only `, …) and there is no top-level "scan this folder" mode. Running `graphify C:\some\dir` fails with `unknown command 'C:\some\dir'`. The published `/build` doc, the rules entry, the cursor-issue-workflow doc, and the README all advertised non-existent flags (`--update`, `--no-viz`, `--mode deep`, `--watch`, `--cluster-only`) that are actually subcommands or per-subcommand flags. diff --git a/.issueflows/04-designs-and-guides/skill-behaviour-knobs.md b/.issueflows/04-designs-and-guides/skill-behaviour-knobs.md index fb38d42e..3bc3a509 100644 --- a/.issueflows/04-designs-and-guides/skill-behaviour-knobs.md +++ b/.issueflows/04-designs-and-guides/skill-behaviour-knobs.md @@ -20,6 +20,7 @@ epic #269). Until Stage 2 ships, only the project file and env exist. | Cleanup defaults (`cleanup_*`) | `cleanup_include_github`, `cleanup_yes_a1`, `cleanup_yes_a2` | | Tool upgrade (event-hook name) | `on_bleeding_edge` | | Auto behaviours (`auto_*`) | `auto_switchback`, `auto_remove_worktree`, `auto_close`, `auto_cleanup`, `auto_plan`, `auto_build`, `auto_graphify_on_plan` | +| Graphify checkout | `graphify_gitignored` | | Start layout | `worktree_first` (issue #329; distinct from `auto_remove_worktree` and from worktree location #328) | | Timing / PR | `early_pr` | | Fix-session | `fix_auto_name` | @@ -40,6 +41,7 @@ epic #269). Until Stage 2 ships, only the project file and env exist. | `on_bleeding_edge` | `false` | When `true`, `/iflow-cleanup` runs `issue-flow agent self-update` after a successful FF pull (`uv tool install issue-flow@latest` then `issue-flow update`). Trailing `bleeding edge` / `no bleeding` override. Skips editable installs (issue #382) | | `suggest_graphify` | `true` | Soft GRAPH_REPORT / rebuild suggestions (never auto-run) | | `auto_graphify_on_plan` | `false` | `/iflow-plan` runs `issue-flow graphify` (AST `update`) before prior-art; missing/fail → note + continue (issue #214) | +| `graphify_gitignored` | `false` | Gitignore all of `graphify-out/` on `init`/`update`. Also runs the plan and epic refresh even when `auto_graphify_on_plan` is false. Prints `git rm -r --cached graphify-out` when paths are still tracked; does not run it (issue #400) | | `auto_switchback` | `true` | After PR, switch to default when clean (`false` ≈ always `stay`) | | `auto_remove_worktree` | `true` | After `/iflow-close` opens or merges a PR, remove the sibling issue worktree when clean (`false` = YES/NO). Skip `stay` / draft / failed merge. Never deletes the branch (issue #273) | | `worktree_first` | `true` | `/iflow-pick` / `/iflow-issue` / `/iflow-fix` start in a sibling worktree. `false` → `git switch -c` on home. Tokens `inplace` / `no worktree` / `worktree` override (issue #329) | diff --git a/.issueflows/04-designs-and-guides/test-registry.md b/.issueflows/04-designs-and-guides/test-registry.md index ab1fadc1..b605806d 100644 --- a/.issueflows/04-designs-and-guides/test-registry.md +++ b/.issueflows/04-designs-and-guides/test-registry.md @@ -43,6 +43,10 @@ current issue**. `/iflow-doctor` may audit the whole suite against this table. | `tests/test_templating.py::test_cleanup_bakes_yes_knobs` | no | no | cleanup/close skill bake of `cleanup_yes_*` / `auto_cleanup` | #388 | Config default bake; leave unmarked | | `tests/test_doc_configuration.py::test_all_settings_table_lists_every_config_key_once` | yes | yes | `docs/configuration.md` vs `CONFIG_KEYS` | #388 | Already essential; digit keys (`cleanup_yes_a1`) must match | | `tests/test_templating.py::test_issue_create_skills_use_body_file_not_bash_heredoc` | no | no | `_gh_body_file.md.j2` included from issue/fix/close | #380 | Docs/skill contract; leave unmarked | +| `tests/test_graphify_gitignore.py` (module) | no | no | `surfaces.sync_graphify_gitignore`, `note_tracked_graphify` | #400 | Temp git repo for the tracked-path note; leave unmarked | +| `tests/test_templating.py::test_iflow_plan_graphify_gitignored_refreshes_without_auto_flag` | no | no | plan / epic / graphify templates when `graphify_gitignored` | #400 | Text contract; leave unmarked | +| `tests/test_config.py` (`resolve_graphify_gitignored` asserts) | no | no | `Settings.resolve_graphify_gitignored` | #400 | Same knob-precedence tests as other `[issueflow]` flags; leave unmarked | +| `tests/test_doc_configuration.py::test_all_settings_table_lists_every_config_key_once` | yes | yes | `docs/configuration.md` vs `CONFIG_KEYS` | #400 | Already essential; a new knob missing from the table fails here | **Columns** diff --git a/HISTORY.md b/HISTORY.md index 774879d0..5c18dd8d 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -10,6 +10,7 @@ than the GitHub release notes they link to. ## [Unreleased] - Iterative fixes: docs-flow-diagram. (#397) +- Graphify totally gitignored. (#400) ## [0.5.17] - 2026-09-26 diff --git a/docs/configuration.md b/docs/configuration.md index c3ac699e..9e404a86 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -61,6 +61,7 @@ an environment-variable fallback, `ISSUEFLOW_` (for example | `on_bleeding_edge` | bool | `false` | `/iflow-cleanup` upgrades the `uv tool` install to `issue-flow@latest` and runs `issue-flow update` after a successful fast-forward pull. Opt in per run with `bleeding edge`; opt out with `no bleeding`. Skips editable installs. | | `suggest_graphify` | bool | `true` | Suggest reading `GRAPH_REPORT.md` / rebuilding graphify (never runs it). | | `auto_graphify_on_plan` | bool | `false` | `/iflow-plan` rebuilds the graphify graph (AST only) before prior-art discovery. | +| `graphify_gitignored` | bool | `false` | Gitignore all of `graphify-out/`. `/iflow-plan` and epic draft then rebuild the graph (AST only) before they read it. Does not untrack files that are already committed. | | `auto_switchback` | bool | `true` | After `/iflow-close` opens a PR, switch back to the default branch when the tree is clean (`false` ≈ always `stay`). | | `auto_remove_worktree` | bool | `true` | Close removes the issue's sibling worktree once the PR is open (or merged) and the tree is clean; `false` asks first. | | `worktree_first` | bool | `true` | `/iflow-pick`, `/iflow-issue` and `/iflow-fix` start in a sibling worktree `../-`; `false` uses `git switch -c` in your checkout. Tokens `inplace` / `worktree` override per run. | diff --git a/src/issue_flow/agent.py b/src/issue_flow/agent.py index bc1a712e..bc526cfb 100644 --- a/src/issue_flow/agent.py +++ b/src/issue_flow/agent.py @@ -5733,7 +5733,8 @@ def _print_config_guide(console: Console, cfg_path: Path) -> None: "[bold]cleanup_include_github[/bold] / [bold]cleanup_yes_a1[/bold] / " "[bold]cleanup_yes_a2[/bold] / [bold]on_bleeding_edge[/bold] / " "[bold]suggest_graphify[/bold] / " - "[bold]auto_graphify_on_plan[/bold]; " + "[bold]auto_graphify_on_plan[/bold] / " + "[bold]graphify_gitignored[/bold]; " "[bold]auto_switchback[/bold] / [bold]auto_remove_worktree[/bold] / " "[bold]worktree_first[/bold] / " "[bold]auto_close[/bold] / [bold]auto_cleanup[/bold] / " diff --git a/src/issue_flow/cli.py b/src/issue_flow/cli.py index 5b6d36e3..f5ad6e46 100644 --- a/src/issue_flow/cli.py +++ b/src/issue_flow/cli.py @@ -1389,7 +1389,8 @@ def config_add( ``noob``, ``cleanup_include_github``, ``cleanup_yes_a1``, ``cleanup_yes_a2``, ``on_bleeding_edge``, ``suggest_graphify``, - ``auto_graphify_on_plan``, ``auto_switchback``, ``auto_remove_worktree``, + ``auto_graphify_on_plan``, ``graphify_gitignored``, + ``auto_switchback``, ``auto_remove_worktree``, ``worktree_first``, ``pr_merge_method``, ``cycle_max_issues``, ``cycle_onfail``, ``cycle_nonyolo``, ``auto_adversarial_loops``, diff --git a/src/issue_flow/config.py b/src/issue_flow/config.py index fb48229d..4de51330 100644 --- a/src/issue_flow/config.py +++ b/src/issue_flow/config.py @@ -27,6 +27,7 @@ DEFAULT_ESSENTIAL_REVIEW, DEFAULT_ESSENTIAL_TESTS, DEFAULT_AUTO_GRAPHIFY_ON_PLAN, + DEFAULT_GRAPHIFY_GITIGNORED, DEFAULT_AUTO_SWITCHBACK, DEFAULT_AUTO_REMOVE_WORKTREE, DEFAULT_WORKTREE_FIRST, @@ -503,6 +504,20 @@ def resolve_auto_graphify_on_plan(self, project_root: Path) -> bool: ), ) + def resolve_graphify_gitignored(self, project_root: Path) -> bool: + """Resolve whether ``graphify-out/`` is fully gitignored.""" + persisted = modes_module.read_graphify_gitignored( + self.config_path(project_root) + ) + if persisted is not None: + return persisted + return self.user_global_or( + "graphify_gitignored", + _env_flag( + "ISSUEFLOW_GRAPHIFY_GITIGNORED", default=DEFAULT_GRAPHIFY_GITIGNORED + ), + ) + def resolve_auto_switchback(self, project_root: Path) -> bool: """Resolve whether ``/iflow-close`` switches back to the default branch.""" persisted = modes_module.read_auto_switchback(self.config_path(project_root)) @@ -987,6 +1002,10 @@ def seed_config_values(self) -> dict[str, object]: "ISSUEFLOW_AUTO_GRAPHIFY_ON_PLAN", default=DEFAULT_AUTO_GRAPHIFY_ON_PLAN, ), + "graphify_gitignored": _env_flag( + "ISSUEFLOW_GRAPHIFY_GITIGNORED", + default=DEFAULT_GRAPHIFY_GITIGNORED, + ), "auto_switchback": _env_flag( "ISSUEFLOW_AUTO_SWITCHBACK", default=DEFAULT_AUTO_SWITCHBACK ), @@ -1076,6 +1095,7 @@ def effective_config(self, project_root: Path) -> dict[str, object]: "on_bleeding_edge": self.resolve_on_bleeding_edge(project_root), "suggest_graphify": self.resolve_suggest_graphify(project_root), "auto_graphify_on_plan": self.resolve_auto_graphify_on_plan(project_root), + "graphify_gitignored": self.resolve_graphify_gitignored(project_root), "auto_switchback": self.resolve_auto_switchback(project_root), "auto_remove_worktree": self.resolve_auto_remove_worktree(project_root), "worktree_first": self.resolve_worktree_first(project_root), @@ -1182,6 +1202,7 @@ def template_context( "on_bleeding_edge": self.resolve_on_bleeding_edge(project_root), "suggest_graphify": self.resolve_suggest_graphify(project_root), "auto_graphify_on_plan": self.resolve_auto_graphify_on_plan(project_root), + "graphify_gitignored": self.resolve_graphify_gitignored(project_root), "auto_switchback": self.resolve_auto_switchback(project_root), "auto_remove_worktree": self.resolve_auto_remove_worktree(project_root), "worktree_first": self.resolve_worktree_first(project_root), diff --git a/src/issue_flow/config_ops.py b/src/issue_flow/config_ops.py index cac3f82c..094f8e04 100644 --- a/src/issue_flow/config_ops.py +++ b/src/issue_flow/config_ops.py @@ -72,6 +72,7 @@ class ConfigKeySpec: "on_bleeding_edge": ConfigKeySpec("bool"), "suggest_graphify": ConfigKeySpec("bool"), "auto_graphify_on_plan": ConfigKeySpec("bool"), + "graphify_gitignored": ConfigKeySpec("bool"), "auto_switchback": ConfigKeySpec("bool"), "auto_remove_worktree": ConfigKeySpec("bool"), "worktree_first": ConfigKeySpec("bool"), diff --git a/src/issue_flow/init.py b/src/issue_flow/init.py index 2c4b83be..55b3b0d4 100644 --- a/src/issue_flow/init.py +++ b/src/issue_flow/init.py @@ -25,6 +25,7 @@ materialize_editor_profile, materialize_user_global_both_skills, maybe_ensure_linguist_gitattributes, + maybe_sync_graphify_gitignore, write_manifest_files, ) from issue_flow.skill_ownership import foreign_skill_reason, load_stamps, stamp_key @@ -530,6 +531,7 @@ def run_init( console_io.console.print() _ensure_dotenv_file(project_root) maybe_ensure_linguist_gitattributes(project_root, settings) + maybe_sync_graphify_gitignore(project_root, settings) console_io.console.print() if not canonical: @@ -683,6 +685,7 @@ def run_update( console_io.console.print() maybe_ensure_linguist_gitattributes(project_root, settings) + maybe_sync_graphify_gitignore(project_root, settings) console_io.console.print() _graphify_postinstall(project_root, profiles, mode_obj) diff --git a/src/issue_flow/modes.py b/src/issue_flow/modes.py index f0d49a79..878afa00 100644 --- a/src/issue_flow/modes.py +++ b/src/issue_flow/modes.py @@ -79,6 +79,7 @@ DEFAULT_ON_BLEEDING_EDGE = False DEFAULT_SUGGEST_GRAPHIFY = True DEFAULT_AUTO_GRAPHIFY_ON_PLAN = False +DEFAULT_GRAPHIFY_GITIGNORED = False DEFAULT_AUTO_SWITCHBACK = True DEFAULT_AUTO_REMOVE_WORKTREE = True DEFAULT_WORKTREE_FIRST = True @@ -785,6 +786,17 @@ def read_auto_graphify_on_plan(cfg_path: Path) -> bool | None: return None +def read_graphify_gitignored(cfg_path: Path) -> bool | None: + """Return the persisted ``[issueflow].graphify_gitignored`` flag.""" + if not cfg_path.is_file(): + return None + data = tomllib.loads(cfg_path.read_text(encoding="utf-8")) + section = data.get("issueflow") + if isinstance(section, dict) and "graphify_gitignored" in section: + return bool(section.get("graphify_gitignored")) + return None + + def read_auto_switchback(cfg_path: Path) -> bool | None: """Return the persisted ``[issueflow].auto_switchback`` flag.""" if not cfg_path.is_file(): @@ -1266,6 +1278,7 @@ def write_default_config( on_bleeding_edge: bool = DEFAULT_ON_BLEEDING_EDGE, suggest_graphify: bool = DEFAULT_SUGGEST_GRAPHIFY, auto_graphify_on_plan: bool = DEFAULT_AUTO_GRAPHIFY_ON_PLAN, + graphify_gitignored: bool = DEFAULT_GRAPHIFY_GITIGNORED, auto_switchback: bool = DEFAULT_AUTO_SWITCHBACK, auto_remove_worktree: bool = DEFAULT_AUTO_REMOVE_WORKTREE, worktree_first: bool = DEFAULT_WORKTREE_FIRST, @@ -1344,6 +1357,7 @@ def write_default_config( section["on_bleeding_edge"] = on_bleeding_edge section["suggest_graphify"] = suggest_graphify section["auto_graphify_on_plan"] = auto_graphify_on_plan + section["graphify_gitignored"] = graphify_gitignored section["auto_switchback"] = auto_switchback section["auto_remove_worktree"] = auto_remove_worktree section["worktree_first"] = worktree_first @@ -1406,6 +1420,7 @@ def write_default_config( on_bleeding_edge, suggest_graphify, auto_graphify_on_plan, + graphify_gitignored, auto_switchback, auto_remove_worktree, worktree_first, @@ -1476,6 +1491,7 @@ def _commented_issueflow_table( on_bleeding_edge: bool, suggest_graphify: bool, auto_graphify_on_plan: bool, + graphify_gitignored: bool, auto_switchback: bool, auto_remove_worktree: bool, worktree_first: bool, @@ -1671,6 +1687,15 @@ def _commented_issueflow_table( ) ) table["auto_graphify_on_plan"] = auto_graphify_on_plan + table.add( + tomlkit.comment( + "When true, gitignore the whole graphify-out/ tree and run " + "issue-flow graphify before /iflow-plan and epic draft " + "(true/false; default false). Does not git rm tracked files. " + "Re-run 'issue-flow update'." + ) + ) + table["graphify_gitignored"] = graphify_gitignored table.add( tomlkit.comment( "After /iflow-close opens a PR, switch back to the default branch " diff --git a/src/issue_flow/surfaces.py b/src/issue_flow/surfaces.py index d65d8a99..5cdd176f 100644 --- a/src/issue_flow/surfaces.py +++ b/src/issue_flow/surfaces.py @@ -4,6 +4,7 @@ import json import shutil +import subprocess from collections.abc import Callable from dataclasses import dataclass from pathlib import Path @@ -48,6 +49,9 @@ _GITIGNORE_MARKER_BEGIN = "# BEGIN issue-flow editor surfaces (generated; do not edit)" _GITIGNORE_MARKER_END = "# END issue-flow editor surfaces" +_GRAPHIFY_GITIGNORE_BEGIN = "# BEGIN issue-flow graphify (generated; do not edit)" +_GRAPHIFY_GITIGNORE_END = "# END issue-flow graphify" + _LINGUIST_MARKER_BEGIN = "# BEGIN issue-flow linguist (generated; do not edit)" _LINGUIST_MARKER_END = "# END issue-flow linguist" @@ -441,3 +445,96 @@ def maybe_ensure_linguist_gitattributes(project_root: Path, settings: Settings) if not settings.resolve_linguist_attributes(project_root): return False return ensure_linguist_gitattributes(project_root) + + +def _strip_managed_block(text: str, begin: str, end: str) -> str: + """Remove one managed block, including a blank line inserted before it.""" + start = text.find(begin) + if start < 0: + return text + end_at = text.find(end, start) + if end_at < 0: + return text + end_at += len(end) + if end_at < len(text) and text[end_at] == "\n": + end_at += 1 + prefix = text[:start] + if prefix.endswith("\n\n"): + prefix = prefix[:-1] + return prefix + text[end_at:] + + +def _graphify_gitignore_block() -> str: + return "\n".join( + [ + _GRAPHIFY_GITIGNORE_BEGIN, + "graphify-out/", + _GRAPHIFY_GITIGNORE_END, + ] + ) + + +def sync_graphify_gitignore(project_root: Path, *, enabled: bool) -> bool: + """Upsert or remove the managed ``graphify-out/`` gitignore block. + + Returns True when ``.gitignore`` changed. Does not untrack files. + """ + path = project_root / ".gitignore" + existing = path.read_text(encoding="utf-8") if path.is_file() else "" + block = _graphify_gitignore_block() + if enabled: + if block in existing: + return False + base = existing + if _GRAPHIFY_GITIGNORE_BEGIN in base: + base = _strip_managed_block( + base, _GRAPHIFY_GITIGNORE_BEGIN, _GRAPHIFY_GITIGNORE_END + ) + updated = ( + base.rstrip("\n") + "\n\n" + block + "\n" if base.strip() else block + "\n" + ) + path.write_text(updated, encoding="utf-8") + console_io.console.print(" [green]write[/green] .gitignore (graphify-out/)") + return True + if _GRAPHIFY_GITIGNORE_BEGIN not in existing: + return False + updated = _strip_managed_block( + existing, _GRAPHIFY_GITIGNORE_BEGIN, _GRAPHIFY_GITIGNORE_END + ) + path.write_text(updated, encoding="utf-8") + console_io.console.print( + " [yellow]remove[/yellow] .gitignore (graphify-out/ block)" + ) + return True + + +def note_tracked_graphify(project_root: Path) -> bool: + """Print the untrack command when ``graphify-out/`` is still in the index. + + Returns True when a note was printed. Never runs ``git rm``. + """ + try: + result = subprocess.run( + ["git", "-C", str(project_root), "ls-files", "--", "graphify-out"], + check=False, + capture_output=True, + text=True, + ) + except OSError: + return False + if result.returncode != 0 or not result.stdout.strip(): + return False + console_io.console.print( + " [yellow]note[/yellow] graphify-out/ is gitignored but still tracked. " + "Untrack with: git rm -r --cached graphify-out" + ) + return True + + +def maybe_sync_graphify_gitignore(project_root: Path, settings: Settings) -> bool: + """Apply ``graphify_gitignored`` to ``.gitignore`` and note tracked paths.""" + enabled = settings.resolve_graphify_gitignored(project_root) + changed = sync_graphify_gitignore(project_root, enabled=enabled) + if enabled: + note_tracked_graphify(project_root) + return changed diff --git a/src/issue_flow/templates/commands/iflow-epic.md.j2 b/src/issue_flow/templates/commands/iflow-epic.md.j2 index 2c4464db..5c57d952 100644 --- a/src/issue_flow/templates/commands/iflow-epic.md.j2 +++ b/src/issue_flow/templates/commands/iflow-epic.md.j2 @@ -23,7 +23,9 @@ Parse the first token **before** the draft path: ## Steps The full playbook lives in `{{ agent_dir }}/skills/iflow-epic/SKILL.md` — follow it. In short: - +{% if auto_graphify_on_plan or graphify_gitignored %} +0. **Refresh knowledge graph (configured).**{% if graphify_gitignored %} `graphify_gitignored = true`: `graphify-out/` is gitignored.{% endif %}{% if auto_graphify_on_plan %} `auto_graphify_on_plan = true`.{% endif %} Before drafting, run `issue-flow graphify -C ` (AST `update`). If `graphify` is missing or exits non-zero, report and continue. Do **not** auto-run `extract`. +{% endif %} 1. **Gather context (read-only):** the anchor issue (`gh issue view `), relevant `{{ issueflows_dir }}/{{ designs_folder }}/` docs, `graphify-out/GRAPH_REPORT.md` when present. 2. **Draft** `epic_plan.md`: Goal / Constraints / Stages (each with `- Goal:`); each stage holds `### Issue:` specs with Spec, `- Goal:`, `- Model: deep|fast|default`, explicit `Depends on:` lines (`#`, or `stage issue ` placeholders), and a per-issue **yolo-fitness judgment**. Publish copies Goal + Model into GitHub issue bodies. 3. **Sizing rules:** one issue = one branch = one PR (~a day or less), crisp acceptance criteria, dependencies explicit. Detail at most 2–3 stages; sketch the rest under `## Later (unstaged)`. diff --git a/src/issue_flow/templates/commands/iflow-graphify.md.j2 b/src/issue_flow/templates/commands/iflow-graphify.md.j2 index 243c0230..0d40fd39 100644 --- a/src/issue_flow/templates/commands/iflow-graphify.md.j2 +++ b/src/issue_flow/templates/commands/iflow-graphify.md.j2 @@ -60,7 +60,7 @@ See the [graphify CLI reference](https://graphify.net/iflow-graphify-cli-command ## Constraints - Do **not** run `/iflow-graphify` automatically from `/iflow-build`, `/iflow-close`, or `/iflow`. The user opts in. -- Do **not** commit `graphify-out/cost.json` or `graphify-out/manifest.json`; both are local-only. The graph itself (`graph.json`, `graph.html`, `GRAPH_REPORT.md`) is fine to commit so teammates start with a map. +- Do **not** commit `graphify-out/cost.json` or `graphify-out/manifest.json`; both are local-only.{% if graphify_gitignored %} `graphify_gitignored = true`: do not commit any of `graphify-out/`.{% else %} The graph itself (`graph.json`, `graph.html`, `GRAPH_REPORT.md`) is fine to commit so teammates start with a map.{% endif %} - Long-running modes (`watch`) keep the process running; ask the user before launching them in an agent context. ## Output to user diff --git a/src/issue_flow/templates/commands/iflow-plan.md.j2 b/src/issue_flow/templates/commands/iflow-plan.md.j2 index 3e6f4d42..61254fa3 100644 --- a/src/issue_flow/templates/commands/iflow-plan.md.j2 +++ b/src/issue_flow/templates/commands/iflow-plan.md.j2 @@ -29,8 +29,8 @@ Optional free-form text after the command. Examples: - Run `git fetch --prune`. - Report current branch, clean/dirty working tree (`git status --porcelain`), and ahead/behind counts vs `origin/` (`git rev-list --left-right --count origin/...HEAD`). - If on the default branch, note it and suggest creating an issue branch (`git switch -c -`) — do **not** auto-run it. Planning itself does not require a branch switch. -{% if auto_graphify_on_plan %} -0.6 **Refresh knowledge graph (configured).** This project has `auto_graphify_on_plan = true`. Before prior-art discovery, run `issue-flow graphify -C ` (default AST `update`, no LLM key). If `graphify` is missing or the command exits non-zero, **report the failure and continue** — do not block planning; fall back to grep-only prior art. Do **not** auto-run `extract` (needs an API key). +{% if auto_graphify_on_plan or graphify_gitignored %} +0.6 **Refresh knowledge graph (configured).**{% if graphify_gitignored %} `graphify_gitignored = true`: `graphify-out/` is gitignored, so rebuild before prior-art.{% endif %}{% if auto_graphify_on_plan %} `auto_graphify_on_plan = true`.{% endif %} Before prior-art discovery, run `issue-flow graphify -C ` (default AST `update`, no LLM key). If `graphify` is missing or the command exits non-zero, **report the failure and continue** — do not block planning; fall back to grep-only prior art. Do **not** auto-run `extract` (needs an API key). {% endif %} 1. **Read the issue.** Load `issue_original.md` and any existing `issue_status.md`. Do not rewrite them from this command. diff --git a/src/issue_flow/templates/skills/iflow_epic/SKILL.md.j2 b/src/issue_flow/templates/skills/iflow_epic/SKILL.md.j2 index 8d36984c..b37eb724 100644 --- a/src/issue_flow/templates/skills/iflow_epic/SKILL.md.j2 +++ b/src/issue_flow/templates/skills/iflow_epic/SKILL.md.j2 @@ -36,7 +36,10 @@ Parse the first token **before** the draft path: > the GitHub lookups. ## Instructions +{% if auto_graphify_on_plan or graphify_gitignored %} +0. **Refresh knowledge graph (configured).**{% if graphify_gitignored %} `graphify_gitignored = true`: `graphify-out/` is gitignored.{% endif %}{% if auto_graphify_on_plan %} `auto_graphify_on_plan = true`.{% endif %} Before drafting, run `issue-flow graphify -C ` (AST `update`, no LLM key). If `graphify` is missing or exits non-zero, report the failure and continue. Do **not** auto-run `extract`. +{% endif %} 1. **Gather context (read-only).** Read the epic anchor (`gh issue view --repo `), skim `{{ issueflows_dir }}/{{ designs_folder }}/` for relevant design docs (cite them in the plan when they shape the approach), and — when `graphify-out/GRAPH_REPORT.md` exists — skim it before grepping.{% if grill_me_default %} Grilling is on by default for this project: run the `grill-me` interview on the epic's goal before drafting.{% endif %} 2. **Draft the staged plan** at `{{ issueflows_dir }}/{{ epics_folder }}/epic_plan.md` using exactly this structure (the publish step parses it): diff --git a/src/issue_flow/templates/skills/iflow_graphify/SKILL.md.j2 b/src/issue_flow/templates/skills/iflow_graphify/SKILL.md.j2 index 6eb88809..6e92d013 100644 --- a/src/issue_flow/templates/skills/iflow_graphify/SKILL.md.j2 +++ b/src/issue_flow/templates/skills/iflow_graphify/SKILL.md.j2 @@ -59,5 +59,7 @@ Do **not** use this skill from `/iflow-build`, `/iflow-close`, or `/iflow`. `/if - Never auto-dispatch `/iflow-graphify` from another slash command. The user opts in explicitly. - Never commit `graphify-out/cost.json` or `graphify-out/manifest.json`; they are local-only. +{% if graphify_gitignored %}- `graphify_gitignored = true`: do not commit `graphify-out/` at all. Rebuild with `issue-flow graphify` when a plan needs the graph. +{% endif %} - Long-running modes (`watch`) keep the process alive; ask the user before launching them in an agent context. - Forward extra arguments verbatim. Do **not** translate or rewrite graphify's flag set inside issue-flow. diff --git a/src/issue_flow/templates/skills/iflow_plan/SKILL.md.j2 b/src/issue_flow/templates/skills/iflow_plan/SKILL.md.j2 index 7b950ade..6e84f6e1 100644 --- a/src/issue_flow/templates/skills/iflow_plan/SKILL.md.j2 +++ b/src/issue_flow/templates/skills/iflow_plan/SKILL.md.j2 @@ -32,8 +32,8 @@ Follow this skill to **design the approach** for the focus issue before touching 1. **Find the focus issue.** Look in `{{ issueflows_dir }}/{{ current_issues_folder }}/` for `issue_original.md`. If it is missing or multiple groups are ambiguous, **stop** and ask. Suggest `/iflow-capture` first. 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 vs `origin/`. If on the default branch, suggest creating an issue branch (`git switch -c -`) but do **not** auto-run it — planning itself does not require a branch switch. -{% if auto_graphify_on_plan %} -2.5 **Refresh knowledge graph (configured).** This project has `auto_graphify_on_plan = true`. Before prior-art discovery, run `issue-flow graphify -C ` (default AST `update`, no LLM key). If `graphify` is missing or the command exits non-zero, **report the failure and continue** — do not block planning; fall back to grep-only prior art. Do **not** auto-run `extract` (needs an API key). +{% if auto_graphify_on_plan or graphify_gitignored %} +2.5 **Refresh knowledge graph (configured).**{% if graphify_gitignored %} `graphify_gitignored = true`: `graphify-out/` is gitignored, so rebuild before prior-art.{% endif %}{% if auto_graphify_on_plan %} `auto_graphify_on_plan = true`.{% endif %} Before prior-art discovery, run `issue-flow graphify -C ` (default AST `update`, no LLM key). If `graphify` is missing or the command exits non-zero, **report the failure and continue** — do not block planning; fall back to grep-only prior art. Do **not** auto-run `extract` (needs an API key). {% endif %} 3. **Read context.** Load `issue_original.md` and any existing `issue_status.md`. If `{{ issueflows_dir }}/{{ designs_folder }}/this-project.md` exists, read it for project-specific context, then skim `{{ issueflows_dir }}/{{ designs_folder }}/` for relevant design docs. diff --git a/tests/test_cli.py b/tests/test_cli.py index 2b16bbd5..93b558ec 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -3113,6 +3113,7 @@ def test_config_add_creates_defaults( assert payload["on_bleeding_edge"] is False assert payload["suggest_graphify"] is True assert payload["auto_graphify_on_plan"] is False + assert payload["graphify_gitignored"] is False assert payload["auto_switchback"] is True assert payload["auto_remove_worktree"] is True assert payload["worktree_first"] is True diff --git a/tests/test_config.py b/tests/test_config.py index c30c246c..d62868ac 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -74,6 +74,7 @@ def test_template_context_keys(tmp_path: Path) -> None: "on_bleeding_edge", "suggest_graphify", "auto_graphify_on_plan", + "graphify_gitignored", "auto_switchback", "auto_remove_worktree", "worktree_first", @@ -462,6 +463,7 @@ def test_skill_behaviour_knob_defaults( "ISSUEFLOW_CONFIRM_CHANGELOG_UPDATE", "ISSUEFLOW_DEFER_CHANGELOG", "ISSUEFLOW_AUTO_GRAPHIFY_ON_PLAN", + "ISSUEFLOW_GRAPHIFY_GITIGNORED", "ISSUEFLOW_CLEANUP_INCLUDE_GITHUB", "ISSUEFLOW_CLEANUP_YES_A1", "ISSUEFLOW_CLEANUP_YES_A2", @@ -481,6 +483,7 @@ def test_skill_behaviour_knob_defaults( assert settings.resolve_on_bleeding_edge(tmp_path) is False assert settings.resolve_suggest_graphify(tmp_path) is True assert settings.resolve_auto_graphify_on_plan(tmp_path) is False + assert settings.resolve_graphify_gitignored(tmp_path) is False assert settings.resolve_auto_switchback(tmp_path) is True assert settings.resolve_auto_remove_worktree(tmp_path) is True assert settings.resolve_worktree_first(tmp_path) is True @@ -517,6 +520,7 @@ def test_skill_behaviour_knobs_from_config(tmp_path: Path) -> None: "on_bleeding_edge = true\n" "suggest_graphify = false\n" "auto_graphify_on_plan = true\n" + "graphify_gitignored = true\n" "auto_switchback = false\n" "auto_remove_worktree = false\n" "worktree_first = false\n" @@ -549,6 +553,7 @@ def test_skill_behaviour_knobs_from_config(tmp_path: Path) -> None: assert settings.resolve_on_bleeding_edge(tmp_path) is True assert settings.resolve_suggest_graphify(tmp_path) is False assert settings.resolve_auto_graphify_on_plan(tmp_path) is True + assert settings.resolve_graphify_gitignored(tmp_path) is True assert settings.resolve_auto_switchback(tmp_path) is False assert settings.resolve_auto_remove_worktree(tmp_path) is False assert settings.resolve_worktree_first(tmp_path) is False diff --git a/tests/test_graphify_gitignore.py b/tests/test_graphify_gitignore.py new file mode 100644 index 00000000..7daff120 --- /dev/null +++ b/tests/test_graphify_gitignore.py @@ -0,0 +1,74 @@ +"""graphify_gitignored: managed .gitignore block and tracked-path note.""" + +from __future__ import annotations + +import subprocess +from pathlib import Path + +import pytest + +from issue_flow.config import Settings +from issue_flow.surfaces import note_tracked_graphify, sync_graphify_gitignore + +_BEGIN = "# BEGIN issue-flow graphify (generated; do not edit)" +_END = "# END issue-flow graphify" + + +def test_sync_writes_block_and_is_idempotent(tmp_path: Path) -> None: + (tmp_path / ".gitignore").write_text("# keep\nfoo.txt\n", encoding="utf-8") + assert sync_graphify_gitignore(tmp_path, enabled=True) is True + text = (tmp_path / ".gitignore").read_text(encoding="utf-8") + assert "# keep" in text + assert "foo.txt" in text + assert _BEGIN in text + assert "graphify-out/" in text + assert _END in text + assert sync_graphify_gitignore(tmp_path, enabled=True) is False + + +def test_sync_remove_keeps_unrelated_lines(tmp_path: Path) -> None: + (tmp_path / ".gitignore").write_text("# keep\nfoo.txt\n", encoding="utf-8") + sync_graphify_gitignore(tmp_path, enabled=True) + assert sync_graphify_gitignore(tmp_path, enabled=False) is True + text = (tmp_path / ".gitignore").read_text(encoding="utf-8") + assert _BEGIN not in text + assert "graphify-out/" not in text + assert "# keep" in text + assert "foo.txt" in text + assert sync_graphify_gitignore(tmp_path, enabled=False) is False + + +def test_sync_disabled_does_not_create_gitignore(tmp_path: Path) -> None: + assert sync_graphify_gitignore(tmp_path, enabled=False) is False + assert not (tmp_path / ".gitignore").exists() + + +def test_note_tracked_graphify_when_indexed(tmp_path: Path) -> None: + subprocess.run(["git", "init"], cwd=tmp_path, check=True, capture_output=True) + out = tmp_path / "graphify-out" + out.mkdir() + (out / "GRAPH_REPORT.md").write_text("x\n", encoding="utf-8") + subprocess.run( + ["git", "add", "graphify-out"], + cwd=tmp_path, + check=True, + capture_output=True, + ) + assert note_tracked_graphify(tmp_path) is True + + +def test_note_tracked_graphify_when_clean(tmp_path: Path) -> None: + subprocess.run(["git", "init"], cwd=tmp_path, check=True, capture_output=True) + assert note_tracked_graphify(tmp_path) is False + + +def test_graphify_gitignored_persisted_beats_env( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setenv("ISSUEFLOW_GRAPHIFY_GITIGNORED", "true") + cfg = tmp_path / ".issueflows" + cfg.mkdir() + (cfg / "config.toml").write_text( + "[issueflow]\ngraphify_gitignored = false\n", encoding="utf-8" + ) + assert Settings().resolve_graphify_gitignored(tmp_path) is False diff --git a/tests/test_modes.py b/tests/test_modes.py index 8ca8473f..4890fcfe 100644 --- a/tests/test_modes.py +++ b/tests/test_modes.py @@ -366,6 +366,7 @@ def test_write_default_config_includes_label_flow_keys(tmp_path: Path) -> None: DEFAULT_REMIND_CLEANUP, DEFAULT_RUFF_AUTOFIX, DEFAULT_AUTO_GRAPHIFY_ON_PLAN, + DEFAULT_GRAPHIFY_GITIGNORED, DEFAULT_SUGGEST_GRAPHIFY, read_confirm_version_bump, read_auto_build, @@ -392,6 +393,7 @@ def test_write_default_config_includes_label_flow_keys(tmp_path: Path) -> None: read_remind_cleanup, read_ruff_autofix, read_auto_graphify_on_plan, + read_graphify_gitignored, read_suggest_graphify, DEFAULT_DEFER_CHANGELOG, DEFAULT_ESSENTIAL_TESTS, @@ -412,6 +414,7 @@ def test_write_default_config_includes_label_flow_keys(tmp_path: Path) -> None: assert read_on_bleeding_edge(cfg) is DEFAULT_ON_BLEEDING_EDGE assert read_suggest_graphify(cfg) is DEFAULT_SUGGEST_GRAPHIFY assert read_auto_graphify_on_plan(cfg) is DEFAULT_AUTO_GRAPHIFY_ON_PLAN + assert read_graphify_gitignored(cfg) is DEFAULT_GRAPHIFY_GITIGNORED assert read_auto_switchback(cfg) is DEFAULT_AUTO_SWITCHBACK assert read_auto_remove_worktree(cfg) is DEFAULT_AUTO_REMOVE_WORKTREE assert read_worktree_first(cfg) is DEFAULT_WORKTREE_FIRST diff --git a/tests/test_templating.py b/tests/test_templating.py index bc876b44..d4bbbeb7 100644 --- a/tests/test_templating.py +++ b/tests/test_templating.py @@ -52,6 +52,7 @@ "on_bleeding_edge": False, "suggest_graphify": True, "auto_graphify_on_plan": False, + "graphify_gitignored": False, "auto_switchback": True, "auto_remove_worktree": True, "worktree_first": True, @@ -517,10 +518,36 @@ def test_iflow_plan_auto_graphify_on_plan_gated() -> None: assert "auto_graphify_on_plan = true" in on_cmd assert "issue-flow graphify -C " in on_cmd assert "auto_graphify_on_plan = true" in on_skill - assert "issue-flow graphify -C " in on_skill + assert on_cmd.count("issue-flow graphify -C ") == 1 + assert on_skill.count("issue-flow graphify -C ") == 1 assert "Do **not** auto-run `extract`" in on_skill +def test_iflow_plan_graphify_gitignored_refreshes_without_auto_flag() -> None: + """Issue #400: gitignore-all enables the plan refresh on its own.""" + ctx = {**_default_context(), "graphify_gitignored": True} + cmd = render_template("commands/iflow-plan.md.j2", ctx) + skill = render_template("skills/iflow_plan/SKILL.md.j2", ctx) + assert "graphify_gitignored = true" in cmd + assert "graphify_gitignored = true" in skill + assert "auto_graphify_on_plan = true" not in cmd + assert "issue-flow graphify -C " in skill + epic = render_template("skills/iflow_epic/SKILL.md.j2", ctx) + assert "graphify_gitignored = true" in epic + assert "issue-flow graphify -C " in epic + both = { + **_default_context(), + "auto_graphify_on_plan": True, + "graphify_gitignored": True, + } + both_skill = render_template("skills/iflow_plan/SKILL.md.j2", both) + assert both_skill.count("issue-flow graphify -C ") == 1 + graphify_cmd = render_template("commands/iflow-graphify.md.j2", ctx) + assert "do not commit any of `graphify-out/`" in graphify_cmd + off = render_template("commands/iflow-graphify.md.j2", _default_context()) + assert "fine to commit" in off + + def test_templates_reference_project_brief() -> None: """Rules, plan, start, and docs should tell agents about this-project.md.""" templates = (