feature-flow-cli 0.3.0__tar.gz → 0.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. {feature_flow_cli-0.3.0/feature_flow_cli.egg-info → feature_flow_cli-0.7.0}/PKG-INFO +34 -9
  2. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/README.md +33 -8
  3. feature_flow_cli-0.7.0/adapters/codex/feature-flow/SKILL.md +80 -0
  4. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/feature_flow/__init__.py +1 -1
  5. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/feature_flow/checks.py +3 -3
  6. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/feature_flow/cli.py +56 -3
  7. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/feature_flow/codehash.py +6 -2
  8. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/feature_flow/command.py +7 -3
  9. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/feature_flow/conductor.py +75 -20
  10. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/feature_flow/gate.py +6 -2
  11. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/feature_flow/git.py +18 -0
  12. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/feature_flow/install.py +72 -45
  13. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/feature_flow/prompts.py +43 -6
  14. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/feature_flow/proof.py +10 -0
  15. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/feature_flow/state.py +48 -6
  16. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/feature_flow/status.py +2 -0
  17. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/feature_flow/tickets.py +4 -0
  18. feature_flow_cli-0.7.0/feature_flow/uninstall.py +235 -0
  19. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0/feature_flow_cli.egg-info}/PKG-INFO +34 -9
  20. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/feature_flow_cli.egg-info/SOURCES.txt +3 -0
  21. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/guides/build.md +1 -1
  22. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/guides/plan-review.md +2 -2
  23. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/guides/plan.md +5 -1
  24. feature_flow_cli-0.7.0/guides/review-single.md +87 -0
  25. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/guides/templates/commands.md +4 -1
  26. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/guides/templates/ticket.md +1 -0
  27. feature_flow_cli-0.7.0/scripts/floor-guard.py +29 -0
  28. feature_flow_cli-0.7.0/scripts/flow-status.py +29 -0
  29. feature_flow_cli-0.7.0/scripts/flow-trust.py +367 -0
  30. feature_flow_cli-0.7.0/scripts/flow-view.py +29 -0
  31. feature_flow_cli-0.7.0/scripts/flow.py +29 -0
  32. feature_flow_cli-0.7.0/scripts/gate.py +29 -0
  33. feature_flow_cli-0.7.0/skills/feature-flow/SKILL.md +80 -0
  34. feature_flow_cli-0.3.0/adapters/codex/feature-flow/SKILL.md +0 -68
  35. feature_flow_cli-0.3.0/scripts/floor-guard.py +0 -14
  36. feature_flow_cli-0.3.0/scripts/flow-status.py +0 -14
  37. feature_flow_cli-0.3.0/scripts/flow-view.py +0 -14
  38. feature_flow_cli-0.3.0/scripts/flow.py +0 -14
  39. feature_flow_cli-0.3.0/scripts/gate.py +0 -14
  40. feature_flow_cli-0.3.0/skills/feature-flow/SKILL.md +0 -68
  41. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/LICENSE +0 -0
  42. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/MANIFEST.in +0 -0
  43. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/adapters/codex/feature-flow/agents/openai.yaml +0 -0
  44. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/agents/feature-planner.md +0 -0
  45. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/agents/plan-reviewer.md +0 -0
  46. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/agents/ticket-builder.md +0 -0
  47. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/agents/ticket-reviewer.md +0 -0
  48. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/feature_flow/__main__.py +0 -0
  49. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/feature_flow/floorguard.py +0 -0
  50. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/feature_flow/proc.py +0 -0
  51. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/feature_flow/released.py +0 -0
  52. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/feature_flow/suggest.py +0 -0
  53. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/feature_flow/view.py +0 -0
  54. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/feature_flow_cli.egg-info/dependency_links.txt +0 -0
  55. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/feature_flow_cli.egg-info/entry_points.txt +0 -0
  56. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/feature_flow_cli.egg-info/top_level.txt +0 -0
  57. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/guides/brief.md +0 -0
  58. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/guides/debug.md +0 -0
  59. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/guides/review-quality.md +0 -0
  60. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/guides/review.md +0 -0
  61. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/guides/show.md +0 -0
  62. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/guides/templates/learnings.md +0 -0
  63. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/guides/templates/map.md +0 -0
  64. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/guides/templates/spec.md +0 -0
  65. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/guides/templates/ui-mockup.md +0 -0
  66. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/install.py +0 -0
  67. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/install.sh +0 -0
  68. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/pyproject.toml +0 -0
  69. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/scripts/flow-view.html +0 -0
  70. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/setup.cfg +0 -0
  71. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/skills/architect-review/SKILL.md +0 -0
  72. {feature_flow_cli-0.3.0 → feature_flow_cli-0.7.0}/skills/automation-design/SKILL.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: feature-flow-cli
3
- Version: 0.3.0
3
+ Version: 0.7.0
4
4
  Summary: A ticket-graph workflow for coding agents: installs the feature-flow skill into a repo
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/hamilton-sky/feature-flow
@@ -57,6 +57,8 @@ The installer lists what it wrote in `.feature-flow/installed.txt`, with each fi
57
57
 
58
58
  To keep the install out of git, so only your plans and tickets get committed, install with `--private`. It lists the installed files in `.git/info/exclude`, which git reads like `.gitignore` but never commits, and later installs keep that list up to date. If you already committed the install, it prints the `git rm --cached` command that untracks it and keeps the files. Every clone then needs its own install.
59
59
 
60
+ To remove it, run `feature-flow uninstall` (or `uvx feature-flow-cli uninstall`) in the repo. It removes exactly the files listed in `.feature-flow/installed.txt`, and only while they still hold the bytes the installer wrote: a file you edited is kept and named, and `--force` removes it too. It also drops the `--private` block from `.git/info/exclude`. Your `plans/`, your tickets and the run logs in `.feature-flow/state/` are never touched. `--dry-run` shows what would go. If you installed with `--user`, `feature-flow uninstall --user` removes the personal copy in `~/.claude` and `~/.agents`, and `~/.feature-flow`, the same way (the repo's own install is separate and needs its own `uninstall`).
61
+
60
62
  Then, in your repo, in the agent:
61
63
 
62
64
  | | Claude Code | Codex |
@@ -69,10 +71,15 @@ Then, in your repo, in the agent:
69
71
 
70
72
  The same command plans when `plans/csv-export/` does not exist yet and builds when it does. After your second yes it commits the new plan folder, and only that folder, then goes straight on to building it; say so if you want it to stop after planning.
71
73
 
72
- **Planning asks you twice.** First the session shows a short feature brief, written from your conversation (what, why, scope, the bar, and anything it had to assume), and waits for yes, edit or cancel. Then a fresh `feature-planner` subagent, given only that brief, reads the code, looks up outside docs where the code cannot answer, and writes a draft plan into the git-ignored `.feature-flow/state/draft/`. A fresh `plan-reviewer` checks the draft against the brief. The session shows you the goal, the commands, the ticket graph, what was dropped and the review result, and waits for a second yes. Only then does `flow.py plan-accept` copy the draft into `plans/<feature>/`, and the session commits it and starts the build.
74
+ **Planning asks you twice.** First the session shows a short feature brief, written from your conversation (what, why, scope, the bar, and anything it had to assume), and waits for yes, edit or cancel. Then a fresh `feature-planner` subagent, given only that brief, reads the code, looks up outside docs where the code cannot answer, and writes a draft plan into the git-ignored `.feature-flow/state/draft/`. The planner folds changes too small to stand alone into the ticket they belong to, and sets `Review: full` on a ticket that deserves both reviews whatever its size. A fresh `plan-reviewer` checks the draft against the brief. The session shows you the goal, the commands, the ticket graph, what was dropped and the review result, and waits for a second yes. Only then does `flow.py plan-accept` copy the draft into `plans/<feature>/`, and the session commits it and starts the build.
73
75
 
74
76
  Try the graph first, with no setup and no agent: `bash examples/demo.sh --open`.
75
77
 
78
+ ### Two ways to install
79
+
80
+ - `feature-flow install --user`: once, for every repo. Skills and roles go to `~/.claude` / `~/.agents`, the conductor, guides and package to `~/.feature-flow/`. `FEATURE_FLOW_HOME` moves that folder. No repo files are written.
81
+ - `feature-flow install <repo>`: one repo. When a repo has its own copy, that copy wins over the user install.
82
+
76
83
  ### What the installer does
77
84
 
78
85
  | | Claude Code (`--agent claude`, the default) | Codex (`--agent codex`) |
@@ -82,7 +89,7 @@ Try the graph first, with no setup and no agent: `bash examples/demo.sh --open`.
82
89
  | Scripts go to | `<repo>/scripts/` | `<repo>/scripts/` |
83
90
  | Guides, roles and the conductor go to | `<repo>/.feature-flow/` | `<repo>/.feature-flow/` |
84
91
 
85
- Next to the `feature-flow` skill it installs `architect-review` and `automation-design`. The installer never deletes anything. A file that already exists and differs is kept and reported; `--force` replaces it. `--dry-run` shows what would change and writes nothing. `CLAUDE_HOME` and `AGENTS_HOME` move the user folders. With `--user` the skill goes to the user folder, but the scripts and `.feature-flow/` stay in the repo, because the skill runs them from there.
92
+ Next to the `feature-flow` skill it installs `architect-review` and `automation-design`. The installer never deletes anything. A file that already exists and differs is kept and reported; `--force` replaces it. `--dry-run` shows what would change and writes nothing. `CLAUDE_HOME` and `AGENTS_HOME` move the user folders. With `--user` the skill and roles go to the user folders, and the scripts, guides, conductor and package go to `~/.feature-flow/` (`FEATURE_FLOW_HOME` moves it); nothing is written into a repo.
86
93
 
87
94
  The Claude Code skill is `skills/feature-flow/`. The Codex skill is written by hand in `adapters/codex/feature-flow/`, with an `agents/openai.yaml` that sets `allow_implicit_invocation: false`, so it runs only when you name it. The steps are the same; only the way a subagent is started differs. Your own `AGENTS.md` and `CLAUDE.md` are never touched.
88
95
 
@@ -114,6 +121,8 @@ The Claude Code skill is `skills/feature-flow/`. The Codex skill is written by h
114
121
  │ PASS │
115
122
  ▼ │
116
123
  quality review: a second fresh reviewer subagent FAIL ─┤
124
+ (a small ticket whose check block passed gets one combined
125
+ review instead of these two; see below)
117
126
  │ PASS ▼
118
127
  ▼ findings written into the ticket,
119
128
  next ticket reopened, built again (3 rounds max);
@@ -123,7 +132,11 @@ The Claude Code skill is `skills/feature-flow/`. The Codex skill is written by h
123
132
 
124
133
  `scripts/flow.py` is the conductor. The session asks it `next`, and it answers with one line: `BUILD`, `REVIEW`, `DONE`, `STOP` or `HANDOFF`. It prints the full prompt for each subagent from runtime-neutral guides, so the session never writes one itself.
125
134
 
126
- **What the script checks and what it trusts.** The conductor verifies the build from the repo: the ticket file must say `resolved`, the tree must be clean and committed, and it runs the gate and the floor guard itself. It also runs the `check` block under each ticket's Done when and compares the exit code and printed text, and for a `Test first: yes` ticket it proves the red test commit itself: it checks out the test-only commit, runs Test, and requires it to fail before the code. A failure of either goes back to the builder before any reviewer is started. It also checks that the reviewer changed no tracked file and made no commit. The review verdicts are different: each reviewer's reply (spec review, then quality review) is relayed by the session, which saves it to a file and hands it to `flow.py verdict`. The script reads `REVIEW: PASS` or `REVIEW: FAIL` from that reply; it cannot prove the session passed it on unedited. A ticket moves on only after two PASS verdicts.
135
+ **What the script checks and what it trusts.** The conductor verifies the build from the repo: the ticket file must say `resolved`, the tree must be clean and committed, and it runs the gate and the floor guard itself. It also runs the `check` block under each ticket's Done when and compares the exit code and printed text, and for a `Test first: yes` ticket it proves the red test commit itself: it checks out the test-only commit, runs Test, and requires it to fail before the code. A failure of either goes back to the builder before any reviewer is started. It also checks that the reviewer changed no tracked file and made no commit. The review verdicts are different: each reviewer's reply (spec review, then quality review) is relayed by the session, which saves it to a file and hands it to `flow.py verdict`. The script reads `REVIEW: PASS` or `REVIEW: FAIL` from that reply; it cannot prove the session passed it on unedited. A ticket moves on only after two PASS verdicts, or one for a ticket the conductor judged small. The size is measured from the repo (`git diff` against the commit the ticket started from), never taken from the builder.
136
+
137
+ **The gate: full or quick.** The gate runs Build, Test and Lint after every ticket. With an optional `Quick:` line in `commands.md` (a faster subset of `Test`), it runs Build, Quick and Lint after the tickets in the middle of a plan, and the full Build, Test and Lint after the last ticket, before a handoff, and under `FLOW_GATE_FULL=1`. A plan without `Quick:` runs the full gate every time. The test-first proof still runs the full `Test` command.
138
+
139
+ **One review for a small ticket.** A ticket whose built diff is 3 files and 60 changed lines at most (the plan folder left out), and whose `check` block ran and passed, gets one combined review instead of the spec and quality reviews. The conductor measures the diff itself. `Review: full` in the ticket (as it was at the start), `Type: convert`, no `check` block, or `FLOW_GATE=off` keep both reviews. Every other conductor check still runs on every ticket.
127
140
 
128
141
  ## The skill and the roles
129
142
 
@@ -219,7 +232,7 @@ One self contained HTML file: no server, no libraries, no network, light and dar
219
232
 
220
233
  ## Long features and handoff
221
234
 
222
- A session's context fills up, so the flow moves to a fresh session every few tickets. After `FLOW_TICKETS_PER_SESSION` tickets (default 4) pass review, `next` prints a `HANDOFF` line instead of the next ticket, for example `HANDOFF /feature-flow csv-export`. The session stops and tells you to open a new session and type that line. The new session picks up exactly where the last one stopped. Set `FLOW_TICKETS_PER_SESSION=0` to never hand off.
235
+ A session's context fills up, so the flow moves to a fresh session every few tickets. After `FLOW_TICKETS_PER_SESSION` tickets (default 4) pass review, `next` prints a `HANDOFF` line instead of the next ticket, for example `HANDOFF /feature-flow csv-export 3f9c0a1b2c3d4e5f.0a1b2c3d4e5f6a7b` (the last word is the runner's digest, which lets the new session check that the flow's files did not change in between). The session stops and tells you to open a new session and type that line. The new session picks up exactly where the last one stopped. Set `FLOW_TICKETS_PER_SESSION=0` to never hand off.
223
236
 
224
237
  - **One owner at a time.** `start` gives the session a token, and every later call must carry it, so two sessions can never drive the same feature. `HANDOFF` and `DONE` release it.
225
238
  - **Resuming after a closed session.** If a session ended without `HANDOFF` (you closed it, it crashed, it ran out of budget), the feature is still owned by it, and a new session stops and says so. Once you are sure the old session is gone, answer yes when the new session asks, or start it with `FLOW_TAKEOVER=1`. With `auto` the skill never takes over on its own.
@@ -232,7 +245,8 @@ The state lives in `.feature-flow/state/` (the state, a log of every step, and t
232
245
  ### Commands and settings
233
246
 
234
247
  ```bash
235
- python3 scripts/flow.py <feature> start|next|prompt|verdict <file> the conductor (the skill runs it)
248
+ python3 scripts/flow.py <feature> start|reset [NN] the conductor (the skill runs it; also plan-prompt, plan-review-prompt, plan-accept)
249
+ FLOW_TRUST=<digest|new> FLOW_SESSION=<token> python3 -I scripts/flow-trust.py <feature> next|prompt|verdict <file> loop commands by hand, through the runner: prints TRUST <digest> first (no hash check, see below)
236
250
  python3 scripts/flow-status.py <feature> table of tickets and which are READY
237
251
  python3 scripts/flow-status.py <feature> --next path of the next ready ticket (exit 10 = done, 11 = stuck)
238
252
  python3 scripts/flow-status.py <feature> --counts one line of counts
@@ -244,6 +258,8 @@ python3 scripts/gate.py <feature> run Build, Test and Li
244
258
  python3 scripts/floor-guard.py <feature> <NN> [base] check a ticket's diff, run from the repo root
245
259
  ```
246
260
 
261
+ `next`, `prompt` and `verdict` run directly stop with `STOP run the conductor through scripts/flow-trust.py, as the skill says`. Pass `FLOW_TRUST=new` on a first call, then the digest from the last `TRUST` line. Run by hand like this, nothing checks `flow-trust.py` itself, so it is for trying things out, not a protected run. The skills never run it this way: they start it through a loader line that first checks the file against the sha256 the skill pins, and stops with `flow-trust.py does not match this skill` if it differs (see `## Calling the conductor` in the skill). For a `--user` install the scripts are in `~/.feature-flow/scripts/` (or `$FEATURE_FLOW_HOME/scripts/`).
262
+
247
263
  The commands in `commands.md` run through `bash -c` on Linux and macOS and through the system shell (`cmd.exe`) on Windows, so write them for the platform your team uses, or call `python` as in the examples.
248
264
 
249
265
  `flow-status.py` also understands the `.scratch/<feature>/issues/` layout and the statuses `done`, `ready-for-agent`, `ready-for-human` and `closed`: `FLOW_DIR=.scratch FLOW_TICKETS=issues`.
@@ -256,13 +272,16 @@ The commands in `commands.md` run through `bash -c` on Linux and macOS and throu
256
272
  | `FLOW_MAX_RETRIES` | `2` | builds or reviews per step before the run stops |
257
273
  | `FLOW_MAX_REVIEW_ROUNDS` | `3` | times a ticket may be sent back before the run stops |
258
274
  | `FLOW_GATE` | `on` | `off` skips Build, Test and Lint after each ticket, and also the Done when and test-first checks |
275
+ | `FLOW_GATE_FULL` | unset | `1` runs the full Build, Test and Lint after every ticket, even when `commands.md` has a `Quick:` line |
276
+ | `FLOW_SMALL_FILES` | `3` | most files a ticket may change and still get the single review |
277
+ | `FLOW_SMALL_LINES` | `60` | most changed lines for the single review; `0` turns the single review off |
259
278
  | `FLOW_GATE_TIMEOUT` | `30` | minutes each gate command and the smoke test may run; `0` means no limit. A failed smoke test writes `.feature-flow/state/<feature>.smoke.log` |
260
279
  | `FLOW_SMOKE` | the `Smoke:` line of `commands.md` | command run before every ticket; the run stops if it fails |
261
280
  | `FLOW_DIR`, `FLOW_TICKETS` | `plans`, `tasks` | where the plans and the ticket folder live |
262
281
  | `FLOW_NO_OPEN` | unset | `1` makes `flow-view.py` never open a browser |
263
282
  | `FLOW_WATCH_SECONDS` | `3` | how often `flow-view.py --watch` rewrites the page |
264
283
 
265
- The run also stops on its own after a number of phases (`BUILD` and `REVIEW` hand-outs, the spec review and the quality review each counting as one) that grows with the ticket count, recomputed at every pick, so a loop of failures cannot go on forever.
284
+ The run also stops on its own after a number of phases (`BUILD` and `REVIEW` hand-outs, the spec review and the quality review each counting as one, a single review counting as one) that grows with the ticket count, recomputed at every pick, so a loop of failures cannot go on forever.
266
285
 
267
286
  ## What is tested
268
287
 
@@ -289,7 +308,7 @@ bash tests/run.sh
289
308
  python3 -m unittest discover -s tests/py
290
309
  ```
291
310
 
292
- Offline and free: no model is called. The suite covers the conductor's every answer and how a run stops, sessions, handoff and takeover, a `.git` the agent cannot write, the gate, the floor guard, plan protection, the prompts and guides, both skills, the installer for both agents, the graph page and its data, the Done when checks, the test-first check, the two reviews and the debugging guide on a send-back, and the acceptance harness. `tests/run.sh` also drives a prepared demo with no model through both checks and both reviews. The page's layout and replay logic are also tested under Node (`tests/viewer-logic.test.js`, skipped when Node is absent). `.github/workflows/tests.yml` runs it on every push to `main` and every pull request, on Ubuntu and on macOS. A fourth job runs the Python unit tests on Windows, including a fixture drive that takes a two-ticket plan through `python scripts/flow.py f start` and `next` to `BUILD` and then `REVIEW`. `tests/run.sh` itself is a bash harness and does not run on Windows. Windows is tested this way only: no real agent run has been done there.
311
+ Offline and free: no model is called. The suite covers the conductor's every answer and how a run stops, sessions, handoff and takeover, a `.git` the agent cannot write, the gate, the floor guard, plan protection, the prompts and guides, both skills, the installer for both agents, the graph page and its data, the Done when checks, the test-first check, the two reviews and the debugging guide on a send-back, and the acceptance harness. `tests/run.sh` also drives a prepared demo with no model through both checks and both reviews. The page's layout and replay logic are also tested under Node (`tests/viewer-logic.test.js`, skipped when Node is absent). `.github/workflows/tests.yml` runs it on every push to `main` and every pull request, on Ubuntu and on macOS. A fourth job runs the Python unit tests on Windows, including a fixture drive that takes a two-ticket plan through `python scripts/flow.py f start` and `next` to `BUILD` and then `REVIEW`, with `FLOW_TRUSTED=1` set as the runner sets it. `tests/run.sh` itself is a bash harness and does not run on Windows. Windows is tested this way only: no real agent run has been done there.
293
312
 
294
313
  A `package` job builds the wheel and the sdist and runs `tests/package_smoke.py` on Ubuntu (Python 3.9 and the latest), macOS and Windows: it installs the wheel in a fresh virtual environment, runs `feature-flow install`, and checks that the repo gets the same files as from `install.py` in a clone. Run it locally with `python3 -m pip install build && python3 tests/package_smoke.py`.
295
314
 
@@ -320,7 +339,13 @@ git tag v0.1.1 && git push origin v0.1.1
320
339
 
321
340
  ## Caution
322
341
 
323
- The builder subagent has edit and shell access and commits after each ticket. Each ticket costs at least two subagents. Build on a branch you can throw away. Ignore build output in `.gitignore`, because the conductor stops if the tree is dirty after a ticket. The gate and the smoke test stop after 30 minutes (`FLOW_GATE_TIMEOUT`). The conductor also checks that the code it runs (the `feature_flow` package, `scripts/*.py` and the build and review role and guide files) did not change during a build or review; if it did, the run stops with `STOP flow code changed while building <ticket>: <files>`, and `Floor: allow flow-edit` on the ticket allows it for the build. That check is a tripwire, not a sandbox. The floor guard is pattern matching: it can miss things and it can raise false alarms, and `Floor: allow` is the release valve. The run stops on its own when a ticket stays unresolved, when the smoke test or the gate keeps failing, when a ticket keeps failing review, or when it reaches its phase limit.
342
+ The builder subagent has edit and shell access and commits after each ticket. Each ticket costs at least two subagents. Build on a branch you can throw away. Ignore build output in `.gitignore`, because the conductor stops if the tree is dirty after a ticket. The gate and the smoke test stop after 30 minutes (`FLOW_GATE_TIMEOUT`). The session runs every conductor call through `scripts/flow-trust.py`, which sits beside the conductor it runs (`scripts/` in the repo, or `~/.feature-flow/scripts/` for a `--user` install). The skill pins the runner's sha256 and starts it with a one-line loader that checks and runs those same bytes, so a subagent cannot change the check. The runner prints `TRUST <code>.<state>` before the conductor's output, and the session passes that digest to the next call as `FLOW_TRUST`.
343
+
344
+ **Protected:** edits by the builder, reviewer or planner subagents during a session to feature-flow's own files (its `feature_flow` package, its files in `scripts/` (`flow.py`, `flow-status.py`, `gate.py`, `floor-guard.py`, `flow-view.py`, `flow-trust.py`), the guides, the roles and the skill folders, in the repo and in the install folders) and to the run state in `.feature-flow/state/`. That includes edits made by code the conductor runs during a step, such as tests, `check` commands and git hooks. Any of these makes the runner print a `STOP` instead of `TRUST`, naming the changed files where it can, and the session stops. It also holds between sessions when the `HANDOFF` line carries its digest (`HANDOFF /feature-flow <feature> <digest>`). `Floor: allow flow-edit` on a ticket allows its build to change the flow's code (never the run state, and never `scripts/flow-trust.py`, whose hash the skill pins: change the runner outside the flow, or build it with a conductor from another install, then update the pinned sha in both skills); the runner then prints `FLOW-EDIT <files>`, and a session run without `auto` tells you which flow files changed.
345
+
346
+ **Not protected:** a subagent that changes `python3` on PATH or the system Python, the agent itself, or anything outside the repo and the install folders. The session's very first conductor call: whatever is on disk then is trusted. A background process left running by a step's own commands: it can swap a flow file in the instant between the runner's check and the conductor loading it, then put it back. A session that stops passing its digest, or a resume from a `HANDOFF` line without one (the session warns). The conductor's own code check (`STOP flow code changed while building <ticket>: <files>`) stays as a second line that catches accidental edits; on its own it cannot stop deliberate tampering.
347
+
348
+ The floor guard is pattern matching: it can miss things and it can raise false alarms, and `Floor: allow` is the release valve. The run stops on its own when a ticket stays unresolved, when the smoke test or the gate keeps failing, when a ticket keeps failing review, or when it reaches its phase limit.
324
349
 
325
350
  ## License
326
351
 
@@ -38,6 +38,8 @@ The installer lists what it wrote in `.feature-flow/installed.txt`, with each fi
38
38
 
39
39
  To keep the install out of git, so only your plans and tickets get committed, install with `--private`. It lists the installed files in `.git/info/exclude`, which git reads like `.gitignore` but never commits, and later installs keep that list up to date. If you already committed the install, it prints the `git rm --cached` command that untracks it and keeps the files. Every clone then needs its own install.
40
40
 
41
+ To remove it, run `feature-flow uninstall` (or `uvx feature-flow-cli uninstall`) in the repo. It removes exactly the files listed in `.feature-flow/installed.txt`, and only while they still hold the bytes the installer wrote: a file you edited is kept and named, and `--force` removes it too. It also drops the `--private` block from `.git/info/exclude`. Your `plans/`, your tickets and the run logs in `.feature-flow/state/` are never touched. `--dry-run` shows what would go. If you installed with `--user`, `feature-flow uninstall --user` removes the personal copy in `~/.claude` and `~/.agents`, and `~/.feature-flow`, the same way (the repo's own install is separate and needs its own `uninstall`).
42
+
41
43
  Then, in your repo, in the agent:
42
44
 
43
45
  | | Claude Code | Codex |
@@ -50,10 +52,15 @@ Then, in your repo, in the agent:
50
52
 
51
53
  The same command plans when `plans/csv-export/` does not exist yet and builds when it does. After your second yes it commits the new plan folder, and only that folder, then goes straight on to building it; say so if you want it to stop after planning.
52
54
 
53
- **Planning asks you twice.** First the session shows a short feature brief, written from your conversation (what, why, scope, the bar, and anything it had to assume), and waits for yes, edit or cancel. Then a fresh `feature-planner` subagent, given only that brief, reads the code, looks up outside docs where the code cannot answer, and writes a draft plan into the git-ignored `.feature-flow/state/draft/`. A fresh `plan-reviewer` checks the draft against the brief. The session shows you the goal, the commands, the ticket graph, what was dropped and the review result, and waits for a second yes. Only then does `flow.py plan-accept` copy the draft into `plans/<feature>/`, and the session commits it and starts the build.
55
+ **Planning asks you twice.** First the session shows a short feature brief, written from your conversation (what, why, scope, the bar, and anything it had to assume), and waits for yes, edit or cancel. Then a fresh `feature-planner` subagent, given only that brief, reads the code, looks up outside docs where the code cannot answer, and writes a draft plan into the git-ignored `.feature-flow/state/draft/`. The planner folds changes too small to stand alone into the ticket they belong to, and sets `Review: full` on a ticket that deserves both reviews whatever its size. A fresh `plan-reviewer` checks the draft against the brief. The session shows you the goal, the commands, the ticket graph, what was dropped and the review result, and waits for a second yes. Only then does `flow.py plan-accept` copy the draft into `plans/<feature>/`, and the session commits it and starts the build.
54
56
 
55
57
  Try the graph first, with no setup and no agent: `bash examples/demo.sh --open`.
56
58
 
59
+ ### Two ways to install
60
+
61
+ - `feature-flow install --user`: once, for every repo. Skills and roles go to `~/.claude` / `~/.agents`, the conductor, guides and package to `~/.feature-flow/`. `FEATURE_FLOW_HOME` moves that folder. No repo files are written.
62
+ - `feature-flow install <repo>`: one repo. When a repo has its own copy, that copy wins over the user install.
63
+
57
64
  ### What the installer does
58
65
 
59
66
  | | Claude Code (`--agent claude`, the default) | Codex (`--agent codex`) |
@@ -63,7 +70,7 @@ Try the graph first, with no setup and no agent: `bash examples/demo.sh --open`.
63
70
  | Scripts go to | `<repo>/scripts/` | `<repo>/scripts/` |
64
71
  | Guides, roles and the conductor go to | `<repo>/.feature-flow/` | `<repo>/.feature-flow/` |
65
72
 
66
- Next to the `feature-flow` skill it installs `architect-review` and `automation-design`. The installer never deletes anything. A file that already exists and differs is kept and reported; `--force` replaces it. `--dry-run` shows what would change and writes nothing. `CLAUDE_HOME` and `AGENTS_HOME` move the user folders. With `--user` the skill goes to the user folder, but the scripts and `.feature-flow/` stay in the repo, because the skill runs them from there.
73
+ Next to the `feature-flow` skill it installs `architect-review` and `automation-design`. The installer never deletes anything. A file that already exists and differs is kept and reported; `--force` replaces it. `--dry-run` shows what would change and writes nothing. `CLAUDE_HOME` and `AGENTS_HOME` move the user folders. With `--user` the skill and roles go to the user folders, and the scripts, guides, conductor and package go to `~/.feature-flow/` (`FEATURE_FLOW_HOME` moves it); nothing is written into a repo.
67
74
 
68
75
  The Claude Code skill is `skills/feature-flow/`. The Codex skill is written by hand in `adapters/codex/feature-flow/`, with an `agents/openai.yaml` that sets `allow_implicit_invocation: false`, so it runs only when you name it. The steps are the same; only the way a subagent is started differs. Your own `AGENTS.md` and `CLAUDE.md` are never touched.
69
76
 
@@ -95,6 +102,8 @@ The Claude Code skill is `skills/feature-flow/`. The Codex skill is written by h
95
102
  │ PASS │
96
103
  ▼ │
97
104
  quality review: a second fresh reviewer subagent FAIL ─┤
105
+ (a small ticket whose check block passed gets one combined
106
+ review instead of these two; see below)
98
107
  │ PASS ▼
99
108
  ▼ findings written into the ticket,
100
109
  next ticket reopened, built again (3 rounds max);
@@ -104,7 +113,11 @@ The Claude Code skill is `skills/feature-flow/`. The Codex skill is written by h
104
113
 
105
114
  `scripts/flow.py` is the conductor. The session asks it `next`, and it answers with one line: `BUILD`, `REVIEW`, `DONE`, `STOP` or `HANDOFF`. It prints the full prompt for each subagent from runtime-neutral guides, so the session never writes one itself.
106
115
 
107
- **What the script checks and what it trusts.** The conductor verifies the build from the repo: the ticket file must say `resolved`, the tree must be clean and committed, and it runs the gate and the floor guard itself. It also runs the `check` block under each ticket's Done when and compares the exit code and printed text, and for a `Test first: yes` ticket it proves the red test commit itself: it checks out the test-only commit, runs Test, and requires it to fail before the code. A failure of either goes back to the builder before any reviewer is started. It also checks that the reviewer changed no tracked file and made no commit. The review verdicts are different: each reviewer's reply (spec review, then quality review) is relayed by the session, which saves it to a file and hands it to `flow.py verdict`. The script reads `REVIEW: PASS` or `REVIEW: FAIL` from that reply; it cannot prove the session passed it on unedited. A ticket moves on only after two PASS verdicts.
116
+ **What the script checks and what it trusts.** The conductor verifies the build from the repo: the ticket file must say `resolved`, the tree must be clean and committed, and it runs the gate and the floor guard itself. It also runs the `check` block under each ticket's Done when and compares the exit code and printed text, and for a `Test first: yes` ticket it proves the red test commit itself: it checks out the test-only commit, runs Test, and requires it to fail before the code. A failure of either goes back to the builder before any reviewer is started. It also checks that the reviewer changed no tracked file and made no commit. The review verdicts are different: each reviewer's reply (spec review, then quality review) is relayed by the session, which saves it to a file and hands it to `flow.py verdict`. The script reads `REVIEW: PASS` or `REVIEW: FAIL` from that reply; it cannot prove the session passed it on unedited. A ticket moves on only after two PASS verdicts, or one for a ticket the conductor judged small. The size is measured from the repo (`git diff` against the commit the ticket started from), never taken from the builder.
117
+
118
+ **The gate: full or quick.** The gate runs Build, Test and Lint after every ticket. With an optional `Quick:` line in `commands.md` (a faster subset of `Test`), it runs Build, Quick and Lint after the tickets in the middle of a plan, and the full Build, Test and Lint after the last ticket, before a handoff, and under `FLOW_GATE_FULL=1`. A plan without `Quick:` runs the full gate every time. The test-first proof still runs the full `Test` command.
119
+
120
+ **One review for a small ticket.** A ticket whose built diff is 3 files and 60 changed lines at most (the plan folder left out), and whose `check` block ran and passed, gets one combined review instead of the spec and quality reviews. The conductor measures the diff itself. `Review: full` in the ticket (as it was at the start), `Type: convert`, no `check` block, or `FLOW_GATE=off` keep both reviews. Every other conductor check still runs on every ticket.
108
121
 
109
122
  ## The skill and the roles
110
123
 
@@ -200,7 +213,7 @@ One self contained HTML file: no server, no libraries, no network, light and dar
200
213
 
201
214
  ## Long features and handoff
202
215
 
203
- A session's context fills up, so the flow moves to a fresh session every few tickets. After `FLOW_TICKETS_PER_SESSION` tickets (default 4) pass review, `next` prints a `HANDOFF` line instead of the next ticket, for example `HANDOFF /feature-flow csv-export`. The session stops and tells you to open a new session and type that line. The new session picks up exactly where the last one stopped. Set `FLOW_TICKETS_PER_SESSION=0` to never hand off.
216
+ A session's context fills up, so the flow moves to a fresh session every few tickets. After `FLOW_TICKETS_PER_SESSION` tickets (default 4) pass review, `next` prints a `HANDOFF` line instead of the next ticket, for example `HANDOFF /feature-flow csv-export 3f9c0a1b2c3d4e5f.0a1b2c3d4e5f6a7b` (the last word is the runner's digest, which lets the new session check that the flow's files did not change in between). The session stops and tells you to open a new session and type that line. The new session picks up exactly where the last one stopped. Set `FLOW_TICKETS_PER_SESSION=0` to never hand off.
204
217
 
205
218
  - **One owner at a time.** `start` gives the session a token, and every later call must carry it, so two sessions can never drive the same feature. `HANDOFF` and `DONE` release it.
206
219
  - **Resuming after a closed session.** If a session ended without `HANDOFF` (you closed it, it crashed, it ran out of budget), the feature is still owned by it, and a new session stops and says so. Once you are sure the old session is gone, answer yes when the new session asks, or start it with `FLOW_TAKEOVER=1`. With `auto` the skill never takes over on its own.
@@ -213,7 +226,8 @@ The state lives in `.feature-flow/state/` (the state, a log of every step, and t
213
226
  ### Commands and settings
214
227
 
215
228
  ```bash
216
- python3 scripts/flow.py <feature> start|next|prompt|verdict <file> the conductor (the skill runs it)
229
+ python3 scripts/flow.py <feature> start|reset [NN] the conductor (the skill runs it; also plan-prompt, plan-review-prompt, plan-accept)
230
+ FLOW_TRUST=<digest|new> FLOW_SESSION=<token> python3 -I scripts/flow-trust.py <feature> next|prompt|verdict <file> loop commands by hand, through the runner: prints TRUST <digest> first (no hash check, see below)
217
231
  python3 scripts/flow-status.py <feature> table of tickets and which are READY
218
232
  python3 scripts/flow-status.py <feature> --next path of the next ready ticket (exit 10 = done, 11 = stuck)
219
233
  python3 scripts/flow-status.py <feature> --counts one line of counts
@@ -225,6 +239,8 @@ python3 scripts/gate.py <feature> run Build, Test and Li
225
239
  python3 scripts/floor-guard.py <feature> <NN> [base] check a ticket's diff, run from the repo root
226
240
  ```
227
241
 
242
+ `next`, `prompt` and `verdict` run directly stop with `STOP run the conductor through scripts/flow-trust.py, as the skill says`. Pass `FLOW_TRUST=new` on a first call, then the digest from the last `TRUST` line. Run by hand like this, nothing checks `flow-trust.py` itself, so it is for trying things out, not a protected run. The skills never run it this way: they start it through a loader line that first checks the file against the sha256 the skill pins, and stops with `flow-trust.py does not match this skill` if it differs (see `## Calling the conductor` in the skill). For a `--user` install the scripts are in `~/.feature-flow/scripts/` (or `$FEATURE_FLOW_HOME/scripts/`).
243
+
228
244
  The commands in `commands.md` run through `bash -c` on Linux and macOS and through the system shell (`cmd.exe`) on Windows, so write them for the platform your team uses, or call `python` as in the examples.
229
245
 
230
246
  `flow-status.py` also understands the `.scratch/<feature>/issues/` layout and the statuses `done`, `ready-for-agent`, `ready-for-human` and `closed`: `FLOW_DIR=.scratch FLOW_TICKETS=issues`.
@@ -237,13 +253,16 @@ The commands in `commands.md` run through `bash -c` on Linux and macOS and throu
237
253
  | `FLOW_MAX_RETRIES` | `2` | builds or reviews per step before the run stops |
238
254
  | `FLOW_MAX_REVIEW_ROUNDS` | `3` | times a ticket may be sent back before the run stops |
239
255
  | `FLOW_GATE` | `on` | `off` skips Build, Test and Lint after each ticket, and also the Done when and test-first checks |
256
+ | `FLOW_GATE_FULL` | unset | `1` runs the full Build, Test and Lint after every ticket, even when `commands.md` has a `Quick:` line |
257
+ | `FLOW_SMALL_FILES` | `3` | most files a ticket may change and still get the single review |
258
+ | `FLOW_SMALL_LINES` | `60` | most changed lines for the single review; `0` turns the single review off |
240
259
  | `FLOW_GATE_TIMEOUT` | `30` | minutes each gate command and the smoke test may run; `0` means no limit. A failed smoke test writes `.feature-flow/state/<feature>.smoke.log` |
241
260
  | `FLOW_SMOKE` | the `Smoke:` line of `commands.md` | command run before every ticket; the run stops if it fails |
242
261
  | `FLOW_DIR`, `FLOW_TICKETS` | `plans`, `tasks` | where the plans and the ticket folder live |
243
262
  | `FLOW_NO_OPEN` | unset | `1` makes `flow-view.py` never open a browser |
244
263
  | `FLOW_WATCH_SECONDS` | `3` | how often `flow-view.py --watch` rewrites the page |
245
264
 
246
- The run also stops on its own after a number of phases (`BUILD` and `REVIEW` hand-outs, the spec review and the quality review each counting as one) that grows with the ticket count, recomputed at every pick, so a loop of failures cannot go on forever.
265
+ The run also stops on its own after a number of phases (`BUILD` and `REVIEW` hand-outs, the spec review and the quality review each counting as one, a single review counting as one) that grows with the ticket count, recomputed at every pick, so a loop of failures cannot go on forever.
247
266
 
248
267
  ## What is tested
249
268
 
@@ -270,7 +289,7 @@ bash tests/run.sh
270
289
  python3 -m unittest discover -s tests/py
271
290
  ```
272
291
 
273
- Offline and free: no model is called. The suite covers the conductor's every answer and how a run stops, sessions, handoff and takeover, a `.git` the agent cannot write, the gate, the floor guard, plan protection, the prompts and guides, both skills, the installer for both agents, the graph page and its data, the Done when checks, the test-first check, the two reviews and the debugging guide on a send-back, and the acceptance harness. `tests/run.sh` also drives a prepared demo with no model through both checks and both reviews. The page's layout and replay logic are also tested under Node (`tests/viewer-logic.test.js`, skipped when Node is absent). `.github/workflows/tests.yml` runs it on every push to `main` and every pull request, on Ubuntu and on macOS. A fourth job runs the Python unit tests on Windows, including a fixture drive that takes a two-ticket plan through `python scripts/flow.py f start` and `next` to `BUILD` and then `REVIEW`. `tests/run.sh` itself is a bash harness and does not run on Windows. Windows is tested this way only: no real agent run has been done there.
292
+ Offline and free: no model is called. The suite covers the conductor's every answer and how a run stops, sessions, handoff and takeover, a `.git` the agent cannot write, the gate, the floor guard, plan protection, the prompts and guides, both skills, the installer for both agents, the graph page and its data, the Done when checks, the test-first check, the two reviews and the debugging guide on a send-back, and the acceptance harness. `tests/run.sh` also drives a prepared demo with no model through both checks and both reviews. The page's layout and replay logic are also tested under Node (`tests/viewer-logic.test.js`, skipped when Node is absent). `.github/workflows/tests.yml` runs it on every push to `main` and every pull request, on Ubuntu and on macOS. A fourth job runs the Python unit tests on Windows, including a fixture drive that takes a two-ticket plan through `python scripts/flow.py f start` and `next` to `BUILD` and then `REVIEW`, with `FLOW_TRUSTED=1` set as the runner sets it. `tests/run.sh` itself is a bash harness and does not run on Windows. Windows is tested this way only: no real agent run has been done there.
274
293
 
275
294
  A `package` job builds the wheel and the sdist and runs `tests/package_smoke.py` on Ubuntu (Python 3.9 and the latest), macOS and Windows: it installs the wheel in a fresh virtual environment, runs `feature-flow install`, and checks that the repo gets the same files as from `install.py` in a clone. Run it locally with `python3 -m pip install build && python3 tests/package_smoke.py`.
276
295
 
@@ -301,7 +320,13 @@ git tag v0.1.1 && git push origin v0.1.1
301
320
 
302
321
  ## Caution
303
322
 
304
- The builder subagent has edit and shell access and commits after each ticket. Each ticket costs at least two subagents. Build on a branch you can throw away. Ignore build output in `.gitignore`, because the conductor stops if the tree is dirty after a ticket. The gate and the smoke test stop after 30 minutes (`FLOW_GATE_TIMEOUT`). The conductor also checks that the code it runs (the `feature_flow` package, `scripts/*.py` and the build and review role and guide files) did not change during a build or review; if it did, the run stops with `STOP flow code changed while building <ticket>: <files>`, and `Floor: allow flow-edit` on the ticket allows it for the build. That check is a tripwire, not a sandbox. The floor guard is pattern matching: it can miss things and it can raise false alarms, and `Floor: allow` is the release valve. The run stops on its own when a ticket stays unresolved, when the smoke test or the gate keeps failing, when a ticket keeps failing review, or when it reaches its phase limit.
323
+ The builder subagent has edit and shell access and commits after each ticket. Each ticket costs at least two subagents. Build on a branch you can throw away. Ignore build output in `.gitignore`, because the conductor stops if the tree is dirty after a ticket. The gate and the smoke test stop after 30 minutes (`FLOW_GATE_TIMEOUT`). The session runs every conductor call through `scripts/flow-trust.py`, which sits beside the conductor it runs (`scripts/` in the repo, or `~/.feature-flow/scripts/` for a `--user` install). The skill pins the runner's sha256 and starts it with a one-line loader that checks and runs those same bytes, so a subagent cannot change the check. The runner prints `TRUST <code>.<state>` before the conductor's output, and the session passes that digest to the next call as `FLOW_TRUST`.
324
+
325
+ **Protected:** edits by the builder, reviewer or planner subagents during a session to feature-flow's own files (its `feature_flow` package, its files in `scripts/` (`flow.py`, `flow-status.py`, `gate.py`, `floor-guard.py`, `flow-view.py`, `flow-trust.py`), the guides, the roles and the skill folders, in the repo and in the install folders) and to the run state in `.feature-flow/state/`. That includes edits made by code the conductor runs during a step, such as tests, `check` commands and git hooks. Any of these makes the runner print a `STOP` instead of `TRUST`, naming the changed files where it can, and the session stops. It also holds between sessions when the `HANDOFF` line carries its digest (`HANDOFF /feature-flow <feature> <digest>`). `Floor: allow flow-edit` on a ticket allows its build to change the flow's code (never the run state, and never `scripts/flow-trust.py`, whose hash the skill pins: change the runner outside the flow, or build it with a conductor from another install, then update the pinned sha in both skills); the runner then prints `FLOW-EDIT <files>`, and a session run without `auto` tells you which flow files changed.
326
+
327
+ **Not protected:** a subagent that changes `python3` on PATH or the system Python, the agent itself, or anything outside the repo and the install folders. The session's very first conductor call: whatever is on disk then is trusted. A background process left running by a step's own commands: it can swap a flow file in the instant between the runner's check and the conductor loading it, then put it back. A session that stops passing its digest, or a resume from a `HANDOFF` line without one (the session warns). The conductor's own code check (`STOP flow code changed while building <ticket>: <files>`) stays as a second line that catches accidental edits; on its own it cannot stop deliberate tampering.
328
+
329
+ The floor guard is pattern matching: it can miss things and it can raise false alarms, and `Floor: allow` is the release valve. The run stops on its own when a ticket stays unresolved, when the smoke test or the gate keeps failing, when a ticket keeps failing review, or when it reaches its phase limit.
305
330
 
306
331
  ## License
307
332
 
@@ -0,0 +1,80 @@
1
+ ---
2
+ name: feature-flow
3
+ description: "Use to plan a feature (from the conversation so far, with a fresh planner and plan reviewer subagent) or build its tickets, one fresh builder and one fresh reviewer subagent per ticket, with scripts/flow.py deciding every step. Also draws the ticket graph with show."
4
+ ---
5
+
6
+ `<arguments>` below stands for the text the user typed after `$feature-flow`.
7
+
8
+ Plan or build the feature in `<arguments>`.
9
+
10
+ The first word is the **feature**. `show` means draw the graph. `auto` means ask nothing and go. If no feature was given and the conversation so far describes work to build, plan it (below) and propose the feature's name in the brief. If no feature was given otherwise, list `plans/*/` and ask which one.
11
+
12
+ The conductor is `python3 scripts/flow.py <feature> <command>`. If `scripts/flow.py` exists in the repo and the feature-flow package is beside it (`.feature-flow/feature_flow/`, or `feature_flow/` in a feature-flow checkout), use it (the repo's own install wins; an unrelated `scripts/flow.py` does not count); otherwise use `python3 "${FEATURE_FLOW_HOME:-$HOME/.feature-flow}/scripts/flow.py"` (`~/.feature-flow/scripts/flow.py`). Wherever this skill or a message names `scripts/flow.py`, `scripts/flow-status.py` or `scripts/flow-view.py`, it means the same folder, and the guides are then in that folder's sibling `guides/` (`~/.feature-flow/guides/`, else `.feature-flow/guides/` in the repo). Run the conductor with the working directory in the repo: state and plans stay there. It decides the order, runs the gate and the floor guard, and keeps its state in `.feature-flow/state/`, a folder git ignores. You ask it, and you do what it says. The guides it uses are in `.feature-flow/guides/` (in a feature-flow checkout, `guides/`). Read the project's `AGENTS.md` for its conventions.
13
+
14
+ ## Calling the conductor
15
+
16
+ Every conductor command (`start`, `next`, `prompt`, `verdict`, `reset`, `plan-prompt`, `plan-review-prompt`, `plan-accept`) runs through `flow-trust.py`, never `flow.py` itself. `<S>` below is the folder of the `flow.py` picked above: `scripts` for the repo's own install, else `"${FEATURE_FLOW_HOME:-$HOME/.feature-flow}/scripts"`. `flow-trust.py` is started by this loader line, which checks the file against the sha256 pinned here and runs those same bytes. Copy it exactly, replacing only `<S>`:
17
+
18
+ python3 -I -c "import hashlib,sys;b=open(sys.argv[1],'rb').read().replace(b'\r\n',b'\n');sys.exit(exec(compile(b,'flow-trust','exec')) if hashlib.sha256(b).hexdigest()==sys.argv[2] else 'flow-trust.py does not match this skill')" <S>/flow-trust.py 3a4b28b079b7c657e9a6d843cf1e69ac3c794639ea072f3be96cf94726ab370e
19
+
20
+ A full conductor command, here `next` (any other command and its arguments go where `next` is):
21
+
22
+ FLOW_TRUST=<digest> FLOW_SESSION=<token> FLOW_INVOKE='$feature-flow' python3 -I -c "import hashlib,sys;b=open(sys.argv[1],'rb').read().replace(b'\r\n',b'\n');sys.exit(exec(compile(b,'flow-trust','exec')) if hashlib.sha256(b).hexdigest()==sys.argv[2] else 'flow-trust.py does not match this skill')" <S>/flow-trust.py 3a4b28b079b7c657e9a6d843cf1e69ac3c794639ea072f3be96cf94726ab370e <feature> next
23
+
24
+ - The first line of the output is `TRUST <digest>`. Keep that digest and pass it as `FLOW_TRUST` on the next conductor call. Treat the rest as the conductor's output and act on it as below (for `prompt`, `plan-prompt` and `plan-review-prompt`, the child agent's prompt is everything after the `TRUST` line). Use `FLOW_TRUST=new` only on this session's first conductor call, or when resuming from an old-style `HANDOFF` line (below).
25
+ - If you no longer have the last `TRUST` digest, stop and say so. Never guess it, and never use `new` in its place.
26
+ - On the `next` right after a `BUILD <ticket> <NN> <sha>`, add `--after-build <ticket> <sha>` before the feature: `... <S>/flow-trust.py 3a4b28b079b7c657e9a6d843cf1e69ac3c794639ea072f3be96cf94726ab370e --after-build <ticket> <sha> <feature> next`. If the output has a `FLOW-EDIT <paths>` line after `TRUST`, the build changed feature-flow's own files and its ticket allows that: carry on; without `auto`, first tell the user which flow files changed.
27
+ - If the first line is `STOP` (from `flow-trust.py`), or the loader prints `flow-trust.py does not match this skill` (say to reinstall feature-flow: the skill and `flow-trust.py` do not match), report it, show the `python3 <S>/flow-status.py <feature>` table, and stop. A `STOP` first line may be followed by the conductor's own output: never act on that. Never fix it and carry on.
28
+ - A `HANDOFF` line from `flow-trust.py` has a third field after the invocation and the feature: the digest. When the feature is followed by a `<16 hex>.<16 hex>` word, the user is resuming from a `HANDOFF` line and that word is the digest: pass it as `FLOW_TRUST` on `start`. When a resumed session gets no digest (an old-style `HANDOFF` line), use `FLOW_TRUST=new` and warn the user that the flow's files could not be checked since the last session.
29
+
30
+ ## Show
31
+
32
+ With `show`, follow `.feature-flow/guides/show.md` for the feature and stop.
33
+
34
+ ## Plan
35
+
36
+ When `plans/<feature>/` does not exist, or you are planning from the conversation, follow `.feature-flow/guides/brief.md` with the user. You write the brief and ask for both yeses (unless `auto`); a child agent plans, another reviews, and you never draft the plan yourself. Once the brief names the feature, run `start` through `flow-trust.py` (see Calling the conductor) with `FLOW_INVOKE='$feature-flow'`, and check that it prints `PLAN` after the `TRUST` line, and check that `feature-planner.md` and `plan-reviewer.md` exist in `.agents/flow-roles/` or in `${FEATURE_FLOW_HOME:-$HOME/.feature-flow}/agents/` (see below).
37
+
38
+ - `plan-prompt <brief>`: start the planner with `spawn_agent(task_name="feature_planner", fork_turns="none", message=...)`. The message is "Work only in <repo>." followed by the whole output. Get its final reply with `wait_agent`. A child cannot be limited to the draft folder, so it works from its instructions, and `plan-accept` is the only way its draft reaches `plans/`. If web search is off in this Codex, the planner plans from the codebase and says so.
39
+ - `plan-review-prompt`: start a new reviewer the same way, with `task_name="plan_reviewer"` and `fork_turns="none"`, never with the planner's reply. Get its final reply with `wait_agent` and save it with the shell into `.feature-flow/state/plan-review-<feature>.txt`.
40
+ - `plan-accept`: writes `plans/<feature>/` from the draft, only after the second yes.
41
+
42
+ Then run `python3 scripts/flow-status.py <feature> --check` and list the files `plan-accept` wrote. The second yes also approves committing the plan, so commit the folder `plan-accept` printed (`OK <folder>`, `plans/<feature>` unless `FLOW_DIR` is set) and nothing else: `git add -- <folder> && git commit -m "docs(<feature>): plan" -- <folder>`. If the commit fails, report why and stop. Then go straight on to building it below, unless the user asked to stop after planning.
43
+
44
+ To add tickets to a plan that exists, edit it by hand following the ticket rules in `.feature-flow/guides/plan.md`.
45
+
46
+ ## Before building
47
+
48
+ With a plan present, check these before `start`, and stop at the first that fails:
49
+
50
+ - `git status --porcelain` is empty, apart from untracked Python bytecode (`__pycache__/`, `*.pyc`) and files listed in `.feature-flow/installed.txt` and that list itself: those are feature-flow's own install or upgrade, so never stop for them (`next` still stops if one of them was edited). Suggest committing them (`git add --pathspec-from-file=.feature-flow/installed.txt && git commit -m "chore: install feature-flow"`) and carry on.
51
+ - `python3 scripts/flow-status.py <feature> --check` prints `OK`.
52
+ - `ticket-builder.md` and `ticket-reviewer.md` are in `.agents/flow-roles/` in the repo or in `${FEATURE_FLOW_HOME:-$HOME/.feature-flow}/agents/` (`~/.feature-flow/agents/` by default). If not, say to run `uvx feature-flow-cli install . --agent codex` in this repo, or `uvx feature-flow-cli install --user --agent codex` (once for every repo) (or `python3 install.py <repo> --agent codex` from a feature-flow clone).
53
+
54
+ Then run `start` through `flow-trust.py` with `FLOW_INVOKE='$feature-flow'`. After the `TRUST` line it prints `OK <token>`. Keep the token and put `FLOW_SESSION=<token>` in front of **every** later conductor command, with `FLOW_INVOKE='$feature-flow'` and the last `FLOW_TRUST`. The conductor keeps its state in `.feature-flow/state/`, a git-ignored folder in the repo, so it never needs to write `.git`. If it prints `STOP cannot write the flow state`, tell the user this session must be allowed to write that folder.
55
+
56
+ If `start` prints `STOP` naming another owner, another session may still be working this feature. Ask the user whether that session is closed. Only on a clear yes, run `start` once more with `FLOW_TAKEOVER=1`. In `auto` mode, never take over: report and stop.
57
+
58
+ If the user says the flow is stuck on a ticket or asks for a reset, run `reset` (or `reset <NN>` to redo one ticket): it reopens a half-built ticket, commits that, and clears the run, then run `start` again. It needs `FLOW_TAKEOVER=1` while a session owns the feature, on the same clear yes. Never reset on your own.
59
+
60
+ Say what will happen: for each ticket, a builder subagent and then a fresh reviewer subagent for each review pass, one or two per ticket. After every few tickets (`FLOW_TICKETS_PER_SESSION`, default 4) you hand off to a new session. Ask for a yes, unless `auto` was given or you came straight from planning: the plan's yes already covered the build.
61
+
62
+ ## The loop
63
+
64
+ Run `next`, act on its one line after `TRUST` (and any `FLOW-EDIT` line), and repeat:
65
+
66
+ - `BUILD <ticket> <NN> <sha>`: run `prompt`. Start the builder with `spawn_agent(task_name="ticket_builder_<NN>_<k>", fork_turns="none", message=...)`. The message is "Work only in <repo>." followed by the whole `prompt` output, which already starts with the builder role. Use a new `task_name` for every spawn: keep a count per ticket in this session, and `<k>` is the number of builders already started for this ticket, plus 1 (1 for the first, then up each time `next` sends the ticket back to a builder). Get its final reply with `wait_agent`, then run `next` with `--after-build <ticket> <sha>` from that `BUILD` line.
67
+ - `REVIEW <ticket> <NN> <sha>`: run `prompt`. Start a new reviewer the same way, with `fork_turns="none"` so it never sees the builder's context, and a unique `task_name` per spawn: `ticket_reviewer_<NN>_<pass>_<k>`, where `<pass>` is `spec`, `quality` or `single` by the pass the prompt names (`(spec pass)`, `(quality pass)` or `(single pass)`; `review` if it names none) and `<k>` is the number of reviews already started for this ticket in this session, plus 1 (1 for the first). Count every review, also one that ended in `RETRY` or a failed review followed by a rebuild, so a repeated pass never reuses a name. Get its final reply with `wait_agent`. A child cannot be made read-only, so the reviewer works from its instructions; the conductor catches any edit it makes. Save the whole reply with the shell: `cat > .feature-flow/state/flow-review-<feature>.txt <<'EOF'` ... `EOF`. Then run `verdict .feature-flow/state/flow-review-<feature>.txt` and `next`. If `verdict` prints `RETRY`, just run `next`.
68
+ - `DONE <summary>`: report it and suggest `$feature-flow <feature> show`.
69
+ - `STOP <reason>`: report the reason, run `python3 scripts/flow-status.py <feature>`, show the table, and stop.
70
+ - `HANDOFF <line>`: stop here. Tell the user to open a new session and type exactly `<line>`, the whole line with its digest at the end. The new session resumes where this one stopped.
71
+
72
+ ## Rules while building
73
+
74
+ These apply from `start` on, once a plan exists.
75
+
76
+ - Never run the gate, the floor guard or a review yourself. The conductor runs the checks, and the reviewer subagent reviews.
77
+ - Never edit a ticket, the plan or the code, and never commit, apart from the approved plan above. The builder does that.
78
+ - Never skip a step, reorder steps, or decide the next step yourself. Only `next` decides.
79
+ - Never read a ticket's `## Answer` into the reviewer's prompt. `prompt` already holds everything it needs.
80
+ - Tell the user one short line per phase (`01 built`, `01 review: PASS`), not the subagents' reports.
@@ -1,3 +1,3 @@
1
1
  """Feature Flow: a ticket-graph workflow for coding agents."""
2
2
 
3
- __version__ = "0.3.0"
3
+ __version__ = "0.7.0"
@@ -55,11 +55,11 @@ def flow_status(scripts, feature, mode, root=None):
55
55
  return Result(code, out.text())
56
56
 
57
57
 
58
- def gate(scripts, feature, timeout=None):
58
+ def gate(scripts, feature, timeout=None, quick=False):
59
59
  """The gate, run in process. stderr is folded into the output as the subprocess call did.
60
- timeout is minutes, None for FLOW_GATE_TIMEOUT."""
60
+ timeout is minutes, None for FLOW_GATE_TIMEOUT. quick runs Quick in the place of Test."""
61
61
  chunks = []
62
- code = gate_module.run(str(feature), chunks.append, chunks.append, timeout)
62
+ code = gate_module.run(str(feature), chunks.append, chunks.append, timeout, quick)
63
63
  out = "".join(chunks).encode("utf-8", "surrogateescape").decode("utf-8", "replace")
64
64
  out = out.replace("\r\n", "\n").replace("\r", "\n")
65
65
  return Result(code, out)
@@ -1,19 +1,69 @@
1
1
  """python3 scripts/flow.py <feature> <command>: the conductor's command line."""
2
2
 
3
+ import os
3
4
  import sys
5
+ import traceback
4
6
  from pathlib import Path
5
7
 
6
- from feature_flow import suggest
8
+ from feature_flow import git, state, suggest
7
9
  from feature_flow.conductor import Conductor, NoPhase, Stop
8
10
 
9
11
  USAGE = ("usage: python3 scripts/flow.py <feature> start | next | prompt | verdict <file> | reset [NN]\n"
10
12
  " python3 scripts/flow.py <feature> plan-prompt <brief> [findings] | plan-review-prompt | plan-accept")
11
13
  COMMANDS = ("start", "next", "prompt", "verdict", "plan-prompt", "plan-review-prompt", "plan-accept", "reset")
12
14
  ARGS = {"verdict": (3,), "plan-prompt": (3, 4), "reset": (2, 3)}
15
+ RUNNER_ONLY = ("next", "prompt", "verdict")
13
16
 
14
17
 
15
18
  def main(argv=None, scripts=None):
16
- args = list(sys.argv[1:] if argv is None else argv)
19
+ """With FLOW_TRUSTED=1 (set by scripts/flow-trust.py) the last stderr line is always
20
+ `flow-state <state sha256 or none> <findings sha256 or none>`, for the bytes the conductor left."""
21
+ trusted = os.environ.get("FLOW_TRUSTED") == "1"
22
+ state.SEEN.clear()
23
+ try:
24
+ args = list(sys.argv[1:] if argv is None else argv)
25
+ if trusted:
26
+ peek(args)
27
+ return run(args, scripts, trusted)
28
+ except SystemExit as err: # say what the interpreter would say, but before the report
29
+ if not trusted:
30
+ raise
31
+ if err.code is None or isinstance(err.code, int):
32
+ return err.code or 0
33
+ print(err.code, file=sys.stderr)
34
+ return 1
35
+ except BaseException: # the traceback goes before the report, which stays the last stderr line
36
+ if not trusted:
37
+ raise
38
+ traceback.print_exc()
39
+ return 1
40
+ finally:
41
+ if trusted:
42
+ sys.stdout.flush()
43
+ sys.stderr.write("flow-state %s\n" % state.reported())
44
+ sys.stderr.flush()
45
+
46
+
47
+ def peek(args):
48
+ """Read the feature's state and findings files before anything can return early, so the report
49
+ names the files that are there even after a usage error. Creates nothing."""
50
+ if not args or not plain(args[0]):
51
+ return
52
+ feature = args[0]
53
+ top = git.toplevel()
54
+ if top is not None:
55
+ folder = top / state.STATE_DIR
56
+ state.read_bytes(state.state_path(folder, feature), "state")
57
+ state.read_bytes(state.file_path(folder, feature, "findings"), "findings")
58
+
59
+
60
+ def plain(feature):
61
+ """True when feature is a plain folder name: not empty, no leading -, no path parts."""
62
+ return (bool(feature) and not feature.startswith("-") and feature not in (".", "..")
63
+ and not any(c in feature for c in "/\\:"))
64
+
65
+
66
+ def run(args, scripts, trusted):
17
67
  if len(args) < 2 or args[1] not in COMMANDS or not args[0] or args[0].startswith("-"):
18
68
  print(USAGE, file=sys.stderr)
19
69
  if len(args) >= 2 and args[0] in COMMANDS:
@@ -22,12 +72,15 @@ def main(argv=None, scripts=None):
22
72
  sys.stderr.write(suggest.hint(args[1], COMMANDS))
23
73
  return 2
24
74
  feature, command = args[0], args[1]
25
- if feature in (".", "..") or any(c in feature for c in "/\\:"):
75
+ if not plain(feature):
26
76
  print("the feature must be a plain folder name, not %s" % feature, file=sys.stderr)
27
77
  return 2
28
78
  if len(args) not in ARGS.get(command, (2,)):
29
79
  print(USAGE, file=sys.stderr)
30
80
  return 2
81
+ if command in RUNNER_ONLY and not trusted:
82
+ print("STOP run the conductor through scripts/flow-trust.py, as the skill says")
83
+ return 1
31
84
  scripts = Path(scripts) if scripts else Path.cwd() / "scripts"
32
85
  conductor = None
33
86
  try:
@@ -7,12 +7,16 @@ from pathlib import Path
7
7
 
8
8
  from feature_flow import git, prompts
9
9
 
10
+ # feature-flow's own scripts; any other .py file in the same folder is the repo's own code
11
+ SCRIPTS = ("flow.py", "flow-status.py", "gate.py", "floor-guard.py", "flow-view.py", "flow-trust.py")
12
+
10
13
 
11
14
  def _files(scripts):
12
15
  package = Path(__file__).resolve().parent
13
16
  found = [p for p in package.rglob("*.py") if "__pycache__" not in p.parts]
14
- found += Path(scripts).resolve().glob("*.py")
15
- for phase in ("build", "review", "review-quality"):
17
+ folder = Path(scripts).resolve()
18
+ found += [folder / name for name in SCRIPTS if (folder / name).is_file()]
19
+ for phase in ("build", "review", "review-quality", "review-single"):
16
20
  for folder, names in (("agents", prompts.ROLES), ("guides", prompts.GUIDES)):
17
21
  try:
18
22
  found.append(prompts.find_file(scripts, folder, names[phase]).resolve())
@@ -9,21 +9,23 @@ import sys
9
9
 
10
10
  from pathlib import Path
11
11
 
12
- from feature_flow import __version__, cli, install, status, suggest, view
12
+ from feature_flow import __version__, cli, install, status, suggest, uninstall, view
13
13
 
14
14
  USAGE = """\
15
15
  usage: feature-flow install [target-repo] [--agent claude|codex|all] [--user] [--private] [--force] [--dry-run]
16
+ feature-flow uninstall [target-repo] [--user] [--force] [--dry-run]
16
17
  feature-flow status <feature> [--next | --counts | --check | --mermaid [plain] | --json]
17
18
  feature-flow view <feature> [--watch] [--no-open] [--out FILE]
18
19
  feature-flow reset <feature> [NN]
19
20
  feature-flow --version
20
- install copies the skill, its roles, scripts and guides into a repo; feature-flow install --help says more.
21
+ install copies the skill, its roles, scripts and guides into a repo; --user installs them once for every repo, in ~/.claude, ~/.agents and ~/.feature-flow, and writes nothing into a repo. feature-flow install --help says more.
22
+ uninstall removes exactly what install wrote (files you edited are kept); --user removes the personal install in ~/.claude, ~/.agents and ~/.feature-flow.
21
23
  status and view read plans/<feature>/ in the current directory, like scripts/flow-status.py and scripts/flow-view.py.
22
24
  reset reopens a half-built ticket (or ticket NN), commits that, and clears the run, like scripts/flow.py <feature> reset.
23
25
  """
24
26
 
25
27
 
26
- COMMANDS = ("install", "status", "view", "reset", "--version", "--help")
28
+ COMMANDS = ("install", "uninstall", "status", "view", "reset", "--version", "--help")
27
29
 
28
30
 
29
31
  def main(argv=None):
@@ -38,6 +40,8 @@ def main(argv=None):
38
40
  return 0
39
41
  if command == "install":
40
42
  return install.main(install.source_root(), rest)
43
+ if command == "uninstall":
44
+ return uninstall.main(rest)
41
45
  if command == "status":
42
46
  return status.main(rest)
43
47
  if command == "view":