feature-flow-cli 0.1.6__tar.gz → 0.3.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 (64) hide show
  1. {feature_flow_cli-0.1.6/feature_flow_cli.egg-info → feature_flow_cli-0.3.0}/PKG-INFO +31 -15
  2. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/README.md +30 -14
  3. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/adapters/codex/feature-flow/SKILL.md +5 -5
  4. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/feature_flow/__init__.py +1 -1
  5. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/feature_flow/checks.py +18 -16
  6. feature_flow_cli-0.3.0/feature_flow/codehash.py +65 -0
  7. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/feature_flow/conductor.py +131 -10
  8. feature_flow_cli-0.3.0/feature_flow/floorguard.py +392 -0
  9. feature_flow_cli-0.3.0/feature_flow/gate.py +115 -0
  10. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/feature_flow/git.py +8 -0
  11. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/feature_flow/install.py +4 -3
  12. feature_flow_cli-0.3.0/feature_flow/proc.py +31 -0
  13. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/feature_flow/prompts.py +15 -7
  14. feature_flow_cli-0.3.0/feature_flow/proof.py +294 -0
  15. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0/feature_flow_cli.egg-info}/PKG-INFO +31 -15
  16. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/feature_flow_cli.egg-info/SOURCES.txt +5 -0
  17. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/guides/brief.md +2 -2
  18. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/guides/build.md +6 -2
  19. feature_flow_cli-0.3.0/guides/debug.md +32 -0
  20. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/guides/plan.md +9 -2
  21. feature_flow_cli-0.3.0/guides/review-quality.md +74 -0
  22. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/guides/review.md +1 -15
  23. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/guides/templates/ticket.md +6 -0
  24. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/skills/feature-flow/SKILL.md +4 -4
  25. feature_flow_cli-0.1.6/feature_flow/floorguard.py +0 -340
  26. feature_flow_cli-0.1.6/feature_flow/gate.py +0 -100
  27. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/LICENSE +0 -0
  28. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/MANIFEST.in +0 -0
  29. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/adapters/codex/feature-flow/agents/openai.yaml +0 -0
  30. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/agents/feature-planner.md +0 -0
  31. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/agents/plan-reviewer.md +0 -0
  32. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/agents/ticket-builder.md +0 -0
  33. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/agents/ticket-reviewer.md +0 -0
  34. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/feature_flow/__main__.py +0 -0
  35. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/feature_flow/cli.py +0 -0
  36. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/feature_flow/command.py +0 -0
  37. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/feature_flow/released.py +0 -0
  38. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/feature_flow/state.py +0 -0
  39. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/feature_flow/status.py +0 -0
  40. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/feature_flow/suggest.py +0 -0
  41. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/feature_flow/tickets.py +0 -0
  42. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/feature_flow/view.py +0 -0
  43. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/feature_flow_cli.egg-info/dependency_links.txt +0 -0
  44. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/feature_flow_cli.egg-info/entry_points.txt +0 -0
  45. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/feature_flow_cli.egg-info/top_level.txt +0 -0
  46. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/guides/plan-review.md +0 -0
  47. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/guides/show.md +0 -0
  48. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/guides/templates/commands.md +0 -0
  49. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/guides/templates/learnings.md +0 -0
  50. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/guides/templates/map.md +0 -0
  51. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/guides/templates/spec.md +0 -0
  52. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/guides/templates/ui-mockup.md +0 -0
  53. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/install.py +0 -0
  54. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/install.sh +0 -0
  55. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/pyproject.toml +0 -0
  56. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/scripts/floor-guard.py +0 -0
  57. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/scripts/flow-status.py +0 -0
  58. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/scripts/flow-view.html +0 -0
  59. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/scripts/flow-view.py +0 -0
  60. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/scripts/flow.py +0 -0
  61. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/scripts/gate.py +0 -0
  62. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/setup.cfg +0 -0
  63. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/skills/architect-review/SKILL.md +0 -0
  64. {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.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.1.6
3
+ Version: 0.3.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
@@ -62,14 +62,14 @@ Then, in your repo, in the agent:
62
62
  | | Claude Code | Codex |
63
63
  |---|---|---|
64
64
  | Plan a feature (no plan yet) | `/feature-flow csv-export` | `$feature-flow csv-export` |
65
- | Plan what you just talked through | `/feature-flow` | `$feature-flow` |
65
+ | Plan and build what you just talked through | `/feature-flow` | `$feature-flow` |
66
66
  | Build its tickets (a plan exists) | `/feature-flow csv-export` | `$feature-flow csv-export` |
67
67
  | The same, asking nothing | `/feature-flow csv-export auto` | `$feature-flow csv-export auto` |
68
68
  | Watch the graph, animated | `/feature-flow csv-export show` | `$feature-flow csv-export show` |
69
69
 
70
- The same command plans when `plans/csv-export/` does not exist yet and builds when it does. Commit the plan before you build it.
70
+ 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
71
 
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>/`.
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.
73
73
 
74
74
  Try the graph first, with no setup and no agent: `bash examples/demo.sh --open`.
75
75
 
@@ -104,16 +104,26 @@ The Claude Code skill is `skills/feature-flow/`. The Codex skill is written by h
104
104
  plan touched beyond own Status + Answer + appended lines? │
105
105
  │ clean │
106
106
  ▼ │
107
- fresh reviewer subagent: sees only the ticket + diff, │
108
- re-runs every Done when FAIL ─┤
107
+ Done when checks (script): runs every ```check block fail ─┤
108
+ test-first check (script): Test first: yes needs a test-only │
109
+ commit that failed Test before the code fail ─┤
110
+ │ pass │
111
+ ▼ │
112
+ spec review: fresh reviewer subagent, sees only the ticket │
113
+ + diff, re-runs every Done when FAIL ─┤
114
+ │ PASS │
115
+ ▼ │
116
+ quality review: a second fresh reviewer subagent FAIL ─┤
109
117
  │ PASS ▼
110
118
  ▼ findings written into the ticket,
111
- next ticket reopened, built again (3 rounds max)
119
+ next ticket reopened, built again (3 rounds max);
120
+ the builder's prompt then carries
121
+ a root-cause debugging guide
112
122
  ```
113
123
 
114
124
  `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.
115
125
 
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 checks that the reviewer changed no tracked file and made no commit. The review verdict is different: the reviewer's reply 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.
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.
117
127
 
118
128
  ## The skill and the roles
119
129
 
@@ -153,7 +163,7 @@ plans/
153
163
 
154
164
  One markdown file per ticket in `plans/<feature>/tasks/NN-slug.md`:
155
165
 
156
- ```
166
+ ````
157
167
  # Add the retry policy to the job runner
158
168
 
159
169
  Type: task task | settle | convert
@@ -166,16 +176,21 @@ Floor: allow config optional, only when a human decides the guard may let
166
176
 
167
177
  ## Not in this ticket
168
178
  ## Done when commands and the results they print
179
+ ```check
180
+ $ python3 -m unittest discover -s tests/py
181
+ exit 0
182
+ prints OK
183
+ ```
169
184
  ## Reference
170
185
  ## Answer Built, Proof, Decisions, Shortcuts taken, Review fixes, For later tickets
171
- ```
186
+ ````
172
187
 
173
188
  - **Order comes from `Blocked by`**, not from numbers. A ticket is *ready* when it is open and every ticket it is blocked by is resolved. Lowest number wins.
174
189
  - **Status lives only in the ticket files.** `map.md` never repeats it.
175
190
  - **The Answers are the handoff between subagents.** The next builder reads the Answers of the tickets it depends on.
176
191
  - `settle` tickets record a decision. `convert` tickets migrate something and must be safe to run twice.
177
192
  - **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.
178
- - `Floor:` categories: `skip`, `suppress`, `empty-catch`, `test-delete`, `threshold`, `config`, `ticket-edit`, `commands-edit`. The line is read from the ticket as it was before the work started, so a worker cannot excuse itself.
193
+ - `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.
179
194
  - The guard needs the plan to be tracked by git. If you keep tickets in an ignored folder, it cannot see changes to them.
180
195
  - The commands in `commands.md` run with nobody watching and must exit non zero on failure.
181
196
 
@@ -240,13 +255,14 @@ The commands in `commands.md` run through `bash -c` on Linux and macOS and throu
240
255
  | `FLOW_TAKEOVER` | unset | `1` lets `start` take a feature over from a session that is gone |
241
256
  | `FLOW_MAX_RETRIES` | `2` | builds or reviews per step before the run stops |
242
257
  | `FLOW_MAX_REVIEW_ROUNDS` | `3` | times a ticket may be sent back before the run stops |
243
- | `FLOW_GATE` | `on` | `off` skips Build, Test and Lint after each ticket |
258
+ | `FLOW_GATE` | `on` | `off` skips Build, Test and Lint after each ticket, and also the Done when and test-first checks |
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` |
244
260
  | `FLOW_SMOKE` | the `Smoke:` line of `commands.md` | command run before every ticket; the run stops if it fails |
245
261
  | `FLOW_DIR`, `FLOW_TICKETS` | `plans`, `tasks` | where the plans and the ticket folder live |
246
262
  | `FLOW_NO_OPEN` | unset | `1` makes `flow-view.py` never open a browser |
247
263
  | `FLOW_WATCH_SECONDS` | `3` | how often `flow-view.py --watch` rewrites the page |
248
264
 
249
- The run also stops on its own after a number of steps that grows with the ticket count, 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) that grows with the ticket count, recomputed at every pick, so a loop of failures cannot go on forever.
250
266
 
251
267
  ## What is tested
252
268
 
@@ -273,7 +289,7 @@ bash tests/run.sh
273
289
  python3 -m unittest discover -s tests/py
274
290
  ```
275
291
 
276
- 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, and the acceptance harness. 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 (once with `mawk`, once with `gawk`) 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`. `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.
277
293
 
278
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`.
279
295
 
@@ -304,7 +320,7 @@ git tag v0.1.1 && git push origin v0.1.1
304
320
 
305
321
  ## Caution
306
322
 
307
- 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 runs your Build, Test and Lint commands with no time limit. 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 step 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 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.
308
324
 
309
325
  ## License
310
326
 
@@ -43,14 +43,14 @@ Then, in your repo, in the agent:
43
43
  | | Claude Code | Codex |
44
44
  |---|---|---|
45
45
  | Plan a feature (no plan yet) | `/feature-flow csv-export` | `$feature-flow csv-export` |
46
- | Plan what you just talked through | `/feature-flow` | `$feature-flow` |
46
+ | Plan and build what you just talked through | `/feature-flow` | `$feature-flow` |
47
47
  | Build its tickets (a plan exists) | `/feature-flow csv-export` | `$feature-flow csv-export` |
48
48
  | The same, asking nothing | `/feature-flow csv-export auto` | `$feature-flow csv-export auto` |
49
49
  | Watch the graph, animated | `/feature-flow csv-export show` | `$feature-flow csv-export show` |
50
50
 
51
- The same command plans when `plans/csv-export/` does not exist yet and builds when it does. Commit the plan before you build it.
51
+ 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
52
 
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>/`.
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.
54
54
 
55
55
  Try the graph first, with no setup and no agent: `bash examples/demo.sh --open`.
56
56
 
@@ -85,16 +85,26 @@ The Claude Code skill is `skills/feature-flow/`. The Codex skill is written by h
85
85
  plan touched beyond own Status + Answer + appended lines? │
86
86
  │ clean │
87
87
  ▼ │
88
- fresh reviewer subagent: sees only the ticket + diff, │
89
- re-runs every Done when FAIL ─┤
88
+ Done when checks (script): runs every ```check block fail ─┤
89
+ test-first check (script): Test first: yes needs a test-only │
90
+ commit that failed Test before the code fail ─┤
91
+ │ pass │
92
+ ▼ │
93
+ spec review: fresh reviewer subagent, sees only the ticket │
94
+ + diff, re-runs every Done when FAIL ─┤
95
+ │ PASS │
96
+ ▼ │
97
+ quality review: a second fresh reviewer subagent FAIL ─┤
90
98
  │ PASS ▼
91
99
  ▼ findings written into the ticket,
92
- next ticket reopened, built again (3 rounds max)
100
+ next ticket reopened, built again (3 rounds max);
101
+ the builder's prompt then carries
102
+ a root-cause debugging guide
93
103
  ```
94
104
 
95
105
  `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.
96
106
 
97
- **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 checks that the reviewer changed no tracked file and made no commit. The review verdict is different: the reviewer's reply 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.
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.
98
108
 
99
109
  ## The skill and the roles
100
110
 
@@ -134,7 +144,7 @@ plans/
134
144
 
135
145
  One markdown file per ticket in `plans/<feature>/tasks/NN-slug.md`:
136
146
 
137
- ```
147
+ ````
138
148
  # Add the retry policy to the job runner
139
149
 
140
150
  Type: task task | settle | convert
@@ -147,16 +157,21 @@ Floor: allow config optional, only when a human decides the guard may let
147
157
 
148
158
  ## Not in this ticket
149
159
  ## Done when commands and the results they print
160
+ ```check
161
+ $ python3 -m unittest discover -s tests/py
162
+ exit 0
163
+ prints OK
164
+ ```
150
165
  ## Reference
151
166
  ## Answer Built, Proof, Decisions, Shortcuts taken, Review fixes, For later tickets
152
- ```
167
+ ````
153
168
 
154
169
  - **Order comes from `Blocked by`**, not from numbers. A ticket is *ready* when it is open and every ticket it is blocked by is resolved. Lowest number wins.
155
170
  - **Status lives only in the ticket files.** `map.md` never repeats it.
156
171
  - **The Answers are the handoff between subagents.** The next builder reads the Answers of the tickets it depends on.
157
172
  - `settle` tickets record a decision. `convert` tickets migrate something and must be safe to run twice.
158
173
  - **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.
159
- - `Floor:` categories: `skip`, `suppress`, `empty-catch`, `test-delete`, `threshold`, `config`, `ticket-edit`, `commands-edit`. The line is read from the ticket as it was before the work started, so a worker cannot excuse itself.
174
+ - `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.
160
175
  - The guard needs the plan to be tracked by git. If you keep tickets in an ignored folder, it cannot see changes to them.
161
176
  - The commands in `commands.md` run with nobody watching and must exit non zero on failure.
162
177
 
@@ -221,13 +236,14 @@ The commands in `commands.md` run through `bash -c` on Linux and macOS and throu
221
236
  | `FLOW_TAKEOVER` | unset | `1` lets `start` take a feature over from a session that is gone |
222
237
  | `FLOW_MAX_RETRIES` | `2` | builds or reviews per step before the run stops |
223
238
  | `FLOW_MAX_REVIEW_ROUNDS` | `3` | times a ticket may be sent back before the run stops |
224
- | `FLOW_GATE` | `on` | `off` skips Build, Test and Lint after each ticket |
239
+ | `FLOW_GATE` | `on` | `off` skips Build, Test and Lint after each ticket, and also the Done when and test-first checks |
240
+ | `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` |
225
241
  | `FLOW_SMOKE` | the `Smoke:` line of `commands.md` | command run before every ticket; the run stops if it fails |
226
242
  | `FLOW_DIR`, `FLOW_TICKETS` | `plans`, `tasks` | where the plans and the ticket folder live |
227
243
  | `FLOW_NO_OPEN` | unset | `1` makes `flow-view.py` never open a browser |
228
244
  | `FLOW_WATCH_SECONDS` | `3` | how often `flow-view.py --watch` rewrites the page |
229
245
 
230
- The run also stops on its own after a number of steps that grows with the ticket count, so a loop of failures cannot go on forever.
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.
231
247
 
232
248
  ## What is tested
233
249
 
@@ -254,7 +270,7 @@ bash tests/run.sh
254
270
  python3 -m unittest discover -s tests/py
255
271
  ```
256
272
 
257
- 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, and the acceptance harness. 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 (once with `mawk`, once with `gawk`) 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.
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.
258
274
 
259
275
  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`.
260
276
 
@@ -285,7 +301,7 @@ git tag v0.1.1 && git push origin v0.1.1
285
301
 
286
302
  ## Caution
287
303
 
288
- 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 runs your Build, Test and Lint commands with no time limit. 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 step limit.
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.
289
305
 
290
306
  ## License
291
307
 
@@ -23,7 +23,7 @@ When `plans/<feature>/` does not exist, or you are planning from the conversatio
23
23
  - `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`.
24
24
  - `plan-accept`: writes `plans/<feature>/` from the draft, only after the second yes.
25
25
 
26
- Then run `python3 scripts/flow-status.py <feature> --check`, list the files `plan-accept` wrote, and suggest the commit command (`git add plans/<feature> && git commit -m "docs(<feature>): plan"`). Say to commit the plan and run `$feature-flow <feature>` again. While the plan is uncommitted, do not say the feature is ready to build.
26
+ 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.
27
27
 
28
28
  To add tickets to a plan that exists, edit it by hand following the ticket rules in `.feature-flow/guides/plan.md`.
29
29
 
@@ -41,14 +41,14 @@ If `start` prints `STOP` naming another owner, another session may still be work
41
41
 
42
42
  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.
43
43
 
44
- 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.
44
+ 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.
45
45
 
46
46
  ## The loop
47
47
 
48
48
  Run `next`, act on its one line, and repeat:
49
49
 
50
- - `BUILD <ticket> <NN> <sha>`: run `prompt`. Start the builder with `spawn_agent(task_name="ticket_builder", fork_turns="none", message=...)`. The message is "Work only in <repo>." followed by the whole `prompt` output, which already starts with the builder role. Get its final reply with `wait_agent`, then run `next`.
51
- - `REVIEW <ticket> <NN> <sha>`: run `prompt`. Start a new reviewer the same way, with `task_name="ticket_reviewer"` and `fork_turns="none"` so it never sees the builder's context. 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`.
50
+ - `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`.
51
+ - `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`.
52
52
  - `DONE <summary>`: report it and suggest `$feature-flow <feature> show`.
53
53
  - `STOP <reason>`: report the reason, run `python3 scripts/flow-status.py <feature>`, show the table, and stop.
54
54
  - `HANDOFF <line>`: stop here. Tell the user to open a new session and type exactly `<line>`. The new session resumes where this one stopped.
@@ -62,7 +62,7 @@ A conductor command looks like this:
62
62
  These apply from `start` on, once a plan exists.
63
63
 
64
64
  - Never run the gate, the floor guard or a review yourself. The conductor runs the checks, and the reviewer subagent reviews.
65
- - Never edit a ticket, the plan or the code, and never commit. The builder does that.
65
+ - Never edit a ticket, the plan or the code, and never commit, apart from the approved plan above. The builder does that.
66
66
  - Never skip a step, reorder steps, or decide the next step yourself. Only `next` decides.
67
67
  - Never read a ticket's `## Answer` into the reviewer's prompt. `prompt` already holds everything it needs.
68
68
  - 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.1.6"
3
+ __version__ = "0.3.0"
@@ -1,13 +1,14 @@
1
- """The checks the conductor runs: the bash scripts it still calls, and the ported ones in process.
1
+ """The checks the conductor runs, in process.
2
2
 
3
3
  Each call lives in one function so plans/interactive-flow-python can swap it for an
4
4
  in-process call without touching the conductor.
5
5
  """
6
6
 
7
7
  import os
8
- import subprocess
8
+ import tempfile
9
9
  from pathlib import Path
10
10
 
11
+ from feature_flow import proc
11
12
  from feature_flow import status
12
13
  from feature_flow import gate as gate_module
13
14
  from feature_flow import floorguard
@@ -36,12 +37,6 @@ class _Collect:
36
37
  return "".join(self.parts)
37
38
 
38
39
 
39
- def _bash(script, *args):
40
- result = subprocess.run(["bash", str(script)] + [str(a) for a in args],
41
- stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True)
42
- return Result(result.returncode, result.stdout)
43
-
44
-
45
40
  def flow_status(scripts, feature, mode, root=None):
46
41
  """The ticket graph reader with --next, --counts or --check, in process. stderr is folded into the output.
47
42
  root, when given, stands in for FLOW_DIR for this one call (the planner's draft folder)."""
@@ -60,11 +55,13 @@ def flow_status(scripts, feature, mode, root=None):
60
55
  return Result(code, out.text())
61
56
 
62
57
 
63
- def gate(scripts, feature):
64
- """The gate, run in process. stderr is folded into the output as the subprocess call did."""
58
+ def gate(scripts, feature, timeout=None):
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."""
65
61
  chunks = []
66
- code = gate_module.run(str(feature), chunks.append, chunks.append)
67
- out = b"".join(chunks).decode("utf-8", "replace").replace("\r\n", "\n").replace("\r", "\n")
62
+ code = gate_module.run(str(feature), chunks.append, chunks.append, timeout)
63
+ out = "".join(chunks).encode("utf-8", "surrogateescape").decode("utf-8", "replace")
64
+ out = out.replace("\r\n", "\n").replace("\r", "\n")
68
65
  return Result(code, out)
69
66
 
70
67
 
@@ -76,8 +73,13 @@ def floor_guard(scripts, feature, num, base):
76
73
  return Result(code, out)
77
74
 
78
75
 
79
- def smoke(command):
76
+ def smoke(command, timeout=None):
77
+ """The smoke command. timeout is minutes, 0 or None for none. Result.timed_out is set when it ran out."""
80
78
  args, use_shell = gate_module.shell(command)
81
- result = subprocess.run(args, shell=use_shell, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
82
- universal_newlines=True)
83
- return Result(result.returncode, result.stdout)
79
+ with tempfile.TemporaryFile() as log:
80
+ code, timed_out = proc.run(args, use_shell, log, timeout)
81
+ log.seek(0)
82
+ out = log.read().decode("utf-8", "replace").replace("\r\n", "\n")
83
+ result = Result(1 if timed_out else code, out)
84
+ result.timed_out = timed_out
85
+ return result
@@ -0,0 +1,65 @@
1
+ """A fingerprint of the code and prompt files a conductor run uses, so a change during a build shows."""
2
+
3
+ import hashlib
4
+ import json
5
+ import os
6
+ from pathlib import Path
7
+
8
+ from feature_flow import git, prompts
9
+
10
+
11
+ def _files(scripts):
12
+ package = Path(__file__).resolve().parent
13
+ 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"):
16
+ for folder, names in (("agents", prompts.ROLES), ("guides", prompts.GUIDES)):
17
+ try:
18
+ found.append(prompts.find_file(scripts, folder, names[phase]).resolve())
19
+ except FileNotFoundError:
20
+ pass
21
+ try:
22
+ found.append(prompts.find_file(scripts, "guides", "debug.md").resolve())
23
+ except FileNotFoundError:
24
+ pass
25
+ return sorted(set(found))
26
+
27
+
28
+ def _name(path, top):
29
+ try:
30
+ return path.relative_to(top).as_posix()
31
+ except ValueError:
32
+ return path.as_posix()
33
+
34
+
35
+ def flow_code(scripts):
36
+ """(the sha256 over every file's path and bytes, {path relative to the repo: sha256 of the file})."""
37
+ top = git.toplevel()
38
+ top = top.resolve() if top is not None else Path(os.getcwd()).resolve()
39
+ each = {}
40
+ for path in _files(scripts):
41
+ try:
42
+ each[_name(path, top)] = hashlib.sha256(path.read_bytes()).hexdigest()
43
+ except OSError:
44
+ each[_name(path, top)] = ""
45
+ whole = hashlib.sha256()
46
+ for name in sorted(each):
47
+ whole.update(name.encode("utf-8") + b"\0" + each[name].encode("ascii") + b"\0")
48
+ return whole.hexdigest(), each
49
+
50
+
51
+ def changed(before, now):
52
+ """The names that differ between two {name: sha256} maps, sorted."""
53
+ return sorted(name for name in set(before) | set(now) if before.get(name) != now.get(name))
54
+
55
+
56
+ def dump(each):
57
+ return json.dumps(each, sort_keys=True)
58
+
59
+
60
+ def load(text):
61
+ try:
62
+ value = json.loads(text)
63
+ except ValueError:
64
+ return {}
65
+ return value if isinstance(value, dict) else {}