feature-flow-cli 0.2.0__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 (62) hide show
  1. {feature_flow_cli-0.2.0/feature_flow_cli.egg-info → feature_flow_cli-0.3.0}/PKG-INFO +25 -10
  2. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/README.md +24 -9
  3. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/adapters/codex/feature-flow/SKILL.md +2 -2
  4. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow/__init__.py +1 -1
  5. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow/codehash.py +5 -1
  6. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow/conductor.py +86 -6
  7. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow/git.py +8 -0
  8. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow/prompts.py +15 -9
  9. feature_flow_cli-0.3.0/feature_flow/proof.py +294 -0
  10. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0/feature_flow_cli.egg-info}/PKG-INFO +25 -10
  11. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow_cli.egg-info/SOURCES.txt +3 -0
  12. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/guides/build.md +4 -2
  13. feature_flow_cli-0.3.0/guides/debug.md +32 -0
  14. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/guides/plan.md +9 -2
  15. feature_flow_cli-0.3.0/guides/review-quality.md +74 -0
  16. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/guides/review.md +1 -15
  17. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/guides/templates/ticket.md +6 -0
  18. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/skills/feature-flow/SKILL.md +2 -2
  19. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/LICENSE +0 -0
  20. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/MANIFEST.in +0 -0
  21. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/adapters/codex/feature-flow/agents/openai.yaml +0 -0
  22. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/agents/feature-planner.md +0 -0
  23. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/agents/plan-reviewer.md +0 -0
  24. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/agents/ticket-builder.md +0 -0
  25. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/agents/ticket-reviewer.md +0 -0
  26. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow/__main__.py +0 -0
  27. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow/checks.py +0 -0
  28. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow/cli.py +0 -0
  29. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow/command.py +0 -0
  30. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow/floorguard.py +0 -0
  31. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow/gate.py +0 -0
  32. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow/install.py +0 -0
  33. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow/proc.py +0 -0
  34. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow/released.py +0 -0
  35. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow/state.py +0 -0
  36. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow/status.py +0 -0
  37. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow/suggest.py +0 -0
  38. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow/tickets.py +0 -0
  39. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow/view.py +0 -0
  40. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow_cli.egg-info/dependency_links.txt +0 -0
  41. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow_cli.egg-info/entry_points.txt +0 -0
  42. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow_cli.egg-info/top_level.txt +0 -0
  43. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/guides/brief.md +0 -0
  44. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/guides/plan-review.md +0 -0
  45. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/guides/show.md +0 -0
  46. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/guides/templates/commands.md +0 -0
  47. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/guides/templates/learnings.md +0 -0
  48. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/guides/templates/map.md +0 -0
  49. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/guides/templates/spec.md +0 -0
  50. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/guides/templates/ui-mockup.md +0 -0
  51. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/install.py +0 -0
  52. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/install.sh +0 -0
  53. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/pyproject.toml +0 -0
  54. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/scripts/floor-guard.py +0 -0
  55. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/scripts/flow-status.py +0 -0
  56. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/scripts/flow-view.html +0 -0
  57. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/scripts/flow-view.py +0 -0
  58. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/scripts/flow.py +0 -0
  59. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/scripts/gate.py +0 -0
  60. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/setup.cfg +0 -0
  61. {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/skills/architect-review/SKILL.md +0 -0
  62. {feature_flow_cli-0.2.0 → 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.2.0
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
@@ -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,9 +176,14 @@ 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.
@@ -240,14 +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 |
244
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` |
245
260
  | `FLOW_SMOKE` | the `Smoke:` line of `commands.md` | command run before every ticket; the run stops if it fails |
246
261
  | `FLOW_DIR`, `FLOW_TICKETS` | `plans`, `tasks` | where the plans and the ticket folder live |
247
262
  | `FLOW_NO_OPEN` | unset | `1` makes `flow-view.py` never open a browser |
248
263
  | `FLOW_WATCH_SECONDS` | `3` | how often `flow-view.py --watch` rewrites the page |
249
264
 
250
- The run also stops on its own after a number of phases (`BUILD` and `REVIEW` hand-outs) that grows with the ticket count, recomputed at every pick, so a loop of failures cannot go on forever.
265
+ The run also stops on its own after a number of phases (`BUILD` and `REVIEW` hand-outs, the spec review and the quality review each counting as one) that grows with the ticket count, recomputed at every pick, so a loop of failures cannot go on forever.
251
266
 
252
267
  ## What is tested
253
268
 
@@ -274,7 +289,7 @@ bash tests/run.sh
274
289
  python3 -m unittest discover -s tests/py
275
290
  ```
276
291
 
277
- 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 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.
278
293
 
279
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`.
280
295
 
@@ -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,9 +157,14 @@ 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.
@@ -221,14 +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 |
225
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` |
226
241
  | `FLOW_SMOKE` | the `Smoke:` line of `commands.md` | command run before every ticket; the run stops if it fails |
227
242
  | `FLOW_DIR`, `FLOW_TICKETS` | `plans`, `tasks` | where the plans and the ticket folder live |
228
243
  | `FLOW_NO_OPEN` | unset | `1` makes `flow-view.py` never open a browser |
229
244
  | `FLOW_WATCH_SECONDS` | `3` | how often `flow-view.py --watch` rewrites the page |
230
245
 
231
- The run also stops on its own after a number of phases (`BUILD` and `REVIEW` hand-outs) that grows with the ticket count, recomputed at every pick, 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.
232
247
 
233
248
  ## What is tested
234
249
 
@@ -255,7 +270,7 @@ bash tests/run.sh
255
270
  python3 -m unittest discover -s tests/py
256
271
  ```
257
272
 
258
- 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 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.
259
274
 
260
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`.
261
276
 
@@ -47,8 +47,8 @@ Say what will happen: for each ticket, a builder subagent and then a reviewer su
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.
@@ -1,3 +1,3 @@
1
1
  """Feature Flow: a ticket-graph workflow for coding agents."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.3.0"
@@ -12,12 +12,16 @@ def _files(scripts):
12
12
  package = Path(__file__).resolve().parent
13
13
  found = [p for p in package.rglob("*.py") if "__pycache__" not in p.parts]
14
14
  found += Path(scripts).resolve().glob("*.py")
15
- for phase in ("build", "review"):
15
+ for phase in ("build", "review", "review-quality"):
16
16
  for folder, names in (("agents", prompts.ROLES), ("guides", prompts.GUIDES)):
17
17
  try:
18
18
  found.append(prompts.find_file(scripts, folder, names[phase]).resolve())
19
19
  except FileNotFoundError:
20
20
  pass
21
+ try:
22
+ found.append(prompts.find_file(scripts, "guides", "debug.md").resolve())
23
+ except FileNotFoundError:
24
+ pass
21
25
  return sorted(set(found))
22
26
 
23
27
 
@@ -10,9 +10,10 @@ import secrets
10
10
  import shutil
11
11
  from pathlib import Path
12
12
 
13
- from feature_flow import checks, codehash, floorguard, gate, git, prompts, state, suggest, tickets
13
+ from feature_flow import checks, codehash, floorguard, gate, git, prompts, proof, state, suggest, tickets
14
14
 
15
15
 
16
+ NOTE_SEP = "\t" # the state file keeps one line per key, so the review notes are tab separated
16
17
  VERDICT = re.compile(r"^REVIEW: (PASS|FAIL)[ \t\r\f\v]*$")
17
18
 
18
19
 
@@ -120,7 +121,7 @@ class Conductor:
120
121
  for word in result.out.split():
121
122
  if word.startswith("total="):
122
123
  total = int(word[len("total="):])
123
- return total * 2 * self.max_retries * (self.max_rounds + 1) + 1
124
+ return total * 3 * self.max_retries * (self.max_rounds + 1) + 1
124
125
 
125
126
  def count_run(self):
126
127
  runs = self.num("runs") + 1
@@ -142,6 +143,7 @@ class Conductor:
142
143
  def hand_out_review(self):
143
144
  self.count_run()
144
145
  self.st["phase"] = "review"
146
+ self.st["review_pass"] = self.get("review_pass") or "spec"
145
147
  self.st["review_attempt"] = self.num("review_attempt") + 1
146
148
  self.st["review_sha"] = git.head()
147
149
  self.st["relay_handoff"] = "1" if self.relay else ""
@@ -162,6 +164,7 @@ class Conductor:
162
164
  % (source, self.ticket_name()))
163
165
  self.st["attempt"] = 0
164
166
  self.st["review_attempt"] = 0
167
+ self.st["review_pass"] = "spec"
165
168
  return self.hand_out_build()
166
169
 
167
170
  def pick_ticket(self):
@@ -190,7 +193,7 @@ class Conductor:
190
193
  path = nxt.out.strip().splitlines()[-1]
191
194
  self.st["limit"] = self.run_limit()
192
195
  self.st.update({"ticket": path, "num": tickets.number(path), "base": git.head(), "phase": "",
193
- "attempt": 0, "review_attempt": 0, "round": 0, "review_sha": "", "guard_warning": ""})
196
+ "attempt": 0, "review_attempt": 0, "round": 0, "review_sha": "", "review_notes": "", "review_pass": "spec"})
194
197
  self.snapshot_code()
195
198
  self.run_smoke()
196
199
  return self.hand_out_build()
@@ -242,9 +245,70 @@ class Conductor:
242
245
  if not result.ok:
243
246
  return self.send_back("floor guard", result.out)
244
247
  warnings = [l for l in result.out.splitlines() if l.startswith("warning:")]
245
- self.st["guard_warning"] = warnings[0] if warnings else ""
248
+ notes = warnings[:1]
249
+ if self.gate_on:
250
+ report, note = self.run_done_when()
251
+ if report is not None:
252
+ return self.send_back("done when", report)
253
+ notes.append(note)
254
+ report, note = self.run_test_first()
255
+ if report is not None:
256
+ return self.send_back("test first", report)
257
+ if note:
258
+ notes.append(note)
259
+ self.st["review_notes"] = NOTE_SEP.join(notes)
246
260
  return self.hand_out_review()
247
261
 
262
+ def ticket_at_base(self):
263
+ """The ticket text as it was at the base commit, so a builder cannot change what is checked."""
264
+ rel = proof.repo_relative(self.ticket())
265
+ listed = git._git("ls-tree", "--name-only", self.get("base"), "--", rel, check=False)
266
+ if listed.returncode != 0:
267
+ raise Stop("cannot read %s at the base commit: %s" % (rel, listed.stderr.strip()))
268
+ if listed.stdout.strip():
269
+ return git.show_text("%s:%s" % (self.get("base"), rel))
270
+ text = Path(self.ticket()).read_text(encoding="utf-8")
271
+ return text.split("\n## Answer", 1)[0]
272
+
273
+ def run_done_when(self):
274
+ """Run the ticket's check block. Returns (failure report or None, note for the reviewer)."""
275
+ try:
276
+ found = proof.checks_in(self.ticket_at_base())
277
+ except proof.ParseError as err:
278
+ raise Stop("%s has a check block the conductor cannot read: %s" % (self.ticket_name(), err))
279
+ if not found:
280
+ self.log("DONEWHEN-SKIP")
281
+ return None, "the conductor ran no Done when checks: the ticket has no check block"
282
+ ok, report = proof.run_checks(found, self.gate_timeout)
283
+ dirty = git.changes()
284
+ if dirty:
285
+ raise Stop("a Done when check of %s changed the working tree: %s. a check command must not change it"
286
+ % (self.ticket_name(), " ".join(dirty[:5])))
287
+ self.log("DONEWHEN-PASS" if ok else "DONEWHEN-FAIL")
288
+ if not ok:
289
+ return report, ""
290
+ return None, "the conductor ran %d Done when check(s) and all passed" % len(found)
291
+
292
+ def run_test_first(self):
293
+ """For a `Test first: yes` ticket, prove a test-only commit failed the Test command.
294
+ Returns (failure report or None, note for the reviewer or "")."""
295
+ if not proof.wants_test_first(self.ticket_at_base()):
296
+ return None, ""
297
+ test = tickets.commands_value(self.commands, "Test")
298
+ if not test:
299
+ self.log("TESTFIRST-SKIP")
300
+ return None, "the conductor did not check test first: commands.md has no Test command"
301
+
302
+ def remember(value):
303
+ self.st["restore"] = value
304
+ self.save()
305
+ try:
306
+ ok, text = proof.test_first(self.get("base"), test, self.plan, self.gate_timeout, remember)
307
+ except proof.GitError as err:
308
+ raise Stop("test first check of %s: %s" % (self.ticket_name(), err))
309
+ self.log("TESTFIRST-PASS" if ok else "TESTFIRST-FAIL")
310
+ return (None, text) if ok else (text, "")
311
+
248
312
  # ---- commands ------------------------------------------------------
249
313
 
250
314
  def handoff(self):
@@ -278,6 +342,14 @@ class Conductor:
278
342
 
279
343
  def next(self):
280
344
  self.check_owner()
345
+ if self.get("restore"):
346
+ try:
347
+ git._git("checkout", "-q", self.get("restore"))
348
+ except RuntimeError as err:
349
+ raise Stop("cannot put the working copy back after the test first check: %s. run `git checkout %s` "
350
+ "by hand" % (err, self.get("restore")))
351
+ self.st["restore"] = ""
352
+ self.save()
281
353
  if not self.plan.is_dir():
282
354
  raise Stop("no plan folder %s" % self.plan)
283
355
  if self.get("relay_handoff") == "1":
@@ -338,6 +410,10 @@ class Conductor:
338
410
  raise Stop("the reviewer changed tracked files, which a reviewer must never do")
339
411
  verdict = self.get("verdict")
340
412
  self.st["verdict"] = ""
413
+ if verdict == "pass" and self.get("review_pass", "spec") != "quality":
414
+ self.st["review_pass"] = "quality"
415
+ self.st["review_attempt"] = 0
416
+ return self.hand_out_review()
341
417
  if verdict == "pass":
342
418
  self.st["phase"] = ""
343
419
  self.st["done"] = self.num("done") + 1
@@ -346,7 +422,8 @@ class Conductor:
346
422
  return self.pick_ticket()
347
423
  if verdict == "fail":
348
424
  findings = self.findings_file.read_text(encoding="utf-8") if self.findings_file.is_file() else ""
349
- return self.send_back("independent review", findings)
425
+ return self.send_back("quality review" if self.get("review_pass") == "quality" else "spec review",
426
+ findings)
350
427
  if self.num("review_attempt") >= self.max_retries:
351
428
  raise Stop("no review verdict for %s after %d attempt(s)" % (self.ticket_name(), self.max_retries))
352
429
  return self.hand_out_review()
@@ -358,8 +435,11 @@ class Conductor:
358
435
  if phase not in ("build", "review"):
359
436
  raise NoPhase("no BUILD or REVIEW is pending for %s" % self.feature)
360
437
  try:
438
+ if phase == "review" and self.get("review_pass") == "quality":
439
+ phase = "review-quality"
361
440
  return prompts.build(phase, self.scripts, self.feature, self.ticket(), self.get("num"), self.get("base"),
362
- self.get("guard_warning") if phase == "review" else "")
441
+ [n for n in self.get("review_notes").split(NOTE_SEP) if n] if phase != "build" else (),
442
+ retry=self.num("round") > 0)
363
443
  except FileNotFoundError as err:
364
444
  raise Stop(str(err))
365
445
 
@@ -13,6 +13,14 @@ def _git(*args, check=True):
13
13
  return result
14
14
 
15
15
 
16
+ def show_text(revision_path):
17
+ """`git show <rev>:<path>` decoded as UTF-8 (the platform's default encoding is not what a ticket is written in)."""
18
+ result = subprocess.run(["git", "show", revision_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
19
+ if result.returncode != 0:
20
+ raise RuntimeError("git show %s failed: %s" % (revision_path, result.stderr.decode("utf-8", "replace").strip()))
21
+ return result.stdout.decode("utf-8", "replace").replace("\r\n", "\n")
22
+
23
+
16
24
  def git_dir():
17
25
  result = _git("rev-parse", "--absolute-git-dir", check=False)
18
26
  if result.returncode != 0:
@@ -5,9 +5,9 @@ The text is runtime neutral: any agent that can read a prompt can follow it.
5
5
 
6
6
  from pathlib import Path
7
7
 
8
- ROLES = {"build": "ticket-builder.md", "review": "ticket-reviewer.md",
8
+ ROLES = {"build": "ticket-builder.md", "review": "ticket-reviewer.md", "review-quality": "ticket-reviewer.md",
9
9
  "plan": "feature-planner.md", "plan-review": "plan-reviewer.md"}
10
- GUIDES = {"build": "build.md", "review": "review.md", "plan": "plan.md", "plan-review": "plan-review.md"}
10
+ GUIDES = {"build": "build.md", "review": "review.md", "review-quality": "review-quality.md", "plan": "plan.md", "plan-review": "plan-review.md"}
11
11
 
12
12
 
13
13
  def find_file(scripts, folder, name):
@@ -32,7 +32,7 @@ def _body(path):
32
32
  return text.strip("\n")
33
33
 
34
34
 
35
- def build(phase, scripts, feature, ticket, num, sha, warning=""):
35
+ def build(phase, scripts, feature, ticket, num, sha, notes=(), retry=False):
36
36
  role = _body(find_file(scripts, "agents", ROLES[phase]))
37
37
  guide = _body(find_file(scripts, "guides", GUIDES[phase]))
38
38
  for placeholder, value in (("<feature>", feature), ("<NN>", num), ("<ticket>", ticket), ("<sha>", sha),
@@ -46,13 +46,19 @@ def build(phase, scripts, feature, ticket, num, sha, warning=""):
46
46
  "commit. Do not review your own work." % (num, feature, feature, num, ticket, sha, fresh))
47
47
  intro = "The build guide follows. Follow it exactly."
48
48
  else:
49
- task = ("Your task: review ticket %s of the feature `%s` (`%s %s %s`). The ticket is `%s`, and the work "
50
- "started at commit `%s`.\n%s\nDo not edit any file or create a commit. Your final reply must end "
51
- "with exactly `REVIEW: PASS` or `REVIEW: FAIL`." % (num, feature, feature, num, sha, ticket, sha, fresh))
52
- if warning:
53
- task += "\nThe floor guard warns: %s" % warning
49
+ task = ("Your task: review ticket %s of the feature `%s` (`%s %s %s`) (%s pass). The ticket is `%s`, and the "
50
+ "work started at commit `%s`.\n%s\nDo not edit any file or create a commit. Your final reply must end "
51
+ "with exactly `REVIEW: PASS` or `REVIEW: FAIL`."
52
+ % (num, feature, feature, num, sha, "quality" if phase == "review-quality" else "spec", ticket, sha,
53
+ fresh))
54
+ for note in notes:
55
+ task += "\nThe conductor notes: %s" % note
54
56
  intro = "The review guide follows. Follow it exactly."
55
- return "\n\n".join([role, "---", intro, guide, "---", task]) + "\n"
57
+ parts = [role, "---", intro, guide, "---", task]
58
+ if retry and phase == "build":
59
+ parts += ["The debugging guide follows. Follow it before you change any code.",
60
+ _body(find_file(scripts, "guides", "debug.md"))]
61
+ return "\n\n".join(parts) + "\n"
56
62
 
57
63
 
58
64
  def plan(phase, scripts, feature, draft, brief, findings=""):