feature-flow-cli 0.1.6__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {feature_flow_cli-0.1.6/feature_flow_cli.egg-info → feature_flow_cli-0.3.0}/PKG-INFO +31 -15
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/README.md +30 -14
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/adapters/codex/feature-flow/SKILL.md +5 -5
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/feature_flow/__init__.py +1 -1
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/feature_flow/checks.py +18 -16
- feature_flow_cli-0.3.0/feature_flow/codehash.py +65 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/feature_flow/conductor.py +131 -10
- feature_flow_cli-0.3.0/feature_flow/floorguard.py +392 -0
- feature_flow_cli-0.3.0/feature_flow/gate.py +115 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/feature_flow/git.py +8 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/feature_flow/install.py +4 -3
- feature_flow_cli-0.3.0/feature_flow/proc.py +31 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/feature_flow/prompts.py +15 -7
- feature_flow_cli-0.3.0/feature_flow/proof.py +294 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0/feature_flow_cli.egg-info}/PKG-INFO +31 -15
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/feature_flow_cli.egg-info/SOURCES.txt +5 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/guides/brief.md +2 -2
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/guides/build.md +6 -2
- feature_flow_cli-0.3.0/guides/debug.md +32 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/guides/plan.md +9 -2
- feature_flow_cli-0.3.0/guides/review-quality.md +74 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/guides/review.md +1 -15
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/guides/templates/ticket.md +6 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/skills/feature-flow/SKILL.md +4 -4
- 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.3.0}/LICENSE +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/MANIFEST.in +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/adapters/codex/feature-flow/agents/openai.yaml +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/agents/feature-planner.md +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/agents/plan-reviewer.md +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/agents/ticket-builder.md +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/agents/ticket-reviewer.md +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/feature_flow/__main__.py +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/feature_flow/cli.py +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/feature_flow/command.py +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/feature_flow/released.py +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/feature_flow/state.py +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/feature_flow/status.py +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/feature_flow/suggest.py +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/feature_flow/tickets.py +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/feature_flow/view.py +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/feature_flow_cli.egg-info/dependency_links.txt +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/feature_flow_cli.egg-info/entry_points.txt +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/feature_flow_cli.egg-info/top_level.txt +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/guides/plan-review.md +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/guides/show.md +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/guides/templates/commands.md +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/guides/templates/learnings.md +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/guides/templates/map.md +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/guides/templates/spec.md +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/guides/templates/ui-mockup.md +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/install.py +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/install.sh +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/pyproject.toml +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/scripts/floor-guard.py +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/scripts/flow-status.py +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/scripts/flow-view.html +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/scripts/flow-view.py +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/scripts/flow.py +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/scripts/gate.py +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/setup.cfg +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/skills/architect-review/SKILL.md +0 -0
- {feature_flow_cli-0.1.6 → feature_flow_cli-0.3.0}/skills/automation-design/SKILL.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: feature-flow-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A ticket-graph workflow for coding agents: installs the feature-flow skill into a repo
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/hamilton-sky/feature-flow
|
|
@@ -62,14 +62,14 @@ Then, in your repo, in the agent:
|
|
|
62
62
|
| | Claude Code | Codex |
|
|
63
63
|
|---|---|---|
|
|
64
64
|
| Plan a feature (no plan yet) | `/feature-flow csv-export` | `$feature-flow csv-export` |
|
|
65
|
-
| Plan what you just talked through | `/feature-flow` | `$feature-flow` |
|
|
65
|
+
| Plan and build what you just talked through | `/feature-flow` | `$feature-flow` |
|
|
66
66
|
| Build its tickets (a plan exists) | `/feature-flow csv-export` | `$feature-flow csv-export` |
|
|
67
67
|
| The same, asking nothing | `/feature-flow csv-export auto` | `$feature-flow csv-export auto` |
|
|
68
68
|
| Watch the graph, animated | `/feature-flow csv-export show` | `$feature-flow csv-export show` |
|
|
69
69
|
|
|
70
|
-
The same command plans when `plans/csv-export/` does not exist yet and builds when it does.
|
|
70
|
+
The same command plans when `plans/csv-export/` does not exist yet and builds when it does. After your second yes it commits the new plan folder, and only that folder, then goes straight on to building it; say so if you want it to stop after planning.
|
|
71
71
|
|
|
72
|
-
**Planning asks you twice.** First the session shows a short feature brief, written from your conversation (what, why, scope, the bar, and anything it had to assume), and waits for yes, edit or cancel. Then a fresh `feature-planner` subagent, given only that brief, reads the code, looks up outside docs where the code cannot answer, and writes a draft plan into the git-ignored `.feature-flow/state/draft/`. A fresh `plan-reviewer` checks the draft against the brief. The session shows you the goal, the commands, the ticket graph, what was dropped and the review result, and waits for a second yes. Only then does `flow.py plan-accept` copy the draft into `plans/<feature
|
|
72
|
+
**Planning asks you twice.** First the session shows a short feature brief, written from your conversation (what, why, scope, the bar, and anything it had to assume), and waits for yes, edit or cancel. Then a fresh `feature-planner` subagent, given only that brief, reads the code, looks up outside docs where the code cannot answer, and writes a draft plan into the git-ignored `.feature-flow/state/draft/`. A fresh `plan-reviewer` checks the draft against the brief. The session shows you the goal, the commands, the ticket graph, what was dropped and the review result, and waits for a second yes. Only then does `flow.py plan-accept` copy the draft into `plans/<feature>/`, and the session commits it and starts the build.
|
|
73
73
|
|
|
74
74
|
Try the graph first, with no setup and no agent: `bash examples/demo.sh --open`.
|
|
75
75
|
|
|
@@ -104,16 +104,26 @@ The Claude Code skill is `skills/feature-flow/`. The Codex skill is written by h
|
|
|
104
104
|
plan touched beyond own Status + Answer + appended lines? │
|
|
105
105
|
│ clean │
|
|
106
106
|
▼ │
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
Done when checks (script): runs every ```check block fail ─┤
|
|
108
|
+
test-first check (script): Test first: yes needs a test-only │
|
|
109
|
+
commit that failed Test before the code fail ─┤
|
|
110
|
+
│ pass │
|
|
111
|
+
▼ │
|
|
112
|
+
spec review: fresh reviewer subagent, sees only the ticket │
|
|
113
|
+
+ diff, re-runs every Done when FAIL ─┤
|
|
114
|
+
│ PASS │
|
|
115
|
+
▼ │
|
|
116
|
+
quality review: a second fresh reviewer subagent FAIL ─┤
|
|
109
117
|
│ PASS ▼
|
|
110
118
|
▼ findings written into the ticket,
|
|
111
|
-
next ticket reopened, built again (3 rounds max)
|
|
119
|
+
next ticket reopened, built again (3 rounds max);
|
|
120
|
+
the builder's prompt then carries
|
|
121
|
+
a root-cause debugging guide
|
|
112
122
|
```
|
|
113
123
|
|
|
114
124
|
`scripts/flow.py` is the conductor. The session asks it `next`, and it answers with one line: `BUILD`, `REVIEW`, `DONE`, `STOP` or `HANDOFF`. It prints the full prompt for each subagent from runtime-neutral guides, so the session never writes one itself.
|
|
115
125
|
|
|
116
|
-
**What the script checks and what it trusts.** The conductor verifies the build from the repo: the ticket file must say `resolved`, the tree must be clean and committed, and it runs the gate and the floor guard itself. It also checks that the reviewer changed no tracked file and made no commit. The review
|
|
126
|
+
**What the script checks and what it trusts.** The conductor verifies the build from the repo: the ticket file must say `resolved`, the tree must be clean and committed, and it runs the gate and the floor guard itself. It also runs the `check` block under each ticket's Done when and compares the exit code and printed text, and for a `Test first: yes` ticket it proves the red test commit itself: it checks out the test-only commit, runs Test, and requires it to fail before the code. A failure of either goes back to the builder before any reviewer is started. It also checks that the reviewer changed no tracked file and made no commit. The review verdicts are different: each reviewer's reply (spec review, then quality review) is relayed by the session, which saves it to a file and hands it to `flow.py verdict`. The script reads `REVIEW: PASS` or `REVIEW: FAIL` from that reply; it cannot prove the session passed it on unedited. A ticket moves on only after two PASS verdicts.
|
|
117
127
|
|
|
118
128
|
## The skill and the roles
|
|
119
129
|
|
|
@@ -153,7 +163,7 @@ plans/
|
|
|
153
163
|
|
|
154
164
|
One markdown file per ticket in `plans/<feature>/tasks/NN-slug.md`:
|
|
155
165
|
|
|
156
|
-
|
|
166
|
+
````
|
|
157
167
|
# Add the retry policy to the job runner
|
|
158
168
|
|
|
159
169
|
Type: task task | settle | convert
|
|
@@ -166,16 +176,21 @@ Floor: allow config optional, only when a human decides the guard may let
|
|
|
166
176
|
|
|
167
177
|
## Not in this ticket
|
|
168
178
|
## Done when commands and the results they print
|
|
179
|
+
```check
|
|
180
|
+
$ python3 -m unittest discover -s tests/py
|
|
181
|
+
exit 0
|
|
182
|
+
prints OK
|
|
183
|
+
```
|
|
169
184
|
## Reference
|
|
170
185
|
## Answer Built, Proof, Decisions, Shortcuts taken, Review fixes, For later tickets
|
|
171
|
-
|
|
186
|
+
````
|
|
172
187
|
|
|
173
188
|
- **Order comes from `Blocked by`**, not from numbers. A ticket is *ready* when it is open and every ticket it is blocked by is resolved. Lowest number wins.
|
|
174
189
|
- **Status lives only in the ticket files.** `map.md` never repeats it.
|
|
175
190
|
- **The Answers are the handoff between subagents.** The next builder reads the Answers of the tickets it depends on.
|
|
176
191
|
- `settle` tickets record a decision. `convert` tickets migrate something and must be safe to run twice.
|
|
177
192
|
- **A worker may change only** its own Status line and Answer, lines appended to `map.md` and `learnings.md`, and the code the ticket calls for. Rewriting its own Done when, editing another ticket, or touching `commands.md` fails the guard.
|
|
178
|
-
- `Floor:` categories: `skip`, `suppress`, `empty-catch`, `test-delete`, `threshold`, `config`, `ticket-edit`, `commands-edit`. The line is read from the ticket as it was before the work started, so a worker cannot excuse itself.
|
|
193
|
+
- `Floor:` categories: `skip`, `suppress`, `empty-catch`, `test-delete`, `threshold`, `config`, `ticket-edit`, `commands-edit`, and `flow-edit` (allows the conductor's code check below, for the build only). `skip` covers skipped, expected-fail and focused tests (`.only(`, `fit(`, `fdescribe(` on test paths); `config` covers more lint and test config files, the lint and test sections of `pyproject.toml` and `setup.cfg`, scripts and test keys in `package.json`, new skip or xfail words in `conftest.py`, and a `Makefile` only when `commands.md` uses `make`. A test file that loses assertions is shown to the reviewer as a warning, not a finding. The line is read from the ticket as it was before the work started, so a worker cannot excuse itself.
|
|
179
194
|
- The guard needs the plan to be tracked by git. If you keep tickets in an ignored folder, it cannot see changes to them.
|
|
180
195
|
- The commands in `commands.md` run with nobody watching and must exit non zero on failure.
|
|
181
196
|
|
|
@@ -240,13 +255,14 @@ The commands in `commands.md` run through `bash -c` on Linux and macOS and throu
|
|
|
240
255
|
| `FLOW_TAKEOVER` | unset | `1` lets `start` take a feature over from a session that is gone |
|
|
241
256
|
| `FLOW_MAX_RETRIES` | `2` | builds or reviews per step before the run stops |
|
|
242
257
|
| `FLOW_MAX_REVIEW_ROUNDS` | `3` | times a ticket may be sent back before the run stops |
|
|
243
|
-
| `FLOW_GATE` | `on` | `off` skips Build, Test and Lint after each ticket |
|
|
258
|
+
| `FLOW_GATE` | `on` | `off` skips Build, Test and Lint after each ticket, and also the Done when and test-first checks |
|
|
259
|
+
| `FLOW_GATE_TIMEOUT` | `30` | minutes each gate command and the smoke test may run; `0` means no limit. A failed smoke test writes `.feature-flow/state/<feature>.smoke.log` |
|
|
244
260
|
| `FLOW_SMOKE` | the `Smoke:` line of `commands.md` | command run before every ticket; the run stops if it fails |
|
|
245
261
|
| `FLOW_DIR`, `FLOW_TICKETS` | `plans`, `tasks` | where the plans and the ticket folder live |
|
|
246
262
|
| `FLOW_NO_OPEN` | unset | `1` makes `flow-view.py` never open a browser |
|
|
247
263
|
| `FLOW_WATCH_SECONDS` | `3` | how often `flow-view.py --watch` rewrites the page |
|
|
248
264
|
|
|
249
|
-
The run also stops on its own after a number of
|
|
265
|
+
The run also stops on its own after a number of phases (`BUILD` and `REVIEW` hand-outs, the spec review and the quality review each counting as one) that grows with the ticket count, recomputed at every pick, so a loop of failures cannot go on forever.
|
|
250
266
|
|
|
251
267
|
## What is tested
|
|
252
268
|
|
|
@@ -273,7 +289,7 @@ bash tests/run.sh
|
|
|
273
289
|
python3 -m unittest discover -s tests/py
|
|
274
290
|
```
|
|
275
291
|
|
|
276
|
-
Offline and free: no model is called. The suite covers the conductor's every answer and how a run stops, sessions, handoff and takeover, a `.git` the agent cannot write, the gate, the floor guard, plan protection, the prompts and guides, both skills, the installer for both agents, the graph page and its data, and the acceptance harness. The page's layout and replay logic are also tested under Node (`tests/viewer-logic.test.js`, skipped when Node is absent). `.github/workflows/tests.yml` runs it on every push to `main` and every pull request, on Ubuntu
|
|
292
|
+
Offline and free: no model is called. The suite covers the conductor's every answer and how a run stops, sessions, handoff and takeover, a `.git` the agent cannot write, the gate, the floor guard, plan protection, the prompts and guides, both skills, the installer for both agents, the graph page and its data, the Done when checks, the test-first check, the two reviews and the debugging guide on a send-back, and the acceptance harness. `tests/run.sh` also drives a prepared demo with no model through both checks and both reviews. The page's layout and replay logic are also tested under Node (`tests/viewer-logic.test.js`, skipped when Node is absent). `.github/workflows/tests.yml` runs it on every push to `main` and every pull request, on Ubuntu and on macOS. A fourth job runs the Python unit tests on Windows, including a fixture drive that takes a two-ticket plan through `python scripts/flow.py f start` and `next` to `BUILD` and then `REVIEW`. `tests/run.sh` itself is a bash harness and does not run on Windows. Windows is tested this way only: no real agent run has been done there.
|
|
277
293
|
|
|
278
294
|
A `package` job builds the wheel and the sdist and runs `tests/package_smoke.py` on Ubuntu (Python 3.9 and the latest), macOS and Windows: it installs the wheel in a fresh virtual environment, runs `feature-flow install`, and checks that the repo gets the same files as from `install.py` in a clone. Run it locally with `python3 -m pip install build && python3 tests/package_smoke.py`.
|
|
279
295
|
|
|
@@ -304,7 +320,7 @@ git tag v0.1.1 && git push origin v0.1.1
|
|
|
304
320
|
|
|
305
321
|
## Caution
|
|
306
322
|
|
|
307
|
-
The builder subagent has edit and shell access and commits after each ticket. Each ticket costs at least two subagents. Build on a branch you can throw away. Ignore build output in `.gitignore`, because the conductor stops if the tree is dirty after a ticket. The gate runs
|
|
323
|
+
The builder subagent has edit and shell access and commits after each ticket. Each ticket costs at least two subagents. Build on a branch you can throw away. Ignore build output in `.gitignore`, because the conductor stops if the tree is dirty after a ticket. The gate and the smoke test stop after 30 minutes (`FLOW_GATE_TIMEOUT`). The conductor also checks that the code it runs (the `feature_flow` package, `scripts/*.py` and the build and review role and guide files) did not change during a build or review; if it did, the run stops with `STOP flow code changed while building <ticket>: <files>`, and `Floor: allow flow-edit` on the ticket allows it for the build. That check is a tripwire, not a sandbox. The floor guard is pattern matching: it can miss things and it can raise false alarms, and `Floor: allow` is the release valve. The run stops on its own when a ticket stays unresolved, when the smoke test or the gate keeps failing, when a ticket keeps failing review, or when it reaches its phase limit.
|
|
308
324
|
|
|
309
325
|
## License
|
|
310
326
|
|
|
@@ -43,14 +43,14 @@ Then, in your repo, in the agent:
|
|
|
43
43
|
| | Claude Code | Codex |
|
|
44
44
|
|---|---|---|
|
|
45
45
|
| Plan a feature (no plan yet) | `/feature-flow csv-export` | `$feature-flow csv-export` |
|
|
46
|
-
| Plan what you just talked through | `/feature-flow` | `$feature-flow` |
|
|
46
|
+
| Plan and build what you just talked through | `/feature-flow` | `$feature-flow` |
|
|
47
47
|
| Build its tickets (a plan exists) | `/feature-flow csv-export` | `$feature-flow csv-export` |
|
|
48
48
|
| The same, asking nothing | `/feature-flow csv-export auto` | `$feature-flow csv-export auto` |
|
|
49
49
|
| Watch the graph, animated | `/feature-flow csv-export show` | `$feature-flow csv-export show` |
|
|
50
50
|
|
|
51
|
-
The same command plans when `plans/csv-export/` does not exist yet and builds when it does.
|
|
51
|
+
The same command plans when `plans/csv-export/` does not exist yet and builds when it does. After your second yes it commits the new plan folder, and only that folder, then goes straight on to building it; say so if you want it to stop after planning.
|
|
52
52
|
|
|
53
|
-
**Planning asks you twice.** First the session shows a short feature brief, written from your conversation (what, why, scope, the bar, and anything it had to assume), and waits for yes, edit or cancel. Then a fresh `feature-planner` subagent, given only that brief, reads the code, looks up outside docs where the code cannot answer, and writes a draft plan into the git-ignored `.feature-flow/state/draft/`. A fresh `plan-reviewer` checks the draft against the brief. The session shows you the goal, the commands, the ticket graph, what was dropped and the review result, and waits for a second yes. Only then does `flow.py plan-accept` copy the draft into `plans/<feature
|
|
53
|
+
**Planning asks you twice.** First the session shows a short feature brief, written from your conversation (what, why, scope, the bar, and anything it had to assume), and waits for yes, edit or cancel. Then a fresh `feature-planner` subagent, given only that brief, reads the code, looks up outside docs where the code cannot answer, and writes a draft plan into the git-ignored `.feature-flow/state/draft/`. A fresh `plan-reviewer` checks the draft against the brief. The session shows you the goal, the commands, the ticket graph, what was dropped and the review result, and waits for a second yes. Only then does `flow.py plan-accept` copy the draft into `plans/<feature>/`, and the session commits it and starts the build.
|
|
54
54
|
|
|
55
55
|
Try the graph first, with no setup and no agent: `bash examples/demo.sh --open`.
|
|
56
56
|
|
|
@@ -85,16 +85,26 @@ The Claude Code skill is `skills/feature-flow/`. The Codex skill is written by h
|
|
|
85
85
|
plan touched beyond own Status + Answer + appended lines? │
|
|
86
86
|
│ clean │
|
|
87
87
|
▼ │
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
Done when checks (script): runs every ```check block fail ─┤
|
|
89
|
+
test-first check (script): Test first: yes needs a test-only │
|
|
90
|
+
commit that failed Test before the code fail ─┤
|
|
91
|
+
│ pass │
|
|
92
|
+
▼ │
|
|
93
|
+
spec review: fresh reviewer subagent, sees only the ticket │
|
|
94
|
+
+ diff, re-runs every Done when FAIL ─┤
|
|
95
|
+
│ PASS │
|
|
96
|
+
▼ │
|
|
97
|
+
quality review: a second fresh reviewer subagent FAIL ─┤
|
|
90
98
|
│ PASS ▼
|
|
91
99
|
▼ findings written into the ticket,
|
|
92
|
-
next ticket reopened, built again (3 rounds max)
|
|
100
|
+
next ticket reopened, built again (3 rounds max);
|
|
101
|
+
the builder's prompt then carries
|
|
102
|
+
a root-cause debugging guide
|
|
93
103
|
```
|
|
94
104
|
|
|
95
105
|
`scripts/flow.py` is the conductor. The session asks it `next`, and it answers with one line: `BUILD`, `REVIEW`, `DONE`, `STOP` or `HANDOFF`. It prints the full prompt for each subagent from runtime-neutral guides, so the session never writes one itself.
|
|
96
106
|
|
|
97
|
-
**What the script checks and what it trusts.** The conductor verifies the build from the repo: the ticket file must say `resolved`, the tree must be clean and committed, and it runs the gate and the floor guard itself. It also checks that the reviewer changed no tracked file and made no commit. The review
|
|
107
|
+
**What the script checks and what it trusts.** The conductor verifies the build from the repo: the ticket file must say `resolved`, the tree must be clean and committed, and it runs the gate and the floor guard itself. It also runs the `check` block under each ticket's Done when and compares the exit code and printed text, and for a `Test first: yes` ticket it proves the red test commit itself: it checks out the test-only commit, runs Test, and requires it to fail before the code. A failure of either goes back to the builder before any reviewer is started. It also checks that the reviewer changed no tracked file and made no commit. The review verdicts are different: each reviewer's reply (spec review, then quality review) is relayed by the session, which saves it to a file and hands it to `flow.py verdict`. The script reads `REVIEW: PASS` or `REVIEW: FAIL` from that reply; it cannot prove the session passed it on unedited. A ticket moves on only after two PASS verdicts.
|
|
98
108
|
|
|
99
109
|
## The skill and the roles
|
|
100
110
|
|
|
@@ -134,7 +144,7 @@ plans/
|
|
|
134
144
|
|
|
135
145
|
One markdown file per ticket in `plans/<feature>/tasks/NN-slug.md`:
|
|
136
146
|
|
|
137
|
-
|
|
147
|
+
````
|
|
138
148
|
# Add the retry policy to the job runner
|
|
139
149
|
|
|
140
150
|
Type: task task | settle | convert
|
|
@@ -147,16 +157,21 @@ Floor: allow config optional, only when a human decides the guard may let
|
|
|
147
157
|
|
|
148
158
|
## Not in this ticket
|
|
149
159
|
## Done when commands and the results they print
|
|
160
|
+
```check
|
|
161
|
+
$ python3 -m unittest discover -s tests/py
|
|
162
|
+
exit 0
|
|
163
|
+
prints OK
|
|
164
|
+
```
|
|
150
165
|
## Reference
|
|
151
166
|
## Answer Built, Proof, Decisions, Shortcuts taken, Review fixes, For later tickets
|
|
152
|
-
|
|
167
|
+
````
|
|
153
168
|
|
|
154
169
|
- **Order comes from `Blocked by`**, not from numbers. A ticket is *ready* when it is open and every ticket it is blocked by is resolved. Lowest number wins.
|
|
155
170
|
- **Status lives only in the ticket files.** `map.md` never repeats it.
|
|
156
171
|
- **The Answers are the handoff between subagents.** The next builder reads the Answers of the tickets it depends on.
|
|
157
172
|
- `settle` tickets record a decision. `convert` tickets migrate something and must be safe to run twice.
|
|
158
173
|
- **A worker may change only** its own Status line and Answer, lines appended to `map.md` and `learnings.md`, and the code the ticket calls for. Rewriting its own Done when, editing another ticket, or touching `commands.md` fails the guard.
|
|
159
|
-
- `Floor:` categories: `skip`, `suppress`, `empty-catch`, `test-delete`, `threshold`, `config`, `ticket-edit`, `commands-edit`. The line is read from the ticket as it was before the work started, so a worker cannot excuse itself.
|
|
174
|
+
- `Floor:` categories: `skip`, `suppress`, `empty-catch`, `test-delete`, `threshold`, `config`, `ticket-edit`, `commands-edit`, and `flow-edit` (allows the conductor's code check below, for the build only). `skip` covers skipped, expected-fail and focused tests (`.only(`, `fit(`, `fdescribe(` on test paths); `config` covers more lint and test config files, the lint and test sections of `pyproject.toml` and `setup.cfg`, scripts and test keys in `package.json`, new skip or xfail words in `conftest.py`, and a `Makefile` only when `commands.md` uses `make`. A test file that loses assertions is shown to the reviewer as a warning, not a finding. The line is read from the ticket as it was before the work started, so a worker cannot excuse itself.
|
|
160
175
|
- The guard needs the plan to be tracked by git. If you keep tickets in an ignored folder, it cannot see changes to them.
|
|
161
176
|
- The commands in `commands.md` run with nobody watching and must exit non zero on failure.
|
|
162
177
|
|
|
@@ -221,13 +236,14 @@ The commands in `commands.md` run through `bash -c` on Linux and macOS and throu
|
|
|
221
236
|
| `FLOW_TAKEOVER` | unset | `1` lets `start` take a feature over from a session that is gone |
|
|
222
237
|
| `FLOW_MAX_RETRIES` | `2` | builds or reviews per step before the run stops |
|
|
223
238
|
| `FLOW_MAX_REVIEW_ROUNDS` | `3` | times a ticket may be sent back before the run stops |
|
|
224
|
-
| `FLOW_GATE` | `on` | `off` skips Build, Test and Lint after each ticket |
|
|
239
|
+
| `FLOW_GATE` | `on` | `off` skips Build, Test and Lint after each ticket, and also the Done when and test-first checks |
|
|
240
|
+
| `FLOW_GATE_TIMEOUT` | `30` | minutes each gate command and the smoke test may run; `0` means no limit. A failed smoke test writes `.feature-flow/state/<feature>.smoke.log` |
|
|
225
241
|
| `FLOW_SMOKE` | the `Smoke:` line of `commands.md` | command run before every ticket; the run stops if it fails |
|
|
226
242
|
| `FLOW_DIR`, `FLOW_TICKETS` | `plans`, `tasks` | where the plans and the ticket folder live |
|
|
227
243
|
| `FLOW_NO_OPEN` | unset | `1` makes `flow-view.py` never open a browser |
|
|
228
244
|
| `FLOW_WATCH_SECONDS` | `3` | how often `flow-view.py --watch` rewrites the page |
|
|
229
245
|
|
|
230
|
-
The run also stops on its own after a number of
|
|
246
|
+
The run also stops on its own after a number of phases (`BUILD` and `REVIEW` hand-outs, the spec review and the quality review each counting as one) that grows with the ticket count, recomputed at every pick, so a loop of failures cannot go on forever.
|
|
231
247
|
|
|
232
248
|
## What is tested
|
|
233
249
|
|
|
@@ -254,7 +270,7 @@ bash tests/run.sh
|
|
|
254
270
|
python3 -m unittest discover -s tests/py
|
|
255
271
|
```
|
|
256
272
|
|
|
257
|
-
Offline and free: no model is called. The suite covers the conductor's every answer and how a run stops, sessions, handoff and takeover, a `.git` the agent cannot write, the gate, the floor guard, plan protection, the prompts and guides, both skills, the installer for both agents, the graph page and its data, and the acceptance harness. The page's layout and replay logic are also tested under Node (`tests/viewer-logic.test.js`, skipped when Node is absent). `.github/workflows/tests.yml` runs it on every push to `main` and every pull request, on Ubuntu
|
|
273
|
+
Offline and free: no model is called. The suite covers the conductor's every answer and how a run stops, sessions, handoff and takeover, a `.git` the agent cannot write, the gate, the floor guard, plan protection, the prompts and guides, both skills, the installer for both agents, the graph page and its data, the Done when checks, the test-first check, the two reviews and the debugging guide on a send-back, and the acceptance harness. `tests/run.sh` also drives a prepared demo with no model through both checks and both reviews. The page's layout and replay logic are also tested under Node (`tests/viewer-logic.test.js`, skipped when Node is absent). `.github/workflows/tests.yml` runs it on every push to `main` and every pull request, on Ubuntu and on macOS. A fourth job runs the Python unit tests on Windows, including a fixture drive that takes a two-ticket plan through `python scripts/flow.py f start` and `next` to `BUILD` and then `REVIEW`. `tests/run.sh` itself is a bash harness and does not run on Windows. Windows is tested this way only: no real agent run has been done there.
|
|
258
274
|
|
|
259
275
|
A `package` job builds the wheel and the sdist and runs `tests/package_smoke.py` on Ubuntu (Python 3.9 and the latest), macOS and Windows: it installs the wheel in a fresh virtual environment, runs `feature-flow install`, and checks that the repo gets the same files as from `install.py` in a clone. Run it locally with `python3 -m pip install build && python3 tests/package_smoke.py`.
|
|
260
276
|
|
|
@@ -285,7 +301,7 @@ git tag v0.1.1 && git push origin v0.1.1
|
|
|
285
301
|
|
|
286
302
|
## Caution
|
|
287
303
|
|
|
288
|
-
The builder subagent has edit and shell access and commits after each ticket. Each ticket costs at least two subagents. Build on a branch you can throw away. Ignore build output in `.gitignore`, because the conductor stops if the tree is dirty after a ticket. The gate runs
|
|
304
|
+
The builder subagent has edit and shell access and commits after each ticket. Each ticket costs at least two subagents. Build on a branch you can throw away. Ignore build output in `.gitignore`, because the conductor stops if the tree is dirty after a ticket. The gate and the smoke test stop after 30 minutes (`FLOW_GATE_TIMEOUT`). The conductor also checks that the code it runs (the `feature_flow` package, `scripts/*.py` and the build and review role and guide files) did not change during a build or review; if it did, the run stops with `STOP flow code changed while building <ticket>: <files>`, and `Floor: allow flow-edit` on the ticket allows it for the build. That check is a tripwire, not a sandbox. The floor guard is pattern matching: it can miss things and it can raise false alarms, and `Floor: allow` is the release valve. The run stops on its own when a ticket stays unresolved, when the smoke test or the gate keeps failing, when a ticket keeps failing review, or when it reaches its phase limit.
|
|
289
305
|
|
|
290
306
|
## License
|
|
291
307
|
|
|
@@ -23,7 +23,7 @@ When `plans/<feature>/` does not exist, or you are planning from the conversatio
|
|
|
23
23
|
- `plan-review-prompt`: start a new reviewer the same way, with `task_name="plan_reviewer"` and `fork_turns="none"`, never with the planner's reply. Get its final reply with `wait_agent` and save it with the shell into `.feature-flow/state/plan-review-<feature>.txt`.
|
|
24
24
|
- `plan-accept`: writes `plans/<feature>/` from the draft, only after the second yes.
|
|
25
25
|
|
|
26
|
-
Then run `python3 scripts/flow-status.py <feature> --check
|
|
26
|
+
Then run `python3 scripts/flow-status.py <feature> --check` and list the files `plan-accept` wrote. The second yes also approves committing the plan, so commit the folder `plan-accept` printed (`OK <folder>`, `plans/<feature>` unless `FLOW_DIR` is set) and nothing else: `git add -- <folder> && git commit -m "docs(<feature>): plan" -- <folder>`. If the commit fails, report why and stop. Then go straight on to building it below, unless the user asked to stop after planning.
|
|
27
27
|
|
|
28
28
|
To add tickets to a plan that exists, edit it by hand following the ticket rules in `.feature-flow/guides/plan.md`.
|
|
29
29
|
|
|
@@ -41,14 +41,14 @@ If `start` prints `STOP` naming another owner, another session may still be work
|
|
|
41
41
|
|
|
42
42
|
If the user says the flow is stuck on a ticket or asks for a reset, run `reset` (or `reset <NN>` to redo one ticket): it reopens a half-built ticket, commits that, and clears the run, then run `start` again. It needs `FLOW_TAKEOVER=1` while a session owns the feature, on the same clear yes. Never reset on your own.
|
|
43
43
|
|
|
44
|
-
Say what will happen: for each ticket, a builder subagent and then a reviewer subagent. After every few tickets (`FLOW_TICKETS_PER_SESSION`, default 4) you hand off to a new session. Ask for a yes, unless `auto` was given.
|
|
44
|
+
Say what will happen: for each ticket, a builder subagent and then a reviewer subagent. After every few tickets (`FLOW_TICKETS_PER_SESSION`, default 4) you hand off to a new session. Ask for a yes, unless `auto` was given or you came straight from planning: the plan's yes already covered the build.
|
|
45
45
|
|
|
46
46
|
## The loop
|
|
47
47
|
|
|
48
48
|
Run `next`, act on its one line, and repeat:
|
|
49
49
|
|
|
50
|
-
- `BUILD <ticket> <NN> <sha>`: run `prompt`. Start the builder with `spawn_agent(task_name="
|
|
51
|
-
- `REVIEW <ticket> <NN> <sha>`: run `prompt`. Start a new reviewer the same way, with `
|
|
50
|
+
- `BUILD <ticket> <NN> <sha>`: run `prompt`. Start the builder with `spawn_agent(task_name="ticket_builder_<NN>_<k>", fork_turns="none", message=...)`. The message is "Work only in <repo>." followed by the whole `prompt` output, which already starts with the builder role. Use a new `task_name` for every spawn: keep a count per ticket in this session, and `<k>` is the number of builders already started for this ticket, plus 1 (1 for the first, then up each time `next` sends the ticket back to a builder). Get its final reply with `wait_agent`, then run `next`.
|
|
51
|
+
- `REVIEW <ticket> <NN> <sha>`: run `prompt`. Start a new reviewer the same way, with `fork_turns="none"` so it never sees the builder's context, and a unique `task_name` per spawn: `ticket_reviewer_<NN>_<pass>_<k>`, where `<pass>` is `spec` or `quality` by the pass the prompt names (`(spec pass)` or `(quality pass)`; `review` if it names none) and `<k>` is the number of reviews already started for this ticket in this session, plus 1 (1 for the first). Count every review, also one that ended in `RETRY` or a failed review followed by a rebuild, so a repeated pass never reuses a name. Get its final reply with `wait_agent`. A child cannot be made read-only, so the reviewer works from its instructions; the conductor catches any edit it makes. Save the whole reply with the shell: `cat > .feature-flow/state/flow-review-<feature>.txt <<'EOF'` ... `EOF`. Then run `verdict .feature-flow/state/flow-review-<feature>.txt` and `next`. If `verdict` prints `RETRY`, just run `next`.
|
|
52
52
|
- `DONE <summary>`: report it and suggest `$feature-flow <feature> show`.
|
|
53
53
|
- `STOP <reason>`: report the reason, run `python3 scripts/flow-status.py <feature>`, show the table, and stop.
|
|
54
54
|
- `HANDOFF <line>`: stop here. Tell the user to open a new session and type exactly `<line>`. The new session resumes where this one stopped.
|
|
@@ -62,7 +62,7 @@ A conductor command looks like this:
|
|
|
62
62
|
These apply from `start` on, once a plan exists.
|
|
63
63
|
|
|
64
64
|
- Never run the gate, the floor guard or a review yourself. The conductor runs the checks, and the reviewer subagent reviews.
|
|
65
|
-
- Never edit a ticket, the plan or the code, and never commit. The builder does that.
|
|
65
|
+
- Never edit a ticket, the plan or the code, and never commit, apart from the approved plan above. The builder does that.
|
|
66
66
|
- Never skip a step, reorder steps, or decide the next step yourself. Only `next` decides.
|
|
67
67
|
- Never read a ticket's `## Answer` into the reviewer's prompt. `prompt` already holds everything it needs.
|
|
68
68
|
- Tell the user one short line per phase (`01 built`, `01 review: PASS`), not the subagents' reports.
|
|
@@ -1,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,65 @@
|
|
|
1
|
+
"""A fingerprint of the code and prompt files a conductor run uses, so a change during a build shows."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from feature_flow import git, prompts
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _files(scripts):
|
|
12
|
+
package = Path(__file__).resolve().parent
|
|
13
|
+
found = [p for p in package.rglob("*.py") if "__pycache__" not in p.parts]
|
|
14
|
+
found += Path(scripts).resolve().glob("*.py")
|
|
15
|
+
for phase in ("build", "review", "review-quality"):
|
|
16
|
+
for folder, names in (("agents", prompts.ROLES), ("guides", prompts.GUIDES)):
|
|
17
|
+
try:
|
|
18
|
+
found.append(prompts.find_file(scripts, folder, names[phase]).resolve())
|
|
19
|
+
except FileNotFoundError:
|
|
20
|
+
pass
|
|
21
|
+
try:
|
|
22
|
+
found.append(prompts.find_file(scripts, "guides", "debug.md").resolve())
|
|
23
|
+
except FileNotFoundError:
|
|
24
|
+
pass
|
|
25
|
+
return sorted(set(found))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _name(path, top):
|
|
29
|
+
try:
|
|
30
|
+
return path.relative_to(top).as_posix()
|
|
31
|
+
except ValueError:
|
|
32
|
+
return path.as_posix()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def flow_code(scripts):
|
|
36
|
+
"""(the sha256 over every file's path and bytes, {path relative to the repo: sha256 of the file})."""
|
|
37
|
+
top = git.toplevel()
|
|
38
|
+
top = top.resolve() if top is not None else Path(os.getcwd()).resolve()
|
|
39
|
+
each = {}
|
|
40
|
+
for path in _files(scripts):
|
|
41
|
+
try:
|
|
42
|
+
each[_name(path, top)] = hashlib.sha256(path.read_bytes()).hexdigest()
|
|
43
|
+
except OSError:
|
|
44
|
+
each[_name(path, top)] = ""
|
|
45
|
+
whole = hashlib.sha256()
|
|
46
|
+
for name in sorted(each):
|
|
47
|
+
whole.update(name.encode("utf-8") + b"\0" + each[name].encode("ascii") + b"\0")
|
|
48
|
+
return whole.hexdigest(), each
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def changed(before, now):
|
|
52
|
+
"""The names that differ between two {name: sha256} maps, sorted."""
|
|
53
|
+
return sorted(name for name in set(before) | set(now) if before.get(name) != now.get(name))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def dump(each):
|
|
57
|
+
return json.dumps(each, sort_keys=True)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def load(text):
|
|
61
|
+
try:
|
|
62
|
+
value = json.loads(text)
|
|
63
|
+
except ValueError:
|
|
64
|
+
return {}
|
|
65
|
+
return value if isinstance(value, dict) else {}
|