feature-flow-cli 0.1.6__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.
- {feature_flow_cli-0.1.6/feature_flow_cli.egg-info → feature_flow_cli-0.2.0}/PKG-INFO +9 -8
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/README.md +8 -7
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/adapters/codex/feature-flow/SKILL.md +3 -3
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/feature_flow/__init__.py +1 -1
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/feature_flow/checks.py +18 -16
- feature_flow_cli-0.2.0/feature_flow/codehash.py +61 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/feature_flow/conductor.py +49 -8
- feature_flow_cli-0.2.0/feature_flow/floorguard.py +392 -0
- feature_flow_cli-0.2.0/feature_flow/gate.py +115 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/feature_flow/install.py +4 -3
- feature_flow_cli-0.2.0/feature_flow/proc.py +31 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/feature_flow/prompts.py +3 -1
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0/feature_flow_cli.egg-info}/PKG-INFO +9 -8
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/feature_flow_cli.egg-info/SOURCES.txt +2 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/guides/brief.md +2 -2
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/guides/build.md +2 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/skills/feature-flow/SKILL.md +3 -3
- feature_flow_cli-0.1.6/feature_flow/floorguard.py +0 -340
- feature_flow_cli-0.1.6/feature_flow/gate.py +0 -100
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/LICENSE +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/MANIFEST.in +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/adapters/codex/feature-flow/agents/openai.yaml +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/agents/feature-planner.md +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/agents/plan-reviewer.md +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/agents/ticket-builder.md +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/agents/ticket-reviewer.md +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/feature_flow/__main__.py +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/feature_flow/cli.py +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/feature_flow/command.py +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/feature_flow/git.py +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/feature_flow/released.py +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/feature_flow/state.py +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/feature_flow/status.py +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/feature_flow/suggest.py +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/feature_flow/tickets.py +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/feature_flow/view.py +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/feature_flow_cli.egg-info/dependency_links.txt +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/feature_flow_cli.egg-info/entry_points.txt +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/feature_flow_cli.egg-info/top_level.txt +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/guides/plan-review.md +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/guides/plan.md +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/guides/review.md +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/guides/show.md +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/guides/templates/commands.md +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/guides/templates/learnings.md +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/guides/templates/map.md +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/guides/templates/spec.md +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/guides/templates/ticket.md +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/guides/templates/ui-mockup.md +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/install.py +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/install.sh +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/pyproject.toml +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/scripts/floor-guard.py +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/scripts/flow-status.py +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/scripts/flow-view.html +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/scripts/flow-view.py +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/scripts/flow.py +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/scripts/gate.py +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/setup.cfg +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.2.0}/skills/architect-review/SKILL.md +0 -0
- {feature_flow_cli-0.1.6 → 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.
|
|
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
|
|
@@ -62,14 +62,14 @@ Then, in your repo, in the agent:
|
|
|
62
62
|
| | Claude Code | Codex |
|
|
63
63
|
|---|---|---|
|
|
64
64
|
| Plan a feature (no plan yet) | `/feature-flow csv-export` | `$feature-flow csv-export` |
|
|
65
|
-
| Plan what you just talked through | `/feature-flow` | `$feature-flow` |
|
|
65
|
+
| Plan and build what you just talked through | `/feature-flow` | `$feature-flow` |
|
|
66
66
|
| Build its tickets (a plan exists) | `/feature-flow csv-export` | `$feature-flow csv-export` |
|
|
67
67
|
| The same, asking nothing | `/feature-flow csv-export auto` | `$feature-flow csv-export auto` |
|
|
68
68
|
| Watch the graph, animated | `/feature-flow csv-export show` | `$feature-flow csv-export show` |
|
|
69
69
|
|
|
70
|
-
The same command plans when `plans/csv-export/` does not exist yet and builds when it does.
|
|
70
|
+
The same command plans when `plans/csv-export/` does not exist yet and builds when it does. After your second yes it commits the new plan folder, and only that folder, then goes straight on to building it; say so if you want it to stop after planning.
|
|
71
71
|
|
|
72
|
-
**Planning asks you twice.** First the session shows a short feature brief, written from your conversation (what, why, scope, the bar, and anything it had to assume), and waits for yes, edit or cancel. Then a fresh `feature-planner` subagent, given only that brief, reads the code, looks up outside docs where the code cannot answer, and writes a draft plan into the git-ignored `.feature-flow/state/draft/`. A fresh `plan-reviewer` checks the draft against the brief. The session shows you the goal, the commands, the ticket graph, what was dropped and the review result, and waits for a second yes. Only then does `flow.py plan-accept` copy the draft into `plans/<feature
|
|
72
|
+
**Planning asks you twice.** First the session shows a short feature brief, written from your conversation (what, why, scope, the bar, and anything it had to assume), and waits for yes, edit or cancel. Then a fresh `feature-planner` subagent, given only that brief, reads the code, looks up outside docs where the code cannot answer, and writes a draft plan into the git-ignored `.feature-flow/state/draft/`. A fresh `plan-reviewer` checks the draft against the brief. The session shows you the goal, the commands, the ticket graph, what was dropped and the review result, and waits for a second yes. Only then does `flow.py plan-accept` copy the draft into `plans/<feature>/`, and the session commits it and starts the build.
|
|
73
73
|
|
|
74
74
|
Try the graph first, with no setup and no agent: `bash examples/demo.sh --open`.
|
|
75
75
|
|
|
@@ -175,7 +175,7 @@ Floor: allow config optional, only when a human decides the guard may let
|
|
|
175
175
|
- **The Answers are the handoff between subagents.** The next builder reads the Answers of the tickets it depends on.
|
|
176
176
|
- `settle` tickets record a decision. `convert` tickets migrate something and must be safe to run twice.
|
|
177
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.
|
|
178
|
-
- `Floor:` categories: `skip`, `suppress`, `empty-catch`, `test-delete`, `threshold`, `config`, `ticket-edit`, `commands-edit`. The line is read from the ticket as it was before the work started, so a worker cannot excuse itself.
|
|
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.
|
|
179
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.
|
|
180
180
|
- The commands in `commands.md` run with nobody watching and must exit non zero on failure.
|
|
181
181
|
|
|
@@ -241,12 +241,13 @@ The commands in `commands.md` run through `bash -c` on Linux and macOS and throu
|
|
|
241
241
|
| `FLOW_MAX_RETRIES` | `2` | builds or reviews per step before the run stops |
|
|
242
242
|
| `FLOW_MAX_REVIEW_ROUNDS` | `3` | times a ticket may be sent back before the run stops |
|
|
243
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` |
|
|
244
245
|
| `FLOW_SMOKE` | the `Smoke:` line of `commands.md` | command run before every ticket; the run stops if it fails |
|
|
245
246
|
| `FLOW_DIR`, `FLOW_TICKETS` | `plans`, `tasks` | where the plans and the ticket folder live |
|
|
246
247
|
| `FLOW_NO_OPEN` | unset | `1` makes `flow-view.py` never open a browser |
|
|
247
248
|
| `FLOW_WATCH_SECONDS` | `3` | how often `flow-view.py --watch` rewrites the page |
|
|
248
249
|
|
|
249
|
-
The run also stops on its own after a number of
|
|
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.
|
|
250
251
|
|
|
251
252
|
## What is tested
|
|
252
253
|
|
|
@@ -273,7 +274,7 @@ bash tests/run.sh
|
|
|
273
274
|
python3 -m unittest discover -s tests/py
|
|
274
275
|
```
|
|
275
276
|
|
|
276
|
-
Offline and free: no model is called. The suite covers the conductor's every answer and how a run stops, sessions, handoff and takeover, a `.git` the agent cannot write, the gate, the floor guard, plan protection, the prompts and guides, both skills, the installer for both agents, the graph page and its data, and the acceptance harness. The page's layout and replay logic are also tested under Node (`tests/viewer-logic.test.js`, skipped when Node is absent). `.github/workflows/tests.yml` runs it on every push to `main` and every pull request, on Ubuntu
|
|
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.
|
|
277
278
|
|
|
278
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`.
|
|
279
280
|
|
|
@@ -304,7 +305,7 @@ git tag v0.1.1 && git push origin v0.1.1
|
|
|
304
305
|
|
|
305
306
|
## Caution
|
|
306
307
|
|
|
307
|
-
The builder subagent has edit and shell access and commits after each ticket. Each ticket costs at least two subagents. Build on a branch you can throw away. Ignore build output in `.gitignore`, because the conductor stops if the tree is dirty after a ticket. The gate runs
|
|
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.
|
|
308
309
|
|
|
309
310
|
## License
|
|
310
311
|
|
|
@@ -43,14 +43,14 @@ Then, in your repo, in the agent:
|
|
|
43
43
|
| | Claude Code | Codex |
|
|
44
44
|
|---|---|---|
|
|
45
45
|
| Plan a feature (no plan yet) | `/feature-flow csv-export` | `$feature-flow csv-export` |
|
|
46
|
-
| Plan what you just talked through | `/feature-flow` | `$feature-flow` |
|
|
46
|
+
| Plan and build what you just talked through | `/feature-flow` | `$feature-flow` |
|
|
47
47
|
| Build its tickets (a plan exists) | `/feature-flow csv-export` | `$feature-flow csv-export` |
|
|
48
48
|
| The same, asking nothing | `/feature-flow csv-export auto` | `$feature-flow csv-export auto` |
|
|
49
49
|
| Watch the graph, animated | `/feature-flow csv-export show` | `$feature-flow csv-export show` |
|
|
50
50
|
|
|
51
|
-
The same command plans when `plans/csv-export/` does not exist yet and builds when it does.
|
|
51
|
+
The same command plans when `plans/csv-export/` does not exist yet and builds when it does. After your second yes it commits the new plan folder, and only that folder, then goes straight on to building it; say so if you want it to stop after planning.
|
|
52
52
|
|
|
53
|
-
**Planning asks you twice.** First the session shows a short feature brief, written from your conversation (what, why, scope, the bar, and anything it had to assume), and waits for yes, edit or cancel. Then a fresh `feature-planner` subagent, given only that brief, reads the code, looks up outside docs where the code cannot answer, and writes a draft plan into the git-ignored `.feature-flow/state/draft/`. A fresh `plan-reviewer` checks the draft against the brief. The session shows you the goal, the commands, the ticket graph, what was dropped and the review result, and waits for a second yes. Only then does `flow.py plan-accept` copy the draft into `plans/<feature
|
|
53
|
+
**Planning asks you twice.** First the session shows a short feature brief, written from your conversation (what, why, scope, the bar, and anything it had to assume), and waits for yes, edit or cancel. Then a fresh `feature-planner` subagent, given only that brief, reads the code, looks up outside docs where the code cannot answer, and writes a draft plan into the git-ignored `.feature-flow/state/draft/`. A fresh `plan-reviewer` checks the draft against the brief. The session shows you the goal, the commands, the ticket graph, what was dropped and the review result, and waits for a second yes. Only then does `flow.py plan-accept` copy the draft into `plans/<feature>/`, and the session commits it and starts the build.
|
|
54
54
|
|
|
55
55
|
Try the graph first, with no setup and no agent: `bash examples/demo.sh --open`.
|
|
56
56
|
|
|
@@ -156,7 +156,7 @@ Floor: allow config optional, only when a human decides the guard may let
|
|
|
156
156
|
- **The Answers are the handoff between subagents.** The next builder reads the Answers of the tickets it depends on.
|
|
157
157
|
- `settle` tickets record a decision. `convert` tickets migrate something and must be safe to run twice.
|
|
158
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.
|
|
159
|
-
- `Floor:` categories: `skip`, `suppress`, `empty-catch`, `test-delete`, `threshold`, `config`, `ticket-edit`, `commands-edit`. The line is read from the ticket as it was before the work started, so a worker cannot excuse itself.
|
|
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.
|
|
160
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.
|
|
161
161
|
- The commands in `commands.md` run with nobody watching and must exit non zero on failure.
|
|
162
162
|
|
|
@@ -222,12 +222,13 @@ The commands in `commands.md` run through `bash -c` on Linux and macOS and throu
|
|
|
222
222
|
| `FLOW_MAX_RETRIES` | `2` | builds or reviews per step before the run stops |
|
|
223
223
|
| `FLOW_MAX_REVIEW_ROUNDS` | `3` | times a ticket may be sent back before the run stops |
|
|
224
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` |
|
|
225
226
|
| `FLOW_SMOKE` | the `Smoke:` line of `commands.md` | command run before every ticket; the run stops if it fails |
|
|
226
227
|
| `FLOW_DIR`, `FLOW_TICKETS` | `plans`, `tasks` | where the plans and the ticket folder live |
|
|
227
228
|
| `FLOW_NO_OPEN` | unset | `1` makes `flow-view.py` never open a browser |
|
|
228
229
|
| `FLOW_WATCH_SECONDS` | `3` | how often `flow-view.py --watch` rewrites the page |
|
|
229
230
|
|
|
230
|
-
The run also stops on its own after a number of
|
|
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.
|
|
231
232
|
|
|
232
233
|
## What is tested
|
|
233
234
|
|
|
@@ -254,7 +255,7 @@ bash tests/run.sh
|
|
|
254
255
|
python3 -m unittest discover -s tests/py
|
|
255
256
|
```
|
|
256
257
|
|
|
257
|
-
Offline and free: no model is called. The suite covers the conductor's every answer and how a run stops, sessions, handoff and takeover, a `.git` the agent cannot write, the gate, the floor guard, plan protection, the prompts and guides, both skills, the installer for both agents, the graph page and its data, and the acceptance harness. The page's layout and replay logic are also tested under Node (`tests/viewer-logic.test.js`, skipped when Node is absent). `.github/workflows/tests.yml` runs it on every push to `main` and every pull request, on Ubuntu
|
|
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.
|
|
258
259
|
|
|
259
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`.
|
|
260
261
|
|
|
@@ -285,7 +286,7 @@ git tag v0.1.1 && git push origin v0.1.1
|
|
|
285
286
|
|
|
286
287
|
## Caution
|
|
287
288
|
|
|
288
|
-
The builder subagent has edit and shell access and commits after each ticket. Each ticket costs at least two subagents. Build on a branch you can throw away. Ignore build output in `.gitignore`, because the conductor stops if the tree is dirty after a ticket. The gate runs
|
|
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.
|
|
289
290
|
|
|
290
291
|
## License
|
|
291
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
|
|
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,13 +1,14 @@
|
|
|
1
|
-
"""The checks the conductor runs
|
|
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
|
|
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 =
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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 {}
|
|
@@ -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
|
-
|
|
107
|
-
|
|
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
|
|
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
|
|