diff --git a/.issueflows/03-solved-issues/issue49_original.md b/.issueflows/03-solved-issues/issue49_original.md new file mode 100644 index 0000000..7a8ded6 --- /dev/null +++ b/.issueflows/03-solved-issues/issue49_original.md @@ -0,0 +1,21 @@ +# Issue #49: add graphify + +Source: https://github.com/jepegit/issue-flow/issues/49 + +## Original issue text + + + +Graphify is a python library (package name is graphifyy). It has several cli commands (see https://graphify.net/graphify-cli-commands.html) +The code lives on https://github.com/safishamsi/graphify + +Tasks: + +Figure out how graphify can be implemented in issue-flow. + +Implement graphify as a issue-flow feature (i.e. that we for example can run `issue-flow init --with graphify` (or `issue-flow update --with graphify`)), and that it then has graphify enabled. It could be that the markdown files (jina-templates, skills, rules) also needs to be updated. And graphify should also run when doing issue-flow init or update. + +We should also add another cli command for issue-flow. It could be called 'build'. And that runs a graphify rebuilding of the graph. + + + diff --git a/.issueflows/03-solved-issues/issue49_plan.md b/.issueflows/03-solved-issues/issue49_plan.md new file mode 100644 index 0000000..9ed27fd --- /dev/null +++ b/.issueflows/03-solved-issues/issue49_plan.md @@ -0,0 +1,60 @@ +# Plan for issue #49: add graphify + +## Goal + +Wire [graphify](https://graphify.net) (PyPI: `graphifyy`, CLI: `graphify`) into issue-flow so that scaffolded projects automatically register the graphify Cursor skill, ship a new `issue-flow build` command (and `/build` slash command) that rebuilds the knowledge graph, and the existing rules / commands tell agents to consult `graphify-out/GRAPH_REPORT.md` when it exists. + +## Decisions confirmed with user + +- **Dependency model:** optional Python extra. `pyproject.toml` gains `[project.optional-dependencies] graphify = ["graphifyy>=0.7"]`. issue-flow always shells out to the `graphify` CLI; the extra just guarantees it gets installed. +- **Activation:** auto-detect, no `--with graphify` flag. If `graphify` is on `PATH` at init/update time we wire it in; otherwise we print a one-line hint. Graphify-aware lines in the scaffolded markdown are always rendered (they no-op when the graph isn't built). +- **Lifecycle depth:** medium. New `/build` slash command, `graphify cursor install` is auto-run from `init`/`update`, and `issueflow-rules.mdc` + `/issue-start` + `/issue-close` get short graphify-aware additions. No automatic `graphify .` rebuilds from `/issue-close` and no `graphify hook install`. + +## Constraints + +- Project rules (`uv` only, `uv add` / `uv sync`, `uv run`). +- Back-compat: existing `init` / `update` flows must keep working unchanged when `graphify` is not installed (no errors, just a hint). `update` must still leave `.issueflows/` issue files untouched. +- Don't add `graphifyy` as a hard dependency — it pulls a large transitive footprint. +- `graphify cursor install` must be best-effort: if it fails, we report and continue (don't fail `init`). + +## Approach + +1. **`graphify` helper module** — new `src/issue_flow/graphify.py` with `is_available`, `register_with_cursor` (best-effort `graphify cursor install`), and `run_build` (subprocess passthrough). +2. **Wire into `init` / `update`** — call a new `_graphify_postinstall(project_root)` near the end of `run_init` / `run_update`. +3. **Add `build` CLI command** — Typer command with extra-args passthrough so `issue-flow build --update`, `--no-viz`, etc. forward verbatim. +4. **New scaffolded `/build` slash command** + matching agent skill, registered in `TEMPLATE_MANIFEST`. +5. **Light edits to existing scaffold templates** — rules, `/iflow`, `/issue-start`, `/issue-close`, workflow doc. +6. **Recommended (non-blocking) dependency hint** for `graphify` in `dependencies.py`. +7. **Design decision record** under `.issueflows/04-designs-and-guides/`. + +## Files to touch + +- `pyproject.toml` — `[project.optional-dependencies] graphify = ["graphifyy>=0.7"]`. +- `src/issue_flow/cli.py` — `build` Typer command. +- `src/issue_flow/init.py` — `_graphify_postinstall` wiring. +- `src/issue_flow/graphify.py` — new module. +- `src/issue_flow/dependencies.py` — non-blocking recommended dep hint. +- `src/issue_flow/templating.py` — manifest entries for `build.md.j2` and `skills/issueflow_build/SKILL.md.j2`. +- `src/issue_flow/templates/commands/build.md.j2` — new `/build` slash command. +- `src/issue_flow/templates/commands/iflow.md.j2` — list `/build` as off-path. +- `src/issue_flow/templates/commands/issue-start.md.j2` — graphify reading hint. +- `src/issue_flow/templates/commands/issue-close.md.j2` — graphify rebuild hint. +- `src/issue_flow/templates/rules/issueflow-rules.mdc.j2` — "Knowledge graph" section. +- `src/issue_flow/templates/docs/cursor-issue-workflow.md.j2` — paragraph on graphify integration. +- `src/issue_flow/templates/skills/issueflow_build/SKILL.md.j2` — new matching skill. +- `readme.md` — integration docs, `[graphify]` extra, `build` command. +- `.issueflows/04-designs-and-guides/graphify-integration.md` — new decision record. + +## Test strategy + +- Re-run existing: `uv run pytest`, `uv run ruff check src/ tests/`. +- New tests: + - `tests/test_graphify.py` — `is_available`, `register_with_cursor` (success + failure), `run_build` (exit code + missing-CLI error). + - `tests/test_init.py` — extend with mocked `graphify.is_available()` branches. + - `tests/test_templating.py` — manifest renders new templates cleanly. + - `tests/test_cli.py` — `issue-flow build` exits 0/1 with mocked subprocess and forwards extra args. + +## Open questions + +- Build skill name `issueflow-build` (chosen for parity). +- Pure passthrough for `build` flags in v1. diff --git a/.issueflows/03-solved-issues/issue49_status.md b/.issueflows/03-solved-issues/issue49_status.md new file mode 100644 index 0000000..9e3448b --- /dev/null +++ b/.issueflows/03-solved-issues/issue49_status.md @@ -0,0 +1,39 @@ +# Status for issue #49: add graphify + +- [x] Done + +## What landed + +- **No Python dependency on graphify** (revised after initial draft — see "Plan deviations" below). graphify is treated as an external CLI, the same way `git` and `gh` are; users install it standalone via `uv tool install graphifyy`. +- **`src/issue_flow/graphify.py`** — `is_available`, `register_with_cursor` (best-effort `graphify cursor install`), and `run_build` (subprocess passthrough). Never raises; falls back to install hints when graphify is absent. +- **`src/issue_flow/dependencies.py`** — added `RECOMMENDED_DEPENDENCIES` (non-blocking) with the graphify entry and a `check_recommended` helper. +- **`src/issue_flow/init.py`** — `_graphify_postinstall` runs at the end of `run_init` and `run_update`, delegating to `register_with_cursor`. No flag plumbing. +- **`src/issue_flow/cli.py`** — new `build` Typer command with `allow_extra_args=True` so `issue-flow build [PROJECT_DIR] [--update --no-viz --mode deep ...]` forwards every flag verbatim to `graphify`. Exits `2` when graphify is missing, propagates graphify's exit code otherwise. +- **`src/issue_flow/templating.py`** — new manifest entries for `commands/build.md.j2` and `skills/issueflow_build/SKILL.md.j2` (manifest count: 21 → 23). +- **New scaffold templates** — `templates/commands/build.md.j2` and `templates/skills/issueflow_build/SKILL.md.j2`. +- **Edits to existing scaffold templates**: + - `iflow.md.j2` — lists `/build` as off-path; adds a graphify-stale hint. + - `issue-start.md.j2` — adds an optional "Knowledge graph" pre-read step pointing at `graphify-out/GRAPH_REPORT.md`. + - `issue-close.md.j2` — adds an optional "Graph freshness" suggestion in the sanity-check step. + - `issueflow-rules.mdc.j2` — new "Knowledge graph (optional, via graphify)" section. + - `cursor-issue-workflow.md.j2` — table updated, new section 7 for `/build`, skill table updated. +- **PATH-orphan detection** (added late) — `find_orphan_install()` probes well-known install dirs (`~/.local/bin`, plus a couple of Windows-specific Scripts dirs). When graphify is missing from PATH but found at a candidate location, the missing-CLI hint switches to a "found but not on PATH" message that names the directory and suggests `uv tool update-shell` plus a shell/Cursor restart. The plain "missing" branch also got an "already installed?" tail so users who just ran `uv tool install graphifyy` don't get confused. README has matching guidance. +- **Tests** — full suite green (100 passing). New files: + - `tests/test_graphify.py` — detection, register_with_cursor success/failure paths, run_build passthrough and missing-CLI handling, PATH-orphan detection (5 new tests). + - `tests/test_cli.py` — Typer CLI smoke tests for `build`. + - Extended `tests/test_init.py` (graphify register wiring, build template scaffold check, knowledge-graph rule section). + - Extended `tests/test_dependencies.py` (recommended graphify entry, `check_recommended`). + - Updated `tests/test_templating.py` manifest count and expected commands/skills. +- **Docs** — `readme.md` gains a "Optional: graphify integration" section, the directory listing, and an `issue-flow build` entry; `.issueflows/04-designs-and-guides/graphify-integration.md` captures the decisions. + +## Verified + +- `uv run pytest` — 95 passed. +- `uv run ruff check src/ tests/` — clean. + +## Plan deviations + +- **Dropped the `[project.optional-dependencies] graphify` extra** mid-implementation. Reason: `uv tool install ` only puts the host package's entry-point scripts on PATH, so `uv tool install 'issue-flow[graphify]'` would install graphifyy into issue-flow's venv but leave the `graphify` CLI invisible to the shell — the extra advertised something it could not deliver to the primary install audience. The integration now treats graphify like `git` / `gh`: an external CLI installed separately. README, `dependencies.py` install hints, and the design doc were all updated to match. +- The plan mentioned "or extend `format_missing_report`" — chose the dedicated `RECOMMENDED_DEPENDENCIES` list + `check_recommended` helper instead. Cleaner separation: required deps still block via `format_missing_report`; recommended deps only inform. +- `run_build` uses a narrow heuristic for path injection: if `extra_args` is empty *or* its first token starts with `-`, we inject the project root; otherwise we trust the user's positional. This handles `--mode deep` (where `deep` is a flag value, not a path) correctly. +- Did not add a `tests/__init__.py` for the new `templates/skills/issueflow_build/` folder (matches the existing skills, which also have none). diff --git a/.issueflows/04-designs-and-guides/graphify-integration.md b/.issueflows/04-designs-and-guides/graphify-integration.md new file mode 100644 index 0000000..907b152 --- /dev/null +++ b/.issueflows/04-designs-and-guides/graphify-integration.md @@ -0,0 +1,83 @@ +# Graphify integration: design decisions + +**Issue:** [#49 — add graphify](https://github.com/jepegit/issue-flow/issues/49) +**Status:** decided 2026-05-14, implemented in the same issue. +**Scope:** how issue-flow integrates with [graphify](https://graphify.net) (PyPI: `graphifyy`, CLI: `graphify`). + +## Context + +Graphify turns a project (code + docs + papers + images + videos) into a queryable knowledge graph that AI assistants can read instead of grepping through files. Issue #49 asked for an opt-in integration so issue-flow's scaffold sets graphify up automatically, agents know the graph exists, and there is a one-shot command to refresh it. + +## Decisions + +### 1. External CLI, no Python dependency + +issue-flow does **not** depend on `graphifyy` — not as a hard dependency and +not as an optional extra. The integration is purely a runtime PATH lookup +plus subprocess passthrough to the `graphify` CLI. Users install graphify as +its own standalone tool (`uv tool install graphifyy`), the same way they +install issue-flow. + +**Alternatives considered** + +- *Hard dependency* — pull `graphifyy` for every install. Rejected: graphify + has a large transitive footprint (tree-sitter, optional video/PDF/MCP + extras). issue-flow has 4 small dependencies today; we want to keep that. +- *Optional Python extra* (`uv tool install 'issue-flow[graphify]'`, + `pyproject.toml` declaring `[project.optional-dependencies] graphify = + ["graphifyy>=0.7"]`). Initially shipped, then **rolled back** before + release. Reason: `uv tool install ` only exposes ``'s own + entry-point scripts on PATH; extras get installed into the same venv but + their CLIs stay hidden. So `uv tool install 'issue-flow[graphify]'` would + pull graphifyy in but leave `/build` and `graphify cursor install` + broken — the extra promised something it could not deliver to the primary + install audience. Plain `pip install issue-flow[graphify]` would work, + but that is not the recommended install path. +- *`issue-flow install-graphify` helper* that runs `uv tool install + graphifyy` for the user. Rejected: too magic, picks the wrong installer + for some users, and the manual two-step install is one extra command for + what's now a fully external dependency. Same posture as `git` / `gh`. + +### 2. Auto-detect at runtime, no `--with graphify` flag + +`init` / `update` call `shutil.which("graphify")`. If the CLI is on `PATH`, they run `graphify cursor install` (best-effort; failures are reported but never abort the parent command). Otherwise they print install hints and continue. + +The graphify-flavored mentions in our scaffolded markdown (rules, `/issue-start`, `/issue-close`, `/iflow`) are **always rendered** so there is nothing to "switch on". Agents are told to consult `graphify-out/GRAPH_REPORT.md` *if it exists* — when the user has not opted in, the file is absent and the guidance is a no-op. + +**Alternatives considered** + +- *`--with graphify` flag persisted in `.env`* (the original issue suggestion). Rejected: introduces hidden state, doubles the surface area of `init`/`update` (`--with` and `--without`), and the auto-detect path achieves the same UX with less code. Surfaced this trade-off to the user; they confirmed auto-detect. +- *One-shot flag, no persistence* — same surface area as the sticky version but with worse UX (must re-pass on every `update`). + +### 3. Medium lifecycle integration + +What we ship: + +- New CLI: `issue-flow build [PROJECT_DIR] [...args]` — pure passthrough wrapper around `graphify` (forward every flag verbatim, do not re-implement graphify's flag set). +- New slash command `/build` and matching `/issueflow-build` agent skill. +- `init` / `update` auto-run `graphify cursor install` (best-effort) when graphify is on PATH. +- `issueflow-rules.mdc` gains a "Knowledge graph" section pointing at `graphify-out/GRAPH_REPORT.md`. +- `/issue-start` suggests skimming the graph report; `/issue-close` suggests `/build` after structural changes. Neither runs `graphify` automatically. + +What we deliberately **do not** ship: + +- No automatic `graphify .` from `/issue-close`. Building the graph can be slow (LLM passes for docs/PDFs) and may have cost implications; we keep it opt-in. +- No `graphify hook install`. The user can run it directly if they want post-commit rebuilds; we do not want to touch `.git/hooks` from `issue-flow`. +- No deep wrapper over graphify flags. `issue-flow build` is a thin passthrough; if graphify adds or renames flags upstream, we do not need a release. + +**Alternatives considered** + +- *Light* (only `/build` + `cursor install`, leave existing rules/commands untouched). Rejected: agents would not know the graph exists. +- *Heavy* (auto-rebuild from `/issue-close` and/or `graphify hook install`). Rejected: too much magic, surprises users who do not know their changes trigger an LLM pass. + +## Consequences + +- Two new modules: `src/issue_flow/graphify.py` (`is_available`, `register_with_cursor`, `run_build`) and a new `RECOMMENDED_DEPENDENCIES` list in `dependencies.py`. +- One new template (`commands/build.md.j2`) + one new skill (`skills/issueflow_build/SKILL.md.j2`); manifest count goes from 21 to 23. +- `issue-flow build` exits `2` (not `1`) when `graphify` is missing, to distinguish "tool not installed" from "graphify ran and failed". +- Graphify is a fast-moving upstream. Because we only shell out to the CLI, version-skew between issue-flow and graphify is harmless: agents see whatever flags the installed `graphify` supports. + +## Notes for future work + +- If we add `issue-flow status` (already on the README's Future plans), it could surface graph freshness (`graphify-out/manifest.json` mtime vs source tree) without re-implementing graphify's freshness check. +- If multi-tool support lands (Claude Code, Windsurf, etc.), `register_with_cursor` should grow a sibling `register_with_` that calls `graphify install`. diff --git a/HISTORY.md b/HISTORY.md index 152472c..4695be6 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -10,6 +10,7 @@ than the GitHub release notes they link to. ## [Unreleased] - `/issue-init` now fetches GitHub issue comments and writes a curated "Comments (curated summary)" section into `issue_original.md` (later comments win over earlier ones). New `issueflow-issue-comments` skill documents the triage rules (three buckets, noise filtering, edge cases). (#45) +- **Optional graphify integration (#49).** New `issue-flow build` CLI and `/build` slash command (plus matching `/issueflow-build` skill) wrap the [graphify](https://graphify.net) CLI. `issue-flow init` / `update` auto-run `graphify cursor install` when `graphify` is on PATH and otherwise print install hints — including PATH-orphan detection that surfaces "found at `` but not on PATH" when the user installed `graphifyy` but uv's bin directory has not been added to PATH yet. The scaffolded rules and `/issue-start` / `/issue-close` point agents at `graphify-out/GRAPH_REPORT.md` when present so they can navigate by graph instead of grepping. Graphify is treated like `git` / `gh` — install standalone with `uv tool install graphifyy`, no Python extra (an `[graphify]` extra or `uv tool install issue-flow --with graphifyy` would leave the `graphify` CLI off PATH). ## [0.2.3] - 2026-04-19 diff --git a/README.md b/README.md index 41c3289..50ec4dd 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ your-project/ issue-close.md # /issue-close — test, commit, push, PR issue-cleanup.md # /issue-cleanup — post-merge branch hygiene issue-yolo.md # /issue-yolo — all-in-one for small, low-risk issues + build.md # /build — rebuild the graphify knowledge graph (optional) skills/ # Optional Agent Skills (explicit / @ invoke) issueflow-iflow/SKILL.md issueflow-issue-init/SKILL.md @@ -36,6 +37,7 @@ your-project/ issueflow-issue-yolo/SKILL.md issueflow-version-bump/SKILL.md issueflow-history-update/SKILL.md + issueflow-build/SKILL.md rules/ issueflow-rules.mdc # Always-on Cursor rule for the workflow docs/ @@ -93,6 +95,56 @@ with `issue-flow init --skip-dep-check` (the same flag is available on `issue-flow update`), and the prompt is also auto-skipped when stdin is not a TTY (e.g. CI pipelines). +### Optional: graphify integration + +issue-flow has a lightweight integration with [graphify](https://graphify.net) +(PyPI: `graphifyy`, CLI: `graphify`) — a tool that turns the project into a +queryable knowledge graph that AI assistants can read instead of grepping +through files. The integration is **opt-in by installing `graphifyy` as its +own tool** (the same way you installed issue-flow): there is no flag, no +`.env` switch, no extras to remember. Detection is purely PATH-based. + +What `issue-flow` does when `graphify` is on PATH: + +- `issue-flow init` and `issue-flow update` run `graphify cursor install` so + the graphify Cursor skill is registered alongside the issue-flow scaffold. + If graphify is not installed, both commands just print install hints and + continue — they never block. +- A new slash command `/build` (and matching `/issueflow-build` skill) wraps + `issue-flow build`, which forwards every argument to the `graphify` CLI + verbatim (`--update`, `--no-viz`, `--mode deep`, `--watch`, …). +- The scaffolded rules and `/issue-start` mention `graphify-out/GRAPH_REPORT.md` + as a recommended pre-read when the file exists. `/build` is **off-path** — + `/iflow` never auto-dispatches to it. + +To enable, install graphify as its own standalone tool: + +```bash +uv tool install graphifyy # recommended +# or +pipx install graphifyy +# or +pip install graphifyy +``` + +> **Why not an `issue-flow[graphify]` extra (or `uv tool install issue-flow --with graphifyy`)?** +> `uv tool install` only puts the **host package's** entry-point scripts on +> PATH. An extra (or `--with graphifyy`) pulls graphifyy into issue-flow's +> venv but leaves the `graphify` CLI invisible to the shell, so `/build` +> and `graphify cursor install` would still fail. Installing graphify as +> its own tool puts a real `graphify` shim on PATH and matches how we +> treat `git` / `gh`. + +> **Just installed graphifyy and `issue-flow init` says it's still missing?** +> uv prints `~/.local/bin is not on your PATH` after the first +> `uv tool install`. Run `uv tool update-shell` (refreshes shell rc files), +> then **restart your shell and Cursor** so the new PATH takes effect. +> issue-flow's missing-CLI hint also detects this case and tells you the +> exact directory to add. + +After installing, run `issue-flow update` once so the graphify Cursor skill +gets registered. + ## Installation Requires Python 3.13+ and [uv](https://docs.astral.sh/uv/). @@ -121,6 +173,7 @@ That's it. Open the project in Cursor and start with `/iflow` (or step through ` ``` issue-flow init [PROJECT_DIR] [--force] [--skip-dep-check] issue-flow update [PROJECT_DIR] [--skip-dep-check] +issue-flow build [PROJECT_DIR] [-- ...graphify args] ``` ### `issue-flow init` @@ -142,6 +195,15 @@ Running `init` again without `--force` is safe: generated scaffold files that al Use `update` after upgrading the **issue-flow** package to refresh the packaged slash commands, Cursor rule, and `docs/cursor-issue-workflow.md` from the version you have installed. This **overwrites** those generated files (unlike a plain second `init`). It still does not modify arbitrary files under `.issueflows/` (for example your `issue*_original.md` / `issue*_status.md` files), and it creates any **new** `.issueflows/` subdirectories required by the current package. +### `issue-flow build` + +| Argument / Option | Description | +|---|---| +| `PROJECT_DIR` | Project root directory to scan with graphify. Defaults to `.`. | +| `...graphify args` | Any extra arguments are forwarded **verbatim** to the `graphify` CLI (`--update`, `--no-viz`, `--mode deep`, `--watch`, …). | + +`build` requires `graphifyy` to be installed (`uv tool install graphifyy`). When the `graphify` CLI is missing, the command prints install hints and exits with code `2`. Outputs land in `graphify-out/` (`graph.html`, `GRAPH_REPORT.md`, `graph.json`). + ### When to use which | Goal | Command | @@ -149,6 +211,7 @@ Use `update` after upgrading the **issue-flow** package to refresh the packaged | First-time setup, or add missing files only | `issue-flow init` | | Pull newer templates after `uv tool upgrade issue-flow` (or similar) | `issue-flow update` | | Replace generated scaffolds without upgrading logic | `issue-flow init --force` | +| Rebuild the graphify knowledge graph | `issue-flow build` | ## Configuration diff --git a/src/issue_flow/cli.py b/src/issue_flow/cli.py index f1984e7..9680292 100644 --- a/src/issue_flow/cli.py +++ b/src/issue_flow/cli.py @@ -5,12 +5,15 @@ from pathlib import Path import typer +from rich.console import Console app = typer.Typer( name="issue-flow", add_completion=False, ) +_console = Console() + @app.callback() def _callback() -> None: @@ -73,6 +76,39 @@ def update( run_update(project_root=project_dir, skip_dep_check=skip_dep_check) +@app.command( + context_settings={ + "allow_extra_args": True, + "ignore_unknown_options": True, + }, +) +def build( + ctx: typer.Context, + project_dir: Path = typer.Argument( + default=Path("."), + help=( + "Project root directory to scan with graphify. " + "Defaults to the current directory." + ), + exists=True, + file_okay=False, + resolve_path=True, + ), +) -> None: + """Rebuild the graphify knowledge graph for the project. + + Forwards every extra argument to the ``graphify`` CLI verbatim, so + flags like ``--update``, ``--no-viz``, ``--mode deep``, or + ``--watch`` pass straight through. Requires ``graphify`` to be on + ``PATH`` (install with ``uv tool install graphifyy``). + """ + from issue_flow.graphify import run_build + + exit_code = run_build(project_dir, ctx.args, _console) + if exit_code != 0: + raise typer.Exit(code=exit_code) + + def main() -> None: """Entry point for the `issue-flow` console script.""" app() diff --git a/src/issue_flow/dependencies.py b/src/issue_flow/dependencies.py index ddd1097..4bef70b 100644 --- a/src/issue_flow/dependencies.py +++ b/src/issue_flow/dependencies.py @@ -70,6 +70,42 @@ class Dependency: ) +# Recommended (not required) external CLIs. Missing entries here only +# trigger a printed hint during ``init``/``update``; they never block +# the scaffold or prompt for confirmation. Used by +# :mod:`issue_flow.graphify` so the data lives next to the required +# tools. +RECOMMENDED_DEPENDENCIES: tuple[Dependency, ...] = ( + Dependency( + name="Graphify", + command="graphify", + purpose=( + "Powers the optional /build slash command and the " + "graphify-out/GRAPH_REPORT.md knowledge graph that " + "/issue-start can consult. Install it standalone (the same " + "way you installed issue-flow) so its CLI ends up on PATH." + ), + docs_url="https://graphify.net", + install_hints=( + ("Recommended (uv)", "uv tool install graphifyy"), + ("pipx", "pipx install graphifyy"), + ("pip", "pip install graphifyy"), + ), + ), +) + + +def check_recommended( + dependencies: tuple[Dependency, ...] = RECOMMENDED_DEPENDENCIES, +) -> list[Dependency]: + """Return the subset of ``dependencies`` not on ``PATH``. + + Mirrors :func:`check_dependencies` but for the recommended (never + blocking) list. ``shutil.which`` only — no subprocess. + """ + return [dep for dep in dependencies if shutil.which(dep.command) is None] + + def check_dependencies( dependencies: tuple[Dependency, ...] = REQUIRED_DEPENDENCIES, ) -> list[Dependency]: diff --git a/src/issue_flow/graphify.py b/src/issue_flow/graphify.py new file mode 100644 index 0000000..6fb68f5 --- /dev/null +++ b/src/issue_flow/graphify.py @@ -0,0 +1,249 @@ +"""Graphify integration for issue-flow. + +Graphify (PyPI: ``graphifyy``, CLI: ``graphify``) turns a project folder +into a queryable knowledge graph that AI coding assistants can read +instead of grepping through files. issue-flow does **not** bundle +graphify and does not declare it as a Python dependency — neither hard +nor optional-extra. Users install ``graphifyy`` as its own standalone +tool (``uv tool install graphifyy``), the same way they install +issue-flow. The wiring here is **best-effort**: if ``graphify`` is on +``PATH``, ``init``/``update`` register it with Cursor; otherwise we +just print a hint and continue. + +This module owns three small responsibilities: + +* :func:`is_available` — cheap PATH lookup, no subprocess. +* :func:`register_with_cursor` — runs ``graphify cursor install`` from + ``init``/``update``. Never raises; failures are logged and ignored. +* :func:`run_build` — backs the ``issue-flow build`` CLI command and the + ``/build`` slash command. Forwards every extra arg verbatim so the + upstream graphify flag set is the source of truth. +""" + +from __future__ import annotations + +import os +import shutil +import subprocess +import sys +from pathlib import Path +from typing import Sequence + +from rich.console import Console + +from issue_flow.dependencies import RECOMMENDED_DEPENDENCIES + +GRAPHIFY_COMMAND = "graphify" +GRAPHIFY_PYPI = "graphifyy" + + +def _graphify_dependency(): + """Return the ``Dependency`` entry for graphify from the recommended list.""" + for dep in RECOMMENDED_DEPENDENCIES: + if dep.command == GRAPHIFY_COMMAND: + return dep + raise RuntimeError( + "graphify is missing from RECOMMENDED_DEPENDENCIES; " + "this should never happen." + ) + + +def is_available() -> bool: + """True iff the ``graphify`` CLI is on the user's ``PATH``.""" + return shutil.which(GRAPHIFY_COMMAND) is not None + + +def _candidate_install_locations() -> list[Path]: + """Well-known install locations to probe when ``graphify`` is missing from PATH. + + Covers the common case where the user did install ``graphifyy`` but + the install directory was never added to ``PATH`` (e.g. fresh + ``uv tool install`` followed by no ``uv tool update-shell`` and no + shell restart). + + The list is best-effort and intentionally short. We do not try to + enumerate every Python user-base layout — ``uv tool`` and modern + ``pipx`` both default to ``~/.local/bin``, which catches the vast + majority of installs across Linux, macOS, and Windows. + """ + home = Path.home() + exe = ".exe" if sys.platform == "win32" else "" + binary = f"{GRAPHIFY_COMMAND}{exe}" + + candidates: list[Path] = [ + home / ".local" / "bin" / binary, + ] + + if sys.platform == "win32": + # pipx default on Windows historically used %USERPROFILE%\AppData\Roaming\Python\Scripts. + appdata = os.environ.get("APPDATA") + if appdata: + candidates.append(Path(appdata) / "Python" / "Scripts" / binary) + # pip --user fallback. + candidates.append(home / "AppData" / "Roaming" / "Python" / "Scripts" / binary) + + return candidates + + +def find_orphan_install() -> Path | None: + """Return the path of an installed-but-not-on-PATH ``graphify`` binary, if any. + + ``None`` if no candidate location contains a ``graphify`` executable + or if ``graphify`` is already on PATH (in which case this question + is moot). + """ + if is_available(): + return None + for path in _candidate_install_locations(): + try: + if path.is_file(): + return path + except OSError: + # Permission errors or weird filesystem states — keep looking. + continue + return None + + +def _print_install_hints(console: Console) -> None: + """Print the install / not-on-PATH hint block. + + Two flavors: + + * **Not installed** — print the normal install snippets. + * **Installed but not on PATH** — point at the orphan binary and + tell the user to run ``uv tool update-shell`` (or restart their + shell / Cursor) so the new directory becomes visible. + """ + dep = _graphify_dependency() + + orphan = find_orphan_install() + if orphan is not None: + console.print( + f" [yellow]Found[/yellow] [cyan]{orphan}[/cyan] but its directory is not on [bold]PATH[/bold]." + ) + console.print( + f" [dim]Fix:[/dim] add [cyan]{orphan.parent}[/cyan] to PATH, " + "then restart your shell (and Cursor)." + ) + console.print( + " [dim]With uv:[/dim] [green]uv tool update-shell[/green] " + "(refreshes the shell rc files; restart afterwards)." + ) + console.print(f" [dim]Docs:[/dim] [blue]{dep.docs_url}[/blue]") + return + + console.print( + f" [dim]Install graphify to enable:[/dim] " + f"[bold]{dep.command}[/bold] not found on PATH." + ) + for label, snippet in dep.install_hints: + console.print(f" - {label}: [green]{snippet}[/green]") + console.print( + " [dim]Already installed?[/dim] If you just ran " + "[green]uv tool install graphifyy[/green], make sure uv's bin " + "directory is on PATH ([green]uv tool update-shell[/green]) and " + "restart your shell (and Cursor) so the new tool is picked up." + ) + console.print(f" [dim]Docs:[/dim] [blue]{dep.docs_url}[/blue]") + + +def register_with_cursor(project_root: Path, console: Console) -> bool: + """Best-effort ``graphify cursor install`` in ``project_root``. + + Returns ``True`` when the install command was attempted and exited + cleanly, ``False`` otherwise. Never raises — graphify is optional, + so a failure here must not break ``issue-flow init`` / ``update``. + """ + if not is_available(): + console.print( + f" [dim]skip[/dim] graphify integration " + f"([cyan]{GRAPHIFY_COMMAND}[/cyan] not on PATH)" + ) + _print_install_hints(console) + return False + + console.print( + f" [green]run[/green] {GRAPHIFY_COMMAND} cursor install" + ) + try: + result = subprocess.run( + [GRAPHIFY_COMMAND, "cursor", "install"], + cwd=project_root, + check=False, + capture_output=True, + text=True, + ) + except OSError as exc: + console.print( + f" [yellow]warn[/yellow] could not run " + f"[cyan]{GRAPHIFY_COMMAND} cursor install[/cyan]: {exc}" + ) + return False + + if result.returncode != 0: + console.print( + f" [yellow]warn[/yellow] " + f"[cyan]{GRAPHIFY_COMMAND} cursor install[/cyan] exited with " + f"code {result.returncode}; continuing." + ) + if result.stderr: + # Indent stderr so it visually nests under the warning. + for line in result.stderr.strip().splitlines()[:5]: + console.print(f" [dim]{line}[/dim]") + return False + + console.print( + " [green]ok[/green] graphify Cursor skill registered" + ) + return True + + +def run_build( + project_root: Path, + extra_args: Sequence[str], + console: Console, +) -> int: + """Run ``graphify [extra_args...]`` and return its exit code. + + When the user supplies an explicit path in ``extra_args`` (e.g. + ``issue-flow build ./docs``), it is forwarded as-is and we do not + inject the project root. Otherwise the project root is passed + explicitly so graphify knows what to scan even if the agent's CWD + differs from the project root. + + Returns ``2`` and prints install hints when graphify is missing. + Re-raises ``KeyboardInterrupt`` so users can ^C a long build. + """ + if not is_available(): + console.print( + "[bold yellow]Graphify is not installed.[/bold yellow] " + f"The [cyan]{GRAPHIFY_COMMAND}[/cyan] CLI was not found on PATH." + ) + _print_install_hints(console) + return 2 + + cmd: list[str] = [GRAPHIFY_COMMAND] + args_list = list(extra_args) + # Only inject the project root when the user did not supply a leading + # positional argument. We use a deliberately narrow rule (first token + # is a flag, or there are no tokens) so we do not misclassify a flag + # value like ``deep`` after ``--mode`` as a path. + has_explicit_path = bool(args_list) and not args_list[0].startswith("-") + if not has_explicit_path: + cmd.append(str(project_root)) + cmd.extend(args_list) + + console.print( + "[dim]running:[/dim] [bold]" + + " ".join(cmd) + + "[/bold]\n" + ) + try: + result = subprocess.run(cmd, cwd=project_root, check=False) + except OSError as exc: + console.print( + f"[red]error[/red] could not invoke [cyan]{GRAPHIFY_COMMAND}[/cyan]: {exc}" + ) + return 1 + + return result.returncode diff --git a/src/issue_flow/init.py b/src/issue_flow/init.py index 1719bf1..6547b4d 100644 --- a/src/issue_flow/init.py +++ b/src/issue_flow/init.py @@ -13,6 +13,7 @@ check_dependencies, prompt_or_skip, ) +from issue_flow.graphify import register_with_cursor as graphify_register_with_cursor from issue_flow.templating import ( TEMPLATE_MANIFEST, render_template, @@ -184,6 +185,9 @@ def run_init( console.print() _ensure_dotenv_file(project_root) + console.print() + _graphify_postinstall(project_root) + console.print() if written_files: console.print(f"[bold green]Created {len(written_files)} file(s).[/bold green]") @@ -234,6 +238,9 @@ def run_update(project_root: Path, skip_dep_check: bool = False) -> None: written_files, _skipped = _write_manifest_files(project_root, context, force=True) + console.print() + _graphify_postinstall(project_root) + console.print() if written_files: console.print( @@ -248,6 +255,20 @@ def run_update(project_root: Path, skip_dep_check: bool = False) -> None: ) +def _graphify_postinstall(project_root: Path) -> None: + """Best-effort graphify integration step for ``run_init`` / ``run_update``. + + Auto-detects the ``graphify`` CLI (the user opts in by installing + ``graphifyy``; there is no flag). When present, runs + ``graphify cursor install`` so the graphify Cursor skill is + registered alongside issue-flow's own scaffold. When absent, + :func:`register_with_cursor` itself prints install hints. Never + raises and never aborts the parent ``init`` / ``update``. + """ + console.print("[bold]Graphify integration[/bold]") + graphify_register_with_cursor(project_root, console) + + def _dependency_gate(skip_dep_check: bool) -> bool: """Run the external-CLI dependency check and decide whether to proceed. diff --git a/src/issue_flow/templates/commands/build.md.j2 b/src/issue_flow/templates/commands/build.md.j2 new file mode 100644 index 0000000..5546619 --- /dev/null +++ b/src/issue_flow/templates/commands/build.md.j2 @@ -0,0 +1,63 @@ +# Rebuild the project's knowledge graph (`/build`) + +`/build` rebuilds the [graphify](https://graphify.net) knowledge graph for this project so the assistant can navigate by graph instead of grepping through files. Outputs land in `graphify-out/` (`graph.html`, `GRAPH_REPORT.md`, `graph.json`). + +This is an **off-path** command — the lifecycle dispatcher (`/iflow`) never auto-runs it. Invoke it explicitly when the project's structure has changed enough that the existing graph is stale (new modules, large refactors, new docs/papers added). + +## Input + +Optional free-form text after the command. Forwarded verbatim to `graphify`. Common combinations: + +- **No extra text** — full rebuild of the project root. +- **`./subdir`** — rebuild for a sub-directory (e.g. `/build ./src`). +- **`--update`** — re-extract only files that changed since the last build (much faster). +- **`--no-viz`** — skip the HTML; produce report + JSON only. +- **`--mode deep`** — more aggressive relationship extraction. +- **`--cluster-only`** — rerun clustering on the existing graph without re-extraction. +- **`--watch`** — auto-sync as files change (long-running). + +See the [graphify CLI reference](https://graphify.net/graphify-cli-commands.html) for the full flag set. + +## Steps + +1. **Preferred path: `issue-flow build`**. From the project root, run: + + ```bash + issue-flow build + ``` + + Pass any of the flags above after the project dir, e.g. `issue-flow build . --update --no-viz`. Extra args are forwarded verbatim to `graphify`. + +2. **Fallback: call `graphify` directly** if `issue-flow` is not on PATH: + + ```bash + graphify . + ``` + + On PowerShell, drop the leading slash: write `graphify .` (a leading `/` is parsed as a path separator). + +3. **Verify outputs.** After a successful run there should be a `graphify-out/` folder with at least `graph.html`, `GRAPH_REPORT.md`, and `graph.json`. Skim `GRAPH_REPORT.md` once to confirm the run picked up new modules or docs. + +4. **If `graphify` is not installed** (`issue-flow build` exits with a "not on PATH" error), suggest the user install it: + + ```bash + uv tool install graphifyy # recommended + pipx install graphifyy + pip install graphifyy + ``` + + `graphifyy` (double-y) is the official PyPI package; the CLI is still `graphify`. After installing, re-run `issue-flow init` (or `issue-flow update`) so `graphify cursor install` registers the graphify Cursor skill. + +## Constraints + +- Do **not** run `/build` automatically from `/issue-start`, `/issue-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. +- Long-running flags (`--watch`) keep the process running; ask the user before launching them in an agent context. + +## Output to user + +Report: +- whether the build ran (or was skipped because graphify is missing) +- the exit code from `graphify` +- the size / mtime of `graphify-out/graph.json` (rough freshness check) +- a short summary of new highlights from `GRAPH_REPORT.md` if the user asked for one diff --git a/src/issue_flow/templates/commands/iflow.md.j2 b/src/issue_flow/templates/commands/iflow.md.j2 index 8a6c980..c391d42 100644 --- a/src/issue_flow/templates/commands/iflow.md.j2 +++ b/src/issue_flow/templates/commands/iflow.md.j2 @@ -2,7 +2,7 @@ `/iflow` inspects the state of the focus issue and **dispatches** to the next logical command in the linear lifecycle — `/issue-init`, `/issue-plan`, `/issue-start`, or `/issue-close`. It never does work those commands don't already do; it just picks the right one for you. -Off-path commands (`/issue-pause`, `/issue-cleanup`, `/issue-yolo`) are **not** auto-dispatched. Invoke them directly when you need them. +Off-path commands (`/issue-pause`, `/issue-cleanup`, `/issue-yolo`, `/build`) are **not** auto-dispatched. Invoke them directly when you need them. Long-lived design docs, design decisions, and project good-practices live under `{{ issueflows_dir }}/{{ designs_folder }}/`. The downstream commands (`/issue-plan`, `/issue-start`, `/issue-close`) read from and add to that folder as they run; `/iflow` itself does not touch it. @@ -53,11 +53,12 @@ Optional free-form text after the command. `/iflow` forwards the raw trailing te - state **D** + PR likely merged → "after the PR merges, run `/issue-cleanup`" - mid-stream context switch needed → "to park this work, run `/issue-pause`" - tiny fix you want in one shot → "consider `/issue-yolo` next time" + - `graphify-out/GRAPH_REPORT.md` looks stale (large refactor, new modules) → "consider `/build` to refresh the graph" ## Constraints - `/iflow` never skips a downstream command's own prompts. If the downstream step asks a question, surface it normally. -- `/iflow` never auto-dispatches to `/issue-pause`, `/issue-cleanup`, or `/issue-yolo`. Those are explicit choices only. +- `/iflow` never auto-dispatches to `/issue-pause`, `/issue-cleanup`, `/issue-yolo`, or `/build`. Those are explicit choices only. - If the focus issue cannot be resolved (multiple active issues, branch ambiguous), stop and ask. Do not pick one silently. - Do not modify files beyond what the downstream command would normally modify. `/iflow` itself writes nothing. diff --git a/src/issue_flow/templates/commands/issue-close.md.j2 b/src/issue_flow/templates/commands/issue-close.md.j2 index bea55af..ff28dd3 100644 --- a/src/issue_flow/templates/commands/issue-close.md.j2 +++ b/src/issue_flow/templates/commands/issue-close.md.j2 @@ -22,6 +22,7 @@ Other optional notes still apply: branch name, PR title, draft PR, skip issue do - Run tests and any checks you rely on (e.g. `uv run pytest`). - Skim the diff so the commit matches what you intend to ship. - Confirm that any design decisions or good-practices that emerged from this issue are captured under `{{ issueflows_dir }}/{{ designs_folder }}/` before committing. If something is missing, add it now (short markdown: context, decision, alternatives, link back to the issue). + - **Graph freshness (optional).** If this change touched the project's structure (new modules, big refactor, removed files) and `graphify-out/` exists, suggest the user run `/build` (or `issue-flow build --update`) before pushing so teammates pull a current `GRAPH_REPORT.md`. Do not run `/build` automatically — it is opt-in. Skip this bullet entirely if `graphify-out/` is not present. 2. **Optional version bump** (only if the user asked for it in the command input) - Read `{{ agent_dir }}/skills/issueflow-version-bump/SKILL.md` and follow it. diff --git a/src/issue_flow/templates/commands/issue-start.md.j2 b/src/issue_flow/templates/commands/issue-start.md.j2 index 81bcca7..0f870fd 100644 --- a/src/issue_flow/templates/commands/issue-start.md.j2 +++ b/src/issue_flow/templates/commands/issue-start.md.j2 @@ -39,6 +39,7 @@ If additional input is added, use that as implementation hints (scope, constrain Wait for an explicit choice. On **(a)**, run the `/issue-plan` flow first (including its user-confirmation stop), then return here. On **(b)**, add a short `- Skipped /issue-plan on ` note to `issue_status.md` and continue. On **(c)**, stop. 2. **Implement** the plan. Prefer minimal, focused diffs. Match existing code style and tooling. Follow project rules under `{{ agent_dir }}/rules/issueflow-rules.mdc` (e.g. `uv run` for Python, `uv add` / `uv remove` / `uv sync` for dependencies). + - **Knowledge graph (optional).** If `graphify-out/GRAPH_REPORT.md` exists, skim it before grepping the codebase — it lists god-nodes and surprising connections that often point straight at the files you'll touch. If the project's structure has changed materially since the last graph build, consider running `/build` (or `issue-flow build`) before diving in. If `graphify-out/` does not exist, ignore this step (the integration is opt-in). - **Designs and guides.** Read any relevant files under `{{ issueflows_dir }}/{{ designs_folder }}/` before making non-trivial decisions. When the work produces a new design decision or establishes a project good-practice (one the plan flagged, or one that only became clear during implementation), add or update a short markdown file under `{{ issueflows_dir }}/{{ designs_folder }}/`: context, the decision, alternatives considered, and a link back to this issue. Keep it terse. 3. **Update the status file.** After meaningful progress, update (or create) `issue_status.md` under `{{ issueflows_dir }}/{{ current_issues_folder }}/` with a `- [ ] Done` checkbox that stays unchecked until fully resolved. Record what has landed and what remains so `/issue-pause` or `/issue-close` has accurate context. diff --git a/src/issue_flow/templates/docs/cursor-issue-workflow.md.j2 b/src/issue_flow/templates/docs/cursor-issue-workflow.md.j2 index fc358f0..5069295 100644 --- a/src/issue_flow/templates/docs/cursor-issue-workflow.md.j2 +++ b/src/issue_flow/templates/docs/cursor-issue-workflow.md.j2 @@ -1,12 +1,12 @@ # Cursor issue workflow (slash commands) -This repo uses eight Cursor **slash commands** under `{{ agent_dir }}/commands/` that line up with how we track GitHub issues in `{{ issueflows_dir }}/{{ current_issues_folder }}/`. +This repo uses nine Cursor **slash commands** under `{{ agent_dir }}/commands/` that line up with how we track GitHub issues in `{{ issueflows_dir }}/{{ current_issues_folder }}/`. **Quick start: just run `/iflow`.** It inspects the state of the focus issue and dispatches to the right linear-flow command (`/issue-init`, `/issue-plan`, `/issue-start`, or `/issue-close`) — so you don't have to remember which step is next. | Command | File | Role | |--------|------|------| -| `/iflow` | `iflow.md` | **Smart dispatcher.** Detect current state and run `/issue-init`, `/issue-plan`, `/issue-start`, or `/issue-close` automatically. Never auto-dispatches to pause / cleanup / yolo. | +| `/iflow` | `iflow.md` | **Smart dispatcher.** Detect current state and run `/issue-init`, `/issue-plan`, `/issue-start`, or `/issue-close` automatically. Never auto-dispatches to pause / cleanup / yolo / build. | | `/issue-init` | `issue-init.md` | Pull an issue from GitHub into the repo as a local markdown file and tidy older current issues. | | `/issue-plan` | `issue-plan.md` | Write a structured `issue_plan.md` and get explicit user confirmation before any code is touched. | | `/issue-start` | `issue-start.md` | Implement the confirmed plan (no planning step of its own any more). | @@ -14,6 +14,7 @@ This repo uses eight Cursor **slash commands** under `{{ agent_dir }}/commands/` | `/issue-close` | `issue-close.md` | Finish: tests, optional semver bump (`uv version --bump …`), `{{ history_file }}` update, issue-folder housekeeping, commit, push, PR. | | `/issue-cleanup` | `issue-cleanup.md` | Post-merge hygiene: switch to default, `git pull --ff-only`, `git fetch --prune`, delete merged local branches (single consolidated confirm). | | `/issue-yolo` | `issue-yolo.md` | All-in-one for small, low-risk issues: chains `init → plan → start → close` with up-front safeguards and a single confirmation. | +| `/build` | `build.md` | **Off-path.** Rebuild the [graphify](https://graphify.net) knowledge graph (`graphify-out/graph.html`, `GRAPH_REPORT.md`, `graph.json`). Wraps `issue-flow build` / `graphify`. Optional: only meaningful when `graphifyy` is installed. | --- @@ -33,6 +34,7 @@ This repo uses eight Cursor **slash commands** under `{{ agent_dir }}/commands/` | `issueflow-issue-yolo` | `/issueflow-issue-yolo` | Chain `init → plan → start → close` with safeguards. | | `issueflow-version-bump` | `@issueflow-version-bump` (often used from `/issue-close`) | Bump `[project]` version in `pyproject.toml` via `uv version --bump patch|minor|major`. | | `issueflow-history-update` | `@issueflow-history-update` (used from `/issue-close`) | Append an entry to `## [Unreleased]` in `{{ history_file }}`, or promote it to a new `## [x.y.z] - YYYY-MM-DD` release section when a version bump happened. | +| `issueflow-build` | `/issueflow-build` | Same flow as `/build`: rebuild the graphify knowledge graph for the project. Off-path; never auto-dispatched. | 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. @@ -190,7 +192,29 @@ The bump runs **after** tests and **before** issue-folder moves and **before** c --- -## 7. `/issue-yolo` — all-in-one for small issues +## 7. `/build` — rebuild the knowledge graph (optional) + +**When:** The project has the optional [graphify](https://graphify.net) integration enabled (the `graphify` CLI is on `PATH` and a `graphify-out/` folder is present), and the graph has gone stale relative to the source tree. + +**What you pass:** Optional graphify args, forwarded verbatim. Common picks: + +- *(nothing)* — full rebuild of the project root. +- `./subdir` — restrict the build to a sub-directory. +- `--update` — re-extract only files changed since the last build. +- `--no-viz` — skip the HTML; produce report + JSON only. +- `--mode deep` — more aggressive relationship extraction. + +**What the assistant does:** + +1. Runs `issue-flow build` (which shells out to the `graphify` CLI). If `issue-flow` is unavailable, falls back to `graphify .` directly. +2. If `graphify` is not installed, prints install hints (`uv tool install graphifyy`) and stops — never silently retries. +3. Verifies that `graphify-out/graph.html`, `GRAPH_REPORT.md`, and `graph.json` exist after a successful run. + +**Result:** A refreshed `graphify-out/` so `/issue-start` can navigate by graph instead of grepping. `/build` is **off-path** — `/iflow`, `/issue-start`, and `/issue-close` may *suggest* a rebuild but never invoke `/build` automatically. + +--- + +## 8. `/issue-yolo` — all-in-one for small issues **When:** The change is genuinely small and low-risk (typo, one-line fix, doc tweak) and you want to skip the usual checkpoints. For anything bigger, use the individual commands. diff --git a/src/issue_flow/templates/rules/issueflow-rules.mdc.j2 b/src/issue_flow/templates/rules/issueflow-rules.mdc.j2 index f347f3f..5da588a 100644 --- a/src/issue_flow/templates/rules/issueflow-rules.mdc.j2 +++ b/src/issue_flow/templates/rules/issueflow-rules.mdc.j2 @@ -123,3 +123,13 @@ Long-lived design docs, design decisions, and project "good practices" live unde - 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. + + +### Knowledge graph (optional, via [graphify](https://graphify.net)) + +If a `graphify-out/` folder exists in the project root, the project has the optional [graphify](https://graphify.net) integration enabled and a knowledge graph is available alongside the source. + +- **Before grepping**, skim `graphify-out/GRAPH_REPORT.md`. It surfaces god-nodes (most-connected concepts), surprising cross-module connections, and suggested questions the graph can answer — often a faster way to locate the files an issue actually touches than full-text search. +- **`/build`** (slash command) or **`issue-flow build`** (CLI) rebuild the graph; both forward extra args (`--update`, `--no-viz`, `--mode deep`, etc.) verbatim to the `graphify` CLI. +- `/build` is **off-path**: never auto-dispatched by `/iflow`, `/issue-start`, or `/issue-close`. It is the user's call. `/issue-start` may *suggest* skimming `GRAPH_REPORT.md`; `/issue-close` may *suggest* a rebuild after large structural changes — neither runs `graphify` automatically. +- If `graphify-out/` is not present, ignore graph-related guidance entirely. The integration is opt-in (install with `uv tool install graphifyy`, then `issue-flow update` to register the graphify Cursor skill). diff --git a/src/issue_flow/templates/skills/issueflow_build/SKILL.md.j2 b/src/issue_flow/templates/skills/issueflow_build/SKILL.md.j2 new file mode 100644 index 0000000..6c7be08 --- /dev/null +++ b/src/issue_flow/templates/skills/issueflow_build/SKILL.md.j2 @@ -0,0 +1,61 @@ +--- +name: issueflow-build +description: >- + Run the /build slash command: rebuild the graphify knowledge graph for the + project (graphify-out/graph.html, GRAPH_REPORT.md, graph.json) by shelling out + to `issue-flow build` (or `graphify` directly). Off-path: never auto-dispatched + by /iflow. Forwards trailing args verbatim to graphify. +disable-model-invocation: true +--- + +# issue-flow — graph rebuild (`/build`) + +Follow this skill when the user wants to refresh the project's [graphify](https://graphify.net) knowledge graph. Matches `{{ agent_dir }}/commands/build.md`. + +## When to use + +- The user runs `/build`, mentions "rebuild the graph", "refresh graphify", "regenerate `GRAPH_REPORT.md`", or similar. +- The project has a `graphify-out/` folder that is stale (large refactor, new modules, new docs/papers added) and the user asks to update it. +- The user installed `graphifyy` for the first time and wants to produce the initial graph. + +Do **not** use this skill from `/issue-start`, `/issue-close`, or `/iflow`. `/build` is opt-in only. + +## Instructions + +1. **Prefer `issue-flow build`** from the project root: + + ```bash + issue-flow build + ``` + + Any extra args from the user (`--update`, `--no-viz`, `--mode deep`, `./subdir`, etc.) pass straight through to `graphify`. Do not invent new wrapper flags. + +2. **Fallback to `graphify` directly** when `issue-flow` is unavailable: + + ```bash + graphify . + ``` + + On Windows PowerShell drop any leading `/` (use `graphify .`, not `/graphify .`). + +3. **Handle missing graphify gracefully.** If the run reports `graphify` is not on PATH, do **not** retry blindly. Tell the user to install it once: + + ```bash + uv tool install graphifyy # recommended + pipx install graphifyy + pip install graphifyy + ``` + + `graphifyy` (double-y) is the official PyPI package; the CLI is still `graphify`. After installing, suggest `issue-flow update` so `graphify cursor install` registers the graphify Cursor skill alongside this one. + +4. **Verify and report.** + - Confirm `graphify-out/graph.json`, `graphify-out/graph.html`, and `graphify-out/GRAPH_REPORT.md` exist after a successful run. + - Surface non-zero exit codes verbatim; do not silently retry. + - When the user asks "what changed?", skim `GRAPH_REPORT.md` (god nodes, surprising connections) for a short summary. + +## Constraints + +- Never auto-dispatch `/build` from another slash command. The user opts in explicitly. +- Never commit `graphify-out/cost.json` or `graphify-out/manifest.json`; they are local-only. +- 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/templating.py b/src/issue_flow/templating.py index 1b5ef70..76b367e 100644 --- a/src/issue_flow/templating.py +++ b/src/issue_flow/templating.py @@ -78,6 +78,7 @@ def render_template(template_name: str, context: dict[str, str]) -> str: ("commands/issue-close.md.j2", "{agent_dir}/commands/issue-close.md"), ("commands/issue-cleanup.md.j2", "{agent_dir}/commands/issue-cleanup.md"), ("commands/issue-yolo.md.j2", "{agent_dir}/commands/issue-yolo.md"), + ("commands/build.md.j2", "{agent_dir}/commands/build.md"), ("rules/issueflow-rules.mdc.j2", "{agent_dir}/rules/issueflow-rules.mdc"), ("docs/cursor-issue-workflow.md.j2", "{docs_dir}/cursor-issue-workflow.md"), ( @@ -124,6 +125,10 @@ def render_template(template_name: str, context: dict[str, str]) -> str: "skills/issueflow_history_update/SKILL.md.j2", "{agent_dir}/skills/issueflow-history-update/SKILL.md", ), + ( + "skills/issueflow_build/SKILL.md.j2", + "{agent_dir}/skills/issueflow-build/SKILL.md", + ), ] diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..e2bddbf --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,118 @@ +"""Tests for the `issue-flow` Typer CLI.""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any + +import pytest +from typer.testing import CliRunner + +from issue_flow.cli import app + + +@pytest.fixture +def runner() -> CliRunner: + return CliRunner() + + +def test_cli_lists_build_command(runner: CliRunner) -> None: + """`issue-flow --help` must mention the new `build` command.""" + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "build" in result.stdout + + +def test_build_help_describes_passthrough(runner: CliRunner) -> None: + result = runner.invoke(app, ["build", "--help"]) + assert result.exit_code == 0 + assert "graphify" in result.stdout.lower() + + +def test_build_invokes_graphify_when_available( + runner: CliRunner, tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """`issue-flow build` should call subprocess.run with the graphify CLI.""" + from issue_flow import graphify as graphify_module + + monkeypatch.setattr(graphify_module.shutil, "which", lambda _cmd: "/usr/bin/graphify") + + captured: dict[str, Any] = {} + + class _Result: + returncode = 0 + + def fake_run(cmd: list[str], **kwargs: Any) -> _Result: + captured["cmd"] = cmd + return _Result() + + monkeypatch.setattr(graphify_module.subprocess, "run", fake_run) + + result = runner.invoke(app, ["build", str(tmp_path)]) + + assert result.exit_code == 0, result.output + assert captured["cmd"][0] == "graphify" + + +def test_build_forwards_extra_args( + runner: CliRunner, tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Extra args after the project dir must reach `graphify` verbatim.""" + from issue_flow import graphify as graphify_module + + monkeypatch.setattr(graphify_module.shutil, "which", lambda _cmd: "/usr/bin/graphify") + + captured: dict[str, Any] = {} + + class _Result: + returncode = 0 + + def fake_run(cmd: list[str], **kwargs: Any) -> _Result: + captured["cmd"] = cmd + return _Result() + + monkeypatch.setattr(graphify_module.subprocess, "run", fake_run) + + result = runner.invoke( + app, ["build", str(tmp_path), "--update", "--no-viz"] + ) + + assert result.exit_code == 0, result.output + assert "--update" in captured["cmd"] + assert "--no-viz" in captured["cmd"] + + +def test_build_exits_nonzero_when_graphify_missing( + runner: CliRunner, tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """When graphify is not installed, `issue-flow build` exits with the error code from run_build.""" + from issue_flow import graphify as graphify_module + + monkeypatch.setattr(graphify_module.shutil, "which", lambda _cmd: None) + + def fail_run(*_a: Any, **_kw: Any) -> Any: + raise AssertionError("subprocess.run must not be called when graphify is missing") + + monkeypatch.setattr(graphify_module.subprocess, "run", fail_run) + + result = runner.invoke(app, ["build", str(tmp_path)]) + + assert result.exit_code == 2 + assert "graphifyy" in result.output + + +def test_build_propagates_graphify_exit_code( + runner: CliRunner, tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + from issue_flow import graphify as graphify_module + + monkeypatch.setattr(graphify_module.shutil, "which", lambda _cmd: "/usr/bin/graphify") + + class _Result: + returncode = 7 + + monkeypatch.setattr(graphify_module.subprocess, "run", lambda *a, **kw: _Result()) + + result = runner.invoke(app, ["build", str(tmp_path)]) + + assert result.exit_code == 7 diff --git a/tests/test_dependencies.py b/tests/test_dependencies.py index bb5f112..f9aabba 100644 --- a/tests/test_dependencies.py +++ b/tests/test_dependencies.py @@ -9,9 +9,11 @@ from issue_flow import dependencies as deps_module from issue_flow.dependencies import ( + RECOMMENDED_DEPENDENCIES, REQUIRED_DEPENDENCIES, Dependency, check_dependencies, + check_recommended, format_missing_report, prompt_or_skip, ) @@ -152,3 +154,26 @@ def test_prompt_or_skip_accepts_tty_confirmation( monkeypatch.setattr(typer, "confirm", lambda *_a, **_kw: True) assert prompt_or_skip([dep], console, skip=False, stdin_is_tty=True) is True + + +def test_recommended_dependencies_includes_graphify() -> None: + """The recommended (non-blocking) list must list graphify alongside install hints.""" + commands = {dep.command for dep in RECOMMENDED_DEPENDENCIES} + assert "graphify" in commands + + graphify_dep = next(d for d in RECOMMENDED_DEPENDENCIES if d.command == "graphify") + install_snippets = {snippet for _label, snippet in graphify_dep.install_hints} + assert any("graphifyy" in snippet for snippet in install_snippets) + + +def test_check_recommended_returns_missing(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(deps_module.shutil, "which", lambda _cmd: None) + missing = check_recommended() + assert "graphify" in {d.command for d in missing} + + +def test_check_recommended_returns_empty_when_present( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr(deps_module.shutil, "which", lambda _cmd: "/usr/bin/fake") + assert check_recommended() == [] diff --git a/tests/test_graphify.py b/tests/test_graphify.py new file mode 100644 index 0000000..dd89b72 --- /dev/null +++ b/tests/test_graphify.py @@ -0,0 +1,305 @@ +"""Tests for issue_flow.graphify (graphify CLI integration helpers).""" + +from __future__ import annotations + +from io import StringIO +from pathlib import Path +from typing import Any + +import pytest +from rich.console import Console + +from issue_flow import graphify as graphify_module +from issue_flow.graphify import ( + GRAPHIFY_COMMAND, + find_orphan_install, + is_available, + register_with_cursor, + run_build, +) + + +def _fake_console() -> tuple[Console, StringIO]: + """A Console whose output is captured to a StringIO for assertion.""" + buffer = StringIO() + return Console(file=buffer, width=120, force_terminal=False), buffer + + +def test_is_available_returns_true_when_graphify_on_path( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr(graphify_module.shutil, "which", lambda cmd: "/usr/bin/graphify" if cmd == GRAPHIFY_COMMAND else None) + assert is_available() is True + + +def test_is_available_returns_false_when_graphify_missing( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr(graphify_module.shutil, "which", lambda _cmd: None) + assert is_available() is False + + +def test_register_with_cursor_skips_when_graphify_missing( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """When graphify is not on PATH, register_with_cursor returns False, prints hints, and never calls subprocess.""" + monkeypatch.setattr(graphify_module.shutil, "which", lambda _cmd: None) + + def fail_run(*_a: Any, **_kw: Any) -> Any: + raise AssertionError("subprocess.run must not be called when graphify is missing") + + monkeypatch.setattr(graphify_module.subprocess, "run", fail_run) + console, buffer = _fake_console() + + result = register_with_cursor(tmp_path, console) + + assert result is False + text = buffer.getvalue() + assert "not on PATH" in text + assert "graphifyy" in text # install hint mentions the PyPI package + + +def test_register_with_cursor_runs_install_when_available( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setattr( + graphify_module.shutil, "which", lambda cmd: "/usr/bin/graphify" if cmd == GRAPHIFY_COMMAND else None + ) + + captured: dict[str, Any] = {} + + class _Result: + returncode = 0 + stderr = "" + + def fake_run(cmd: list[str], **kwargs: Any) -> _Result: + captured["cmd"] = cmd + captured["cwd"] = kwargs.get("cwd") + return _Result() + + monkeypatch.setattr(graphify_module.subprocess, "run", fake_run) + console, buffer = _fake_console() + + result = register_with_cursor(tmp_path, console) + + assert result is True + assert captured["cmd"] == [GRAPHIFY_COMMAND, "cursor", "install"] + assert captured["cwd"] == tmp_path + assert "registered" in buffer.getvalue().lower() + + +def test_register_with_cursor_does_not_raise_on_nonzero_exit( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """A non-zero exit from `graphify cursor install` must not break init/update.""" + monkeypatch.setattr(graphify_module.shutil, "which", lambda _cmd: "/usr/bin/graphify") + + class _Result: + returncode = 7 + stderr = "boom\n" + + monkeypatch.setattr(graphify_module.subprocess, "run", lambda *a, **kw: _Result()) + console, buffer = _fake_console() + + result = register_with_cursor(tmp_path, console) + + assert result is False + text = buffer.getvalue() + assert "code 7" in text + assert "continuing" in text + + +def test_register_with_cursor_swallows_oserror( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """If subprocess raises OSError (e.g. binary unexpectedly missing), we recover.""" + monkeypatch.setattr(graphify_module.shutil, "which", lambda _cmd: "/usr/bin/graphify") + + def boom(*_a: Any, **_kw: Any) -> Any: + raise OSError("permission denied") + + monkeypatch.setattr(graphify_module.subprocess, "run", boom) + console, buffer = _fake_console() + + result = register_with_cursor(tmp_path, console) + + assert result is False + assert "permission denied" in buffer.getvalue() + + +def test_run_build_returns_2_and_prints_hints_when_missing( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setattr(graphify_module.shutil, "which", lambda _cmd: None) + + def fail_run(*_a: Any, **_kw: Any) -> Any: + raise AssertionError("subprocess.run must not be called when graphify is missing") + + monkeypatch.setattr(graphify_module.subprocess, "run", fail_run) + console, buffer = _fake_console() + + exit_code = run_build(tmp_path, [], console) + + assert exit_code == 2 + text = buffer.getvalue() + assert "not installed" in text.lower() or "not found on PATH" in text + assert "graphifyy" in text + + +def test_run_build_forwards_args_verbatim( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Extra args must pass straight through to graphify.""" + monkeypatch.setattr(graphify_module.shutil, "which", lambda _cmd: "/usr/bin/graphify") + + captured: dict[str, Any] = {} + + class _Result: + returncode = 0 + + def fake_run(cmd: list[str], **kwargs: Any) -> _Result: + captured["cmd"] = cmd + captured["cwd"] = kwargs.get("cwd") + return _Result() + + monkeypatch.setattr(graphify_module.subprocess, "run", fake_run) + console, _buffer = _fake_console() + + exit_code = run_build(tmp_path, ["--update", "--no-viz", "--mode", "deep"], console) + + assert exit_code == 0 + assert captured["cmd"][0] == GRAPHIFY_COMMAND + # No explicit path argument from the user → run_build inserts the project dir + assert str(tmp_path) in captured["cmd"] + assert "--update" in captured["cmd"] + assert "--no-viz" in captured["cmd"] + assert "--mode" in captured["cmd"] + assert "deep" in captured["cmd"] + assert captured["cwd"] == tmp_path + + +def test_run_build_does_not_inject_path_when_user_supplied_one( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """If the user passes ./subdir, do not also pass the project root.""" + monkeypatch.setattr(graphify_module.shutil, "which", lambda _cmd: "/usr/bin/graphify") + + captured: dict[str, Any] = {} + + class _Result: + returncode = 0 + + def fake_run(cmd: list[str], **kwargs: Any) -> _Result: + captured["cmd"] = cmd + return _Result() + + monkeypatch.setattr(graphify_module.subprocess, "run", fake_run) + console, _buffer = _fake_console() + + run_build(tmp_path, ["./docs", "--update"], console) + + # Only one positional arg: the user's "./docs" — not the project root. + positional = [a for a in captured["cmd"][1:] if not a.startswith("-")] + assert positional == ["./docs"] + + +def test_run_build_propagates_nonzero_exit_code( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setattr(graphify_module.shutil, "which", lambda _cmd: "/usr/bin/graphify") + + class _Result: + returncode = 5 + + monkeypatch.setattr(graphify_module.subprocess, "run", lambda *a, **kw: _Result()) + console, _buffer = _fake_console() + + assert run_build(tmp_path, [], console) == 5 + + +def test_run_build_returns_1_on_oserror( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setattr(graphify_module.shutil, "which", lambda _cmd: "/usr/bin/graphify") + + def boom(*_a: Any, **_kw: Any) -> Any: + raise OSError("exec format error") + + monkeypatch.setattr(graphify_module.subprocess, "run", boom) + console, buffer = _fake_console() + + exit_code = run_build(tmp_path, [], console) + + assert exit_code == 1 + assert "exec format error" in buffer.getvalue() + + +def test_find_orphan_install_returns_none_when_on_path( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """If graphify is already on PATH, the orphan question is moot.""" + monkeypatch.setattr(graphify_module.shutil, "which", lambda _cmd: "/usr/bin/graphify") + assert find_orphan_install() is None + + +def test_find_orphan_install_returns_none_when_no_candidates( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """No graphify on PATH and no candidate install path → no orphan.""" + monkeypatch.setattr(graphify_module.shutil, "which", lambda _cmd: None) + monkeypatch.setattr(graphify_module, "_candidate_install_locations", lambda: []) + assert find_orphan_install() is None + + +def test_find_orphan_install_returns_path_when_unreachable( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """A graphify binary in a candidate dir that is not on PATH counts as an orphan.""" + monkeypatch.setattr(graphify_module.shutil, "which", lambda _cmd: None) + fake_bin = tmp_path / "graphify" + fake_bin.write_text("#!/usr/bin/env python3\n") + monkeypatch.setattr( + graphify_module, "_candidate_install_locations", lambda: [fake_bin] + ) + + result = find_orphan_install() + + assert result == fake_bin + + +def test_install_hints_include_orphan_message( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """When an orphan binary exists, hints must point at it and recommend update-shell.""" + monkeypatch.setattr(graphify_module.shutil, "which", lambda _cmd: None) + fake_bin = tmp_path / "fake_local_bin" / "graphify" + fake_bin.parent.mkdir() + fake_bin.write_text("shim") + monkeypatch.setattr( + graphify_module, "_candidate_install_locations", lambda: [fake_bin] + ) + console, buffer = _fake_console() + + graphify_module._print_install_hints(console) + + text = buffer.getvalue() + assert str(fake_bin) in text + assert "PATH" in text + assert "uv tool update-shell" in text + + +def test_install_hints_include_path_advice_when_no_orphan( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """When graphify is plainly missing, hints still mention the update-shell escape hatch.""" + monkeypatch.setattr(graphify_module.shutil, "which", lambda _cmd: None) + monkeypatch.setattr(graphify_module, "_candidate_install_locations", lambda: []) + console, buffer = _fake_console() + + graphify_module._print_install_hints(console) + + text = buffer.getvalue() + assert "graphifyy" in text + assert "update-shell" in text + # And the standard install snippets are still printed. + assert "uv tool install" in text diff --git a/tests/test_init.py b/tests/test_init.py index 48d3d03..c2c3ed3 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -322,6 +322,79 @@ def test_init_aborts_cleanly_when_user_declines_prompt( assert not (tmp_path / ".issueflows").exists() +def test_init_calls_graphify_register_when_available( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """When graphify is on PATH, run_init must call register_with_cursor.""" + from issue_flow import graphify as graphify_module + + monkeypatch.setattr(graphify_module.shutil, "which", lambda cmd: "/usr/bin/graphify" if cmd == "graphify" else None) + + calls: list[Path] = [] + + class _Result: + returncode = 0 + stderr = "" + + def fake_run(cmd: list[str], **kwargs: object) -> _Result: + calls.append(kwargs.get("cwd")) # type: ignore[arg-type] + return _Result() + + monkeypatch.setattr(graphify_module.subprocess, "run", fake_run) + + run_init(tmp_path) + + assert calls == [tmp_path] + + +def test_init_skips_graphify_when_unavailable( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """When graphify is missing, run_init must not call subprocess and must still succeed.""" + from issue_flow import graphify as graphify_module + + monkeypatch.setattr(graphify_module.shutil, "which", lambda _cmd: None) + + def fail_run(*_a: object, **_kw: object) -> object: + raise AssertionError("subprocess.run must not be called when graphify is missing") + + monkeypatch.setattr(graphify_module.subprocess, "run", fail_run) + + run_init(tmp_path) + + assert (tmp_path / ".cursor" / "commands" / "build.md").is_file() + + +def test_init_creates_build_command_and_skill(tmp_path: Path) -> None: + """The new /build slash command and matching skill must be scaffolded.""" + run_init(tmp_path) + + build_cmd = tmp_path / ".cursor" / "commands" / "build.md" + build_skill = tmp_path / ".cursor" / "skills" / "issueflow-build" / "SKILL.md" + assert build_cmd.is_file() + assert build_skill.is_file() + + cmd_content = build_cmd.read_text(encoding="utf-8") + assert "graphify" in cmd_content.lower() + assert "issue-flow build" in cmd_content + assert "graphify-out" in cmd_content + + skill_content = build_skill.read_text(encoding="utf-8") + assert "name: issueflow-build" in skill_content + assert "disable-model-invocation: true" in skill_content + + +def test_init_rule_documents_knowledge_graph_section(tmp_path: Path) -> None: + """The generated rule file should mention the optional graphify knowledge graph.""" + run_init(tmp_path) + rule = (tmp_path / ".cursor" / "rules" / "issueflow-rules.mdc").read_text( + encoding="utf-8" + ) + assert "Knowledge graph" in rule + assert "graphify-out/GRAPH_REPORT.md" in rule + assert "/build" in rule + + def test_init_detects_project_name(tmp_path: Path) -> None: """If a pyproject.toml exists, its name should appear in the rule file.""" pyproject = tmp_path / "pyproject.toml" diff --git a/tests/test_templating.py b/tests/test_templating.py index 4738a76..38010c9 100644 --- a/tests/test_templating.py +++ b/tests/test_templating.py @@ -55,8 +55,8 @@ def test_resolve_output_path() -> None: def test_manifest_entry_count() -> None: - # 8 commands + 1 rule + 1 doc + 11 skills = 21 - assert len(TEMPLATE_MANIFEST) == 21 + # 9 commands + 1 rule + 1 doc + 12 skills = 23 + assert len(TEMPLATE_MANIFEST) == 23 def test_manifest_has_expected_commands_and_skills() -> None: @@ -71,6 +71,7 @@ def test_manifest_has_expected_commands_and_skills() -> None: "issue-close", "issue-cleanup", "issue-yolo", + "build", ): assert f"commands/{command}.md.j2" in template_names for skill in ( @@ -85,6 +86,7 @@ def test_manifest_has_expected_commands_and_skills() -> None: "issueflow_issue_yolo", "issueflow_version_bump", "issueflow_history_update", + "issueflow_build", ): assert f"skills/{skill}/SKILL.md.j2" in template_names