feature-flow-cli 0.5.0__tar.gz → 0.8.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.5.0/feature_flow_cli.egg-info → feature_flow_cli-0.8.0}/PKG-INFO +35 -9
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/README.md +34 -8
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/adapters/codex/feature-flow/SKILL.md +5 -5
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow/__init__.py +1 -1
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow/checks.py +3 -3
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow/cli.py +1 -1
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow/codehash.py +1 -1
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow/command.py +6 -2
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow/conductor.py +49 -8
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow/floorguard.py +11 -2
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow/gate.py +6 -2
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow/git.py +18 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow/prompts.py +4 -2
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow/proof.py +10 -0
- feature_flow_cli-0.8.0/feature_flow/report.py +89 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow/status.py +55 -9
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow/tickets.py +4 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0/feature_flow_cli.egg-info}/PKG-INFO +35 -9
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow_cli.egg-info/SOURCES.txt +2 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/guides/plan-review.md +2 -2
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/guides/plan.md +6 -2
- feature_flow_cli-0.8.0/guides/review-single.md +87 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/guides/templates/commands.md +4 -1
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/guides/templates/ticket.md +2 -1
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/scripts/flow-trust.py +1 -1
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/skills/feature-flow/SKILL.md +4 -4
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/LICENSE +0 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/MANIFEST.in +0 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/adapters/codex/feature-flow/agents/openai.yaml +0 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/agents/feature-planner.md +0 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/agents/plan-reviewer.md +0 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/agents/ticket-builder.md +0 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/agents/ticket-reviewer.md +0 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow/__main__.py +0 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow/install.py +0 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow/proc.py +0 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow/released.py +0 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow/state.py +0 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow/suggest.py +0 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow/uninstall.py +0 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow/view.py +0 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow_cli.egg-info/dependency_links.txt +0 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow_cli.egg-info/entry_points.txt +0 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/feature_flow_cli.egg-info/top_level.txt +0 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/guides/brief.md +0 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/guides/build.md +0 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/guides/debug.md +0 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/guides/review-quality.md +0 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/guides/review.md +0 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/guides/show.md +0 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/guides/templates/learnings.md +0 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/guides/templates/map.md +0 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/guides/templates/spec.md +0 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/guides/templates/ui-mockup.md +0 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/install.py +0 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/install.sh +0 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/pyproject.toml +0 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/scripts/floor-guard.py +0 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/scripts/flow-status.py +0 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/scripts/flow-view.html +0 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/scripts/flow-view.py +0 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/scripts/flow.py +0 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/scripts/gate.py +0 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/setup.cfg +0 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.0}/skills/architect-review/SKILL.md +0 -0
- {feature_flow_cli-0.5.0 → feature_flow_cli-0.8.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.8.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
|
|
@@ -25,10 +25,10 @@ Dynamic: license-file
|
|
|
25
25
|
|
|
26
26
|
Plan a feature as a graph of small tickets, then build them in the session you are already in. One command, `/feature-flow`, does both. Every ticket is built by one fresh subagent and checked by another that never saw the builder's reasoning. A script, not the agent, decides the order of the steps, runs the build and tests, and watches for work that weakens the checks, and the whole run has limits.
|
|
27
27
|
|
|
28
|
-
One skill,
|
|
28
|
+
One skill, 4 agent roles, a Python conductor, 6 Python scripts (`flow`, `flow-trust`, `flow-status`, `flow-view`, `gate`, `floor-guard`), an installer for both agents, a demo and a test suite. All of it is Python (standard library only), so it runs on Linux, macOS and Windows. MIT licensed.
|
|
29
29
|
|
|
30
30
|
- [Quick start](#quick-start) · [How a ticket flows](#how-a-ticket-flows) · [The skill and the roles](#the-skill-and-the-roles) · [Plans and tickets](#plans-and-tickets)
|
|
31
|
-
- [See the graph](#see-the-graph) · [Long features and handoff](#long-features-and-handoff) · [What is tested](#what-is-tested) · [Upgrading](#upgrading) · [Caution](#caution)
|
|
31
|
+
- [See the graph](#see-the-graph) · [Long features and handoff](#long-features-and-handoff) · [What is tested](#what-is-tested) · [Limits](#limits) · [Upgrading](#upgrading) · [Caution](#caution)
|
|
32
32
|
|
|
33
33
|
## Quick start
|
|
34
34
|
|
|
@@ -51,7 +51,7 @@ Without a clone, the same installer runs from [PyPI](https://pypi.org/project/fe
|
|
|
51
51
|
uvx feature-flow-cli install /path/to/your/repo --agent all # or: pipx run feature-flow-cli install ...
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
The package puts a `feature-flow` command on your PATH with `install`, `status <feature>` and `
|
|
54
|
+
The package puts a `feature-flow` command on your PATH with `install`, `uninstall`, `status <feature>`, `view <feature>`, `reset <feature>` and `report <feature>`. It installs the same files, byte for byte, as `install.py` from a clone.
|
|
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
|
|
|
@@ -71,7 +71,7 @@ Then, in your repo, in the agent:
|
|
|
71
71
|
|
|
72
72
|
The same command plans when `plans/csv-export/` does not exist yet and builds when it does. After your second yes it commits the new plan folder, and only that folder, then goes straight on to building it; say so if you want it to stop after planning.
|
|
73
73
|
|
|
74
|
-
**Planning asks you twice.** First the session shows a short feature brief, written from your conversation (what, why, scope, the bar, and anything it had to assume), and waits for yes, edit or cancel. Then a fresh `feature-planner` subagent, given only that brief, reads the code, looks up outside docs where the code cannot answer, and writes a draft plan into the git-ignored `.feature-flow/state/draft/`. A fresh `plan-reviewer` checks the draft against the brief. The session shows you the goal, the commands, the ticket graph, what was dropped and the review result, and waits for a second yes. Only then does `flow.py plan-accept` copy the draft into `plans/<feature>/`, and the session commits it and starts the build.
|
|
74
|
+
**Planning asks you twice.** First the session shows a short feature brief, written from your conversation (what, why, scope, the bar, and anything it had to assume), and waits for yes, edit or cancel. Then a fresh `feature-planner` subagent, given only that brief, reads the code, looks up outside docs where the code cannot answer, and writes a draft plan into the git-ignored `.feature-flow/state/draft/`. The planner folds changes too small to stand alone into the ticket they belong to, and sets `Review: full` on a ticket that deserves both reviews whatever its size. A fresh `plan-reviewer` checks the draft against the brief. The session shows you the goal, the commands, the ticket graph, what was dropped and the review result, and waits for a second yes. Only then does `flow.py plan-accept` copy the draft into `plans/<feature>/`, and the session commits it and starts the build.
|
|
75
75
|
|
|
76
76
|
Try the graph first, with no setup and no agent: `bash examples/demo.sh --open`.
|
|
77
77
|
|
|
@@ -121,6 +121,8 @@ The Claude Code skill is `skills/feature-flow/`. The Codex skill is written by h
|
|
|
121
121
|
│ PASS │
|
|
122
122
|
▼ │
|
|
123
123
|
quality review: a second fresh reviewer subagent FAIL ─┤
|
|
124
|
+
(a small ticket whose check block passed gets one combined
|
|
125
|
+
review instead of these two; see below)
|
|
124
126
|
│ PASS ▼
|
|
125
127
|
▼ findings written into the ticket,
|
|
126
128
|
next ticket reopened, built again (3 rounds max);
|
|
@@ -130,7 +132,11 @@ The Claude Code skill is `skills/feature-flow/`. The Codex skill is written by h
|
|
|
130
132
|
|
|
131
133
|
`scripts/flow.py` is the conductor. The session asks it `next`, and it answers with one line: `BUILD`, `REVIEW`, `DONE`, `STOP` or `HANDOFF`. It prints the full prompt for each subagent from runtime-neutral guides, so the session never writes one itself.
|
|
132
134
|
|
|
133
|
-
**What the script checks and what it trusts.** The conductor verifies the build from the repo: the ticket file must say `resolved`, the tree must be clean and committed, and it runs the gate and the floor guard itself. It also runs the `check` block under each ticket's Done when and compares the exit code and printed text, and for a `Test first: yes` ticket it proves the red test commit itself: it checks out the test-only commit, runs Test, and requires it to fail before the code. A failure of either goes back to the builder before any reviewer is started. It also checks that the reviewer changed no tracked file and made no commit. The review verdicts are different: each reviewer's reply (spec review, then quality review) is relayed by the session, which saves it to a file and hands it to `flow.py verdict`. The script reads `REVIEW: PASS` or `REVIEW: FAIL` from that reply; it cannot prove the session passed it on unedited. A ticket moves on only after two PASS verdicts.
|
|
135
|
+
**What the script checks and what it trusts.** The conductor verifies the build from the repo: the ticket file must say `resolved`, the tree must be clean and committed, and it runs the gate and the floor guard itself. It also runs the `check` block under each ticket's Done when and compares the exit code and printed text, and for a `Test first: yes` ticket it proves the red test commit itself: it checks out the test-only commit, runs Test, and requires it to fail before the code. A failure of either goes back to the builder before any reviewer is started. It also checks that the reviewer changed no tracked file and made no commit. The review verdicts are different: each reviewer's reply (spec review, then quality review) is relayed by the session, which saves it to a file and hands it to `flow.py verdict`. The script reads `REVIEW: PASS` or `REVIEW: FAIL` from that reply; it cannot prove the session passed it on unedited. A ticket moves on only after two PASS verdicts, or one for a ticket the conductor judged small. The size is measured from the repo (`git diff` against the commit the ticket started from), never taken from the builder.
|
|
136
|
+
|
|
137
|
+
**The gate: full or quick.** The gate runs Build, Test and Lint after every ticket. With an optional `Quick:` line in `commands.md` (a faster subset of `Test`), it runs Build, Quick and Lint after the tickets in the middle of a plan, and the full Build, Test and Lint after the last ticket, before a handoff, and under `FLOW_GATE_FULL=1`. A plan without `Quick:` runs the full gate every time. The test-first proof still runs the full `Test` command.
|
|
138
|
+
|
|
139
|
+
**One review for a small ticket.** A ticket whose built diff is 3 files and 60 changed lines at most (the plan folder left out), and whose `check` block ran and passed, gets one combined review instead of the spec and quality reviews. The conductor measures the diff itself. `Review: full` in the ticket (as it was at the start), `Type: convert`, no `check` block, or `FLOW_GATE=off` keep both reviews. Every other conductor check still runs on every ticket.
|
|
134
140
|
|
|
135
141
|
## The skill and the roles
|
|
136
142
|
|
|
@@ -197,14 +203,18 @@ prints OK
|
|
|
197
203
|
- **The Answers are the handoff between subagents.** The next builder reads the Answers of the tickets it depends on.
|
|
198
204
|
- `settle` tickets record a decision. `convert` tickets migrate something and must be safe to run twice.
|
|
199
205
|
- **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.
|
|
200
|
-
- `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.
|
|
206
|
+
- `Floor:` categories: `skip`, `suppress`, `empty-catch`, `test-delete`, `threshold`, `trivial-assert` (an assertion that cannot fail, added to a test file: `assert True`, `assertEqual(x, x)`, `expect(v).toBe(v)`), `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.
|
|
201
207
|
- The guard needs the plan to be tracked by git. If you keep tickets in an ignored folder, it cannot see changes to them.
|
|
202
208
|
- The commands in `commands.md` run with nobody watching and must exit non zero on failure.
|
|
203
209
|
|
|
204
210
|
## See the graph
|
|
205
211
|
|
|
212
|
+

|
|
213
|
+
|
|
214
|
+
*The page `flow-view` writes, shown for the demo plan from `examples/demo.sh`: resolved tickets in green, ready ones in amber, the one being worked in blue, the rest waiting. Press Replay to watch the run back from git history.*
|
|
215
|
+
|
|
206
216
|
```bash
|
|
207
|
-
python3 scripts/flow-view.py csv-export write the page and open it
|
|
217
|
+
python3 scripts/flow-view.py csv-export write the page and open it (or: feature-flow view csv-export)
|
|
208
218
|
python3 scripts/flow-view.py csv-export --watch keep it updating while tickets are built
|
|
209
219
|
bash examples/demo.sh --open try it on a made up project, no setup needed
|
|
210
220
|
```
|
|
@@ -245,6 +255,8 @@ python3 scripts/flow-status.py <feature> table of tickets and w
|
|
|
245
255
|
python3 scripts/flow-status.py <feature> --next path of the next ready ticket (exit 10 = done, 11 = stuck)
|
|
246
256
|
python3 scripts/flow-status.py <feature> --counts one line of counts
|
|
247
257
|
python3 scripts/flow-status.py <feature> --check cycles, missing blockers, missing Done when, unordered mentions
|
|
258
|
+
python3 scripts/flow-status.py <feature> --lint --check plus: a check block (or `Check: none <why>`) on every ticket, a Test command
|
|
259
|
+
feature-flow report <feature> what a run did, from its log: builds, reviews, failed checks, stops
|
|
248
260
|
python3 scripts/flow-status.py <feature> --mermaid the ticket graph, coloured by status (add "plain" for none)
|
|
249
261
|
python3 scripts/flow-status.py <feature> --json every ticket with status, blockers and readiness
|
|
250
262
|
python3 scripts/flow-view.py <feature> [--watch] the animated graph page, opened in your browser
|
|
@@ -266,13 +278,16 @@ The commands in `commands.md` run through `bash -c` on Linux and macOS and throu
|
|
|
266
278
|
| `FLOW_MAX_RETRIES` | `2` | builds or reviews per step before the run stops |
|
|
267
279
|
| `FLOW_MAX_REVIEW_ROUNDS` | `3` | times a ticket may be sent back before the run stops |
|
|
268
280
|
| `FLOW_GATE` | `on` | `off` skips Build, Test and Lint after each ticket, and also the Done when and test-first checks |
|
|
281
|
+
| `FLOW_GATE_FULL` | unset | `1` runs the full Build, Test and Lint after every ticket, even when `commands.md` has a `Quick:` line |
|
|
282
|
+
| `FLOW_SMALL_FILES` | `3` | most files a ticket may change and still get the single review |
|
|
283
|
+
| `FLOW_SMALL_LINES` | `60` | most changed lines for the single review; `0` turns the single review off |
|
|
269
284
|
| `FLOW_GATE_TIMEOUT` | `30` | minutes each gate command and the smoke test may run; `0` means no limit. A failed smoke test writes `.feature-flow/state/<feature>.smoke.log` |
|
|
270
285
|
| `FLOW_SMOKE` | the `Smoke:` line of `commands.md` | command run before every ticket; the run stops if it fails |
|
|
271
286
|
| `FLOW_DIR`, `FLOW_TICKETS` | `plans`, `tasks` | where the plans and the ticket folder live |
|
|
272
287
|
| `FLOW_NO_OPEN` | unset | `1` makes `flow-view.py` never open a browser |
|
|
273
288
|
| `FLOW_WATCH_SECONDS` | `3` | how often `flow-view.py --watch` rewrites the page |
|
|
274
289
|
|
|
275
|
-
The run also stops on its own after a number of phases (`BUILD` and `REVIEW` hand-outs, the spec review and the quality review each counting as one) that grows with the ticket count, recomputed at every pick, so a loop of failures cannot go on forever.
|
|
290
|
+
The run also stops on its own after a number of phases (`BUILD` and `REVIEW` hand-outs, the spec review and the quality review each counting as one, a single review counting as one) that grows with the ticket count, recomputed at every pick, so a loop of failures cannot go on forever.
|
|
276
291
|
|
|
277
292
|
## What is tested
|
|
278
293
|
|
|
@@ -305,6 +320,17 @@ A `package` job builds the wheel and the sdist and runs `tests/package_smoke.py`
|
|
|
305
320
|
|
|
306
321
|
`RUN_REAL=1 bash tests/smoke-real.sh --interactive` is the acceptance run with Claude Code. It runs real sessions on the demo project and spends money, which is why it asks for `RUN_REAL=1`.
|
|
307
322
|
|
|
323
|
+
## Limits
|
|
324
|
+
|
|
325
|
+
feature-flow's layers are tripwires, not a security boundary. They catch accidents and lazy shortcuts by an agent; a determined agent that controls the session could still get around them.
|
|
326
|
+
|
|
327
|
+
- **The verdict relay is trusted.** The session saves a reviewer's reply and hands it to `flow.py verdict`. The conductor reads `REVIEW: PASS` or `REVIEW: FAIL` from that file and cannot prove the session passed it on unedited.
|
|
328
|
+
- **The floor guard reads diffs with patterns.** It does not see a test made trivially true by other means (a mock that returns the expected answer, an `assert` on something that is always set, a test that no longer exercises the code), or an assertion weakened without being deleted. It does catch assertions that cannot fail (`trivial-assert`) and warns the reviewer when a test file loses assertions. A new test with no assertion at all is not caught. The red-first proof only covers tickets with `Test first: yes`.
|
|
329
|
+
- **A plan can be wrong and still pass.** The plan lint checks that every ticket has a way to be proven and that `commands.md` has a Test command. It cannot tell whether the plan builds the right thing; the second yes is yours.
|
|
330
|
+
- **The trust runner stops accidents, not the session.** It catches a builder editing the conductor mid-run. The session that runs it is the thing it cannot check; a hand-run `flow-trust.py` is unprotected (see above).
|
|
331
|
+
- **Little real-world use so far.** Almost everything has been built by feature-flow on its own repo. The Codex skill and the Codex planner have not been run by hand, and the Quick gate has only been run by tests. See [docs/field-run.md](docs/field-run.md) for how to try it on your own repo and what to send back.
|
|
332
|
+
- **One ticket at a time**, with a handoff to a new session every few tickets.
|
|
333
|
+
|
|
308
334
|
## Upgrading
|
|
309
335
|
|
|
310
336
|
Earlier versions had seven skills and an unattended loop. The five flow skills are gone, and `/feature-flow` replaces them:
|
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
|
|
7
7
|
Plan a feature as a graph of small tickets, then build them in the session you are already in. One command, `/feature-flow`, does both. Every ticket is built by one fresh subagent and checked by another that never saw the builder's reasoning. A script, not the agent, decides the order of the steps, runs the build and tests, and watches for work that weakens the checks, and the whole run has limits.
|
|
8
8
|
|
|
9
|
-
One skill,
|
|
9
|
+
One skill, 4 agent roles, a Python conductor, 6 Python scripts (`flow`, `flow-trust`, `flow-status`, `flow-view`, `gate`, `floor-guard`), an installer for both agents, a demo and a test suite. All of it is Python (standard library only), so it runs on Linux, macOS and Windows. MIT licensed.
|
|
10
10
|
|
|
11
11
|
- [Quick start](#quick-start) · [How a ticket flows](#how-a-ticket-flows) · [The skill and the roles](#the-skill-and-the-roles) · [Plans and tickets](#plans-and-tickets)
|
|
12
|
-
- [See the graph](#see-the-graph) · [Long features and handoff](#long-features-and-handoff) · [What is tested](#what-is-tested) · [Upgrading](#upgrading) · [Caution](#caution)
|
|
12
|
+
- [See the graph](#see-the-graph) · [Long features and handoff](#long-features-and-handoff) · [What is tested](#what-is-tested) · [Limits](#limits) · [Upgrading](#upgrading) · [Caution](#caution)
|
|
13
13
|
|
|
14
14
|
## Quick start
|
|
15
15
|
|
|
@@ -32,7 +32,7 @@ Without a clone, the same installer runs from [PyPI](https://pypi.org/project/fe
|
|
|
32
32
|
uvx feature-flow-cli install /path/to/your/repo --agent all # or: pipx run feature-flow-cli install ...
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
The package puts a `feature-flow` command on your PATH with `install`, `status <feature>` and `
|
|
35
|
+
The package puts a `feature-flow` command on your PATH with `install`, `uninstall`, `status <feature>`, `view <feature>`, `reset <feature>` and `report <feature>`. It installs the same files, byte for byte, as `install.py` from a clone.
|
|
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
|
|
|
@@ -52,7 +52,7 @@ Then, in your repo, in the agent:
|
|
|
52
52
|
|
|
53
53
|
The same command plans when `plans/csv-export/` does not exist yet and builds when it does. After your second yes it commits the new plan folder, and only that folder, then goes straight on to building it; say so if you want it to stop after planning.
|
|
54
54
|
|
|
55
|
-
**Planning asks you twice.** First the session shows a short feature brief, written from your conversation (what, why, scope, the bar, and anything it had to assume), and waits for yes, edit or cancel. Then a fresh `feature-planner` subagent, given only that brief, reads the code, looks up outside docs where the code cannot answer, and writes a draft plan into the git-ignored `.feature-flow/state/draft/`. A fresh `plan-reviewer` checks the draft against the brief. The session shows you the goal, the commands, the ticket graph, what was dropped and the review result, and waits for a second yes. Only then does `flow.py plan-accept` copy the draft into `plans/<feature>/`, and the session commits it and starts the build.
|
|
55
|
+
**Planning asks you twice.** First the session shows a short feature brief, written from your conversation (what, why, scope, the bar, and anything it had to assume), and waits for yes, edit or cancel. Then a fresh `feature-planner` subagent, given only that brief, reads the code, looks up outside docs where the code cannot answer, and writes a draft plan into the git-ignored `.feature-flow/state/draft/`. The planner folds changes too small to stand alone into the ticket they belong to, and sets `Review: full` on a ticket that deserves both reviews whatever its size. A fresh `plan-reviewer` checks the draft against the brief. The session shows you the goal, the commands, the ticket graph, what was dropped and the review result, and waits for a second yes. Only then does `flow.py plan-accept` copy the draft into `plans/<feature>/`, and the session commits it and starts the build.
|
|
56
56
|
|
|
57
57
|
Try the graph first, with no setup and no agent: `bash examples/demo.sh --open`.
|
|
58
58
|
|
|
@@ -102,6 +102,8 @@ The Claude Code skill is `skills/feature-flow/`. The Codex skill is written by h
|
|
|
102
102
|
│ PASS │
|
|
103
103
|
▼ │
|
|
104
104
|
quality review: a second fresh reviewer subagent FAIL ─┤
|
|
105
|
+
(a small ticket whose check block passed gets one combined
|
|
106
|
+
review instead of these two; see below)
|
|
105
107
|
│ PASS ▼
|
|
106
108
|
▼ findings written into the ticket,
|
|
107
109
|
next ticket reopened, built again (3 rounds max);
|
|
@@ -111,7 +113,11 @@ The Claude Code skill is `skills/feature-flow/`. The Codex skill is written by h
|
|
|
111
113
|
|
|
112
114
|
`scripts/flow.py` is the conductor. The session asks it `next`, and it answers with one line: `BUILD`, `REVIEW`, `DONE`, `STOP` or `HANDOFF`. It prints the full prompt for each subagent from runtime-neutral guides, so the session never writes one itself.
|
|
113
115
|
|
|
114
|
-
**What the script checks and what it trusts.** The conductor verifies the build from the repo: the ticket file must say `resolved`, the tree must be clean and committed, and it runs the gate and the floor guard itself. It also runs the `check` block under each ticket's Done when and compares the exit code and printed text, and for a `Test first: yes` ticket it proves the red test commit itself: it checks out the test-only commit, runs Test, and requires it to fail before the code. A failure of either goes back to the builder before any reviewer is started. It also checks that the reviewer changed no tracked file and made no commit. The review verdicts are different: each reviewer's reply (spec review, then quality review) is relayed by the session, which saves it to a file and hands it to `flow.py verdict`. The script reads `REVIEW: PASS` or `REVIEW: FAIL` from that reply; it cannot prove the session passed it on unedited. A ticket moves on only after two PASS verdicts.
|
|
116
|
+
**What the script checks and what it trusts.** The conductor verifies the build from the repo: the ticket file must say `resolved`, the tree must be clean and committed, and it runs the gate and the floor guard itself. It also runs the `check` block under each ticket's Done when and compares the exit code and printed text, and for a `Test first: yes` ticket it proves the red test commit itself: it checks out the test-only commit, runs Test, and requires it to fail before the code. A failure of either goes back to the builder before any reviewer is started. It also checks that the reviewer changed no tracked file and made no commit. The review verdicts are different: each reviewer's reply (spec review, then quality review) is relayed by the session, which saves it to a file and hands it to `flow.py verdict`. The script reads `REVIEW: PASS` or `REVIEW: FAIL` from that reply; it cannot prove the session passed it on unedited. A ticket moves on only after two PASS verdicts, or one for a ticket the conductor judged small. The size is measured from the repo (`git diff` against the commit the ticket started from), never taken from the builder.
|
|
117
|
+
|
|
118
|
+
**The gate: full or quick.** The gate runs Build, Test and Lint after every ticket. With an optional `Quick:` line in `commands.md` (a faster subset of `Test`), it runs Build, Quick and Lint after the tickets in the middle of a plan, and the full Build, Test and Lint after the last ticket, before a handoff, and under `FLOW_GATE_FULL=1`. A plan without `Quick:` runs the full gate every time. The test-first proof still runs the full `Test` command.
|
|
119
|
+
|
|
120
|
+
**One review for a small ticket.** A ticket whose built diff is 3 files and 60 changed lines at most (the plan folder left out), and whose `check` block ran and passed, gets one combined review instead of the spec and quality reviews. The conductor measures the diff itself. `Review: full` in the ticket (as it was at the start), `Type: convert`, no `check` block, or `FLOW_GATE=off` keep both reviews. Every other conductor check still runs on every ticket.
|
|
115
121
|
|
|
116
122
|
## The skill and the roles
|
|
117
123
|
|
|
@@ -178,14 +184,18 @@ prints OK
|
|
|
178
184
|
- **The Answers are the handoff between subagents.** The next builder reads the Answers of the tickets it depends on.
|
|
179
185
|
- `settle` tickets record a decision. `convert` tickets migrate something and must be safe to run twice.
|
|
180
186
|
- **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.
|
|
181
|
-
- `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.
|
|
187
|
+
- `Floor:` categories: `skip`, `suppress`, `empty-catch`, `test-delete`, `threshold`, `trivial-assert` (an assertion that cannot fail, added to a test file: `assert True`, `assertEqual(x, x)`, `expect(v).toBe(v)`), `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.
|
|
182
188
|
- The guard needs the plan to be tracked by git. If you keep tickets in an ignored folder, it cannot see changes to them.
|
|
183
189
|
- The commands in `commands.md` run with nobody watching and must exit non zero on failure.
|
|
184
190
|
|
|
185
191
|
## See the graph
|
|
186
192
|
|
|
193
|
+

|
|
194
|
+
|
|
195
|
+
*The page `flow-view` writes, shown for the demo plan from `examples/demo.sh`: resolved tickets in green, ready ones in amber, the one being worked in blue, the rest waiting. Press Replay to watch the run back from git history.*
|
|
196
|
+
|
|
187
197
|
```bash
|
|
188
|
-
python3 scripts/flow-view.py csv-export write the page and open it
|
|
198
|
+
python3 scripts/flow-view.py csv-export write the page and open it (or: feature-flow view csv-export)
|
|
189
199
|
python3 scripts/flow-view.py csv-export --watch keep it updating while tickets are built
|
|
190
200
|
bash examples/demo.sh --open try it on a made up project, no setup needed
|
|
191
201
|
```
|
|
@@ -226,6 +236,8 @@ python3 scripts/flow-status.py <feature> table of tickets and w
|
|
|
226
236
|
python3 scripts/flow-status.py <feature> --next path of the next ready ticket (exit 10 = done, 11 = stuck)
|
|
227
237
|
python3 scripts/flow-status.py <feature> --counts one line of counts
|
|
228
238
|
python3 scripts/flow-status.py <feature> --check cycles, missing blockers, missing Done when, unordered mentions
|
|
239
|
+
python3 scripts/flow-status.py <feature> --lint --check plus: a check block (or `Check: none <why>`) on every ticket, a Test command
|
|
240
|
+
feature-flow report <feature> what a run did, from its log: builds, reviews, failed checks, stops
|
|
229
241
|
python3 scripts/flow-status.py <feature> --mermaid the ticket graph, coloured by status (add "plain" for none)
|
|
230
242
|
python3 scripts/flow-status.py <feature> --json every ticket with status, blockers and readiness
|
|
231
243
|
python3 scripts/flow-view.py <feature> [--watch] the animated graph page, opened in your browser
|
|
@@ -247,13 +259,16 @@ The commands in `commands.md` run through `bash -c` on Linux and macOS and throu
|
|
|
247
259
|
| `FLOW_MAX_RETRIES` | `2` | builds or reviews per step before the run stops |
|
|
248
260
|
| `FLOW_MAX_REVIEW_ROUNDS` | `3` | times a ticket may be sent back before the run stops |
|
|
249
261
|
| `FLOW_GATE` | `on` | `off` skips Build, Test and Lint after each ticket, and also the Done when and test-first checks |
|
|
262
|
+
| `FLOW_GATE_FULL` | unset | `1` runs the full Build, Test and Lint after every ticket, even when `commands.md` has a `Quick:` line |
|
|
263
|
+
| `FLOW_SMALL_FILES` | `3` | most files a ticket may change and still get the single review |
|
|
264
|
+
| `FLOW_SMALL_LINES` | `60` | most changed lines for the single review; `0` turns the single review off |
|
|
250
265
|
| `FLOW_GATE_TIMEOUT` | `30` | minutes each gate command and the smoke test may run; `0` means no limit. A failed smoke test writes `.feature-flow/state/<feature>.smoke.log` |
|
|
251
266
|
| `FLOW_SMOKE` | the `Smoke:` line of `commands.md` | command run before every ticket; the run stops if it fails |
|
|
252
267
|
| `FLOW_DIR`, `FLOW_TICKETS` | `plans`, `tasks` | where the plans and the ticket folder live |
|
|
253
268
|
| `FLOW_NO_OPEN` | unset | `1` makes `flow-view.py` never open a browser |
|
|
254
269
|
| `FLOW_WATCH_SECONDS` | `3` | how often `flow-view.py --watch` rewrites the page |
|
|
255
270
|
|
|
256
|
-
The run also stops on its own after a number of phases (`BUILD` and `REVIEW` hand-outs, the spec review and the quality review each counting as one) that grows with the ticket count, recomputed at every pick, so a loop of failures cannot go on forever.
|
|
271
|
+
The run also stops on its own after a number of phases (`BUILD` and `REVIEW` hand-outs, the spec review and the quality review each counting as one, a single review counting as one) that grows with the ticket count, recomputed at every pick, so a loop of failures cannot go on forever.
|
|
257
272
|
|
|
258
273
|
## What is tested
|
|
259
274
|
|
|
@@ -286,6 +301,17 @@ A `package` job builds the wheel and the sdist and runs `tests/package_smoke.py`
|
|
|
286
301
|
|
|
287
302
|
`RUN_REAL=1 bash tests/smoke-real.sh --interactive` is the acceptance run with Claude Code. It runs real sessions on the demo project and spends money, which is why it asks for `RUN_REAL=1`.
|
|
288
303
|
|
|
304
|
+
## Limits
|
|
305
|
+
|
|
306
|
+
feature-flow's layers are tripwires, not a security boundary. They catch accidents and lazy shortcuts by an agent; a determined agent that controls the session could still get around them.
|
|
307
|
+
|
|
308
|
+
- **The verdict relay is trusted.** The session saves a reviewer's reply and hands it to `flow.py verdict`. The conductor reads `REVIEW: PASS` or `REVIEW: FAIL` from that file and cannot prove the session passed it on unedited.
|
|
309
|
+
- **The floor guard reads diffs with patterns.** It does not see a test made trivially true by other means (a mock that returns the expected answer, an `assert` on something that is always set, a test that no longer exercises the code), or an assertion weakened without being deleted. It does catch assertions that cannot fail (`trivial-assert`) and warns the reviewer when a test file loses assertions. A new test with no assertion at all is not caught. The red-first proof only covers tickets with `Test first: yes`.
|
|
310
|
+
- **A plan can be wrong and still pass.** The plan lint checks that every ticket has a way to be proven and that `commands.md` has a Test command. It cannot tell whether the plan builds the right thing; the second yes is yours.
|
|
311
|
+
- **The trust runner stops accidents, not the session.** It catches a builder editing the conductor mid-run. The session that runs it is the thing it cannot check; a hand-run `flow-trust.py` is unprotected (see above).
|
|
312
|
+
- **Little real-world use so far.** Almost everything has been built by feature-flow on its own repo. The Codex skill and the Codex planner have not been run by hand, and the Quick gate has only been run by tests. See [docs/field-run.md](docs/field-run.md) for how to try it on your own repo and what to send back.
|
|
313
|
+
- **One ticket at a time**, with a handoff to a new session every few tickets.
|
|
314
|
+
|
|
289
315
|
## Upgrading
|
|
290
316
|
|
|
291
317
|
Earlier versions had seven skills and an unattended loop. The five flow skills are gone, and `/feature-flow` replaces them:
|
|
@@ -15,15 +15,15 @@ The conductor is `python3 scripts/flow.py <feature> <command>`. If `scripts/flow
|
|
|
15
15
|
|
|
16
16
|
Every conductor command (`start`, `next`, `prompt`, `verdict`, `reset`, `plan-prompt`, `plan-review-prompt`, `plan-accept`) runs through `flow-trust.py`, never `flow.py` itself. `<S>` below is the folder of the `flow.py` picked above: `scripts` for the repo's own install, else `"${FEATURE_FLOW_HOME:-$HOME/.feature-flow}/scripts"`. `flow-trust.py` is started by this loader line, which checks the file against the sha256 pinned here and runs those same bytes. Copy it exactly, replacing only `<S>`:
|
|
17
17
|
|
|
18
|
-
python3 -I -c "import hashlib,sys;b=open(sys.argv[1],'rb').read().replace(b'\r\n',b'\n');sys.exit(exec(compile(b,'flow-trust','exec')) if hashlib.sha256(b).hexdigest()==sys.argv[2] else 'flow-trust.py does not match this skill')" <S>/flow-trust.py
|
|
18
|
+
python3 -I -c "import hashlib,sys;b=open(sys.argv[1],'rb').read().replace(b'\r\n',b'\n');sys.exit(exec(compile(b,'flow-trust','exec')) if hashlib.sha256(b).hexdigest()==sys.argv[2] else 'flow-trust.py does not match this skill')" <S>/flow-trust.py 3a4b28b079b7c657e9a6d843cf1e69ac3c794639ea072f3be96cf94726ab370e
|
|
19
19
|
|
|
20
20
|
A full conductor command, here `next` (any other command and its arguments go where `next` is):
|
|
21
21
|
|
|
22
|
-
FLOW_TRUST=<digest> FLOW_SESSION=<token> FLOW_INVOKE='$feature-flow' python3 -I -c "import hashlib,sys;b=open(sys.argv[1],'rb').read().replace(b'\r\n',b'\n');sys.exit(exec(compile(b,'flow-trust','exec')) if hashlib.sha256(b).hexdigest()==sys.argv[2] else 'flow-trust.py does not match this skill')" <S>/flow-trust.py
|
|
22
|
+
FLOW_TRUST=<digest> FLOW_SESSION=<token> FLOW_INVOKE='$feature-flow' python3 -I -c "import hashlib,sys;b=open(sys.argv[1],'rb').read().replace(b'\r\n',b'\n');sys.exit(exec(compile(b,'flow-trust','exec')) if hashlib.sha256(b).hexdigest()==sys.argv[2] else 'flow-trust.py does not match this skill')" <S>/flow-trust.py 3a4b28b079b7c657e9a6d843cf1e69ac3c794639ea072f3be96cf94726ab370e <feature> next
|
|
23
23
|
|
|
24
24
|
- The first line of the output is `TRUST <digest>`. Keep that digest and pass it as `FLOW_TRUST` on the next conductor call. Treat the rest as the conductor's output and act on it as below (for `prompt`, `plan-prompt` and `plan-review-prompt`, the child agent's prompt is everything after the `TRUST` line). Use `FLOW_TRUST=new` only on this session's first conductor call, or when resuming from an old-style `HANDOFF` line (below).
|
|
25
25
|
- If you no longer have the last `TRUST` digest, stop and say so. Never guess it, and never use `new` in its place.
|
|
26
|
-
- On the `next` right after a `BUILD <ticket> <NN> <sha>`, add `--after-build <ticket> <sha>` before the feature: `... <S>/flow-trust.py
|
|
26
|
+
- On the `next` right after a `BUILD <ticket> <NN> <sha>`, add `--after-build <ticket> <sha>` before the feature: `... <S>/flow-trust.py 3a4b28b079b7c657e9a6d843cf1e69ac3c794639ea072f3be96cf94726ab370e --after-build <ticket> <sha> <feature> next`. If the output has a `FLOW-EDIT <paths>` line after `TRUST`, the build changed feature-flow's own files and its ticket allows that: carry on; without `auto`, first tell the user which flow files changed.
|
|
27
27
|
- If the first line is `STOP` (from `flow-trust.py`), or the loader prints `flow-trust.py does not match this skill` (say to reinstall feature-flow: the skill and `flow-trust.py` do not match), report it, show the `python3 <S>/flow-status.py <feature>` table, and stop. A `STOP` first line may be followed by the conductor's own output: never act on that. Never fix it and carry on.
|
|
28
28
|
- A `HANDOFF` line from `flow-trust.py` has a third field after the invocation and the feature: the digest. When the feature is followed by a `<16 hex>.<16 hex>` word, the user is resuming from a `HANDOFF` line and that word is the digest: pass it as `FLOW_TRUST` on `start`. When a resumed session gets no digest (an old-style `HANDOFF` line), use `FLOW_TRUST=new` and warn the user that the flow's files could not be checked since the last session.
|
|
29
29
|
|
|
@@ -57,14 +57,14 @@ If `start` prints `STOP` naming another owner, another session may still be work
|
|
|
57
57
|
|
|
58
58
|
If the user says the flow is stuck on a ticket or asks for a reset, run `reset` (or `reset <NN>` to redo one ticket): it reopens a half-built ticket, commits that, and clears the run, then run `start` again. It needs `FLOW_TAKEOVER=1` while a session owns the feature, on the same clear yes. Never reset on your own.
|
|
59
59
|
|
|
60
|
-
Say what will happen: for each ticket, a builder subagent and then a reviewer subagent. After every few tickets (`FLOW_TICKETS_PER_SESSION`, default 4) you hand off to a new session. Ask for a yes, unless `auto` was given or you came straight from planning: the plan's yes already covered the build.
|
|
60
|
+
Say what will happen: for each ticket, a builder subagent and then a fresh reviewer subagent for each review pass, one or two per ticket. After every few tickets (`FLOW_TICKETS_PER_SESSION`, default 4) you hand off to a new session. Ask for a yes, unless `auto` was given or you came straight from planning: the plan's yes already covered the build.
|
|
61
61
|
|
|
62
62
|
## The loop
|
|
63
63
|
|
|
64
64
|
Run `next`, act on its one line after `TRUST` (and any `FLOW-EDIT` line), and repeat:
|
|
65
65
|
|
|
66
66
|
- `BUILD <ticket> <NN> <sha>`: run `prompt`. Start the builder with `spawn_agent(task_name="ticket_builder_<NN>_<k>", fork_turns="none", message=...)`. The message is "Work only in <repo>." followed by the whole `prompt` output, which already starts with the builder role. Use a new `task_name` for every spawn: keep a count per ticket in this session, and `<k>` is the number of builders already started for this ticket, plus 1 (1 for the first, then up each time `next` sends the ticket back to a builder). Get its final reply with `wait_agent`, then run `next` with `--after-build <ticket> <sha>` from that `BUILD` line.
|
|
67
|
-
- `REVIEW <ticket> <NN> <sha>`: run `prompt`. Start a new reviewer the same way, with `fork_turns="none"` so it never sees the builder's context, and a unique `task_name` per spawn: `ticket_reviewer_<NN>_<pass>_<k>`, where `<pass>` is `spec` or `
|
|
67
|
+
- `REVIEW <ticket> <NN> <sha>`: run `prompt`. Start a new reviewer the same way, with `fork_turns="none"` so it never sees the builder's context, and a unique `task_name` per spawn: `ticket_reviewer_<NN>_<pass>_<k>`, where `<pass>` is `spec`, `quality` or `single` by the pass the prompt names (`(spec pass)`, `(quality pass)` or `(single pass)`; `review` if it names none) and `<k>` is the number of reviews already started for this ticket in this session, plus 1 (1 for the first). Count every review, also one that ended in `RETRY` or a failed review followed by a rebuild, so a repeated pass never reuses a name. Get its final reply with `wait_agent`. A child cannot be made read-only, so the reviewer works from its instructions; the conductor catches any edit it makes. Save the whole reply with the shell: `cat > .feature-flow/state/flow-review-<feature>.txt <<'EOF'` ... `EOF`. Then run `verdict .feature-flow/state/flow-review-<feature>.txt` and `next`. If `verdict` prints `RETRY`, just run `next`.
|
|
68
68
|
- `DONE <summary>`: report it and suggest `$feature-flow <feature> show`.
|
|
69
69
|
- `STOP <reason>`: report the reason, run `python3 scripts/flow-status.py <feature>`, show the table, and stop.
|
|
70
70
|
- `HANDOFF <line>`: stop here. Tell the user to open a new session and type exactly `<line>`, the whole line with its digest at the end. The new session resumes where this one stopped.
|
|
@@ -55,11 +55,11 @@ def flow_status(scripts, feature, mode, root=None):
|
|
|
55
55
|
return Result(code, out.text())
|
|
56
56
|
|
|
57
57
|
|
|
58
|
-
def gate(scripts, feature, timeout=None):
|
|
58
|
+
def gate(scripts, feature, timeout=None, quick=False):
|
|
59
59
|
"""The gate, run in process. stderr is folded into the output as the subprocess call did.
|
|
60
|
-
timeout is minutes, None for FLOW_GATE_TIMEOUT."""
|
|
60
|
+
timeout is minutes, None for FLOW_GATE_TIMEOUT. quick runs Quick in the place of Test."""
|
|
61
61
|
chunks = []
|
|
62
|
-
code = gate_module.run(str(feature), chunks.append, chunks.append, timeout)
|
|
62
|
+
code = gate_module.run(str(feature), chunks.append, chunks.append, timeout, quick)
|
|
63
63
|
out = "".join(chunks).encode("utf-8", "surrogateescape").decode("utf-8", "replace")
|
|
64
64
|
out = out.replace("\r\n", "\n").replace("\r", "\n")
|
|
65
65
|
return Result(code, out)
|
|
@@ -107,7 +107,7 @@ def run(args, scripts, trusted):
|
|
|
107
107
|
except Stop as stop:
|
|
108
108
|
line = "STOP %s" % stop
|
|
109
109
|
if conductor is not None:
|
|
110
|
-
conductor.log("STOP")
|
|
110
|
+
conductor.log("STOP " + " ".join(str(stop).split())[:200])
|
|
111
111
|
print(line)
|
|
112
112
|
return 1
|
|
113
113
|
out = sys.stdout
|
|
@@ -16,7 +16,7 @@ def _files(scripts):
|
|
|
16
16
|
found = [p for p in package.rglob("*.py") if "__pycache__" not in p.parts]
|
|
17
17
|
folder = Path(scripts).resolve()
|
|
18
18
|
found += [folder / name for name in SCRIPTS if (folder / name).is_file()]
|
|
19
|
-
for phase in ("build", "review", "review-quality"):
|
|
19
|
+
for phase in ("build", "review", "review-quality", "review-single"):
|
|
20
20
|
for folder, names in (("agents", prompts.ROLES), ("guides", prompts.GUIDES)):
|
|
21
21
|
try:
|
|
22
22
|
found.append(prompts.find_file(scripts, folder, names[phase]).resolve())
|
|
@@ -9,7 +9,7 @@ import sys
|
|
|
9
9
|
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
|
|
12
|
-
from feature_flow import __version__, cli, install, status, suggest, uninstall, view
|
|
12
|
+
from feature_flow import __version__, cli, install, report, status, suggest, uninstall, view
|
|
13
13
|
|
|
14
14
|
USAGE = """\
|
|
15
15
|
usage: feature-flow install [target-repo] [--agent claude|codex|all] [--user] [--private] [--force] [--dry-run]
|
|
@@ -17,15 +17,17 @@ usage: feature-flow install [target-repo] [--agent claude|codex|all] [--user] [-
|
|
|
17
17
|
feature-flow status <feature> [--next | --counts | --check | --mermaid [plain] | --json]
|
|
18
18
|
feature-flow view <feature> [--watch] [--no-open] [--out FILE]
|
|
19
19
|
feature-flow reset <feature> [NN]
|
|
20
|
+
feature-flow report <feature>
|
|
20
21
|
feature-flow --version
|
|
21
22
|
install copies the skill, its roles, scripts and guides into a repo; --user installs them once for every repo, in ~/.claude, ~/.agents and ~/.feature-flow, and writes nothing into a repo. feature-flow install --help says more.
|
|
22
23
|
uninstall removes exactly what install wrote (files you edited are kept); --user removes the personal install in ~/.claude, ~/.agents and ~/.feature-flow.
|
|
23
24
|
status and view read plans/<feature>/ in the current directory, like scripts/flow-status.py and scripts/flow-view.py.
|
|
24
25
|
reset reopens a half-built ticket (or ticket NN), commits that, and clears the run, like scripts/flow.py <feature> reset.
|
|
26
|
+
report prints what a run did (builds, reviews, failed checks, stops) from its log in .feature-flow/state/. Read-only.
|
|
25
27
|
"""
|
|
26
28
|
|
|
27
29
|
|
|
28
|
-
COMMANDS = ("install", "uninstall", "status", "view", "reset", "--version", "--help")
|
|
30
|
+
COMMANDS = ("install", "uninstall", "status", "view", "reset", "report", "--version", "--help")
|
|
29
31
|
|
|
30
32
|
|
|
31
33
|
def main(argv=None):
|
|
@@ -46,6 +48,8 @@ def main(argv=None):
|
|
|
46
48
|
return status.main(rest)
|
|
47
49
|
if command == "view":
|
|
48
50
|
return view.main(rest, here=os.path.join(install.source_root(), "scripts"))
|
|
51
|
+
if command == "report":
|
|
52
|
+
return report.main(rest)
|
|
49
53
|
if command == "reset":
|
|
50
54
|
if not rest or len(rest) > 2:
|
|
51
55
|
sys.stderr.write(USAGE)
|
|
@@ -51,6 +51,9 @@ class Conductor:
|
|
|
51
51
|
self.gate_timeout = _env_int("FLOW_GATE_TIMEOUT", 30)
|
|
52
52
|
self.gate_on = os.environ.get("FLOW_GATE", "on") != "off"
|
|
53
53
|
self.per_session = _env_int("FLOW_TICKETS_PER_SESSION", 4)
|
|
54
|
+
self.small_files = _env_int("FLOW_SMALL_FILES", 3)
|
|
55
|
+
self.small_lines = _env_int("FLOW_SMALL_LINES", 60)
|
|
56
|
+
self.checks_ran = False
|
|
54
57
|
self.relay = os.environ.get("FLOW_RELAY", "0") == "1"
|
|
55
58
|
self.invoke = os.environ.get("FLOW_INVOKE") or "/feature-flow"
|
|
56
59
|
top = git.toplevel()
|
|
@@ -125,6 +128,21 @@ class Conductor:
|
|
|
125
128
|
% ("timed out after %d minutes" % self.gate_timeout if result.timed_out else "failed",
|
|
126
129
|
self.ticket_name(), cmd, gate.TAIL, log))
|
|
127
130
|
|
|
131
|
+
def gate_quick(self):
|
|
132
|
+
"""True when this ticket's gate may run Quick in place of Test."""
|
|
133
|
+
if os.environ.get("FLOW_GATE_FULL") == "1":
|
|
134
|
+
return False
|
|
135
|
+
if not tickets.commands_value(self.commands, "Quick"):
|
|
136
|
+
return False
|
|
137
|
+
if self.per_session > 0 and self.num("session_done") + 1 >= self.per_session:
|
|
138
|
+
return False
|
|
139
|
+
counts = {}
|
|
140
|
+
for word in checks.flow_status(self.scripts, self.feature, "--counts").out.split():
|
|
141
|
+
key, _, value = word.partition("=")
|
|
142
|
+
if value.isdigit():
|
|
143
|
+
counts[key] = int(value)
|
|
144
|
+
return sum(counts.get(k, 0) for k in ("open", "claimed", "waiting", "unknown")) > 0
|
|
145
|
+
|
|
128
146
|
def run_limit(self):
|
|
129
147
|
result = checks.flow_status(self.scripts, self.feature, "--counts")
|
|
130
148
|
total = 0
|
|
@@ -246,10 +264,12 @@ class Conductor:
|
|
|
246
264
|
if not git.is_clean():
|
|
247
265
|
raise Stop("working tree is dirty after %s, it should have been committed" % self.ticket_name())
|
|
248
266
|
if self.gate_on:
|
|
249
|
-
|
|
267
|
+
quick = self.gate_quick()
|
|
268
|
+
result = checks.gate(self.scripts, self.feature, self.gate_timeout, quick=quick)
|
|
269
|
+
self.log("GATE-QUICK" if quick else "GATE-FULL")
|
|
250
270
|
self.log("GATE-PASS" if result.ok else "GATE-FAIL")
|
|
251
271
|
if not result.ok:
|
|
252
|
-
return self.send_back("gate", result.out)
|
|
272
|
+
return self.send_back("gate (quick)" if quick else "gate (full)", result.out)
|
|
253
273
|
result = checks.floor_guard(self.scripts, self.feature, self.get("num"), self.get("base"))
|
|
254
274
|
self.log("GUARD-PASS" if result.ok else "GUARD-FAIL")
|
|
255
275
|
if not result.ok:
|
|
@@ -266,9 +286,29 @@ class Conductor:
|
|
|
266
286
|
return self.send_back("test first", report)
|
|
267
287
|
if note:
|
|
268
288
|
notes.append(note)
|
|
289
|
+
self.st["review_pass"] = "spec"
|
|
290
|
+
if self.gate_on and self.checks_ran:
|
|
291
|
+
small = self.small_note()
|
|
292
|
+
if small:
|
|
293
|
+
self.st["review_pass"] = "single"
|
|
294
|
+
self.log("REVIEW-SMALL")
|
|
295
|
+
notes.append(small)
|
|
269
296
|
self.st["review_notes"] = NOTE_SEP.join(notes)
|
|
270
297
|
return self.hand_out_review()
|
|
271
298
|
|
|
299
|
+
def small_note(self):
|
|
300
|
+
"""The note for the reviewer when the built diff is small enough for one review, else ""."""
|
|
301
|
+
base = self.ticket_at_base()
|
|
302
|
+
if proof.review_mode(base) != "auto" or self.small_lines <= 0:
|
|
303
|
+
return ""
|
|
304
|
+
if re.search(r"^Type:[ \t]*convert\b", base.split("\n## ", 1)[0], re.IGNORECASE | re.MULTILINE):
|
|
305
|
+
return ""
|
|
306
|
+
files, lines, binary = git.diff_size(self.get("base"), "HEAD", self.plan.as_posix())
|
|
307
|
+
if binary or files > self.small_files or lines > self.small_lines:
|
|
308
|
+
return ""
|
|
309
|
+
return ("the conductor judged this ticket small (%d files, %d lines): one review covers the spec and "
|
|
310
|
+
"quality passes" % (files, lines))
|
|
311
|
+
|
|
272
312
|
def ticket_at_base(self):
|
|
273
313
|
"""The ticket text as it was at the base commit, so a builder cannot change what is checked."""
|
|
274
314
|
rel = proof.repo_relative(self.ticket())
|
|
@@ -286,6 +326,7 @@ class Conductor:
|
|
|
286
326
|
found = proof.checks_in(self.ticket_at_base())
|
|
287
327
|
except proof.ParseError as err:
|
|
288
328
|
raise Stop("%s has a check block the conductor cannot read: %s" % (self.ticket_name(), err))
|
|
329
|
+
self.checks_ran = bool(found)
|
|
289
330
|
if not found:
|
|
290
331
|
self.log("DONEWHEN-SKIP")
|
|
291
332
|
return None, "the conductor ran no Done when checks: the ticket has no check block"
|
|
@@ -424,7 +465,7 @@ class Conductor:
|
|
|
424
465
|
raise Stop("the reviewer changed tracked files, which a reviewer must never do")
|
|
425
466
|
verdict = self.get("verdict")
|
|
426
467
|
self.st["verdict"] = ""
|
|
427
|
-
if verdict == "pass" and self.get("review_pass", "spec")
|
|
468
|
+
if verdict == "pass" and self.get("review_pass", "spec") == "spec":
|
|
428
469
|
self.st["review_pass"] = "quality"
|
|
429
470
|
self.st["review_attempt"] = 0
|
|
430
471
|
return self.hand_out_review()
|
|
@@ -436,8 +477,8 @@ class Conductor:
|
|
|
436
477
|
return self.pick_ticket()
|
|
437
478
|
if verdict == "fail":
|
|
438
479
|
findings = state.read_text(self.findings_file, "findings") or ""
|
|
439
|
-
return self.send_back("quality
|
|
440
|
-
|
|
480
|
+
return self.send_back({"quality": "quality review", "single": "single review"}.get(
|
|
481
|
+
self.get("review_pass"), "spec review"), findings)
|
|
441
482
|
if self.num("review_attempt") >= self.max_retries:
|
|
442
483
|
raise Stop("no review verdict for %s after %d attempt(s)" % (self.ticket_name(), self.max_retries))
|
|
443
484
|
return self.hand_out_review()
|
|
@@ -449,8 +490,8 @@ class Conductor:
|
|
|
449
490
|
if phase not in ("build", "review"):
|
|
450
491
|
raise NoPhase("no BUILD or REVIEW is pending for %s" % self.feature)
|
|
451
492
|
try:
|
|
452
|
-
if phase == "review" and self.get("review_pass")
|
|
453
|
-
phase = "review-
|
|
493
|
+
if phase == "review" and self.get("review_pass") in ("quality", "single"):
|
|
494
|
+
phase = "review-" + self.get("review_pass")
|
|
454
495
|
return prompts.build(phase, self.scripts, self.feature, self.ticket(), self.get("num"), self.get("base"),
|
|
455
496
|
[n for n in self.get("review_notes").split(NOTE_SEP) if n] if phase != "build" else (),
|
|
456
497
|
retry=self.num("round") > 0)
|
|
@@ -532,7 +573,7 @@ class Conductor:
|
|
|
532
573
|
self.no_plan_yet()
|
|
533
574
|
if not self.draft.is_dir():
|
|
534
575
|
raise Stop("no draft plan in %s. run the planner first" % self.draft_text())
|
|
535
|
-
check = checks.flow_status(self.scripts, self.feature, "--
|
|
576
|
+
check = checks.flow_status(self.scripts, self.feature, "--lint", root=self.draft_root)
|
|
536
577
|
if not check.ok:
|
|
537
578
|
raise Stop("the draft fails the plan check, fix it first: %s"
|
|
538
579
|
% " ".join(check.out.split())[:400])
|
|
@@ -7,7 +7,7 @@ lowered thresholds, edited lint or test config.
|
|
|
7
7
|
it also protects the plan from the worker: only the ticket's own Status line and Answer,
|
|
8
8
|
appended lines in map.md and learnings.md may change, and nothing else under the plans folder.
|
|
9
9
|
a ticket can allow categories with a line "Floor: allow skip, suppress" (skip, suppress,
|
|
10
|
-
empty-catch, test-delete, threshold, config, ticket-edit, commands-edit). the line is
|
|
10
|
+
empty-catch, test-delete, threshold, trivial-assert, config, ticket-edit, commands-edit). the line is
|
|
11
11
|
read from the ticket as it was at base-commit, so a worker cannot excuse itself.
|
|
12
12
|
run it from the repo root. exit 0 clean, 1 findings, 2 usage error.
|
|
13
13
|
|
|
@@ -29,6 +29,13 @@ FOCUSED = re.compile(r"\.only\(|(?<!def )(?<![A-Za-z0-9_.])(fit|fdescribe)\(")
|
|
|
29
29
|
SUPPRESS = re.compile(r"# noqa|# type: ignore|# pylint: disable|# pragma: no cover|# fmt: off|# ruff: noqa"
|
|
30
30
|
r"|eslint-disable|@ts-ignore|@ts-expect-error|// nolint|#\[allow\(|--no-verify")
|
|
31
31
|
EMPTY_CATCH = re.compile(r"except[^:]*:[ \t]*pass[ \t]*$|catch[ \t]*(\([^)]*\))?[ \t]*\{[ \t]*\}")
|
|
32
|
+
# an assertion that cannot fail, added to a test file: assert True, assertEqual(x, x), expect(true), expect(x).toBe(x)
|
|
33
|
+
TRIVIAL = (re.compile(r"(^|[^A-Za-z_.])assert\s+(True|1\s*==\s*1|not\s+False)\s*(#.*)?$"),
|
|
34
|
+
re.compile(r"assertTrue\(\s*True\s*[,)]"),
|
|
35
|
+
re.compile(r"assertEqual\(\s*([A-Za-z_][\w.]*|\d+)\s*,\s*\1\s*[,)]"),
|
|
36
|
+
re.compile(r"(^|[^A-Za-z_.])assert\s+([A-Za-z_][\w.]*)\s*==\s*\2\s*(#.*)?$"),
|
|
37
|
+
re.compile(r"expect\(\s*true\s*\)\.(toBe\(\s*true\s*\)|toBeTruthy\(\))"),
|
|
38
|
+
re.compile(r"expect\(\s*([A-Za-z_][\w.]*)\s*\)\.toBe\(\s*\1\s*\)"))
|
|
32
39
|
THRESHOLD = re.compile(r"fail_under|fail-under|coverageThreshold|cov-fail-under")
|
|
33
40
|
DELETED_TEST = re.compile(r"(^|/)(test_[^/]*\.py|[^/]*_test\.[a-z]+|[^/]*\.(test|spec)\.[a-z]+|tests?/|__tests__/)")
|
|
34
41
|
# Makefile is judged by content, not name: a Makefile change only counts when the plan's commands.md has a
|
|
@@ -77,7 +84,7 @@ def allow_line(source):
|
|
|
77
84
|
|
|
78
85
|
|
|
79
86
|
def diff_findings(diff, allow):
|
|
80
|
-
"""The pass over added lines: skip, suppress, empty-catch and
|
|
87
|
+
"""The pass over added lines: skip, suppress, empty-catch, threshold and trivial-assert."""
|
|
81
88
|
allow = " " + allow + " "
|
|
82
89
|
out = []
|
|
83
90
|
file = ""
|
|
@@ -105,6 +112,8 @@ def diff_findings(diff, allow):
|
|
|
105
112
|
report("empty-catch", line)
|
|
106
113
|
if THRESHOLD.search(line):
|
|
107
114
|
report("threshold", line)
|
|
115
|
+
if DELETED_TEST.search(file) and any(r.search(line) for r in TRIVIAL):
|
|
116
|
+
report("trivial-assert", line)
|
|
108
117
|
return "".join(out)
|
|
109
118
|
|
|
110
119
|
|
|
@@ -60,9 +60,10 @@ def timeout_minutes():
|
|
|
60
60
|
raise ValueError("FLOW_GATE_TIMEOUT must be a whole number, not %s" % value)
|
|
61
61
|
|
|
62
62
|
|
|
63
|
-
def run(feature, out, err, timeout=None):
|
|
63
|
+
def run(feature, out, err, timeout=None, quick=False):
|
|
64
64
|
"""Run the gate for feature. out and err take text. timeout is minutes (a float is fine), 0 for none;
|
|
65
|
-
None reads FLOW_GATE_TIMEOUT.
|
|
65
|
+
None reads FLOW_GATE_TIMEOUT. With quick, a usable Quick: value runs in the place of Test.
|
|
66
|
+
Returns the exit code."""
|
|
66
67
|
if not feature:
|
|
67
68
|
err("usage: python3 scripts/gate.py <feature>\n")
|
|
68
69
|
return 2
|
|
@@ -77,8 +78,11 @@ def run(feature, out, err, timeout=None):
|
|
|
77
78
|
out("gate: no commands.md for " + feature + ", nothing to run\n")
|
|
78
79
|
return 0
|
|
79
80
|
ran = 0
|
|
81
|
+
quick_cmd = command(path, "Quick") if quick else ""
|
|
80
82
|
for key in KEYS:
|
|
81
83
|
cmd = command(path, key)
|
|
84
|
+
if key == "Test" and quick_cmd != "" and not quick_cmd.startswith("<"):
|
|
85
|
+
key, cmd = "Quick", quick_cmd
|
|
82
86
|
if cmd == "" or cmd.startswith("<"):
|
|
83
87
|
continue
|
|
84
88
|
ran += 1
|
|
@@ -39,6 +39,24 @@ def head():
|
|
|
39
39
|
return _git("rev-parse", "HEAD").stdout.strip()
|
|
40
40
|
|
|
41
41
|
|
|
42
|
+
def diff_size(base, head, exclude):
|
|
43
|
+
"""(files, lines, binary) of the diff base..head outside the exclude path: the changed paths, the
|
|
44
|
+
added plus removed lines of the text files, and whether any file is binary (`-` counts in numstat).
|
|
45
|
+
Renames are off so a renamed file is one delete plus one add whatever `diff.renames` says."""
|
|
46
|
+
out = _git("diff", "--numstat", "--no-renames", base, head, "--", ".", ":(exclude)%s" % exclude).stdout
|
|
47
|
+
files, lines, binary = 0, 0, False
|
|
48
|
+
for line in out.splitlines():
|
|
49
|
+
if not line.strip():
|
|
50
|
+
continue
|
|
51
|
+
added, removed = line.split("\t", 2)[:2]
|
|
52
|
+
files += 1
|
|
53
|
+
if added == "-" or removed == "-":
|
|
54
|
+
binary = True
|
|
55
|
+
else:
|
|
56
|
+
lines += int(added) + int(removed)
|
|
57
|
+
return files, lines, binary
|
|
58
|
+
|
|
59
|
+
|
|
42
60
|
# the installer's record of what it wrote: the paths, and the sha256 of each as it wrote it
|
|
43
61
|
INSTALLED = Path(".feature-flow") / "installed.txt"
|
|
44
62
|
HASHES = Path(".feature-flow") / "installed.sha256"
|