feature-flow-cli 0.2.0__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.2.0/feature_flow_cli.egg-info → feature_flow_cli-0.3.0}/PKG-INFO +25 -10
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/README.md +24 -9
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/adapters/codex/feature-flow/SKILL.md +2 -2
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow/__init__.py +1 -1
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow/codehash.py +5 -1
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow/conductor.py +86 -6
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow/git.py +8 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow/prompts.py +15 -9
- feature_flow_cli-0.3.0/feature_flow/proof.py +294 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0/feature_flow_cli.egg-info}/PKG-INFO +25 -10
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow_cli.egg-info/SOURCES.txt +3 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/guides/build.md +4 -2
- feature_flow_cli-0.3.0/guides/debug.md +32 -0
- {feature_flow_cli-0.2.0 → 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.2.0 → feature_flow_cli-0.3.0}/guides/review.md +1 -15
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/guides/templates/ticket.md +6 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/skills/feature-flow/SKILL.md +2 -2
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/LICENSE +0 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/MANIFEST.in +0 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/adapters/codex/feature-flow/agents/openai.yaml +0 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/agents/feature-planner.md +0 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/agents/plan-reviewer.md +0 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/agents/ticket-builder.md +0 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/agents/ticket-reviewer.md +0 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow/__main__.py +0 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow/checks.py +0 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow/cli.py +0 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow/command.py +0 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow/floorguard.py +0 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow/gate.py +0 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow/install.py +0 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow/proc.py +0 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow/released.py +0 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow/state.py +0 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow/status.py +0 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow/suggest.py +0 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow/tickets.py +0 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow/view.py +0 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow_cli.egg-info/dependency_links.txt +0 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow_cli.egg-info/entry_points.txt +0 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/feature_flow_cli.egg-info/top_level.txt +0 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/guides/brief.md +0 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/guides/plan-review.md +0 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/guides/show.md +0 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/guides/templates/commands.md +0 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/guides/templates/learnings.md +0 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/guides/templates/map.md +0 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/guides/templates/spec.md +0 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/guides/templates/ui-mockup.md +0 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/install.py +0 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/install.sh +0 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/pyproject.toml +0 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/scripts/floor-guard.py +0 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/scripts/flow-status.py +0 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/scripts/flow-view.html +0 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/scripts/flow-view.py +0 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/scripts/flow.py +0 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/scripts/gate.py +0 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/setup.cfg +0 -0
- {feature_flow_cli-0.2.0 → feature_flow_cli-0.3.0}/skills/architect-review/SKILL.md +0 -0
- {feature_flow_cli-0.2.0 → 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
|
|
@@ -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,9 +176,14 @@ 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.
|
|
@@ -240,14 +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 |
|
|
244
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` |
|
|
245
260
|
| `FLOW_SMOKE` | the `Smoke:` line of `commands.md` | command run before every ticket; the run stops if it fails |
|
|
246
261
|
| `FLOW_DIR`, `FLOW_TICKETS` | `plans`, `tasks` | where the plans and the ticket folder live |
|
|
247
262
|
| `FLOW_NO_OPEN` | unset | `1` makes `flow-view.py` never open a browser |
|
|
248
263
|
| `FLOW_WATCH_SECONDS` | `3` | how often `flow-view.py --watch` rewrites the page |
|
|
249
264
|
|
|
250
|
-
The run also stops on its own after a number of phases (`BUILD` and `REVIEW` hand-outs) that grows with the ticket count, recomputed at every pick, so a loop of failures cannot go on forever.
|
|
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.
|
|
251
266
|
|
|
252
267
|
## What is tested
|
|
253
268
|
|
|
@@ -274,7 +289,7 @@ bash tests/run.sh
|
|
|
274
289
|
python3 -m unittest discover -s tests/py
|
|
275
290
|
```
|
|
276
291
|
|
|
277
|
-
Offline and free: no model is called. The suite covers the conductor's every answer and how a run stops, sessions, handoff and takeover, a `.git` the agent cannot write, the gate, the floor guard, plan protection, the prompts and guides, both skills, the installer for both agents, the graph page and its data, and the acceptance harness. The page's layout and replay logic are also tested under Node (`tests/viewer-logic.test.js`, skipped when Node is absent). `.github/workflows/tests.yml` runs it on every push to `main` and every pull request, on Ubuntu and on macOS. A fourth job runs the Python unit tests on Windows, including a fixture drive that takes a two-ticket plan through `python scripts/flow.py f start` and `next` to `BUILD` and then `REVIEW`. `tests/run.sh` itself is a bash harness and does not run on Windows. Windows is tested this way only: no real agent run has been done there.
|
|
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.
|
|
278
293
|
|
|
279
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`.
|
|
280
295
|
|
|
@@ -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,9 +157,14 @@ 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.
|
|
@@ -221,14 +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 |
|
|
225
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` |
|
|
226
241
|
| `FLOW_SMOKE` | the `Smoke:` line of `commands.md` | command run before every ticket; the run stops if it fails |
|
|
227
242
|
| `FLOW_DIR`, `FLOW_TICKETS` | `plans`, `tasks` | where the plans and the ticket folder live |
|
|
228
243
|
| `FLOW_NO_OPEN` | unset | `1` makes `flow-view.py` never open a browser |
|
|
229
244
|
| `FLOW_WATCH_SECONDS` | `3` | how often `flow-view.py --watch` rewrites the page |
|
|
230
245
|
|
|
231
|
-
The run also stops on its own after a number of phases (`BUILD` and `REVIEW` hand-outs) that grows with the ticket count, recomputed at every pick, so a loop of failures cannot go on forever.
|
|
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.
|
|
232
247
|
|
|
233
248
|
## What is tested
|
|
234
249
|
|
|
@@ -255,7 +270,7 @@ bash tests/run.sh
|
|
|
255
270
|
python3 -m unittest discover -s tests/py
|
|
256
271
|
```
|
|
257
272
|
|
|
258
|
-
Offline and free: no model is called. The suite covers the conductor's every answer and how a run stops, sessions, handoff and takeover, a `.git` the agent cannot write, the gate, the floor guard, plan protection, the prompts and guides, both skills, the installer for both agents, the graph page and its data, and the acceptance harness. The page's layout and replay logic are also tested under Node (`tests/viewer-logic.test.js`, skipped when Node is absent). `.github/workflows/tests.yml` runs it on every push to `main` and every pull request, on Ubuntu and on macOS. A fourth job runs the Python unit tests on Windows, including a fixture drive that takes a two-ticket plan through `python scripts/flow.py f start` and `next` to `BUILD` and then `REVIEW`. `tests/run.sh` itself is a bash harness and does not run on Windows. Windows is tested this way only: no real agent run has been done there.
|
|
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.
|
|
259
274
|
|
|
260
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`.
|
|
261
276
|
|
|
@@ -47,8 +47,8 @@ Say what will happen: for each ticket, a builder subagent and then a reviewer su
|
|
|
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.
|
|
@@ -12,12 +12,16 @@ def _files(scripts):
|
|
|
12
12
|
package = Path(__file__).resolve().parent
|
|
13
13
|
found = [p for p in package.rglob("*.py") if "__pycache__" not in p.parts]
|
|
14
14
|
found += Path(scripts).resolve().glob("*.py")
|
|
15
|
-
for phase in ("build", "review"):
|
|
15
|
+
for phase in ("build", "review", "review-quality"):
|
|
16
16
|
for folder, names in (("agents", prompts.ROLES), ("guides", prompts.GUIDES)):
|
|
17
17
|
try:
|
|
18
18
|
found.append(prompts.find_file(scripts, folder, names[phase]).resolve())
|
|
19
19
|
except FileNotFoundError:
|
|
20
20
|
pass
|
|
21
|
+
try:
|
|
22
|
+
found.append(prompts.find_file(scripts, "guides", "debug.md").resolve())
|
|
23
|
+
except FileNotFoundError:
|
|
24
|
+
pass
|
|
21
25
|
return sorted(set(found))
|
|
22
26
|
|
|
23
27
|
|
|
@@ -10,9 +10,10 @@ import secrets
|
|
|
10
10
|
import shutil
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
|
|
13
|
-
from feature_flow import checks, codehash, floorguard, gate, git, prompts, state, suggest, tickets
|
|
13
|
+
from feature_flow import checks, codehash, floorguard, gate, git, prompts, proof, state, suggest, tickets
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
NOTE_SEP = "\t" # the state file keeps one line per key, so the review notes are tab separated
|
|
16
17
|
VERDICT = re.compile(r"^REVIEW: (PASS|FAIL)[ \t\r\f\v]*$")
|
|
17
18
|
|
|
18
19
|
|
|
@@ -120,7 +121,7 @@ class Conductor:
|
|
|
120
121
|
for word in result.out.split():
|
|
121
122
|
if word.startswith("total="):
|
|
122
123
|
total = int(word[len("total="):])
|
|
123
|
-
return total *
|
|
124
|
+
return total * 3 * self.max_retries * (self.max_rounds + 1) + 1
|
|
124
125
|
|
|
125
126
|
def count_run(self):
|
|
126
127
|
runs = self.num("runs") + 1
|
|
@@ -142,6 +143,7 @@ class Conductor:
|
|
|
142
143
|
def hand_out_review(self):
|
|
143
144
|
self.count_run()
|
|
144
145
|
self.st["phase"] = "review"
|
|
146
|
+
self.st["review_pass"] = self.get("review_pass") or "spec"
|
|
145
147
|
self.st["review_attempt"] = self.num("review_attempt") + 1
|
|
146
148
|
self.st["review_sha"] = git.head()
|
|
147
149
|
self.st["relay_handoff"] = "1" if self.relay else ""
|
|
@@ -162,6 +164,7 @@ class Conductor:
|
|
|
162
164
|
% (source, self.ticket_name()))
|
|
163
165
|
self.st["attempt"] = 0
|
|
164
166
|
self.st["review_attempt"] = 0
|
|
167
|
+
self.st["review_pass"] = "spec"
|
|
165
168
|
return self.hand_out_build()
|
|
166
169
|
|
|
167
170
|
def pick_ticket(self):
|
|
@@ -190,7 +193,7 @@ class Conductor:
|
|
|
190
193
|
path = nxt.out.strip().splitlines()[-1]
|
|
191
194
|
self.st["limit"] = self.run_limit()
|
|
192
195
|
self.st.update({"ticket": path, "num": tickets.number(path), "base": git.head(), "phase": "",
|
|
193
|
-
"attempt": 0, "review_attempt": 0, "round": 0, "review_sha": "", "
|
|
196
|
+
"attempt": 0, "review_attempt": 0, "round": 0, "review_sha": "", "review_notes": "", "review_pass": "spec"})
|
|
194
197
|
self.snapshot_code()
|
|
195
198
|
self.run_smoke()
|
|
196
199
|
return self.hand_out_build()
|
|
@@ -242,9 +245,70 @@ class Conductor:
|
|
|
242
245
|
if not result.ok:
|
|
243
246
|
return self.send_back("floor guard", result.out)
|
|
244
247
|
warnings = [l for l in result.out.splitlines() if l.startswith("warning:")]
|
|
245
|
-
|
|
248
|
+
notes = warnings[:1]
|
|
249
|
+
if self.gate_on:
|
|
250
|
+
report, note = self.run_done_when()
|
|
251
|
+
if report is not None:
|
|
252
|
+
return self.send_back("done when", report)
|
|
253
|
+
notes.append(note)
|
|
254
|
+
report, note = self.run_test_first()
|
|
255
|
+
if report is not None:
|
|
256
|
+
return self.send_back("test first", report)
|
|
257
|
+
if note:
|
|
258
|
+
notes.append(note)
|
|
259
|
+
self.st["review_notes"] = NOTE_SEP.join(notes)
|
|
246
260
|
return self.hand_out_review()
|
|
247
261
|
|
|
262
|
+
def ticket_at_base(self):
|
|
263
|
+
"""The ticket text as it was at the base commit, so a builder cannot change what is checked."""
|
|
264
|
+
rel = proof.repo_relative(self.ticket())
|
|
265
|
+
listed = git._git("ls-tree", "--name-only", self.get("base"), "--", rel, check=False)
|
|
266
|
+
if listed.returncode != 0:
|
|
267
|
+
raise Stop("cannot read %s at the base commit: %s" % (rel, listed.stderr.strip()))
|
|
268
|
+
if listed.stdout.strip():
|
|
269
|
+
return git.show_text("%s:%s" % (self.get("base"), rel))
|
|
270
|
+
text = Path(self.ticket()).read_text(encoding="utf-8")
|
|
271
|
+
return text.split("\n## Answer", 1)[0]
|
|
272
|
+
|
|
273
|
+
def run_done_when(self):
|
|
274
|
+
"""Run the ticket's check block. Returns (failure report or None, note for the reviewer)."""
|
|
275
|
+
try:
|
|
276
|
+
found = proof.checks_in(self.ticket_at_base())
|
|
277
|
+
except proof.ParseError as err:
|
|
278
|
+
raise Stop("%s has a check block the conductor cannot read: %s" % (self.ticket_name(), err))
|
|
279
|
+
if not found:
|
|
280
|
+
self.log("DONEWHEN-SKIP")
|
|
281
|
+
return None, "the conductor ran no Done when checks: the ticket has no check block"
|
|
282
|
+
ok, report = proof.run_checks(found, self.gate_timeout)
|
|
283
|
+
dirty = git.changes()
|
|
284
|
+
if dirty:
|
|
285
|
+
raise Stop("a Done when check of %s changed the working tree: %s. a check command must not change it"
|
|
286
|
+
% (self.ticket_name(), " ".join(dirty[:5])))
|
|
287
|
+
self.log("DONEWHEN-PASS" if ok else "DONEWHEN-FAIL")
|
|
288
|
+
if not ok:
|
|
289
|
+
return report, ""
|
|
290
|
+
return None, "the conductor ran %d Done when check(s) and all passed" % len(found)
|
|
291
|
+
|
|
292
|
+
def run_test_first(self):
|
|
293
|
+
"""For a `Test first: yes` ticket, prove a test-only commit failed the Test command.
|
|
294
|
+
Returns (failure report or None, note for the reviewer or "")."""
|
|
295
|
+
if not proof.wants_test_first(self.ticket_at_base()):
|
|
296
|
+
return None, ""
|
|
297
|
+
test = tickets.commands_value(self.commands, "Test")
|
|
298
|
+
if not test:
|
|
299
|
+
self.log("TESTFIRST-SKIP")
|
|
300
|
+
return None, "the conductor did not check test first: commands.md has no Test command"
|
|
301
|
+
|
|
302
|
+
def remember(value):
|
|
303
|
+
self.st["restore"] = value
|
|
304
|
+
self.save()
|
|
305
|
+
try:
|
|
306
|
+
ok, text = proof.test_first(self.get("base"), test, self.plan, self.gate_timeout, remember)
|
|
307
|
+
except proof.GitError as err:
|
|
308
|
+
raise Stop("test first check of %s: %s" % (self.ticket_name(), err))
|
|
309
|
+
self.log("TESTFIRST-PASS" if ok else "TESTFIRST-FAIL")
|
|
310
|
+
return (None, text) if ok else (text, "")
|
|
311
|
+
|
|
248
312
|
# ---- commands ------------------------------------------------------
|
|
249
313
|
|
|
250
314
|
def handoff(self):
|
|
@@ -278,6 +342,14 @@ class Conductor:
|
|
|
278
342
|
|
|
279
343
|
def next(self):
|
|
280
344
|
self.check_owner()
|
|
345
|
+
if self.get("restore"):
|
|
346
|
+
try:
|
|
347
|
+
git._git("checkout", "-q", self.get("restore"))
|
|
348
|
+
except RuntimeError as err:
|
|
349
|
+
raise Stop("cannot put the working copy back after the test first check: %s. run `git checkout %s` "
|
|
350
|
+
"by hand" % (err, self.get("restore")))
|
|
351
|
+
self.st["restore"] = ""
|
|
352
|
+
self.save()
|
|
281
353
|
if not self.plan.is_dir():
|
|
282
354
|
raise Stop("no plan folder %s" % self.plan)
|
|
283
355
|
if self.get("relay_handoff") == "1":
|
|
@@ -338,6 +410,10 @@ class Conductor:
|
|
|
338
410
|
raise Stop("the reviewer changed tracked files, which a reviewer must never do")
|
|
339
411
|
verdict = self.get("verdict")
|
|
340
412
|
self.st["verdict"] = ""
|
|
413
|
+
if verdict == "pass" and self.get("review_pass", "spec") != "quality":
|
|
414
|
+
self.st["review_pass"] = "quality"
|
|
415
|
+
self.st["review_attempt"] = 0
|
|
416
|
+
return self.hand_out_review()
|
|
341
417
|
if verdict == "pass":
|
|
342
418
|
self.st["phase"] = ""
|
|
343
419
|
self.st["done"] = self.num("done") + 1
|
|
@@ -346,7 +422,8 @@ class Conductor:
|
|
|
346
422
|
return self.pick_ticket()
|
|
347
423
|
if verdict == "fail":
|
|
348
424
|
findings = self.findings_file.read_text(encoding="utf-8") if self.findings_file.is_file() else ""
|
|
349
|
-
return self.send_back("
|
|
425
|
+
return self.send_back("quality review" if self.get("review_pass") == "quality" else "spec review",
|
|
426
|
+
findings)
|
|
350
427
|
if self.num("review_attempt") >= self.max_retries:
|
|
351
428
|
raise Stop("no review verdict for %s after %d attempt(s)" % (self.ticket_name(), self.max_retries))
|
|
352
429
|
return self.hand_out_review()
|
|
@@ -358,8 +435,11 @@ class Conductor:
|
|
|
358
435
|
if phase not in ("build", "review"):
|
|
359
436
|
raise NoPhase("no BUILD or REVIEW is pending for %s" % self.feature)
|
|
360
437
|
try:
|
|
438
|
+
if phase == "review" and self.get("review_pass") == "quality":
|
|
439
|
+
phase = "review-quality"
|
|
361
440
|
return prompts.build(phase, self.scripts, self.feature, self.ticket(), self.get("num"), self.get("base"),
|
|
362
|
-
self.get("
|
|
441
|
+
[n for n in self.get("review_notes").split(NOTE_SEP) if n] if phase != "build" else (),
|
|
442
|
+
retry=self.num("round") > 0)
|
|
363
443
|
except FileNotFoundError as err:
|
|
364
444
|
raise Stop(str(err))
|
|
365
445
|
|
|
@@ -13,6 +13,14 @@ def _git(*args, check=True):
|
|
|
13
13
|
return result
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
def show_text(revision_path):
|
|
17
|
+
"""`git show <rev>:<path>` decoded as UTF-8 (the platform's default encoding is not what a ticket is written in)."""
|
|
18
|
+
result = subprocess.run(["git", "show", revision_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
19
|
+
if result.returncode != 0:
|
|
20
|
+
raise RuntimeError("git show %s failed: %s" % (revision_path, result.stderr.decode("utf-8", "replace").strip()))
|
|
21
|
+
return result.stdout.decode("utf-8", "replace").replace("\r\n", "\n")
|
|
22
|
+
|
|
23
|
+
|
|
16
24
|
def git_dir():
|
|
17
25
|
result = _git("rev-parse", "--absolute-git-dir", check=False)
|
|
18
26
|
if result.returncode != 0:
|
|
@@ -5,9 +5,9 @@ The text is runtime neutral: any agent that can read a prompt can follow it.
|
|
|
5
5
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
|
-
ROLES = {"build": "ticket-builder.md", "review": "ticket-reviewer.md",
|
|
8
|
+
ROLES = {"build": "ticket-builder.md", "review": "ticket-reviewer.md", "review-quality": "ticket-reviewer.md",
|
|
9
9
|
"plan": "feature-planner.md", "plan-review": "plan-reviewer.md"}
|
|
10
|
-
GUIDES = {"build": "build.md", "review": "review.md", "plan": "plan.md", "plan-review": "plan-review.md"}
|
|
10
|
+
GUIDES = {"build": "build.md", "review": "review.md", "review-quality": "review-quality.md", "plan": "plan.md", "plan-review": "plan-review.md"}
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def find_file(scripts, folder, name):
|
|
@@ -32,7 +32,7 @@ def _body(path):
|
|
|
32
32
|
return text.strip("\n")
|
|
33
33
|
|
|
34
34
|
|
|
35
|
-
def build(phase, scripts, feature, ticket, num, sha,
|
|
35
|
+
def build(phase, scripts, feature, ticket, num, sha, notes=(), retry=False):
|
|
36
36
|
role = _body(find_file(scripts, "agents", ROLES[phase]))
|
|
37
37
|
guide = _body(find_file(scripts, "guides", GUIDES[phase]))
|
|
38
38
|
for placeholder, value in (("<feature>", feature), ("<NN>", num), ("<ticket>", ticket), ("<sha>", sha),
|
|
@@ -46,13 +46,19 @@ def build(phase, scripts, feature, ticket, num, sha, warning=""):
|
|
|
46
46
|
"commit. Do not review your own work." % (num, feature, feature, num, ticket, sha, fresh))
|
|
47
47
|
intro = "The build guide follows. Follow it exactly."
|
|
48
48
|
else:
|
|
49
|
-
task = ("Your task: review ticket %s of the feature `%s` (`%s %s %s`). The ticket is `%s`, and the
|
|
50
|
-
"started at commit `%s`.\n%s\nDo not edit any file or create a commit. Your final reply must end "
|
|
51
|
-
"with exactly `REVIEW: PASS` or `REVIEW: FAIL`."
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
task = ("Your task: review ticket %s of the feature `%s` (`%s %s %s`) (%s pass). The ticket is `%s`, and the "
|
|
50
|
+
"work started at commit `%s`.\n%s\nDo not edit any file or create a commit. Your final reply must end "
|
|
51
|
+
"with exactly `REVIEW: PASS` or `REVIEW: FAIL`."
|
|
52
|
+
% (num, feature, feature, num, sha, "quality" if phase == "review-quality" else "spec", ticket, sha,
|
|
53
|
+
fresh))
|
|
54
|
+
for note in notes:
|
|
55
|
+
task += "\nThe conductor notes: %s" % note
|
|
54
56
|
intro = "The review guide follows. Follow it exactly."
|
|
55
|
-
|
|
57
|
+
parts = [role, "---", intro, guide, "---", task]
|
|
58
|
+
if retry and phase == "build":
|
|
59
|
+
parts += ["The debugging guide follows. Follow it before you change any code.",
|
|
60
|
+
_body(find_file(scripts, "guides", "debug.md"))]
|
|
61
|
+
return "\n\n".join(parts) + "\n"
|
|
56
62
|
|
|
57
63
|
|
|
58
64
|
def plan(phase, scripts, feature, draft, brief, findings=""):
|