feature-flow-cli 0.5.0__tar.gz → 0.8.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 (66) hide show
  1. {feature_flow_cli-0.5.0/feature_flow_cli.egg-info → feature_flow_cli-0.8.0}/PKG-INFO +35 -9
  2. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/README.md +34 -8
  3. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/adapters/codex/feature-flow/SKILL.md +5 -5
  4. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow/__init__.py +1 -1
  5. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow/checks.py +3 -3
  6. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow/cli.py +1 -1
  7. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow/codehash.py +1 -1
  8. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow/command.py +6 -2
  9. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow/conductor.py +49 -8
  10. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow/floorguard.py +11 -2
  11. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow/gate.py +6 -2
  12. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow/git.py +18 -0
  13. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow/prompts.py +4 -2
  14. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow/proof.py +10 -0
  15. feature_flow_cli-0.8.0/feature_flow/report.py +89 -0
  16. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow/status.py +55 -9
  17. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow/tickets.py +4 -0
  18. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0/feature_flow_cli.egg-info}/PKG-INFO +35 -9
  19. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow_cli.egg-info/SOURCES.txt +2 -0
  20. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/guides/plan-review.md +2 -2
  21. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/guides/plan.md +6 -2
  22. feature_flow_cli-0.8.0/guides/review-single.md +87 -0
  23. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/guides/templates/commands.md +4 -1
  24. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/guides/templates/ticket.md +2 -1
  25. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/scripts/flow-trust.py +1 -1
  26. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/skills/feature-flow/SKILL.md +4 -4
  27. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/LICENSE +0 -0
  28. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/MANIFEST.in +0 -0
  29. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/adapters/codex/feature-flow/agents/openai.yaml +0 -0
  30. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/agents/feature-planner.md +0 -0
  31. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/agents/plan-reviewer.md +0 -0
  32. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/agents/ticket-builder.md +0 -0
  33. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/agents/ticket-reviewer.md +0 -0
  34. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow/__main__.py +0 -0
  35. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow/install.py +0 -0
  36. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow/proc.py +0 -0
  37. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow/released.py +0 -0
  38. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow/state.py +0 -0
  39. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow/suggest.py +0 -0
  40. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow/uninstall.py +0 -0
  41. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow/view.py +0 -0
  42. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow_cli.egg-info/dependency_links.txt +0 -0
  43. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow_cli.egg-info/entry_points.txt +0 -0
  44. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow_cli.egg-info/top_level.txt +0 -0
  45. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/guides/brief.md +0 -0
  46. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/guides/build.md +0 -0
  47. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/guides/debug.md +0 -0
  48. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/guides/review-quality.md +0 -0
  49. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/guides/review.md +0 -0
  50. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/guides/show.md +0 -0
  51. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/guides/templates/learnings.md +0 -0
  52. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/guides/templates/map.md +0 -0
  53. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/guides/templates/spec.md +0 -0
  54. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/guides/templates/ui-mockup.md +0 -0
  55. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/install.py +0 -0
  56. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/install.sh +0 -0
  57. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/pyproject.toml +0 -0
  58. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/scripts/floor-guard.py +0 -0
  59. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/scripts/flow-status.py +0 -0
  60. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/scripts/flow-view.html +0 -0
  61. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/scripts/flow-view.py +0 -0
  62. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/scripts/flow.py +0 -0
  63. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/scripts/gate.py +0 -0
  64. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/setup.cfg +0 -0
  65. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/skills/architect-review/SKILL.md +0 -0
  66. {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.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.5.0
3
+ Version: 0.8.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
@@ -25,10 +25,10 @@ Dynamic: license-file
25
25
 
26
26
  Plan a feature as a graph of small tickets, then build them in the session you are already in. One command, `/feature-flow`, does both. Every ticket is built by one fresh subagent and checked by another that never saw the builder's reasoning. A script, not the agent, decides the order of the steps, runs the build and tests, and watches for work that weakens the checks, and the whole run has limits.
27
27
 
28
- One skill, 2 agent roles, a Python conductor, 4 Python scripts, an installer for both agents, a demo and a test suite. All of it is Python (standard library only), so it runs on Linux, macOS and Windows. MIT licensed.
28
+ One skill, 4 agent roles, a Python conductor, 6 Python scripts (`flow`, `flow-trust`, `flow-status`, `flow-view`, `gate`, `floor-guard`), an installer for both agents, a demo and a test suite. All of it is Python (standard library only), so it runs on Linux, macOS and Windows. MIT licensed.
29
29
 
30
30
  - [Quick start](#quick-start) · [How a ticket flows](#how-a-ticket-flows) · [The skill and the roles](#the-skill-and-the-roles) · [Plans and tickets](#plans-and-tickets)
31
- - [See the graph](#see-the-graph) · [Long features and handoff](#long-features-and-handoff) · [What is tested](#what-is-tested) · [Upgrading](#upgrading) · [Caution](#caution)
31
+ - [See the graph](#see-the-graph) · [Long features and handoff](#long-features-and-handoff) · [What is tested](#what-is-tested) · [Limits](#limits) · [Upgrading](#upgrading) · [Caution](#caution)
32
32
 
33
33
  ## Quick start
34
34
 
@@ -51,7 +51,7 @@ Without a clone, the same installer runs from [PyPI](https://pypi.org/project/fe
51
51
  uvx feature-flow-cli install /path/to/your/repo --agent all # or: pipx run feature-flow-cli install ...
52
52
  ```
53
53
 
54
- The package puts a `feature-flow` command on your PATH with `install`, `status <feature>` and `view <feature>`. It installs the same files, byte for byte, as `install.py` from a clone.
54
+ The package puts a `feature-flow` command on your PATH with `install`, `uninstall`, `status <feature>`, `view <feature>`, `reset <feature>` and `report <feature>`. It installs the same files, byte for byte, as `install.py` from a clone.
55
55
 
56
56
  The installer lists what it wrote in `.feature-flow/installed.txt`, with each file's hash in `.feature-flow/installed.sha256`. Commit those files (`git add --pathspec-from-file=.feature-flow/installed.txt`); until you do, the flow does not count them as uncommitted changes. Installing a newer version over an older one updates every file nobody edited since it was installed, and keeps (and names) the ones you changed; `--force` replaces those too.
57
57
 
@@ -71,7 +71,7 @@ Then, in your repo, in the agent:
71
71
 
72
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.
73
73
 
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/`. 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.
75
75
 
76
76
  Try the graph first, with no setup and no agent: `bash examples/demo.sh --open`.
77
77
 
@@ -121,6 +121,8 @@ The Claude Code skill is `skills/feature-flow/`. The Codex skill is written by h
121
121
  │ PASS │
122
122
  ▼ │
123
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)
124
126
  │ PASS ▼
125
127
  ▼ findings written into the ticket,
126
128
  next ticket reopened, built again (3 rounds max);
@@ -130,7 +132,11 @@ The Claude Code skill is `skills/feature-flow/`. The Codex skill is written by h
130
132
 
131
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.
132
134
 
133
- **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.
134
140
 
135
141
  ## The skill and the roles
136
142
 
@@ -197,14 +203,18 @@ prints OK
197
203
  - **The Answers are the handoff between subagents.** The next builder reads the Answers of the tickets it depends on.
198
204
  - `settle` tickets record a decision. `convert` tickets migrate something and must be safe to run twice.
199
205
  - **A worker may change only** its own Status line and Answer, lines appended to `map.md` and `learnings.md`, and the code the ticket calls for. Rewriting its own Done when, editing another ticket, or touching `commands.md` fails the guard.
200
- - `Floor:` categories: `skip`, `suppress`, `empty-catch`, `test-delete`, `threshold`, `config`, `ticket-edit`, `commands-edit`, and `flow-edit` (allows the conductor's code check below, for the build only). `skip` covers skipped, expected-fail and focused tests (`.only(`, `fit(`, `fdescribe(` on test paths); `config` covers more lint and test config files, the lint and test sections of `pyproject.toml` and `setup.cfg`, scripts and test keys in `package.json`, new skip or xfail words in `conftest.py`, and a `Makefile` only when `commands.md` uses `make`. A test file that loses assertions is shown to the reviewer as a warning, not a finding. The line is read from the ticket as it was before the work started, so a worker cannot excuse itself.
206
+ - `Floor:` categories: `skip`, `suppress`, `empty-catch`, `test-delete`, `threshold`, `trivial-assert` (an assertion that cannot fail, added to a test file: `assert True`, `assertEqual(x, x)`, `expect(v).toBe(v)`), `config`, `ticket-edit`, `commands-edit`, and `flow-edit` (allows the conductor's code check below, for the build only). `skip` covers skipped, expected-fail and focused tests (`.only(`, `fit(`, `fdescribe(` on test paths); `config` covers more lint and test config files, the lint and test sections of `pyproject.toml` and `setup.cfg`, scripts and test keys in `package.json`, new skip or xfail words in `conftest.py`, and a `Makefile` only when `commands.md` uses `make`. A test file that loses assertions is shown to the reviewer as a warning, not a finding. The line is read from the ticket as it was before the work started, so a worker cannot excuse itself.
201
207
  - The guard needs the plan to be tracked by git. If you keep tickets in an ignored folder, it cannot see changes to them.
202
208
  - The commands in `commands.md` run with nobody watching and must exit non zero on failure.
203
209
 
204
210
  ## See the graph
205
211
 
212
+ ![The animated ticket graph for the demo plan: five of ten tickets resolved, two ready, one in progress](docs/flow-view.png)
213
+
214
+ *The page `flow-view` writes, shown for the demo plan from `examples/demo.sh`: resolved tickets in green, ready ones in amber, the one being worked in blue, the rest waiting. Press Replay to watch the run back from git history.*
215
+
206
216
  ```bash
207
- python3 scripts/flow-view.py csv-export write the page and open it
217
+ python3 scripts/flow-view.py csv-export write the page and open it (or: feature-flow view csv-export)
208
218
  python3 scripts/flow-view.py csv-export --watch keep it updating while tickets are built
209
219
  bash examples/demo.sh --open try it on a made up project, no setup needed
210
220
  ```
@@ -245,6 +255,8 @@ python3 scripts/flow-status.py <feature> table of tickets and w
245
255
  python3 scripts/flow-status.py <feature> --next path of the next ready ticket (exit 10 = done, 11 = stuck)
246
256
  python3 scripts/flow-status.py <feature> --counts one line of counts
247
257
  python3 scripts/flow-status.py <feature> --check cycles, missing blockers, missing Done when, unordered mentions
258
+ python3 scripts/flow-status.py <feature> --lint --check plus: a check block (or `Check: none <why>`) on every ticket, a Test command
259
+ feature-flow report <feature> what a run did, from its log: builds, reviews, failed checks, stops
248
260
  python3 scripts/flow-status.py <feature> --mermaid the ticket graph, coloured by status (add "plain" for none)
249
261
  python3 scripts/flow-status.py <feature> --json every ticket with status, blockers and readiness
250
262
  python3 scripts/flow-view.py <feature> [--watch] the animated graph page, opened in your browser
@@ -266,13 +278,16 @@ The commands in `commands.md` run through `bash -c` on Linux and macOS and throu
266
278
  | `FLOW_MAX_RETRIES` | `2` | builds or reviews per step before the run stops |
267
279
  | `FLOW_MAX_REVIEW_ROUNDS` | `3` | times a ticket may be sent back before the run stops |
268
280
  | `FLOW_GATE` | `on` | `off` skips Build, Test and Lint after each ticket, and also the Done when and test-first checks |
281
+ | `FLOW_GATE_FULL` | unset | `1` runs the full Build, Test and Lint after every ticket, even when `commands.md` has a `Quick:` line |
282
+ | `FLOW_SMALL_FILES` | `3` | most files a ticket may change and still get the single review |
283
+ | `FLOW_SMALL_LINES` | `60` | most changed lines for the single review; `0` turns the single review off |
269
284
  | `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` |
270
285
  | `FLOW_SMOKE` | the `Smoke:` line of `commands.md` | command run before every ticket; the run stops if it fails |
271
286
  | `FLOW_DIR`, `FLOW_TICKETS` | `plans`, `tasks` | where the plans and the ticket folder live |
272
287
  | `FLOW_NO_OPEN` | unset | `1` makes `flow-view.py` never open a browser |
273
288
  | `FLOW_WATCH_SECONDS` | `3` | how often `flow-view.py --watch` rewrites the page |
274
289
 
275
- 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.
290
+ 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.
276
291
 
277
292
  ## What is tested
278
293
 
@@ -305,6 +320,17 @@ A `package` job builds the wheel and the sdist and runs `tests/package_smoke.py`
305
320
 
306
321
  `RUN_REAL=1 bash tests/smoke-real.sh --interactive` is the acceptance run with Claude Code. It runs real sessions on the demo project and spends money, which is why it asks for `RUN_REAL=1`.
307
322
 
323
+ ## Limits
324
+
325
+ feature-flow's layers are tripwires, not a security boundary. They catch accidents and lazy shortcuts by an agent; a determined agent that controls the session could still get around them.
326
+
327
+ - **The verdict relay is trusted.** The session saves a reviewer's reply and hands it to `flow.py verdict`. The conductor reads `REVIEW: PASS` or `REVIEW: FAIL` from that file and cannot prove the session passed it on unedited.
328
+ - **The floor guard reads diffs with patterns.** It does not see a test made trivially true by other means (a mock that returns the expected answer, an `assert` on something that is always set, a test that no longer exercises the code), or an assertion weakened without being deleted. It does catch assertions that cannot fail (`trivial-assert`) and warns the reviewer when a test file loses assertions. A new test with no assertion at all is not caught. The red-first proof only covers tickets with `Test first: yes`.
329
+ - **A plan can be wrong and still pass.** The plan lint checks that every ticket has a way to be proven and that `commands.md` has a Test command. It cannot tell whether the plan builds the right thing; the second yes is yours.
330
+ - **The trust runner stops accidents, not the session.** It catches a builder editing the conductor mid-run. The session that runs it is the thing it cannot check; a hand-run `flow-trust.py` is unprotected (see above).
331
+ - **Little real-world use so far.** Almost everything has been built by feature-flow on its own repo. The Codex skill and the Codex planner have not been run by hand, and the Quick gate has only been run by tests. See [docs/field-run.md](docs/field-run.md) for how to try it on your own repo and what to send back.
332
+ - **One ticket at a time**, with a handoff to a new session every few tickets.
333
+
308
334
  ## Upgrading
309
335
 
310
336
  Earlier versions had seven skills and an unattended loop. The five flow skills are gone, and `/feature-flow` replaces them:
@@ -6,10 +6,10 @@
6
6
 
7
7
  Plan a feature as a graph of small tickets, then build them in the session you are already in. One command, `/feature-flow`, does both. Every ticket is built by one fresh subagent and checked by another that never saw the builder's reasoning. A script, not the agent, decides the order of the steps, runs the build and tests, and watches for work that weakens the checks, and the whole run has limits.
8
8
 
9
- One skill, 2 agent roles, a Python conductor, 4 Python scripts, an installer for both agents, a demo and a test suite. All of it is Python (standard library only), so it runs on Linux, macOS and Windows. MIT licensed.
9
+ One skill, 4 agent roles, a Python conductor, 6 Python scripts (`flow`, `flow-trust`, `flow-status`, `flow-view`, `gate`, `floor-guard`), an installer for both agents, a demo and a test suite. All of it is Python (standard library only), so it runs on Linux, macOS and Windows. MIT licensed.
10
10
 
11
11
  - [Quick start](#quick-start) · [How a ticket flows](#how-a-ticket-flows) · [The skill and the roles](#the-skill-and-the-roles) · [Plans and tickets](#plans-and-tickets)
12
- - [See the graph](#see-the-graph) · [Long features and handoff](#long-features-and-handoff) · [What is tested](#what-is-tested) · [Upgrading](#upgrading) · [Caution](#caution)
12
+ - [See the graph](#see-the-graph) · [Long features and handoff](#long-features-and-handoff) · [What is tested](#what-is-tested) · [Limits](#limits) · [Upgrading](#upgrading) · [Caution](#caution)
13
13
 
14
14
  ## Quick start
15
15
 
@@ -32,7 +32,7 @@ Without a clone, the same installer runs from [PyPI](https://pypi.org/project/fe
32
32
  uvx feature-flow-cli install /path/to/your/repo --agent all # or: pipx run feature-flow-cli install ...
33
33
  ```
34
34
 
35
- The package puts a `feature-flow` command on your PATH with `install`, `status <feature>` and `view <feature>`. It installs the same files, byte for byte, as `install.py` from a clone.
35
+ The package puts a `feature-flow` command on your PATH with `install`, `uninstall`, `status <feature>`, `view <feature>`, `reset <feature>` and `report <feature>`. It installs the same files, byte for byte, as `install.py` from a clone.
36
36
 
37
37
  The installer lists what it wrote in `.feature-flow/installed.txt`, with each file's hash in `.feature-flow/installed.sha256`. Commit those files (`git add --pathspec-from-file=.feature-flow/installed.txt`); until you do, the flow does not count them as uncommitted changes. Installing a newer version over an older one updates every file nobody edited since it was installed, and keeps (and names) the ones you changed; `--force` replaces those too.
38
38
 
@@ -52,7 +52,7 @@ Then, in your repo, in the agent:
52
52
 
53
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.
54
54
 
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/`. 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.
56
56
 
57
57
  Try the graph first, with no setup and no agent: `bash examples/demo.sh --open`.
58
58
 
@@ -102,6 +102,8 @@ The Claude Code skill is `skills/feature-flow/`. The Codex skill is written by h
102
102
  │ PASS │
103
103
  ▼ │
104
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)
105
107
  │ PASS ▼
106
108
  ▼ findings written into the ticket,
107
109
  next ticket reopened, built again (3 rounds max);
@@ -111,7 +113,11 @@ The Claude Code skill is `skills/feature-flow/`. The Codex skill is written by h
111
113
 
112
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.
113
115
 
114
- **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.
115
121
 
116
122
  ## The skill and the roles
117
123
 
@@ -178,14 +184,18 @@ prints OK
178
184
  - **The Answers are the handoff between subagents.** The next builder reads the Answers of the tickets it depends on.
179
185
  - `settle` tickets record a decision. `convert` tickets migrate something and must be safe to run twice.
180
186
  - **A worker may change only** its own Status line and Answer, lines appended to `map.md` and `learnings.md`, and the code the ticket calls for. Rewriting its own Done when, editing another ticket, or touching `commands.md` fails the guard.
181
- - `Floor:` categories: `skip`, `suppress`, `empty-catch`, `test-delete`, `threshold`, `config`, `ticket-edit`, `commands-edit`, and `flow-edit` (allows the conductor's code check below, for the build only). `skip` covers skipped, expected-fail and focused tests (`.only(`, `fit(`, `fdescribe(` on test paths); `config` covers more lint and test config files, the lint and test sections of `pyproject.toml` and `setup.cfg`, scripts and test keys in `package.json`, new skip or xfail words in `conftest.py`, and a `Makefile` only when `commands.md` uses `make`. A test file that loses assertions is shown to the reviewer as a warning, not a finding. The line is read from the ticket as it was before the work started, so a worker cannot excuse itself.
187
+ - `Floor:` categories: `skip`, `suppress`, `empty-catch`, `test-delete`, `threshold`, `trivial-assert` (an assertion that cannot fail, added to a test file: `assert True`, `assertEqual(x, x)`, `expect(v).toBe(v)`), `config`, `ticket-edit`, `commands-edit`, and `flow-edit` (allows the conductor's code check below, for the build only). `skip` covers skipped, expected-fail and focused tests (`.only(`, `fit(`, `fdescribe(` on test paths); `config` covers more lint and test config files, the lint and test sections of `pyproject.toml` and `setup.cfg`, scripts and test keys in `package.json`, new skip or xfail words in `conftest.py`, and a `Makefile` only when `commands.md` uses `make`. A test file that loses assertions is shown to the reviewer as a warning, not a finding. The line is read from the ticket as it was before the work started, so a worker cannot excuse itself.
182
188
  - The guard needs the plan to be tracked by git. If you keep tickets in an ignored folder, it cannot see changes to them.
183
189
  - The commands in `commands.md` run with nobody watching and must exit non zero on failure.
184
190
 
185
191
  ## See the graph
186
192
 
193
+ ![The animated ticket graph for the demo plan: five of ten tickets resolved, two ready, one in progress](docs/flow-view.png)
194
+
195
+ *The page `flow-view` writes, shown for the demo plan from `examples/demo.sh`: resolved tickets in green, ready ones in amber, the one being worked in blue, the rest waiting. Press Replay to watch the run back from git history.*
196
+
187
197
  ```bash
188
- python3 scripts/flow-view.py csv-export write the page and open it
198
+ python3 scripts/flow-view.py csv-export write the page and open it (or: feature-flow view csv-export)
189
199
  python3 scripts/flow-view.py csv-export --watch keep it updating while tickets are built
190
200
  bash examples/demo.sh --open try it on a made up project, no setup needed
191
201
  ```
@@ -226,6 +236,8 @@ python3 scripts/flow-status.py <feature> table of tickets and w
226
236
  python3 scripts/flow-status.py <feature> --next path of the next ready ticket (exit 10 = done, 11 = stuck)
227
237
  python3 scripts/flow-status.py <feature> --counts one line of counts
228
238
  python3 scripts/flow-status.py <feature> --check cycles, missing blockers, missing Done when, unordered mentions
239
+ python3 scripts/flow-status.py <feature> --lint --check plus: a check block (or `Check: none <why>`) on every ticket, a Test command
240
+ feature-flow report <feature> what a run did, from its log: builds, reviews, failed checks, stops
229
241
  python3 scripts/flow-status.py <feature> --mermaid the ticket graph, coloured by status (add "plain" for none)
230
242
  python3 scripts/flow-status.py <feature> --json every ticket with status, blockers and readiness
231
243
  python3 scripts/flow-view.py <feature> [--watch] the animated graph page, opened in your browser
@@ -247,13 +259,16 @@ The commands in `commands.md` run through `bash -c` on Linux and macOS and throu
247
259
  | `FLOW_MAX_RETRIES` | `2` | builds or reviews per step before the run stops |
248
260
  | `FLOW_MAX_REVIEW_ROUNDS` | `3` | times a ticket may be sent back before the run stops |
249
261
  | `FLOW_GATE` | `on` | `off` skips Build, Test and Lint after each ticket, and also the Done when and test-first checks |
262
+ | `FLOW_GATE_FULL` | unset | `1` runs the full Build, Test and Lint after every ticket, even when `commands.md` has a `Quick:` line |
263
+ | `FLOW_SMALL_FILES` | `3` | most files a ticket may change and still get the single review |
264
+ | `FLOW_SMALL_LINES` | `60` | most changed lines for the single review; `0` turns the single review off |
250
265
  | `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` |
251
266
  | `FLOW_SMOKE` | the `Smoke:` line of `commands.md` | command run before every ticket; the run stops if it fails |
252
267
  | `FLOW_DIR`, `FLOW_TICKETS` | `plans`, `tasks` | where the plans and the ticket folder live |
253
268
  | `FLOW_NO_OPEN` | unset | `1` makes `flow-view.py` never open a browser |
254
269
  | `FLOW_WATCH_SECONDS` | `3` | how often `flow-view.py --watch` rewrites the page |
255
270
 
256
- 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.
271
+ 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.
257
272
 
258
273
  ## What is tested
259
274
 
@@ -286,6 +301,17 @@ A `package` job builds the wheel and the sdist and runs `tests/package_smoke.py`
286
301
 
287
302
  `RUN_REAL=1 bash tests/smoke-real.sh --interactive` is the acceptance run with Claude Code. It runs real sessions on the demo project and spends money, which is why it asks for `RUN_REAL=1`.
288
303
 
304
+ ## Limits
305
+
306
+ feature-flow's layers are tripwires, not a security boundary. They catch accidents and lazy shortcuts by an agent; a determined agent that controls the session could still get around them.
307
+
308
+ - **The verdict relay is trusted.** The session saves a reviewer's reply and hands it to `flow.py verdict`. The conductor reads `REVIEW: PASS` or `REVIEW: FAIL` from that file and cannot prove the session passed it on unedited.
309
+ - **The floor guard reads diffs with patterns.** It does not see a test made trivially true by other means (a mock that returns the expected answer, an `assert` on something that is always set, a test that no longer exercises the code), or an assertion weakened without being deleted. It does catch assertions that cannot fail (`trivial-assert`) and warns the reviewer when a test file loses assertions. A new test with no assertion at all is not caught. The red-first proof only covers tickets with `Test first: yes`.
310
+ - **A plan can be wrong and still pass.** The plan lint checks that every ticket has a way to be proven and that `commands.md` has a Test command. It cannot tell whether the plan builds the right thing; the second yes is yours.
311
+ - **The trust runner stops accidents, not the session.** It catches a builder editing the conductor mid-run. The session that runs it is the thing it cannot check; a hand-run `flow-trust.py` is unprotected (see above).
312
+ - **Little real-world use so far.** Almost everything has been built by feature-flow on its own repo. The Codex skill and the Codex planner have not been run by hand, and the Quick gate has only been run by tests. See [docs/field-run.md](docs/field-run.md) for how to try it on your own repo and what to send back.
313
+ - **One ticket at a time**, with a handoff to a new session every few tickets.
314
+
289
315
  ## Upgrading
290
316
 
291
317
  Earlier versions had seven skills and an unattended loop. The five flow skills are gone, and `/feature-flow` replaces them:
@@ -15,15 +15,15 @@ The conductor is `python3 scripts/flow.py <feature> <command>`. If `scripts/flow
15
15
 
16
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
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 ec4e2430b3ed4dae4fa5d9190c3e1726294a0f854bf92584c0c756a07b9029d9
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
19
 
20
20
  A full conductor command, here `next` (any other command and its arguments go where `next` is):
21
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 ec4e2430b3ed4dae4fa5d9190c3e1726294a0f854bf92584c0c756a07b9029d9 <feature> next
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
23
 
24
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
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 ec4e2430b3ed4dae4fa5d9190c3e1726294a0f854bf92584c0c756a07b9029d9 --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.
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
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
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
29
 
@@ -57,14 +57,14 @@ If `start` prints `STOP` naming another owner, another session may still be work
57
57
 
58
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
59
 
60
- Say what will happen: for each ticket, a builder subagent and then a reviewer subagent. 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.
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
61
 
62
62
  ## The loop
63
63
 
64
64
  Run `next`, act on its one line after `TRUST` (and any `FLOW-EDIT` line), and repeat:
65
65
 
66
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` or `quality` by the pass the prompt names (`(spec pass)` or `(quality 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`.
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
68
  - `DONE <summary>`: report it and suggest `$feature-flow <feature> show`.
69
69
  - `STOP <reason>`: report the reason, run `python3 scripts/flow-status.py <feature>`, show the table, and stop.
70
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.
@@ -1,3 +1,3 @@
1
1
  """Feature Flow: a ticket-graph workflow for coding agents."""
2
2
 
3
- __version__ = "0.5.0"
3
+ __version__ = "0.8.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)
@@ -107,7 +107,7 @@ def run(args, scripts, trusted):
107
107
  except Stop as stop:
108
108
  line = "STOP %s" % stop
109
109
  if conductor is not None:
110
- conductor.log("STOP")
110
+ conductor.log("STOP " + " ".join(str(stop).split())[:200])
111
111
  print(line)
112
112
  return 1
113
113
  out = sys.stdout
@@ -16,7 +16,7 @@ def _files(scripts):
16
16
  found = [p for p in package.rglob("*.py") if "__pycache__" not in p.parts]
17
17
  folder = Path(scripts).resolve()
18
18
  found += [folder / name for name in SCRIPTS if (folder / name).is_file()]
19
- for phase in ("build", "review", "review-quality"):
19
+ for phase in ("build", "review", "review-quality", "review-single"):
20
20
  for folder, names in (("agents", prompts.ROLES), ("guides", prompts.GUIDES)):
21
21
  try:
22
22
  found.append(prompts.find_file(scripts, folder, names[phase]).resolve())
@@ -9,7 +9,7 @@ import sys
9
9
 
10
10
  from pathlib import Path
11
11
 
12
- from feature_flow import __version__, cli, install, status, suggest, uninstall, view
12
+ from feature_flow import __version__, cli, install, report, 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]
@@ -17,15 +17,17 @@ usage: feature-flow install [target-repo] [--agent claude|codex|all] [--user] [-
17
17
  feature-flow status <feature> [--next | --counts | --check | --mermaid [plain] | --json]
18
18
  feature-flow view <feature> [--watch] [--no-open] [--out FILE]
19
19
  feature-flow reset <feature> [NN]
20
+ feature-flow report <feature>
20
21
  feature-flow --version
21
22
  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
23
  uninstall removes exactly what install wrote (files you edited are kept); --user removes the personal install in ~/.claude, ~/.agents and ~/.feature-flow.
23
24
  status and view read plans/<feature>/ in the current directory, like scripts/flow-status.py and scripts/flow-view.py.
24
25
  reset reopens a half-built ticket (or ticket NN), commits that, and clears the run, like scripts/flow.py <feature> reset.
26
+ report prints what a run did (builds, reviews, failed checks, stops) from its log in .feature-flow/state/. Read-only.
25
27
  """
26
28
 
27
29
 
28
- COMMANDS = ("install", "uninstall", "status", "view", "reset", "--version", "--help")
30
+ COMMANDS = ("install", "uninstall", "status", "view", "reset", "report", "--version", "--help")
29
31
 
30
32
 
31
33
  def main(argv=None):
@@ -46,6 +48,8 @@ def main(argv=None):
46
48
  return status.main(rest)
47
49
  if command == "view":
48
50
  return view.main(rest, here=os.path.join(install.source_root(), "scripts"))
51
+ if command == "report":
52
+ return report.main(rest)
49
53
  if command == "reset":
50
54
  if not rest or len(rest) > 2:
51
55
  sys.stderr.write(USAGE)
@@ -51,6 +51,9 @@ class Conductor:
51
51
  self.gate_timeout = _env_int("FLOW_GATE_TIMEOUT", 30)
52
52
  self.gate_on = os.environ.get("FLOW_GATE", "on") != "off"
53
53
  self.per_session = _env_int("FLOW_TICKETS_PER_SESSION", 4)
54
+ self.small_files = _env_int("FLOW_SMALL_FILES", 3)
55
+ self.small_lines = _env_int("FLOW_SMALL_LINES", 60)
56
+ self.checks_ran = False
54
57
  self.relay = os.environ.get("FLOW_RELAY", "0") == "1"
55
58
  self.invoke = os.environ.get("FLOW_INVOKE") or "/feature-flow"
56
59
  top = git.toplevel()
@@ -125,6 +128,21 @@ class Conductor:
125
128
  % ("timed out after %d minutes" % self.gate_timeout if result.timed_out else "failed",
126
129
  self.ticket_name(), cmd, gate.TAIL, log))
127
130
 
131
+ def gate_quick(self):
132
+ """True when this ticket's gate may run Quick in place of Test."""
133
+ if os.environ.get("FLOW_GATE_FULL") == "1":
134
+ return False
135
+ if not tickets.commands_value(self.commands, "Quick"):
136
+ return False
137
+ if self.per_session > 0 and self.num("session_done") + 1 >= self.per_session:
138
+ return False
139
+ counts = {}
140
+ for word in checks.flow_status(self.scripts, self.feature, "--counts").out.split():
141
+ key, _, value = word.partition("=")
142
+ if value.isdigit():
143
+ counts[key] = int(value)
144
+ return sum(counts.get(k, 0) for k in ("open", "claimed", "waiting", "unknown")) > 0
145
+
128
146
  def run_limit(self):
129
147
  result = checks.flow_status(self.scripts, self.feature, "--counts")
130
148
  total = 0
@@ -246,10 +264,12 @@ class Conductor:
246
264
  if not git.is_clean():
247
265
  raise Stop("working tree is dirty after %s, it should have been committed" % self.ticket_name())
248
266
  if self.gate_on:
249
- result = checks.gate(self.scripts, self.feature, self.gate_timeout)
267
+ quick = self.gate_quick()
268
+ result = checks.gate(self.scripts, self.feature, self.gate_timeout, quick=quick)
269
+ self.log("GATE-QUICK" if quick else "GATE-FULL")
250
270
  self.log("GATE-PASS" if result.ok else "GATE-FAIL")
251
271
  if not result.ok:
252
- return self.send_back("gate", result.out)
272
+ return self.send_back("gate (quick)" if quick else "gate (full)", result.out)
253
273
  result = checks.floor_guard(self.scripts, self.feature, self.get("num"), self.get("base"))
254
274
  self.log("GUARD-PASS" if result.ok else "GUARD-FAIL")
255
275
  if not result.ok:
@@ -266,9 +286,29 @@ class Conductor:
266
286
  return self.send_back("test first", report)
267
287
  if note:
268
288
  notes.append(note)
289
+ self.st["review_pass"] = "spec"
290
+ if self.gate_on and self.checks_ran:
291
+ small = self.small_note()
292
+ if small:
293
+ self.st["review_pass"] = "single"
294
+ self.log("REVIEW-SMALL")
295
+ notes.append(small)
269
296
  self.st["review_notes"] = NOTE_SEP.join(notes)
270
297
  return self.hand_out_review()
271
298
 
299
+ def small_note(self):
300
+ """The note for the reviewer when the built diff is small enough for one review, else ""."""
301
+ base = self.ticket_at_base()
302
+ if proof.review_mode(base) != "auto" or self.small_lines <= 0:
303
+ return ""
304
+ if re.search(r"^Type:[ \t]*convert\b", base.split("\n## ", 1)[0], re.IGNORECASE | re.MULTILINE):
305
+ return ""
306
+ files, lines, binary = git.diff_size(self.get("base"), "HEAD", self.plan.as_posix())
307
+ if binary or files > self.small_files or lines > self.small_lines:
308
+ return ""
309
+ return ("the conductor judged this ticket small (%d files, %d lines): one review covers the spec and "
310
+ "quality passes" % (files, lines))
311
+
272
312
  def ticket_at_base(self):
273
313
  """The ticket text as it was at the base commit, so a builder cannot change what is checked."""
274
314
  rel = proof.repo_relative(self.ticket())
@@ -286,6 +326,7 @@ class Conductor:
286
326
  found = proof.checks_in(self.ticket_at_base())
287
327
  except proof.ParseError as err:
288
328
  raise Stop("%s has a check block the conductor cannot read: %s" % (self.ticket_name(), err))
329
+ self.checks_ran = bool(found)
289
330
  if not found:
290
331
  self.log("DONEWHEN-SKIP")
291
332
  return None, "the conductor ran no Done when checks: the ticket has no check block"
@@ -424,7 +465,7 @@ class Conductor:
424
465
  raise Stop("the reviewer changed tracked files, which a reviewer must never do")
425
466
  verdict = self.get("verdict")
426
467
  self.st["verdict"] = ""
427
- if verdict == "pass" and self.get("review_pass", "spec") != "quality":
468
+ if verdict == "pass" and self.get("review_pass", "spec") == "spec":
428
469
  self.st["review_pass"] = "quality"
429
470
  self.st["review_attempt"] = 0
430
471
  return self.hand_out_review()
@@ -436,8 +477,8 @@ class Conductor:
436
477
  return self.pick_ticket()
437
478
  if verdict == "fail":
438
479
  findings = state.read_text(self.findings_file, "findings") or ""
439
- return self.send_back("quality review" if self.get("review_pass") == "quality" else "spec review",
440
- findings)
480
+ return self.send_back({"quality": "quality review", "single": "single review"}.get(
481
+ self.get("review_pass"), "spec review"), findings)
441
482
  if self.num("review_attempt") >= self.max_retries:
442
483
  raise Stop("no review verdict for %s after %d attempt(s)" % (self.ticket_name(), self.max_retries))
443
484
  return self.hand_out_review()
@@ -449,8 +490,8 @@ class Conductor:
449
490
  if phase not in ("build", "review"):
450
491
  raise NoPhase("no BUILD or REVIEW is pending for %s" % self.feature)
451
492
  try:
452
- if phase == "review" and self.get("review_pass") == "quality":
453
- phase = "review-quality"
493
+ if phase == "review" and self.get("review_pass") in ("quality", "single"):
494
+ phase = "review-" + self.get("review_pass")
454
495
  return prompts.build(phase, self.scripts, self.feature, self.ticket(), self.get("num"), self.get("base"),
455
496
  [n for n in self.get("review_notes").split(NOTE_SEP) if n] if phase != "build" else (),
456
497
  retry=self.num("round") > 0)
@@ -532,7 +573,7 @@ class Conductor:
532
573
  self.no_plan_yet()
533
574
  if not self.draft.is_dir():
534
575
  raise Stop("no draft plan in %s. run the planner first" % self.draft_text())
535
- check = checks.flow_status(self.scripts, self.feature, "--check", root=self.draft_root)
576
+ check = checks.flow_status(self.scripts, self.feature, "--lint", root=self.draft_root)
536
577
  if not check.ok:
537
578
  raise Stop("the draft fails the plan check, fix it first: %s"
538
579
  % " ".join(check.out.split())[:400])
@@ -7,7 +7,7 @@ lowered thresholds, edited lint or test config.
7
7
  it also protects the plan from the worker: only the ticket's own Status line and Answer,
8
8
  appended lines in map.md and learnings.md may change, and nothing else under the plans folder.
9
9
  a ticket can allow categories with a line "Floor: allow skip, suppress" (skip, suppress,
10
- empty-catch, test-delete, threshold, config, ticket-edit, commands-edit). the line is
10
+ empty-catch, test-delete, threshold, trivial-assert, config, ticket-edit, commands-edit). the line is
11
11
  read from the ticket as it was at base-commit, so a worker cannot excuse itself.
12
12
  run it from the repo root. exit 0 clean, 1 findings, 2 usage error.
13
13
 
@@ -29,6 +29,13 @@ FOCUSED = re.compile(r"\.only\(|(?<!def )(?<![A-Za-z0-9_.])(fit|fdescribe)\(")
29
29
  SUPPRESS = re.compile(r"# noqa|# type: ignore|# pylint: disable|# pragma: no cover|# fmt: off|# ruff: noqa"
30
30
  r"|eslint-disable|@ts-ignore|@ts-expect-error|// nolint|#\[allow\(|--no-verify")
31
31
  EMPTY_CATCH = re.compile(r"except[^:]*:[ \t]*pass[ \t]*$|catch[ \t]*(\([^)]*\))?[ \t]*\{[ \t]*\}")
32
+ # an assertion that cannot fail, added to a test file: assert True, assertEqual(x, x), expect(true), expect(x).toBe(x)
33
+ TRIVIAL = (re.compile(r"(^|[^A-Za-z_.])assert\s+(True|1\s*==\s*1|not\s+False)\s*(#.*)?$"),
34
+ re.compile(r"assertTrue\(\s*True\s*[,)]"),
35
+ re.compile(r"assertEqual\(\s*([A-Za-z_][\w.]*|\d+)\s*,\s*\1\s*[,)]"),
36
+ re.compile(r"(^|[^A-Za-z_.])assert\s+([A-Za-z_][\w.]*)\s*==\s*\2\s*(#.*)?$"),
37
+ re.compile(r"expect\(\s*true\s*\)\.(toBe\(\s*true\s*\)|toBeTruthy\(\))"),
38
+ re.compile(r"expect\(\s*([A-Za-z_][\w.]*)\s*\)\.toBe\(\s*\1\s*\)"))
32
39
  THRESHOLD = re.compile(r"fail_under|fail-under|coverageThreshold|cov-fail-under")
33
40
  DELETED_TEST = re.compile(r"(^|/)(test_[^/]*\.py|[^/]*_test\.[a-z]+|[^/]*\.(test|spec)\.[a-z]+|tests?/|__tests__/)")
34
41
  # Makefile is judged by content, not name: a Makefile change only counts when the plan's commands.md has a
@@ -77,7 +84,7 @@ def allow_line(source):
77
84
 
78
85
 
79
86
  def diff_findings(diff, allow):
80
- """The pass over added lines: skip, suppress, empty-catch and threshold."""
87
+ """The pass over added lines: skip, suppress, empty-catch, threshold and trivial-assert."""
81
88
  allow = " " + allow + " "
82
89
  out = []
83
90
  file = ""
@@ -105,6 +112,8 @@ def diff_findings(diff, allow):
105
112
  report("empty-catch", line)
106
113
  if THRESHOLD.search(line):
107
114
  report("threshold", line)
115
+ if DELETED_TEST.search(file) and any(r.search(line) for r in TRIVIAL):
116
+ report("trivial-assert", line)
108
117
  return "".join(out)
109
118
 
110
119
 
@@ -60,9 +60,10 @@ def timeout_minutes():
60
60
  raise ValueError("FLOW_GATE_TIMEOUT must be a whole number, not %s" % value)
61
61
 
62
62
 
63
- def run(feature, out, err, timeout=None):
63
+ def run(feature, out, err, timeout=None, quick=False):
64
64
  """Run the gate for feature. out and err take text. timeout is minutes (a float is fine), 0 for none;
65
- None reads FLOW_GATE_TIMEOUT. Returns the exit code."""
65
+ None reads FLOW_GATE_TIMEOUT. With quick, a usable Quick: value runs in the place of Test.
66
+ Returns the exit code."""
66
67
  if not feature:
67
68
  err("usage: python3 scripts/gate.py <feature>\n")
68
69
  return 2
@@ -77,8 +78,11 @@ def run(feature, out, err, timeout=None):
77
78
  out("gate: no commands.md for " + feature + ", nothing to run\n")
78
79
  return 0
79
80
  ran = 0
81
+ quick_cmd = command(path, "Quick") if quick else ""
80
82
  for key in KEYS:
81
83
  cmd = command(path, key)
84
+ if key == "Test" and quick_cmd != "" and not quick_cmd.startswith("<"):
85
+ key, cmd = "Quick", quick_cmd
82
86
  if cmd == "" or cmd.startswith("<"):
83
87
  continue
84
88
  ran += 1
@@ -39,6 +39,24 @@ def head():
39
39
  return _git("rev-parse", "HEAD").stdout.strip()
40
40
 
41
41
 
42
+ def diff_size(base, head, exclude):
43
+ """(files, lines, binary) of the diff base..head outside the exclude path: the changed paths, the
44
+ added plus removed lines of the text files, and whether any file is binary (`-` counts in numstat).
45
+ Renames are off so a renamed file is one delete plus one add whatever `diff.renames` says."""
46
+ out = _git("diff", "--numstat", "--no-renames", base, head, "--", ".", ":(exclude)%s" % exclude).stdout
47
+ files, lines, binary = 0, 0, False
48
+ for line in out.splitlines():
49
+ if not line.strip():
50
+ continue
51
+ added, removed = line.split("\t", 2)[:2]
52
+ files += 1
53
+ if added == "-" or removed == "-":
54
+ binary = True
55
+ else:
56
+ lines += int(added) + int(removed)
57
+ return files, lines, binary
58
+
59
+
42
60
  # the installer's record of what it wrote: the paths, and the sha256 of each as it wrote it
43
61
  INSTALLED = Path(".feature-flow") / "installed.txt"
44
62
  HASHES = Path(".feature-flow") / "installed.sha256"