feature-flow-cli 0.1.5__tar.gz → 0.2.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 (61) hide show
  1. {feature_flow_cli-0.1.5/feature_flow_cli.egg-info → feature_flow_cli-0.2.0}/PKG-INFO +11 -8
  2. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/README.md +10 -7
  3. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/adapters/codex/feature-flow/SKILL.md +3 -3
  4. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/feature_flow/__init__.py +1 -1
  5. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/feature_flow/checks.py +18 -16
  6. feature_flow_cli-0.2.0/feature_flow/codehash.py +61 -0
  7. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/feature_flow/command.py +1 -1
  8. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/feature_flow/conductor.py +49 -8
  9. feature_flow_cli-0.2.0/feature_flow/floorguard.py +392 -0
  10. feature_flow_cli-0.2.0/feature_flow/gate.py +115 -0
  11. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/feature_flow/install.py +79 -8
  12. feature_flow_cli-0.2.0/feature_flow/proc.py +31 -0
  13. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/feature_flow/prompts.py +3 -1
  14. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0/feature_flow_cli.egg-info}/PKG-INFO +11 -8
  15. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/feature_flow_cli.egg-info/SOURCES.txt +2 -0
  16. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/guides/brief.md +2 -2
  17. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/guides/build.md +2 -0
  18. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/install.py +1 -1
  19. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/install.sh +1 -1
  20. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/skills/feature-flow/SKILL.md +3 -3
  21. feature_flow_cli-0.1.5/feature_flow/floorguard.py +0 -340
  22. feature_flow_cli-0.1.5/feature_flow/gate.py +0 -100
  23. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/LICENSE +0 -0
  24. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/MANIFEST.in +0 -0
  25. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/adapters/codex/feature-flow/agents/openai.yaml +0 -0
  26. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/agents/feature-planner.md +0 -0
  27. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/agents/plan-reviewer.md +0 -0
  28. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/agents/ticket-builder.md +0 -0
  29. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/agents/ticket-reviewer.md +0 -0
  30. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/feature_flow/__main__.py +0 -0
  31. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/feature_flow/cli.py +0 -0
  32. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/feature_flow/git.py +0 -0
  33. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/feature_flow/released.py +0 -0
  34. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/feature_flow/state.py +0 -0
  35. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/feature_flow/status.py +0 -0
  36. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/feature_flow/suggest.py +0 -0
  37. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/feature_flow/tickets.py +0 -0
  38. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/feature_flow/view.py +0 -0
  39. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/feature_flow_cli.egg-info/dependency_links.txt +0 -0
  40. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/feature_flow_cli.egg-info/entry_points.txt +0 -0
  41. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/feature_flow_cli.egg-info/top_level.txt +0 -0
  42. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/guides/plan-review.md +0 -0
  43. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/guides/plan.md +0 -0
  44. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/guides/review.md +0 -0
  45. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/guides/show.md +0 -0
  46. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/guides/templates/commands.md +0 -0
  47. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/guides/templates/learnings.md +0 -0
  48. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/guides/templates/map.md +0 -0
  49. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/guides/templates/spec.md +0 -0
  50. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/guides/templates/ticket.md +0 -0
  51. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/guides/templates/ui-mockup.md +0 -0
  52. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/pyproject.toml +0 -0
  53. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/scripts/floor-guard.py +0 -0
  54. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/scripts/flow-status.py +0 -0
  55. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/scripts/flow-view.html +0 -0
  56. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/scripts/flow-view.py +0 -0
  57. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/scripts/flow.py +0 -0
  58. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/scripts/gate.py +0 -0
  59. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/setup.cfg +0 -0
  60. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/skills/architect-review/SKILL.md +0 -0
  61. {feature_flow_cli-0.1.5 → feature_flow_cli-0.2.0}/skills/automation-design/SKILL.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: feature-flow-cli
3
- Version: 0.1.5
3
+ Version: 0.2.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
@@ -55,19 +55,21 @@ The package puts a `feature-flow` command on your PATH with `install`, `status <
55
55
 
56
56
  The installer lists what it wrote in `.feature-flow/installed.txt`, with each file's hash in `.feature-flow/installed.sha256`. Commit those files (`git add --pathspec-from-file=.feature-flow/installed.txt`); until you do, the flow does not count them as uncommitted changes. Installing a newer version over an older one updates every file nobody edited since it was installed, and keeps (and names) the ones you changed; `--force` replaces those too.
57
57
 
58
+ To keep the install out of git, so only your plans and tickets get committed, install with `--private`. It lists the installed files in `.git/info/exclude`, which git reads like `.gitignore` but never commits, and later installs keep that list up to date. If you already committed the install, it prints the `git rm --cached` command that untracks it and keeps the files. Every clone then needs its own install.
59
+
58
60
  Then, in your repo, in the agent:
59
61
 
60
62
  | | Claude Code | Codex |
61
63
  |---|---|---|
62
64
  | Plan a feature (no plan yet) | `/feature-flow csv-export` | `$feature-flow csv-export` |
63
- | Plan what you just talked through | `/feature-flow` | `$feature-flow` |
65
+ | Plan and build what you just talked through | `/feature-flow` | `$feature-flow` |
64
66
  | Build its tickets (a plan exists) | `/feature-flow csv-export` | `$feature-flow csv-export` |
65
67
  | The same, asking nothing | `/feature-flow csv-export auto` | `$feature-flow csv-export auto` |
66
68
  | Watch the graph, animated | `/feature-flow csv-export show` | `$feature-flow csv-export show` |
67
69
 
68
- The same command plans when `plans/csv-export/` does not exist yet and builds when it does. Commit the plan before you build it.
70
+ The same command plans when `plans/csv-export/` does not exist yet and builds when it does. After your second yes it commits the new plan folder, and only that folder, then goes straight on to building it; say so if you want it to stop after planning.
69
71
 
70
- **Planning asks you twice.** First the session shows a short feature brief, written from your conversation (what, why, scope, the bar, and anything it had to assume), and waits for yes, edit or cancel. Then a fresh `feature-planner` subagent, given only that brief, reads the code, looks up outside docs where the code cannot answer, and writes a draft plan into the git-ignored `.feature-flow/state/draft/`. A fresh `plan-reviewer` checks the draft against the brief. The session shows you the goal, the commands, the ticket graph, what was dropped and the review result, and waits for a second yes. Only then does `flow.py plan-accept` copy the draft into `plans/<feature>/`.
72
+ **Planning asks you twice.** First the session shows a short feature brief, written from your conversation (what, why, scope, the bar, and anything it had to assume), and waits for yes, edit or cancel. Then a fresh `feature-planner` subagent, given only that brief, reads the code, looks up outside docs where the code cannot answer, and writes a draft plan into the git-ignored `.feature-flow/state/draft/`. A fresh `plan-reviewer` checks the draft against the brief. The session shows you the goal, the commands, the ticket graph, what was dropped and the review result, and waits for a second yes. Only then does `flow.py plan-accept` copy the draft into `plans/<feature>/`, and the session commits it and starts the build.
71
73
 
72
74
  Try the graph first, with no setup and no agent: `bash examples/demo.sh --open`.
73
75
 
@@ -173,7 +175,7 @@ Floor: allow config optional, only when a human decides the guard may let
173
175
  - **The Answers are the handoff between subagents.** The next builder reads the Answers of the tickets it depends on.
174
176
  - `settle` tickets record a decision. `convert` tickets migrate something and must be safe to run twice.
175
177
  - **A worker may change only** its own Status line and Answer, lines appended to `map.md` and `learnings.md`, and the code the ticket calls for. Rewriting its own Done when, editing another ticket, or touching `commands.md` fails the guard.
176
- - `Floor:` categories: `skip`, `suppress`, `empty-catch`, `test-delete`, `threshold`, `config`, `ticket-edit`, `commands-edit`. The line is read from the ticket as it was before the work started, so a worker cannot excuse itself.
178
+ - `Floor:` categories: `skip`, `suppress`, `empty-catch`, `test-delete`, `threshold`, `config`, `ticket-edit`, `commands-edit`, and `flow-edit` (allows the conductor's code check below, for the build only). `skip` covers skipped, expected-fail and focused tests (`.only(`, `fit(`, `fdescribe(` on test paths); `config` covers more lint and test config files, the lint and test sections of `pyproject.toml` and `setup.cfg`, scripts and test keys in `package.json`, new skip or xfail words in `conftest.py`, and a `Makefile` only when `commands.md` uses `make`. A test file that loses assertions is shown to the reviewer as a warning, not a finding. The line is read from the ticket as it was before the work started, so a worker cannot excuse itself.
177
179
  - The guard needs the plan to be tracked by git. If you keep tickets in an ignored folder, it cannot see changes to them.
178
180
  - The commands in `commands.md` run with nobody watching and must exit non zero on failure.
179
181
 
@@ -239,12 +241,13 @@ The commands in `commands.md` run through `bash -c` on Linux and macOS and throu
239
241
  | `FLOW_MAX_RETRIES` | `2` | builds or reviews per step before the run stops |
240
242
  | `FLOW_MAX_REVIEW_ROUNDS` | `3` | times a ticket may be sent back before the run stops |
241
243
  | `FLOW_GATE` | `on` | `off` skips Build, Test and Lint after each ticket |
244
+ | `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` |
242
245
  | `FLOW_SMOKE` | the `Smoke:` line of `commands.md` | command run before every ticket; the run stops if it fails |
243
246
  | `FLOW_DIR`, `FLOW_TICKETS` | `plans`, `tasks` | where the plans and the ticket folder live |
244
247
  | `FLOW_NO_OPEN` | unset | `1` makes `flow-view.py` never open a browser |
245
248
  | `FLOW_WATCH_SECONDS` | `3` | how often `flow-view.py --watch` rewrites the page |
246
249
 
247
- The run also stops on its own after a number of steps that grows with the ticket count, so a loop of failures cannot go on forever.
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.
248
251
 
249
252
  ## What is tested
250
253
 
@@ -271,7 +274,7 @@ bash tests/run.sh
271
274
  python3 -m unittest discover -s tests/py
272
275
  ```
273
276
 
274
- Offline and free: no model is called. The suite covers the conductor's every answer and how a run stops, sessions, handoff and takeover, a `.git` the agent cannot write, the gate, the floor guard, plan protection, the prompts and guides, both skills, the installer for both agents, the graph page and its data, and the acceptance harness. The page's layout and replay logic are also tested under Node (`tests/viewer-logic.test.js`, skipped when Node is absent). `.github/workflows/tests.yml` runs it on every push to `main` and every pull request, on Ubuntu (once with `mawk`, once with `gawk`) and on macOS. A fourth job runs the Python unit tests on Windows, including a fixture drive that takes a two-ticket plan through `python scripts/flow.py f start` and `next` to `BUILD` and then `REVIEW`. `tests/run.sh` itself is a bash harness and does not run on Windows. Windows is tested this way only: no real agent run has been done there.
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.
275
278
 
276
279
  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`.
277
280
 
@@ -302,7 +305,7 @@ git tag v0.1.1 && git push origin v0.1.1
302
305
 
303
306
  ## Caution
304
307
 
305
- The builder subagent has edit and shell access and commits after each ticket. Each ticket costs at least two subagents. Build on a branch you can throw away. Ignore build output in `.gitignore`, because the conductor stops if the tree is dirty after a ticket. The gate runs your Build, Test and Lint commands with no time limit. The floor guard is pattern matching: it can miss things and it can raise false alarms, and `Floor: allow` is the release valve. The run stops on its own when a ticket stays unresolved, when the smoke test or the gate keeps failing, when a ticket keeps failing review, or when it reaches its step limit.
308
+ The builder subagent has edit and shell access and commits after each ticket. Each ticket costs at least two subagents. Build on a branch you can throw away. Ignore build output in `.gitignore`, because the conductor stops if the tree is dirty after a ticket. The gate and the smoke test stop after 30 minutes (`FLOW_GATE_TIMEOUT`). The conductor also checks that the code it runs (the `feature_flow` package, `scripts/*.py` and the build and review role and guide files) did not change during a build or review; if it did, the run stops with `STOP flow code changed while building <ticket>: <files>`, and `Floor: allow flow-edit` on the ticket allows it for the build. That check is a tripwire, not a sandbox. The floor guard is pattern matching: it can miss things and it can raise false alarms, and `Floor: allow` is the release valve. The run stops on its own when a ticket stays unresolved, when the smoke test or the gate keeps failing, when a ticket keeps failing review, or when it reaches its phase limit.
306
309
 
307
310
  ## License
308
311
 
@@ -36,19 +36,21 @@ The package puts a `feature-flow` command on your PATH with `install`, `status <
36
36
 
37
37
  The installer lists what it wrote in `.feature-flow/installed.txt`, with each file's hash in `.feature-flow/installed.sha256`. Commit those files (`git add --pathspec-from-file=.feature-flow/installed.txt`); until you do, the flow does not count them as uncommitted changes. Installing a newer version over an older one updates every file nobody edited since it was installed, and keeps (and names) the ones you changed; `--force` replaces those too.
38
38
 
39
+ To keep the install out of git, so only your plans and tickets get committed, install with `--private`. It lists the installed files in `.git/info/exclude`, which git reads like `.gitignore` but never commits, and later installs keep that list up to date. If you already committed the install, it prints the `git rm --cached` command that untracks it and keeps the files. Every clone then needs its own install.
40
+
39
41
  Then, in your repo, in the agent:
40
42
 
41
43
  | | Claude Code | Codex |
42
44
  |---|---|---|
43
45
  | Plan a feature (no plan yet) | `/feature-flow csv-export` | `$feature-flow csv-export` |
44
- | Plan what you just talked through | `/feature-flow` | `$feature-flow` |
46
+ | Plan and build what you just talked through | `/feature-flow` | `$feature-flow` |
45
47
  | Build its tickets (a plan exists) | `/feature-flow csv-export` | `$feature-flow csv-export` |
46
48
  | The same, asking nothing | `/feature-flow csv-export auto` | `$feature-flow csv-export auto` |
47
49
  | Watch the graph, animated | `/feature-flow csv-export show` | `$feature-flow csv-export show` |
48
50
 
49
- The same command plans when `plans/csv-export/` does not exist yet and builds when it does. Commit the plan before you build it.
51
+ The same command plans when `plans/csv-export/` does not exist yet and builds when it does. After your second yes it commits the new plan folder, and only that folder, then goes straight on to building it; say so if you want it to stop after planning.
50
52
 
51
- **Planning asks you twice.** First the session shows a short feature brief, written from your conversation (what, why, scope, the bar, and anything it had to assume), and waits for yes, edit or cancel. Then a fresh `feature-planner` subagent, given only that brief, reads the code, looks up outside docs where the code cannot answer, and writes a draft plan into the git-ignored `.feature-flow/state/draft/`. A fresh `plan-reviewer` checks the draft against the brief. The session shows you the goal, the commands, the ticket graph, what was dropped and the review result, and waits for a second yes. Only then does `flow.py plan-accept` copy the draft into `plans/<feature>/`.
53
+ **Planning asks you twice.** First the session shows a short feature brief, written from your conversation (what, why, scope, the bar, and anything it had to assume), and waits for yes, edit or cancel. Then a fresh `feature-planner` subagent, given only that brief, reads the code, looks up outside docs where the code cannot answer, and writes a draft plan into the git-ignored `.feature-flow/state/draft/`. A fresh `plan-reviewer` checks the draft against the brief. The session shows you the goal, the commands, the ticket graph, what was dropped and the review result, and waits for a second yes. Only then does `flow.py plan-accept` copy the draft into `plans/<feature>/`, and the session commits it and starts the build.
52
54
 
53
55
  Try the graph first, with no setup and no agent: `bash examples/demo.sh --open`.
54
56
 
@@ -154,7 +156,7 @@ Floor: allow config optional, only when a human decides the guard may let
154
156
  - **The Answers are the handoff between subagents.** The next builder reads the Answers of the tickets it depends on.
155
157
  - `settle` tickets record a decision. `convert` tickets migrate something and must be safe to run twice.
156
158
  - **A worker may change only** its own Status line and Answer, lines appended to `map.md` and `learnings.md`, and the code the ticket calls for. Rewriting its own Done when, editing another ticket, or touching `commands.md` fails the guard.
157
- - `Floor:` categories: `skip`, `suppress`, `empty-catch`, `test-delete`, `threshold`, `config`, `ticket-edit`, `commands-edit`. The line is read from the ticket as it was before the work started, so a worker cannot excuse itself.
159
+ - `Floor:` categories: `skip`, `suppress`, `empty-catch`, `test-delete`, `threshold`, `config`, `ticket-edit`, `commands-edit`, and `flow-edit` (allows the conductor's code check below, for the build only). `skip` covers skipped, expected-fail and focused tests (`.only(`, `fit(`, `fdescribe(` on test paths); `config` covers more lint and test config files, the lint and test sections of `pyproject.toml` and `setup.cfg`, scripts and test keys in `package.json`, new skip or xfail words in `conftest.py`, and a `Makefile` only when `commands.md` uses `make`. A test file that loses assertions is shown to the reviewer as a warning, not a finding. The line is read from the ticket as it was before the work started, so a worker cannot excuse itself.
158
160
  - The guard needs the plan to be tracked by git. If you keep tickets in an ignored folder, it cannot see changes to them.
159
161
  - The commands in `commands.md` run with nobody watching and must exit non zero on failure.
160
162
 
@@ -220,12 +222,13 @@ The commands in `commands.md` run through `bash -c` on Linux and macOS and throu
220
222
  | `FLOW_MAX_RETRIES` | `2` | builds or reviews per step before the run stops |
221
223
  | `FLOW_MAX_REVIEW_ROUNDS` | `3` | times a ticket may be sent back before the run stops |
222
224
  | `FLOW_GATE` | `on` | `off` skips Build, Test and Lint after each ticket |
225
+ | `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` |
223
226
  | `FLOW_SMOKE` | the `Smoke:` line of `commands.md` | command run before every ticket; the run stops if it fails |
224
227
  | `FLOW_DIR`, `FLOW_TICKETS` | `plans`, `tasks` | where the plans and the ticket folder live |
225
228
  | `FLOW_NO_OPEN` | unset | `1` makes `flow-view.py` never open a browser |
226
229
  | `FLOW_WATCH_SECONDS` | `3` | how often `flow-view.py --watch` rewrites the page |
227
230
 
228
- The run also stops on its own after a number of steps that grows with the ticket count, so a loop of failures cannot go on forever.
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.
229
232
 
230
233
  ## What is tested
231
234
 
@@ -252,7 +255,7 @@ bash tests/run.sh
252
255
  python3 -m unittest discover -s tests/py
253
256
  ```
254
257
 
255
- Offline and free: no model is called. The suite covers the conductor's every answer and how a run stops, sessions, handoff and takeover, a `.git` the agent cannot write, the gate, the floor guard, plan protection, the prompts and guides, both skills, the installer for both agents, the graph page and its data, and the acceptance harness. The page's layout and replay logic are also tested under Node (`tests/viewer-logic.test.js`, skipped when Node is absent). `.github/workflows/tests.yml` runs it on every push to `main` and every pull request, on Ubuntu (once with `mawk`, once with `gawk`) and on macOS. A fourth job runs the Python unit tests on Windows, including a fixture drive that takes a two-ticket plan through `python scripts/flow.py f start` and `next` to `BUILD` and then `REVIEW`. `tests/run.sh` itself is a bash harness and does not run on Windows. Windows is tested this way only: no real agent run has been done there.
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.
256
259
 
257
260
  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`.
258
261
 
@@ -283,7 +286,7 @@ git tag v0.1.1 && git push origin v0.1.1
283
286
 
284
287
  ## Caution
285
288
 
286
- The builder subagent has edit and shell access and commits after each ticket. Each ticket costs at least two subagents. Build on a branch you can throw away. Ignore build output in `.gitignore`, because the conductor stops if the tree is dirty after a ticket. The gate runs your Build, Test and Lint commands with no time limit. The floor guard is pattern matching: it can miss things and it can raise false alarms, and `Floor: allow` is the release valve. The run stops on its own when a ticket stays unresolved, when the smoke test or the gate keeps failing, when a ticket keeps failing review, or when it reaches its step limit.
289
+ The builder subagent has edit and shell access and commits after each ticket. Each ticket costs at least two subagents. Build on a branch you can throw away. Ignore build output in `.gitignore`, because the conductor stops if the tree is dirty after a ticket. The gate and the smoke test stop after 30 minutes (`FLOW_GATE_TIMEOUT`). The conductor also checks that the code it runs (the `feature_flow` package, `scripts/*.py` and the build and review role and guide files) did not change during a build or review; if it did, the run stops with `STOP flow code changed while building <ticket>: <files>`, and `Floor: allow flow-edit` on the ticket allows it for the build. That check is a tripwire, not a sandbox. The floor guard is pattern matching: it can miss things and it can raise false alarms, and `Floor: allow` is the release valve. The run stops on its own when a ticket stays unresolved, when the smoke test or the gate keeps failing, when a ticket keeps failing review, or when it reaches its phase limit.
287
290
 
288
291
  ## License
289
292
 
@@ -23,7 +23,7 @@ When `plans/<feature>/` does not exist, or you are planning from the conversatio
23
23
  - `plan-review-prompt`: start a new reviewer the same way, with `task_name="plan_reviewer"` and `fork_turns="none"`, never with the planner's reply. Get its final reply with `wait_agent` and save it with the shell into `.feature-flow/state/plan-review-<feature>.txt`.
24
24
  - `plan-accept`: writes `plans/<feature>/` from the draft, only after the second yes.
25
25
 
26
- Then run `python3 scripts/flow-status.py <feature> --check`, list the files `plan-accept` wrote, and suggest the commit command (`git add plans/<feature> && git commit -m "docs(<feature>): plan"`). Say to commit the plan and run `$feature-flow <feature>` again. While the plan is uncommitted, do not say the feature is ready to build.
26
+ Then run `python3 scripts/flow-status.py <feature> --check` and list the files `plan-accept` wrote. The second yes also approves committing the plan, so commit the folder `plan-accept` printed (`OK <folder>`, `plans/<feature>` unless `FLOW_DIR` is set) and nothing else: `git add -- <folder> && git commit -m "docs(<feature>): plan" -- <folder>`. If the commit fails, report why and stop. Then go straight on to building it below, unless the user asked to stop after planning.
27
27
 
28
28
  To add tickets to a plan that exists, edit it by hand following the ticket rules in `.feature-flow/guides/plan.md`.
29
29
 
@@ -41,7 +41,7 @@ If `start` prints `STOP` naming another owner, another session may still be work
41
41
 
42
42
  If the user says the flow is stuck on a ticket or asks for a reset, run `reset` (or `reset <NN>` to redo one ticket): it reopens a half-built ticket, commits that, and clears the run, then run `start` again. It needs `FLOW_TAKEOVER=1` while a session owns the feature, on the same clear yes. Never reset on your own.
43
43
 
44
- Say what will happen: for each ticket, a builder subagent and then a reviewer subagent. After every few tickets (`FLOW_TICKETS_PER_SESSION`, default 4) you hand off to a new session. Ask for a yes, unless `auto` was given.
44
+ Say what will happen: for each ticket, a builder subagent and then a reviewer subagent. After every few tickets (`FLOW_TICKETS_PER_SESSION`, default 4) you hand off to a new session. Ask for a yes, unless `auto` was given or you came straight from planning: the plan's yes already covered the build.
45
45
 
46
46
  ## The loop
47
47
 
@@ -62,7 +62,7 @@ A conductor command looks like this:
62
62
  These apply from `start` on, once a plan exists.
63
63
 
64
64
  - Never run the gate, the floor guard or a review yourself. The conductor runs the checks, and the reviewer subagent reviews.
65
- - Never edit a ticket, the plan or the code, and never commit. The builder does that.
65
+ - Never edit a ticket, the plan or the code, and never commit, apart from the approved plan above. The builder does that.
66
66
  - Never skip a step, reorder steps, or decide the next step yourself. Only `next` decides.
67
67
  - Never read a ticket's `## Answer` into the reviewer's prompt. `prompt` already holds everything it needs.
68
68
  - Tell the user one short line per phase (`01 built`, `01 review: PASS`), not the subagents' reports.
@@ -1,3 +1,3 @@
1
1
  """Feature Flow: a ticket-graph workflow for coding agents."""
2
2
 
3
- __version__ = "0.1.5"
3
+ __version__ = "0.2.0"
@@ -1,13 +1,14 @@
1
- """The checks the conductor runs: the bash scripts it still calls, and the ported ones in process.
1
+ """The checks the conductor runs, in process.
2
2
 
3
3
  Each call lives in one function so plans/interactive-flow-python can swap it for an
4
4
  in-process call without touching the conductor.
5
5
  """
6
6
 
7
7
  import os
8
- import subprocess
8
+ import tempfile
9
9
  from pathlib import Path
10
10
 
11
+ from feature_flow import proc
11
12
  from feature_flow import status
12
13
  from feature_flow import gate as gate_module
13
14
  from feature_flow import floorguard
@@ -36,12 +37,6 @@ class _Collect:
36
37
  return "".join(self.parts)
37
38
 
38
39
 
39
- def _bash(script, *args):
40
- result = subprocess.run(["bash", str(script)] + [str(a) for a in args],
41
- stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True)
42
- return Result(result.returncode, result.stdout)
43
-
44
-
45
40
  def flow_status(scripts, feature, mode, root=None):
46
41
  """The ticket graph reader with --next, --counts or --check, in process. stderr is folded into the output.
47
42
  root, when given, stands in for FLOW_DIR for this one call (the planner's draft folder)."""
@@ -60,11 +55,13 @@ def flow_status(scripts, feature, mode, root=None):
60
55
  return Result(code, out.text())
61
56
 
62
57
 
63
- def gate(scripts, feature):
64
- """The gate, run in process. stderr is folded into the output as the subprocess call did."""
58
+ def gate(scripts, feature, timeout=None):
59
+ """The gate, run in process. stderr is folded into the output as the subprocess call did.
60
+ timeout is minutes, None for FLOW_GATE_TIMEOUT."""
65
61
  chunks = []
66
- code = gate_module.run(str(feature), chunks.append, chunks.append)
67
- out = b"".join(chunks).decode("utf-8", "replace").replace("\r\n", "\n").replace("\r", "\n")
62
+ code = gate_module.run(str(feature), chunks.append, chunks.append, timeout)
63
+ out = "".join(chunks).encode("utf-8", "surrogateescape").decode("utf-8", "replace")
64
+ out = out.replace("\r\n", "\n").replace("\r", "\n")
68
65
  return Result(code, out)
69
66
 
70
67
 
@@ -76,8 +73,13 @@ def floor_guard(scripts, feature, num, base):
76
73
  return Result(code, out)
77
74
 
78
75
 
79
- def smoke(command):
76
+ def smoke(command, timeout=None):
77
+ """The smoke command. timeout is minutes, 0 or None for none. Result.timed_out is set when it ran out."""
80
78
  args, use_shell = gate_module.shell(command)
81
- result = subprocess.run(args, shell=use_shell, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
82
- universal_newlines=True)
83
- return Result(result.returncode, result.stdout)
79
+ with tempfile.TemporaryFile() as log:
80
+ code, timed_out = proc.run(args, use_shell, log, timeout)
81
+ log.seek(0)
82
+ out = log.read().decode("utf-8", "replace").replace("\r\n", "\n")
83
+ result = Result(1 if timed_out else code, out)
84
+ result.timed_out = timed_out
85
+ return result
@@ -0,0 +1,61 @@
1
+ """A fingerprint of the code and prompt files a conductor run uses, so a change during a build shows."""
2
+
3
+ import hashlib
4
+ import json
5
+ import os
6
+ from pathlib import Path
7
+
8
+ from feature_flow import git, prompts
9
+
10
+
11
+ def _files(scripts):
12
+ package = Path(__file__).resolve().parent
13
+ found = [p for p in package.rglob("*.py") if "__pycache__" not in p.parts]
14
+ found += Path(scripts).resolve().glob("*.py")
15
+ for phase in ("build", "review"):
16
+ for folder, names in (("agents", prompts.ROLES), ("guides", prompts.GUIDES)):
17
+ try:
18
+ found.append(prompts.find_file(scripts, folder, names[phase]).resolve())
19
+ except FileNotFoundError:
20
+ pass
21
+ return sorted(set(found))
22
+
23
+
24
+ def _name(path, top):
25
+ try:
26
+ return path.relative_to(top).as_posix()
27
+ except ValueError:
28
+ return path.as_posix()
29
+
30
+
31
+ def flow_code(scripts):
32
+ """(the sha256 over every file's path and bytes, {path relative to the repo: sha256 of the file})."""
33
+ top = git.toplevel()
34
+ top = top.resolve() if top is not None else Path(os.getcwd()).resolve()
35
+ each = {}
36
+ for path in _files(scripts):
37
+ try:
38
+ each[_name(path, top)] = hashlib.sha256(path.read_bytes()).hexdigest()
39
+ except OSError:
40
+ each[_name(path, top)] = ""
41
+ whole = hashlib.sha256()
42
+ for name in sorted(each):
43
+ whole.update(name.encode("utf-8") + b"\0" + each[name].encode("ascii") + b"\0")
44
+ return whole.hexdigest(), each
45
+
46
+
47
+ def changed(before, now):
48
+ """The names that differ between two {name: sha256} maps, sorted."""
49
+ return sorted(name for name in set(before) | set(now) if before.get(name) != now.get(name))
50
+
51
+
52
+ def dump(each):
53
+ return json.dumps(each, sort_keys=True)
54
+
55
+
56
+ def load(text):
57
+ try:
58
+ value = json.loads(text)
59
+ except ValueError:
60
+ return {}
61
+ return value if isinstance(value, dict) else {}
@@ -12,7 +12,7 @@ from pathlib import Path
12
12
  from feature_flow import __version__, cli, install, status, suggest, view
13
13
 
14
14
  USAGE = """\
15
- usage: feature-flow install [target-repo] [--agent claude|codex|all] [--user] [--force] [--dry-run]
15
+ usage: feature-flow install [target-repo] [--agent claude|codex|all] [--user] [--private] [--force] [--dry-run]
16
16
  feature-flow status <feature> [--next | --counts | --check | --mermaid [plain] | --json]
17
17
  feature-flow view <feature> [--watch] [--no-open] [--out FILE]
18
18
  feature-flow reset <feature> [NN]
@@ -10,7 +10,7 @@ import secrets
10
10
  import shutil
11
11
  from pathlib import Path
12
12
 
13
- from feature_flow import checks, git, prompts, state, suggest, tickets
13
+ from feature_flow import checks, codehash, floorguard, gate, git, prompts, state, suggest, tickets
14
14
 
15
15
 
16
16
  VERDICT = re.compile(r"^REVIEW: (PASS|FAIL)[ \t\r\f\v]*$")
@@ -41,6 +41,7 @@ class Conductor:
41
41
  self.commands = self.plan / "commands.md"
42
42
  self.max_retries = _env_int("FLOW_MAX_RETRIES", 2)
43
43
  self.max_rounds = _env_int("FLOW_MAX_REVIEW_ROUNDS", 3)
44
+ self.gate_timeout = _env_int("FLOW_GATE_TIMEOUT", 30)
44
45
  self.gate_on = os.environ.get("FLOW_GATE", "on") != "off"
45
46
  self.per_session = _env_int("FLOW_TICKETS_PER_SESSION", 4)
46
47
  self.relay = os.environ.get("FLOW_RELAY", "0") == "1"
@@ -101,10 +102,17 @@ class Conductor:
101
102
  cmd = self.smoke_command()
102
103
  if not cmd:
103
104
  return
104
- result = checks.smoke(cmd)
105
+ result = checks.smoke(cmd, self.gate_timeout)
105
106
  if not result.ok:
106
- raise Stop("smoke test failed before %s: the base is already broken. fix it first. command: %s"
107
- % (self.ticket_name(), cmd))
107
+ log = self.state_file.with_name("%s.smoke.log" % self.feature)
108
+ text = result.out
109
+ if result.timed_out:
110
+ text += "\nsmoke test timed out after %d minutes\n" % self.gate_timeout
111
+ log.write_text(gate.tail(text), encoding="utf-8")
112
+ raise Stop("smoke test %s before %s: the base is already broken. fix it first. command: %s. "
113
+ "the last %d lines are in %s"
114
+ % ("timed out after %d minutes" % self.gate_timeout if result.timed_out else "failed",
115
+ self.ticket_name(), cmd, gate.TAIL, log))
108
116
 
109
117
  def run_limit(self):
110
118
  result = checks.flow_status(self.scripts, self.feature, "--counts")
@@ -118,7 +126,7 @@ class Conductor:
118
126
  runs = self.num("runs") + 1
119
127
  limit = self.num("limit")
120
128
  if runs > limit:
121
- raise Stop("run limit of %d sessions reached, stopping" % limit)
129
+ raise Stop("run limit of %d phases reached, stopping" % limit)
122
130
  self.st["runs"] = runs
123
131
 
124
132
  # ---- phases --------------------------------------------------------
@@ -180,12 +188,41 @@ class Conductor:
180
188
  if self.per_session > 0 and self.num("session_done") >= self.per_session:
181
189
  return self.handoff()
182
190
  path = nxt.out.strip().splitlines()[-1]
191
+ self.st["limit"] = self.run_limit()
183
192
  self.st.update({"ticket": path, "num": tickets.number(path), "base": git.head(), "phase": "",
184
- "attempt": 0, "review_attempt": 0, "round": 0, "review_sha": ""})
193
+ "attempt": 0, "review_attempt": 0, "round": 0, "review_sha": "", "guard_warning": ""})
194
+ self.snapshot_code()
185
195
  self.run_smoke()
186
196
  return self.hand_out_build()
187
197
 
198
+ def snapshot_code(self):
199
+ whole, each = codehash.flow_code(self.scripts)
200
+ self.st["code_sha"] = whole
201
+ self.st["code_files"] = codehash.dump(each)
202
+
203
+ def check_code(self, allowed=False):
204
+ """Stop when the files the conductor runs from changed since the ticket was picked."""
205
+ whole, each = codehash.flow_code(self.scripts)
206
+ if whole == self.get("code_sha"):
207
+ return
208
+ if allowed:
209
+ self.snapshot_code()
210
+ return
211
+ names = codehash.changed(codehash.load(self.get("code_files")), each)
212
+ raise Stop("flow code changed while building %s: %s. if intended, the ticket needs a line: "
213
+ "Floor: allow flow-edit" % (self.ticket_name(), ", ".join(names)))
214
+
215
+ def flow_edit_allowed(self):
216
+ try:
217
+ source = git._git("show", "%s:%s" % (self.get("base"), Path(self.ticket()).as_posix()), check=False)
218
+ except OSError:
219
+ return False
220
+ if source.returncode != 0:
221
+ return False
222
+ return "flow-edit" in floorguard.allow_line(source.stdout).split()
223
+
188
224
  def judge_build(self):
225
+ self.check_code(self.flow_edit_allowed())
189
226
  status = tickets.status(self.ticket())
190
227
  if status not in tickets.DONE:
191
228
  if status in tickets.RESET:
@@ -196,7 +233,7 @@ class Conductor:
196
233
  if not git.is_clean():
197
234
  raise Stop("working tree is dirty after %s, it should have been committed" % self.ticket_name())
198
235
  if self.gate_on:
199
- result = checks.gate(self.scripts, self.feature)
236
+ result = checks.gate(self.scripts, self.feature, self.gate_timeout)
200
237
  self.log("GATE-PASS" if result.ok else "GATE-FAIL")
201
238
  if not result.ok:
202
239
  return self.send_back("gate", result.out)
@@ -204,6 +241,8 @@ class Conductor:
204
241
  self.log("GUARD-PASS" if result.ok else "GUARD-FAIL")
205
242
  if not result.ok:
206
243
  return self.send_back("floor guard", result.out)
244
+ warnings = [l for l in result.out.splitlines() if l.startswith("warning:")]
245
+ self.st["guard_warning"] = warnings[0] if warnings else ""
207
246
  return self.hand_out_review()
208
247
 
209
248
  # ---- commands ------------------------------------------------------
@@ -294,6 +333,7 @@ class Conductor:
294
333
  return "OK %s. run %s %s" % (", ".join(done), self.invoke, self.feature)
295
334
 
296
335
  def judge_review(self):
336
+ self.check_code()
297
337
  if not git.tracked_clean() or git.head() != self.get("review_sha"):
298
338
  raise Stop("the reviewer changed tracked files, which a reviewer must never do")
299
339
  verdict = self.get("verdict")
@@ -318,7 +358,8 @@ class Conductor:
318
358
  if phase not in ("build", "review"):
319
359
  raise NoPhase("no BUILD or REVIEW is pending for %s" % self.feature)
320
360
  try:
321
- return prompts.build(phase, self.scripts, self.feature, self.ticket(), self.get("num"), self.get("base"))
361
+ 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 "")
322
363
  except FileNotFoundError as err:
323
364
  raise Stop(str(err))
324
365