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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. {feature_flow_cli-0.5.0/feature_flow_cli.egg-info → feature_flow_cli-0.7.0}/PKG-INFO +13 -4
  2. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/README.md +12 -3
  3. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/adapters/codex/feature-flow/SKILL.md +5 -5
  4. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/feature_flow/__init__.py +1 -1
  5. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/feature_flow/checks.py +3 -3
  6. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/feature_flow/codehash.py +1 -1
  7. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/feature_flow/conductor.py +48 -7
  8. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/feature_flow/gate.py +6 -2
  9. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/feature_flow/git.py +18 -0
  10. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/feature_flow/prompts.py +4 -2
  11. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/feature_flow/proof.py +10 -0
  12. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/feature_flow/status.py +2 -0
  13. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/feature_flow/tickets.py +4 -0
  14. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0/feature_flow_cli.egg-info}/PKG-INFO +13 -4
  15. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/feature_flow_cli.egg-info/SOURCES.txt +1 -0
  16. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/guides/plan-review.md +2 -2
  17. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/guides/plan.md +5 -1
  18. feature_flow_cli-0.7.0/guides/review-single.md +87 -0
  19. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/guides/templates/commands.md +4 -1
  20. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/guides/templates/ticket.md +1 -0
  21. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/scripts/flow-trust.py +1 -1
  22. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/skills/feature-flow/SKILL.md +4 -4
  23. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/LICENSE +0 -0
  24. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/MANIFEST.in +0 -0
  25. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/adapters/codex/feature-flow/agents/openai.yaml +0 -0
  26. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/agents/feature-planner.md +0 -0
  27. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/agents/plan-reviewer.md +0 -0
  28. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/agents/ticket-builder.md +0 -0
  29. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/agents/ticket-reviewer.md +0 -0
  30. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/feature_flow/__main__.py +0 -0
  31. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/feature_flow/cli.py +0 -0
  32. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/feature_flow/command.py +0 -0
  33. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/feature_flow/floorguard.py +0 -0
  34. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/feature_flow/install.py +0 -0
  35. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/feature_flow/proc.py +0 -0
  36. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/feature_flow/released.py +0 -0
  37. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/feature_flow/state.py +0 -0
  38. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/feature_flow/suggest.py +0 -0
  39. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/feature_flow/uninstall.py +0 -0
  40. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/feature_flow/view.py +0 -0
  41. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/feature_flow_cli.egg-info/dependency_links.txt +0 -0
  42. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/feature_flow_cli.egg-info/entry_points.txt +0 -0
  43. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/feature_flow_cli.egg-info/top_level.txt +0 -0
  44. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/guides/brief.md +0 -0
  45. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/guides/build.md +0 -0
  46. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/guides/debug.md +0 -0
  47. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/guides/review-quality.md +0 -0
  48. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/guides/review.md +0 -0
  49. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/guides/show.md +0 -0
  50. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/guides/templates/learnings.md +0 -0
  51. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/guides/templates/map.md +0 -0
  52. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/guides/templates/spec.md +0 -0
  53. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/guides/templates/ui-mockup.md +0 -0
  54. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/install.py +0 -0
  55. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/install.sh +0 -0
  56. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/pyproject.toml +0 -0
  57. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/scripts/floor-guard.py +0 -0
  58. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/scripts/flow-status.py +0 -0
  59. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/scripts/flow-view.html +0 -0
  60. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/scripts/flow-view.py +0 -0
  61. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/scripts/flow.py +0 -0
  62. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/scripts/gate.py +0 -0
  63. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/setup.cfg +0 -0
  64. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/skills/architect-review/SKILL.md +0 -0
  65. {feature_flow_cli-0.5.0 → feature_flow_cli-0.7.0}/skills/automation-design/SKILL.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: feature-flow-cli
3
- Version: 0.5.0
3
+ Version: 0.7.0
4
4
  Summary: A ticket-graph workflow for coding agents: installs the feature-flow skill into a repo
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/hamilton-sky/feature-flow
@@ -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
 
@@ -266,13 +272,16 @@ The commands in `commands.md` run through `bash -c` on Linux and macOS and throu
266
272
  | `FLOW_MAX_RETRIES` | `2` | builds or reviews per step before the run stops |
267
273
  | `FLOW_MAX_REVIEW_ROUNDS` | `3` | times a ticket may be sent back before the run stops |
268
274
  | `FLOW_GATE` | `on` | `off` skips Build, Test and Lint after each ticket, and also the Done when and test-first checks |
275
+ | `FLOW_GATE_FULL` | unset | `1` runs the full Build, Test and Lint after every ticket, even when `commands.md` has a `Quick:` line |
276
+ | `FLOW_SMALL_FILES` | `3` | most files a ticket may change and still get the single review |
277
+ | `FLOW_SMALL_LINES` | `60` | most changed lines for the single review; `0` turns the single review off |
269
278
  | `FLOW_GATE_TIMEOUT` | `30` | minutes each gate command and the smoke test may run; `0` means no limit. A failed smoke test writes `.feature-flow/state/<feature>.smoke.log` |
270
279
  | `FLOW_SMOKE` | the `Smoke:` line of `commands.md` | command run before every ticket; the run stops if it fails |
271
280
  | `FLOW_DIR`, `FLOW_TICKETS` | `plans`, `tasks` | where the plans and the ticket folder live |
272
281
  | `FLOW_NO_OPEN` | unset | `1` makes `flow-view.py` never open a browser |
273
282
  | `FLOW_WATCH_SECONDS` | `3` | how often `flow-view.py --watch` rewrites the page |
274
283
 
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.
284
+ The run also stops on its own after a number of phases (`BUILD` and `REVIEW` hand-outs, the spec review and the quality review each counting as one, a single review counting as one) that grows with the ticket count, recomputed at every pick, so a loop of failures cannot go on forever.
276
285
 
277
286
  ## What is tested
278
287
 
@@ -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
 
@@ -247,13 +253,16 @@ The commands in `commands.md` run through `bash -c` on Linux and macOS and throu
247
253
  | `FLOW_MAX_RETRIES` | `2` | builds or reviews per step before the run stops |
248
254
  | `FLOW_MAX_REVIEW_ROUNDS` | `3` | times a ticket may be sent back before the run stops |
249
255
  | `FLOW_GATE` | `on` | `off` skips Build, Test and Lint after each ticket, and also the Done when and test-first checks |
256
+ | `FLOW_GATE_FULL` | unset | `1` runs the full Build, Test and Lint after every ticket, even when `commands.md` has a `Quick:` line |
257
+ | `FLOW_SMALL_FILES` | `3` | most files a ticket may change and still get the single review |
258
+ | `FLOW_SMALL_LINES` | `60` | most changed lines for the single review; `0` turns the single review off |
250
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` |
251
260
  | `FLOW_SMOKE` | the `Smoke:` line of `commands.md` | command run before every ticket; the run stops if it fails |
252
261
  | `FLOW_DIR`, `FLOW_TICKETS` | `plans`, `tasks` | where the plans and the ticket folder live |
253
262
  | `FLOW_NO_OPEN` | unset | `1` makes `flow-view.py` never open a browser |
254
263
  | `FLOW_WATCH_SECONDS` | `3` | how often `flow-view.py --watch` rewrites the page |
255
264
 
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.
265
+ The run also stops on its own after a number of phases (`BUILD` and `REVIEW` hand-outs, the spec review and the quality review each counting as one, a single review counting as one) that grows with the ticket count, recomputed at every pick, so a loop of failures cannot go on forever.
257
266
 
258
267
  ## What is tested
259
268
 
@@ -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.7.0"
@@ -55,11 +55,11 @@ def flow_status(scripts, feature, mode, root=None):
55
55
  return Result(code, out.text())
56
56
 
57
57
 
58
- def gate(scripts, feature, timeout=None):
58
+ def gate(scripts, feature, timeout=None, quick=False):
59
59
  """The gate, run in process. stderr is folded into the output as the subprocess call did.
60
- timeout is minutes, None for FLOW_GATE_TIMEOUT."""
60
+ timeout is minutes, None for FLOW_GATE_TIMEOUT. quick runs Quick in the place of Test."""
61
61
  chunks = []
62
- code = gate_module.run(str(feature), chunks.append, chunks.append, timeout)
62
+ code = gate_module.run(str(feature), chunks.append, chunks.append, timeout, quick)
63
63
  out = "".join(chunks).encode("utf-8", "surrogateescape").decode("utf-8", "replace")
64
64
  out = out.replace("\r\n", "\n").replace("\r", "\n")
65
65
  return Result(code, out)
@@ -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())
@@ -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)
@@ -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"
@@ -8,8 +8,10 @@ import shlex
8
8
  from pathlib import Path
9
9
 
10
10
  ROLES = {"build": "ticket-builder.md", "review": "ticket-reviewer.md", "review-quality": "ticket-reviewer.md",
11
+ "review-single": "ticket-reviewer.md",
11
12
  "plan": "feature-planner.md", "plan-review": "plan-reviewer.md"}
12
- GUIDES = {"build": "build.md", "review": "review.md", "review-quality": "review-quality.md", "plan": "plan.md", "plan-review": "plan-review.md"}
13
+ GUIDES = {"build": "build.md", "review": "review.md", "review-quality": "review-quality.md",
14
+ "review-single": "review-single.md", "plan": "plan.md", "plan-review": "plan-review.md"}
13
15
 
14
16
 
15
17
  def find_file(scripts, folder, name):
@@ -83,7 +85,7 @@ def build(phase, scripts, feature, ticket, num, sha, notes=(), retry=False):
83
85
  task = ("Your task: review ticket %s of the feature `%s` (`%s %s %s`) (%s pass). The ticket is `%s`, and the "
84
86
  "work started at commit `%s`.\n%s\nDo not edit any file or create a commit. Your final reply must end "
85
87
  "with exactly `REVIEW: PASS` or `REVIEW: FAIL`."
86
- % (num, feature, feature, num, sha, "quality" if phase == "review-quality" else "spec", ticket, sha,
88
+ % (num, feature, feature, num, sha, {"review-quality": "quality", "review-single": "single"}.get(phase, "spec"), ticket, sha,
87
89
  fresh))
88
90
  for note in notes:
89
91
  task += "\nThe conductor notes: %s" % note
@@ -152,6 +152,16 @@ def wants_test_first(text):
152
152
  return False
153
153
 
154
154
 
155
+ def review_mode(text):
156
+ """`full` when the ticket's header lines (before its first `## ` heading) say `Review: full`, else `auto`."""
157
+ for line in text.split("\n"):
158
+ if line.startswith("## "):
159
+ break
160
+ if re.match(r"^Review:[ \t]*full\b", line, re.IGNORECASE):
161
+ return "full"
162
+ return "auto"
163
+
164
+
155
165
  def repo_relative(path):
156
166
  """path as a posix path relative to the repository root (git's own spelling), when it is inside it."""
157
167
  top = git.toplevel()
@@ -162,6 +162,8 @@ def _check(g, ready, nready, unfinished, out):
162
162
  errors.append(lab + ": no ## Done when section")
163
163
  if t in g.hastf and g.tf[t] not in ("yes", "no"):
164
164
  errors.append(lab + ": Test first must be yes or no")
165
+ if t in g.hasreview and g.review[t] not in ("full", "auto"):
166
+ errors.append(lab + ": Review must be full or auto")
165
167
  for d in g.blk[t]:
166
168
  if d == t:
167
169
  errors.append(lab + ": blocked by itself")
@@ -109,6 +109,7 @@ class Graph:
109
109
  self.hasstatus, self.hasdone, self.blk = {}, {}, {}
110
110
  self.hasblk, self.hastype, self.hastf, self.hastitle = set(), set(), set(), set()
111
111
  self.typ, self.tf = {}, {}
112
+ self.hasreview, self.review = set(), {}
112
113
  self.mentions = {}
113
114
  for p in paths:
114
115
  self._read(p)
@@ -147,6 +148,9 @@ class Graph:
147
148
  if head and line.startswith("Test first:") and cur not in self.hastf:
148
149
  self.hastf.add(cur)
149
150
  self.tf[cur] = _first_word(line.lower(), "test first:")
151
+ if head and line.startswith("Review:") and cur not in self.hasreview:
152
+ self.hasreview.add(cur)
153
+ self.review[cur] = _first_word(line.lower(), "review:")
150
154
  if line.startswith("# ") and cur not in self.hastitle:
151
155
  self.hastitle.add(cur)
152
156
  self.title[cur] = line[2:]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: feature-flow-cli
3
- Version: 0.5.0
3
+ Version: 0.7.0
4
4
  Summary: A ticket-graph workflow for coding agents: installs the feature-flow skill into a repo
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/hamilton-sky/feature-flow
@@ -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
 
@@ -266,13 +272,16 @@ The commands in `commands.md` run through `bash -c` on Linux and macOS and throu
266
272
  | `FLOW_MAX_RETRIES` | `2` | builds or reviews per step before the run stops |
267
273
  | `FLOW_MAX_REVIEW_ROUNDS` | `3` | times a ticket may be sent back before the run stops |
268
274
  | `FLOW_GATE` | `on` | `off` skips Build, Test and Lint after each ticket, and also the Done when and test-first checks |
275
+ | `FLOW_GATE_FULL` | unset | `1` runs the full Build, Test and Lint after every ticket, even when `commands.md` has a `Quick:` line |
276
+ | `FLOW_SMALL_FILES` | `3` | most files a ticket may change and still get the single review |
277
+ | `FLOW_SMALL_LINES` | `60` | most changed lines for the single review; `0` turns the single review off |
269
278
  | `FLOW_GATE_TIMEOUT` | `30` | minutes each gate command and the smoke test may run; `0` means no limit. A failed smoke test writes `.feature-flow/state/<feature>.smoke.log` |
270
279
  | `FLOW_SMOKE` | the `Smoke:` line of `commands.md` | command run before every ticket; the run stops if it fails |
271
280
  | `FLOW_DIR`, `FLOW_TICKETS` | `plans`, `tasks` | where the plans and the ticket folder live |
272
281
  | `FLOW_NO_OPEN` | unset | `1` makes `flow-view.py` never open a browser |
273
282
  | `FLOW_WATCH_SECONDS` | `3` | how often `flow-view.py --watch` rewrites the page |
274
283
 
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.
284
+ The run also stops on its own after a number of phases (`BUILD` and `REVIEW` hand-outs, the spec review and the quality review each counting as one, a single review counting as one) that grows with the ticket count, recomputed at every pick, so a loop of failures cannot go on forever.
276
285
 
277
286
  ## What is tested
278
287
 
@@ -42,6 +42,7 @@ guides/debug.md
42
42
  guides/plan-review.md
43
43
  guides/plan.md
44
44
  guides/review-quality.md
45
+ guides/review-single.md
45
46
  guides/review.md
46
47
  guides/show.md
47
48
  guides/templates/commands.md
@@ -9,10 +9,10 @@ You cannot edit or write anything. You may read, search, run the plan check, and
9
9
  1. **The brief is covered.** Every part of the brief's What and In scope is planned by some ticket. Name each part that is not.
10
10
  2. **Nothing outside the brief.** No ticket builds what the brief puts under Out, or what the brief never asked for and the bar does not need.
11
11
  3. **The bar holds.** The spec's bar matches the brief's bar, and the last ticket is the acceptance ticket that runs it.
12
- 4. **The commands are real.** Each command in `commands.md` exists in the project's own config, and Smoke runs and exits 0 now. Run it.
12
+ 4. **The commands are real.** Each command in `commands.md` exists in the project's own config, and Smoke runs and exits 0 now. Run it. If `commands.md` has `Quick:`, run it too and report that it exits 0, that it is part of what `Test` runs, and whether it covers the areas the tickets touch.
13
13
  5. **Done when is checkable by a stranger.** Each bullet names a command and the result it should print, or an outcome someone who has not read the plan can observe.
14
14
  6. **Forks belong to the right person.** A choice that changes what the user gets is a `settle` ticket or an open question, not a decision the planner made quietly.
15
- 7. **The graph is sound.** Run `FLOW_DIR=.feature-flow/state/draft python3 scripts/flow-status.py <feature> --check` and report any problem or warning. Tickets are small (about 5 Done when bullets at most), and `Blocked by` lists only real dependencies.
15
+ 7. **The graph is sound.** Run `FLOW_DIR=.feature-flow/state/draft python3 scripts/flow-status.py <feature> --check` and report any problem or warning. Tickets are small (about 5 Done when bullets at most), and `Blocked by` lists only real dependencies. No ticket is so small it should have been folded into another: name it and the ticket it belongs with.
16
16
  8. **Outside facts have sources.** Each fact about a library or outside API that a ticket relies on has a source line in `map.md`.
17
17
 
18
18
  Do not check style, wording or ticket order beyond what the list says.
@@ -31,6 +31,8 @@ If your prompt carries review findings from an earlier round, the draft is alrea
31
31
 
32
32
  **The codebase first.** Find similar features and how they were built, the files that will change, the existing types and APIs the feature touches, and the test conventions. Find the **real** build, test and lint commands: read them from `package.json`, `Makefile`, `Taskfile.yml`, CI config or the project's agent instructions file (CLAUDE.md or AGENTS.md). Do not guess. Pick a **smoke command**: the quickest one that proves the base is healthy (usually build plus the fast tests). The flow runs it before every ticket, and runs Build, Test and Lint after every ticket, so all of them must run without asking anything and exit non zero on failure.
33
33
 
34
+ Add a `Quick:` line to `commands.md` only when the project's own config has a clearly faster subset of Test (a unit-only target, a fast marker or folder). It must be a strict subset of `Test`, must exit non zero on failure, and must run without asking anything. Otherwise leave it out: no `Quick:` means the full Test runs every time. The planner never invents one.
35
+
34
36
  **Outside only where the code cannot answer.** For a library or API the feature uses, read the version the lockfile pins, then that version's official documentation. When the brief names a hard problem, look for how others solved it. Never research what the codebase already decides. Write every outside fact you rely on into `map.md` under Decisions so far with its source (`docs: <url>`). If you cannot reach the web, say so in your reply and plan from the codebase.
35
37
 
36
38
  ## Step 3: Draft the graph
@@ -51,6 +53,7 @@ Then a lazy pass over your draft. For each ticket ask, in order: does the bar st
51
53
  ### Ticket rules
52
54
 
53
55
  1. **One ticket is one change that leaves the build green**, small enough for one session. If Done when needs more than about 5 bullets, or Not in this ticket spans two areas, split it.
56
+ 1a. **A change too small to stand alone is folded in.** A rename, a doc line, a config value or a one-line fix that has no Done when of its own goes into the ticket whose work it belongs to, not a ticket of its own. Every ticket costs a builder, the gate and at least one reviewer, whatever its size.
54
57
  2. **`Blocked by` only for a real dependency**: the ticket cannot start without another's output. Independent tickets stay unblocked. Do not chain tickets just because of their numbers.
55
58
  3. **Types**:
56
59
  - `task`: normal work.
@@ -68,6 +71,7 @@ prints OK
68
71
  6. **Survive code drift**: no line numbers. Name functions and classes, and give a search hint for insertion points ("after the call to `parseConfig()`").
69
72
  7. **Tests ride with the code they cover**: every ticket adds its own. Frontend tickets come after the API shape is stable.
70
73
  8. **Set `Test first:`** on every ticket. `yes` for anything that adds or changes logic (the builder must commit a failing test alone first, and the conductor checks it), `no` for config, docs, renames and migrations that a command already proves. Do not set `yes` on a ticket whose test cannot fail before the code.
74
+ 8a. **Set `Review: full`** on a ticket that is small but risky (validation at a trust boundary, security, the flow's own code, data that could be lost). Leave it out otherwise: the conductor gives a small ticket whose check block passed one combined review, and a larger one the spec and quality passes. `Type: convert` always gets both.
71
75
  9. **The last ticket is the acceptance ticket**: it runs the map's bar end to end and is blocked by every ticket that leaves the bar unproven.
72
76
  10. **Number from 01 and never renumber.** Tickets added later get the next number.
73
77
  11. **More than about 12 tickets?** Plan in rounds: `plans/<feature>/` first, then `plans/<feature>-round-two/` with its own map that links back.
@@ -77,7 +81,7 @@ prints OK
77
81
 
78
82
  ## Step 5: Check
79
83
 
80
- Run `FLOW_DIR=.feature-flow/state/draft python3 scripts/flow-status.py <feature> --check`. It fails on a missing blocker, a cycle, a missing Done when, a duplicate number, a bad `Test first` value, or no ready ticket, and it warns about tickets that name another ticket in their text without being ordered against it. If the script is not installed, check those by hand. Fix every problem and every warning before you reply.
84
+ Run `FLOW_DIR=.feature-flow/state/draft python3 scripts/flow-status.py <feature> --check`. It fails on a missing blocker, a cycle, a missing Done when, a duplicate number, a bad `Test first` value, a bad `Review` value, or no ready ticket, and it warns about tickets that name another ticket in their text without being ordered against it. If the script is not installed, check those by hand. Fix every problem and every warning before you reply.
81
85
 
82
86
  ## Step 6: Reply
83
87
 
@@ -0,0 +1,87 @@
1
+ # Review one ticket: single pass
2
+
3
+ You are the **reviewer**, not the author. You did not write this change and you do not trust the author's account of it.
4
+
5
+ You are given: the feature, the ticket number (two digits), and optionally the commit the work started from. If no base commit is given, review `HEAD~1..HEAD`.
6
+
7
+ **You must be a fresh session.** If this conversation shows that you wrote or edited code for this ticket, stop. Do not review it here. Tell the user to hand the review to a fresh reviewer instead (a subagent, or a new session, with the ticket-reviewer role and this guide), and give them the arguments filled in: `<feature> <NN> <base-commit>`.
8
+
9
+ ## What you may read
10
+
11
+ The author writes the `## Answer` into the ticket itself, so the ticket as it stands now carries their account of the change, and so does the ticket's own hunk in the diff. Both routes leak it into your context before you notice. Avoid both:
12
+
13
+ **The ticket, as it was before the work.** At `<base>` the `## Answer` heading is still empty, so this gives you the whole ticket with none of the claim. Resolve the exact filename first, then ask for that one blob:
14
+
15
+ ```
16
+ TICKET=$(git ls-files 'plans/<feature>/tasks/NN-*.md') # exactly one path, or fix the pattern
17
+ git show "<base>:$TICKET"
18
+ ```
19
+
20
+ Never put a `*` inside the `<rev>:<path>` argument. `git show '<base>:plans/<feature>/tasks/NN-*.md'` does not glob and does not fail: git rereads it as a pathspec, prints the **HEAD** commit instead, and hands you the ticket hunk with the Answer in it. Exit status is 0 either way, so the only tell is the commit header at the top of the output.
21
+
22
+ Take from the ticket the description, `Type`, `Test first`, `Not in this ticket`, `Done when`, `Reference`, and any `## Review findings` from earlier rounds. If the ticket did not exist at `<base>` (it was added later), read the working copy but cut it off at the Answer: `sed '/^## Answer/q' "$TICKET"`.
23
+
24
+ **The range: stop at the ticket's own last commit, not at `HEAD`.** `HEAD` keeps moving. A later ticket, a tooling change, anything committed after this ticket lands inside `<base>..HEAD`, and the scope check then charges it to an author who never touched it. Find where this ticket's work actually ends:
25
+
26
+ ```
27
+ END=$(git log --format=%H "<base>..HEAD" -- "$TICKET" | head -1) # newest commit touching this ticket
28
+ [ -n "$END" ] || END=HEAD # none found: fall back, and say so
29
+ ```
30
+
31
+ Every commit for a ticket touches its ticket file, because the author commits the code, the ticket and the map together. If `$END` is not `HEAD`, there are later commits you are deliberately not reviewing: say so in your report.
32
+
33
+ **The diff, with the ticket folder held back:**
34
+
35
+ ```
36
+ git diff --stat "<base>..$END" # every changed file, for the scope check
37
+ git diff "<base>..$END" -- . ':(exclude)plans/<feature>/tasks/' # the content you review
38
+ ```
39
+
40
+ Read the files that diff touches. For the ticket's own `Status` line, grep the hunk rather than opening it: `git diff "<base>..$END" -- "$TICKET" | grep '^[+-]Status:'`.
41
+
42
+ (Substitute the repo's own ticket folder if it keeps them elsewhere.)
43
+
44
+ **Do not read `## Answer`.** It is the author's claim. Your job is to check the work without it. If it reaches you anyway, do not pretend it did not: say so in your report, and name any verdict you had already formed before it arrived.
45
+
46
+ ## What you must not do
47
+
48
+ Edit, write, stage, commit, stash, check out or reset anything. You may run read-only commands and the commands in `Done when`. If a command leaves files behind, say so in your report and leave them.
49
+
50
+ ## Verdict: does it meet the ticket, and is it good?
51
+
52
+ This is a small ticket, so one pass covers both the spec and the quality. Do these four in order.
53
+
54
+ 1. **Scope**: over `<base>..$END` only, list every changed file that the ticket does not call for, or that `Not in this ticket` rules out. Unrelated refactors, renames and reformatting count. A file changed by a commit outside that range is not this author's and is not a scope violation.
55
+
56
+ 2. **Done when**: for every bullet, write `MET` or `NOT MET` with what the command printed. If the conductor's notes say the ticket's `check` block passed, write `MET (conductor)` for each bullet that block covers and do not run it again. Run the prose-only bullets yourself, now. A bullet you could not run is `NOT MET`.
57
+
58
+ 3. **Test first**: if the ticket says `Test first: yes`, check that the diff adds or changes a test, and say whether that test would fail without the production change.
59
+
60
+ 4. **Quality**: look for, in this order:
61
+
62
+ - bugs and missing handling at boundaries (input, files, network, empty and error cases)
63
+ - checks made weaker instead of being met: skipped or deleted tests, silenced linters, empty catches, lowered thresholds, tests that cannot fail
64
+ - code that need not exist: something the standard library, the platform or the codebase already provides; a feature nobody asked for; a longer way of doing a shorter thing
65
+ - anything a later ticket will trip over
66
+
67
+ Rate each finding blocker, major or minor, name the file, and say what to change.
68
+
69
+ ## Output
70
+
71
+ Use exactly this shape. Keep it short; no praise.
72
+
73
+ SPEC
74
+ - <Done when bullet>: MET | MET (conductor) | NOT MET - <command and what it printed>
75
+ - scope: OK | <files outside the ticket>
76
+ - test first: n/a | OK | MISSING
77
+
78
+ QUALITY
79
+ 1. blocker|major|minor <file>: <finding> - <what to change>
80
+
81
+ REVIEW: PASS
82
+
83
+ The indentation above only marks where the template starts and stops. Write your own report flush left, as plain text, and **do not wrap it in a code fence** - a fence closes with a line of its own after the verdict, which breaks the rule below.
84
+
85
+ The last line of your whole reply must be exactly `REVIEW: PASS` or `REVIEW: FAIL`, flush left: no closing fence, no trailing note, no sign-off, nothing after it. The flow (`scripts/flow.py`) takes the last line that matches `^REVIEW: (PASS|FAIL)[[:space:]]*$`, so a verdict line that is indented, bulleted, bolded or otherwise decorated is not found at all and the flow records the review as having returned no verdict. Anything you still want to say goes above the verdict line.
86
+
87
+ `PASS` only if every Done when bullet is `MET` or `MET (conductor)`, scope is `OK`, test first is not `MISSING`, and there is no `blocker` or `major` finding. `minor` findings are listed but do not fail the review. When in doubt, fail it and say why.
@@ -5,11 +5,14 @@ Taskfile, CI). Approved by a human when the plan is approved. Workers read this
5
5
  must not change it.
6
6
 
7
7
  The flow runs Smoke before every ticket and Build, Test and Lint after every
8
- ticket, so each must run without asking anything and exit non zero on failure. Delete a line
8
+ ticket, so each must run without asking anything and exit non zero on failure. `Quick:` is
9
+ optional: when set, it runs in place of Test after the tickets in the middle of the plan, and
10
+ Test runs after the last ticket and before each handoff. Delete a line
9
11
  you do not have; a line left as a placeholder is skipped.
10
12
 
11
13
  Build: `<command>`
12
14
  Test: `<command>`
15
+ Quick: `<optional: the fast subset of Test, run after the tickets in the middle of the plan; Test runs after the last ticket and before each handoff>`
13
16
  Lint: `<command>`
14
17
  Smoke: `<the quickest command that proves the base is healthy, run before every ticket>`
15
18
  Run: `<command, if the feature has something to start>`
@@ -4,6 +4,7 @@ Type: task
4
4
  Status: open
5
5
  Blocked by: —
6
6
  Test first: yes
7
+ Review: auto
7
8
 
8
9
  <What to do and why, in prose. Name files, functions and types. Never use line numbers.
9
10
  For Type: settle, say what has to be decided and who decides. For Type: convert, say what
@@ -42,7 +42,7 @@ DURING = "STOP flow code changed during %s: %s"
42
42
  STATE_CHANGED = "STOP the run state was changed by something other than the conductor during %s"
43
43
  NO_REPORT = "STOP the conductor did not report its state (it crashed?) during %s"
44
44
  SCRIPTS = ("flow.py", "flow-status.py", "gate.py", "floor-guard.py", "flow-view.py", "flow-trust.py")
45
- GUIDES = ("build.md", "review.md", "review-quality.md", "plan.md", "plan-review.md", "debug.md") # prompts.GUIDES + debug.md
45
+ GUIDES = ("build.md", "review.md", "review-quality.md", "review-single.md", "plan.md", "plan-review.md", "debug.md") # prompts.GUIDES + debug.md
46
46
  SKILL_GUIDES = ("brief.md", "show.md") # read by the skills themselves
47
47
  TEMPLATES = ("commands.md", "learnings.md", "map.md", "spec.md", "ticket.md", "ui-mockup.md") # guides/templates/
48
48
  ROLES = ("ticket-builder.md", "ticket-reviewer.md", "feature-planner.md", "plan-reviewer.md") # prompts.ROLES
@@ -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 subagent'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,7 +57,7 @@ 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 fresh reviewer subagent for each review pass. 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