feature-flow-cli 0.3.0__tar.gz → 0.5.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.3.0/feature_flow_cli.egg-info → feature_flow_cli-0.5.0}/PKG-INFO +22 -6
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/README.md +21 -5
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/adapters/codex/feature-flow/SKILL.md +23 -11
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow/__init__.py +1 -1
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow/cli.py +56 -3
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow/codehash.py +5 -1
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow/command.py +7 -3
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow/conductor.py +27 -13
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow/install.py +72 -45
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow/prompts.py +39 -4
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow/state.py +48 -6
- feature_flow_cli-0.5.0/feature_flow/uninstall.py +235 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0/feature_flow_cli.egg-info}/PKG-INFO +22 -6
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow_cli.egg-info/SOURCES.txt +2 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/guides/build.md +1 -1
- feature_flow_cli-0.5.0/scripts/floor-guard.py +29 -0
- feature_flow_cli-0.5.0/scripts/flow-status.py +29 -0
- feature_flow_cli-0.5.0/scripts/flow-trust.py +367 -0
- feature_flow_cli-0.5.0/scripts/flow-view.py +29 -0
- feature_flow_cli-0.5.0/scripts/flow.py +29 -0
- feature_flow_cli-0.5.0/scripts/gate.py +29 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/skills/feature-flow/SKILL.md +23 -11
- feature_flow_cli-0.3.0/scripts/floor-guard.py +0 -14
- feature_flow_cli-0.3.0/scripts/flow-status.py +0 -14
- feature_flow_cli-0.3.0/scripts/flow-view.py +0 -14
- feature_flow_cli-0.3.0/scripts/flow.py +0 -14
- feature_flow_cli-0.3.0/scripts/gate.py +0 -14
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/LICENSE +0 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/MANIFEST.in +0 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/adapters/codex/feature-flow/agents/openai.yaml +0 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/agents/feature-planner.md +0 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/agents/plan-reviewer.md +0 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/agents/ticket-builder.md +0 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/agents/ticket-reviewer.md +0 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow/__main__.py +0 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow/checks.py +0 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow/floorguard.py +0 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow/gate.py +0 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow/git.py +0 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow/proc.py +0 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow/proof.py +0 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow/released.py +0 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow/status.py +0 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow/suggest.py +0 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow/tickets.py +0 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow/view.py +0 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow_cli.egg-info/dependency_links.txt +0 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow_cli.egg-info/entry_points.txt +0 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow_cli.egg-info/top_level.txt +0 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/guides/brief.md +0 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/guides/debug.md +0 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/guides/plan-review.md +0 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/guides/plan.md +0 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/guides/review-quality.md +0 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/guides/review.md +0 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/guides/show.md +0 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/guides/templates/commands.md +0 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/guides/templates/learnings.md +0 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/guides/templates/map.md +0 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/guides/templates/spec.md +0 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/guides/templates/ticket.md +0 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/guides/templates/ui-mockup.md +0 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/install.py +0 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/install.sh +0 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/pyproject.toml +0 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/scripts/flow-view.html +0 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/setup.cfg +0 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/skills/architect-review/SKILL.md +0 -0
- {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.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.5.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
|
|
@@ -57,6 +57,8 @@ The installer lists what it wrote in `.feature-flow/installed.txt`, with each fi
|
|
|
57
57
|
|
|
58
58
|
To keep the install out of git, so only your plans and tickets get committed, install with `--private`. It lists the installed files in `.git/info/exclude`, which git reads like `.gitignore` but never commits, and later installs keep that list up to date. If you already committed the install, it prints the `git rm --cached` command that untracks it and keeps the files. Every clone then needs its own install.
|
|
59
59
|
|
|
60
|
+
To remove it, run `feature-flow uninstall` (or `uvx feature-flow-cli uninstall`) in the repo. It removes exactly the files listed in `.feature-flow/installed.txt`, and only while they still hold the bytes the installer wrote: a file you edited is kept and named, and `--force` removes it too. It also drops the `--private` block from `.git/info/exclude`. Your `plans/`, your tickets and the run logs in `.feature-flow/state/` are never touched. `--dry-run` shows what would go. If you installed with `--user`, `feature-flow uninstall --user` removes the personal copy in `~/.claude` and `~/.agents`, and `~/.feature-flow`, the same way (the repo's own install is separate and needs its own `uninstall`).
|
|
61
|
+
|
|
60
62
|
Then, in your repo, in the agent:
|
|
61
63
|
|
|
62
64
|
| | Claude Code | Codex |
|
|
@@ -73,6 +75,11 @@ The same command plans when `plans/csv-export/` does not exist yet and builds wh
|
|
|
73
75
|
|
|
74
76
|
Try the graph first, with no setup and no agent: `bash examples/demo.sh --open`.
|
|
75
77
|
|
|
78
|
+
### Two ways to install
|
|
79
|
+
|
|
80
|
+
- `feature-flow install --user`: once, for every repo. Skills and roles go to `~/.claude` / `~/.agents`, the conductor, guides and package to `~/.feature-flow/`. `FEATURE_FLOW_HOME` moves that folder. No repo files are written.
|
|
81
|
+
- `feature-flow install <repo>`: one repo. When a repo has its own copy, that copy wins over the user install.
|
|
82
|
+
|
|
76
83
|
### What the installer does
|
|
77
84
|
|
|
78
85
|
| | Claude Code (`--agent claude`, the default) | Codex (`--agent codex`) |
|
|
@@ -82,7 +89,7 @@ Try the graph first, with no setup and no agent: `bash examples/demo.sh --open`.
|
|
|
82
89
|
| Scripts go to | `<repo>/scripts/` | `<repo>/scripts/` |
|
|
83
90
|
| Guides, roles and the conductor go to | `<repo>/.feature-flow/` | `<repo>/.feature-flow/` |
|
|
84
91
|
|
|
85
|
-
Next to the `feature-flow` skill it installs `architect-review` and `automation-design`. The installer never deletes anything. A file that already exists and differs is kept and reported; `--force` replaces it. `--dry-run` shows what would change and writes nothing. `CLAUDE_HOME` and `AGENTS_HOME` move the user folders. With `--user` the skill
|
|
92
|
+
Next to the `feature-flow` skill it installs `architect-review` and `automation-design`. The installer never deletes anything. A file that already exists and differs is kept and reported; `--force` replaces it. `--dry-run` shows what would change and writes nothing. `CLAUDE_HOME` and `AGENTS_HOME` move the user folders. With `--user` the skill and roles go to the user folders, and the scripts, guides, conductor and package go to `~/.feature-flow/` (`FEATURE_FLOW_HOME` moves it); nothing is written into a repo.
|
|
86
93
|
|
|
87
94
|
The Claude Code skill is `skills/feature-flow/`. The Codex skill is written by hand in `adapters/codex/feature-flow/`, with an `agents/openai.yaml` that sets `allow_implicit_invocation: false`, so it runs only when you name it. The steps are the same; only the way a subagent is started differs. Your own `AGENTS.md` and `CLAUDE.md` are never touched.
|
|
88
95
|
|
|
@@ -219,7 +226,7 @@ One self contained HTML file: no server, no libraries, no network, light and dar
|
|
|
219
226
|
|
|
220
227
|
## Long features and handoff
|
|
221
228
|
|
|
222
|
-
A session's context fills up, so the flow moves to a fresh session every few tickets. After `FLOW_TICKETS_PER_SESSION` tickets (default 4) pass review, `next` prints a `HANDOFF` line instead of the next ticket, for example `HANDOFF /feature-flow csv-export
|
|
229
|
+
A session's context fills up, so the flow moves to a fresh session every few tickets. After `FLOW_TICKETS_PER_SESSION` tickets (default 4) pass review, `next` prints a `HANDOFF` line instead of the next ticket, for example `HANDOFF /feature-flow csv-export 3f9c0a1b2c3d4e5f.0a1b2c3d4e5f6a7b` (the last word is the runner's digest, which lets the new session check that the flow's files did not change in between). The session stops and tells you to open a new session and type that line. The new session picks up exactly where the last one stopped. Set `FLOW_TICKETS_PER_SESSION=0` to never hand off.
|
|
223
230
|
|
|
224
231
|
- **One owner at a time.** `start` gives the session a token, and every later call must carry it, so two sessions can never drive the same feature. `HANDOFF` and `DONE` release it.
|
|
225
232
|
- **Resuming after a closed session.** If a session ended without `HANDOFF` (you closed it, it crashed, it ran out of budget), the feature is still owned by it, and a new session stops and says so. Once you are sure the old session is gone, answer yes when the new session asks, or start it with `FLOW_TAKEOVER=1`. With `auto` the skill never takes over on its own.
|
|
@@ -232,7 +239,8 @@ The state lives in `.feature-flow/state/` (the state, a log of every step, and t
|
|
|
232
239
|
### Commands and settings
|
|
233
240
|
|
|
234
241
|
```bash
|
|
235
|
-
python3 scripts/flow.py <feature> start|
|
|
242
|
+
python3 scripts/flow.py <feature> start|reset [NN] the conductor (the skill runs it; also plan-prompt, plan-review-prompt, plan-accept)
|
|
243
|
+
FLOW_TRUST=<digest|new> FLOW_SESSION=<token> python3 -I scripts/flow-trust.py <feature> next|prompt|verdict <file> loop commands by hand, through the runner: prints TRUST <digest> first (no hash check, see below)
|
|
236
244
|
python3 scripts/flow-status.py <feature> table of tickets and which are READY
|
|
237
245
|
python3 scripts/flow-status.py <feature> --next path of the next ready ticket (exit 10 = done, 11 = stuck)
|
|
238
246
|
python3 scripts/flow-status.py <feature> --counts one line of counts
|
|
@@ -244,6 +252,8 @@ python3 scripts/gate.py <feature> run Build, Test and Li
|
|
|
244
252
|
python3 scripts/floor-guard.py <feature> <NN> [base] check a ticket's diff, run from the repo root
|
|
245
253
|
```
|
|
246
254
|
|
|
255
|
+
`next`, `prompt` and `verdict` run directly stop with `STOP run the conductor through scripts/flow-trust.py, as the skill says`. Pass `FLOW_TRUST=new` on a first call, then the digest from the last `TRUST` line. Run by hand like this, nothing checks `flow-trust.py` itself, so it is for trying things out, not a protected run. The skills never run it this way: they start it through a loader line that first checks the file against the sha256 the skill pins, and stops with `flow-trust.py does not match this skill` if it differs (see `## Calling the conductor` in the skill). For a `--user` install the scripts are in `~/.feature-flow/scripts/` (or `$FEATURE_FLOW_HOME/scripts/`).
|
|
256
|
+
|
|
247
257
|
The commands in `commands.md` run through `bash -c` on Linux and macOS and through the system shell (`cmd.exe`) on Windows, so write them for the platform your team uses, or call `python` as in the examples.
|
|
248
258
|
|
|
249
259
|
`flow-status.py` also understands the `.scratch/<feature>/issues/` layout and the statuses `done`, `ready-for-agent`, `ready-for-human` and `closed`: `FLOW_DIR=.scratch FLOW_TICKETS=issues`.
|
|
@@ -289,7 +299,7 @@ bash tests/run.sh
|
|
|
289
299
|
python3 -m unittest discover -s tests/py
|
|
290
300
|
```
|
|
291
301
|
|
|
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
|
|
302
|
+
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`, with `FLOW_TRUSTED=1` set as the runner sets it. `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.
|
|
293
303
|
|
|
294
304
|
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`.
|
|
295
305
|
|
|
@@ -320,7 +330,13 @@ git tag v0.1.1 && git push origin v0.1.1
|
|
|
320
330
|
|
|
321
331
|
## Caution
|
|
322
332
|
|
|
323
|
-
The builder subagent has edit and shell access and commits after each ticket. Each ticket costs at least two subagents. Build on a branch you can throw away. Ignore build output in `.gitignore`, because the conductor stops if the tree is dirty after a ticket. The gate and the smoke test stop after 30 minutes (`FLOW_GATE_TIMEOUT`). The conductor
|
|
333
|
+
The builder subagent has edit and shell access and commits after each ticket. Each ticket costs at least two subagents. Build on a branch you can throw away. Ignore build output in `.gitignore`, because the conductor stops if the tree is dirty after a ticket. The gate and the smoke test stop after 30 minutes (`FLOW_GATE_TIMEOUT`). The session runs every conductor call through `scripts/flow-trust.py`, which sits beside the conductor it runs (`scripts/` in the repo, or `~/.feature-flow/scripts/` for a `--user` install). The skill pins the runner's sha256 and starts it with a one-line loader that checks and runs those same bytes, so a subagent cannot change the check. The runner prints `TRUST <code>.<state>` before the conductor's output, and the session passes that digest to the next call as `FLOW_TRUST`.
|
|
334
|
+
|
|
335
|
+
**Protected:** edits by the builder, reviewer or planner subagents during a session to feature-flow's own files (its `feature_flow` package, its files in `scripts/` (`flow.py`, `flow-status.py`, `gate.py`, `floor-guard.py`, `flow-view.py`, `flow-trust.py`), the guides, the roles and the skill folders, in the repo and in the install folders) and to the run state in `.feature-flow/state/`. That includes edits made by code the conductor runs during a step, such as tests, `check` commands and git hooks. Any of these makes the runner print a `STOP` instead of `TRUST`, naming the changed files where it can, and the session stops. It also holds between sessions when the `HANDOFF` line carries its digest (`HANDOFF /feature-flow <feature> <digest>`). `Floor: allow flow-edit` on a ticket allows its build to change the flow's code (never the run state, and never `scripts/flow-trust.py`, whose hash the skill pins: change the runner outside the flow, or build it with a conductor from another install, then update the pinned sha in both skills); the runner then prints `FLOW-EDIT <files>`, and a session run without `auto` tells you which flow files changed.
|
|
336
|
+
|
|
337
|
+
**Not protected:** a subagent that changes `python3` on PATH or the system Python, the agent itself, or anything outside the repo and the install folders. The session's very first conductor call: whatever is on disk then is trusted. A background process left running by a step's own commands: it can swap a flow file in the instant between the runner's check and the conductor loading it, then put it back. A session that stops passing its digest, or a resume from a `HANDOFF` line without one (the session warns). The conductor's own code check (`STOP flow code changed while building <ticket>: <files>`) stays as a second line that catches accidental edits; on its own it cannot stop deliberate tampering.
|
|
338
|
+
|
|
339
|
+
The floor guard is pattern matching: it can miss things and it can raise false alarms, and `Floor: allow` is the release valve. The run stops on its own when a ticket stays unresolved, when the smoke test or the gate keeps failing, when a ticket keeps failing review, or when it reaches its phase limit.
|
|
324
340
|
|
|
325
341
|
## License
|
|
326
342
|
|
|
@@ -38,6 +38,8 @@ The installer lists what it wrote in `.feature-flow/installed.txt`, with each fi
|
|
|
38
38
|
|
|
39
39
|
To keep the install out of git, so only your plans and tickets get committed, install with `--private`. It lists the installed files in `.git/info/exclude`, which git reads like `.gitignore` but never commits, and later installs keep that list up to date. If you already committed the install, it prints the `git rm --cached` command that untracks it and keeps the files. Every clone then needs its own install.
|
|
40
40
|
|
|
41
|
+
To remove it, run `feature-flow uninstall` (or `uvx feature-flow-cli uninstall`) in the repo. It removes exactly the files listed in `.feature-flow/installed.txt`, and only while they still hold the bytes the installer wrote: a file you edited is kept and named, and `--force` removes it too. It also drops the `--private` block from `.git/info/exclude`. Your `plans/`, your tickets and the run logs in `.feature-flow/state/` are never touched. `--dry-run` shows what would go. If you installed with `--user`, `feature-flow uninstall --user` removes the personal copy in `~/.claude` and `~/.agents`, and `~/.feature-flow`, the same way (the repo's own install is separate and needs its own `uninstall`).
|
|
42
|
+
|
|
41
43
|
Then, in your repo, in the agent:
|
|
42
44
|
|
|
43
45
|
| | Claude Code | Codex |
|
|
@@ -54,6 +56,11 @@ The same command plans when `plans/csv-export/` does not exist yet and builds wh
|
|
|
54
56
|
|
|
55
57
|
Try the graph first, with no setup and no agent: `bash examples/demo.sh --open`.
|
|
56
58
|
|
|
59
|
+
### Two ways to install
|
|
60
|
+
|
|
61
|
+
- `feature-flow install --user`: once, for every repo. Skills and roles go to `~/.claude` / `~/.agents`, the conductor, guides and package to `~/.feature-flow/`. `FEATURE_FLOW_HOME` moves that folder. No repo files are written.
|
|
62
|
+
- `feature-flow install <repo>`: one repo. When a repo has its own copy, that copy wins over the user install.
|
|
63
|
+
|
|
57
64
|
### What the installer does
|
|
58
65
|
|
|
59
66
|
| | Claude Code (`--agent claude`, the default) | Codex (`--agent codex`) |
|
|
@@ -63,7 +70,7 @@ Try the graph first, with no setup and no agent: `bash examples/demo.sh --open`.
|
|
|
63
70
|
| Scripts go to | `<repo>/scripts/` | `<repo>/scripts/` |
|
|
64
71
|
| Guides, roles and the conductor go to | `<repo>/.feature-flow/` | `<repo>/.feature-flow/` |
|
|
65
72
|
|
|
66
|
-
Next to the `feature-flow` skill it installs `architect-review` and `automation-design`. The installer never deletes anything. A file that already exists and differs is kept and reported; `--force` replaces it. `--dry-run` shows what would change and writes nothing. `CLAUDE_HOME` and `AGENTS_HOME` move the user folders. With `--user` the skill
|
|
73
|
+
Next to the `feature-flow` skill it installs `architect-review` and `automation-design`. The installer never deletes anything. A file that already exists and differs is kept and reported; `--force` replaces it. `--dry-run` shows what would change and writes nothing. `CLAUDE_HOME` and `AGENTS_HOME` move the user folders. With `--user` the skill and roles go to the user folders, and the scripts, guides, conductor and package go to `~/.feature-flow/` (`FEATURE_FLOW_HOME` moves it); nothing is written into a repo.
|
|
67
74
|
|
|
68
75
|
The Claude Code skill is `skills/feature-flow/`. The Codex skill is written by hand in `adapters/codex/feature-flow/`, with an `agents/openai.yaml` that sets `allow_implicit_invocation: false`, so it runs only when you name it. The steps are the same; only the way a subagent is started differs. Your own `AGENTS.md` and `CLAUDE.md` are never touched.
|
|
69
76
|
|
|
@@ -200,7 +207,7 @@ One self contained HTML file: no server, no libraries, no network, light and dar
|
|
|
200
207
|
|
|
201
208
|
## Long features and handoff
|
|
202
209
|
|
|
203
|
-
A session's context fills up, so the flow moves to a fresh session every few tickets. After `FLOW_TICKETS_PER_SESSION` tickets (default 4) pass review, `next` prints a `HANDOFF` line instead of the next ticket, for example `HANDOFF /feature-flow csv-export
|
|
210
|
+
A session's context fills up, so the flow moves to a fresh session every few tickets. After `FLOW_TICKETS_PER_SESSION` tickets (default 4) pass review, `next` prints a `HANDOFF` line instead of the next ticket, for example `HANDOFF /feature-flow csv-export 3f9c0a1b2c3d4e5f.0a1b2c3d4e5f6a7b` (the last word is the runner's digest, which lets the new session check that the flow's files did not change in between). The session stops and tells you to open a new session and type that line. The new session picks up exactly where the last one stopped. Set `FLOW_TICKETS_PER_SESSION=0` to never hand off.
|
|
204
211
|
|
|
205
212
|
- **One owner at a time.** `start` gives the session a token, and every later call must carry it, so two sessions can never drive the same feature. `HANDOFF` and `DONE` release it.
|
|
206
213
|
- **Resuming after a closed session.** If a session ended without `HANDOFF` (you closed it, it crashed, it ran out of budget), the feature is still owned by it, and a new session stops and says so. Once you are sure the old session is gone, answer yes when the new session asks, or start it with `FLOW_TAKEOVER=1`. With `auto` the skill never takes over on its own.
|
|
@@ -213,7 +220,8 @@ The state lives in `.feature-flow/state/` (the state, a log of every step, and t
|
|
|
213
220
|
### Commands and settings
|
|
214
221
|
|
|
215
222
|
```bash
|
|
216
|
-
python3 scripts/flow.py <feature> start|
|
|
223
|
+
python3 scripts/flow.py <feature> start|reset [NN] the conductor (the skill runs it; also plan-prompt, plan-review-prompt, plan-accept)
|
|
224
|
+
FLOW_TRUST=<digest|new> FLOW_SESSION=<token> python3 -I scripts/flow-trust.py <feature> next|prompt|verdict <file> loop commands by hand, through the runner: prints TRUST <digest> first (no hash check, see below)
|
|
217
225
|
python3 scripts/flow-status.py <feature> table of tickets and which are READY
|
|
218
226
|
python3 scripts/flow-status.py <feature> --next path of the next ready ticket (exit 10 = done, 11 = stuck)
|
|
219
227
|
python3 scripts/flow-status.py <feature> --counts one line of counts
|
|
@@ -225,6 +233,8 @@ python3 scripts/gate.py <feature> run Build, Test and Li
|
|
|
225
233
|
python3 scripts/floor-guard.py <feature> <NN> [base] check a ticket's diff, run from the repo root
|
|
226
234
|
```
|
|
227
235
|
|
|
236
|
+
`next`, `prompt` and `verdict` run directly stop with `STOP run the conductor through scripts/flow-trust.py, as the skill says`. Pass `FLOW_TRUST=new` on a first call, then the digest from the last `TRUST` line. Run by hand like this, nothing checks `flow-trust.py` itself, so it is for trying things out, not a protected run. The skills never run it this way: they start it through a loader line that first checks the file against the sha256 the skill pins, and stops with `flow-trust.py does not match this skill` if it differs (see `## Calling the conductor` in the skill). For a `--user` install the scripts are in `~/.feature-flow/scripts/` (or `$FEATURE_FLOW_HOME/scripts/`).
|
|
237
|
+
|
|
228
238
|
The commands in `commands.md` run through `bash -c` on Linux and macOS and through the system shell (`cmd.exe`) on Windows, so write them for the platform your team uses, or call `python` as in the examples.
|
|
229
239
|
|
|
230
240
|
`flow-status.py` also understands the `.scratch/<feature>/issues/` layout and the statuses `done`, `ready-for-agent`, `ready-for-human` and `closed`: `FLOW_DIR=.scratch FLOW_TICKETS=issues`.
|
|
@@ -270,7 +280,7 @@ bash tests/run.sh
|
|
|
270
280
|
python3 -m unittest discover -s tests/py
|
|
271
281
|
```
|
|
272
282
|
|
|
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
|
|
283
|
+
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`, with `FLOW_TRUSTED=1` set as the runner sets it. `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.
|
|
274
284
|
|
|
275
285
|
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`.
|
|
276
286
|
|
|
@@ -301,7 +311,13 @@ git tag v0.1.1 && git push origin v0.1.1
|
|
|
301
311
|
|
|
302
312
|
## Caution
|
|
303
313
|
|
|
304
|
-
The builder subagent has edit and shell access and commits after each ticket. Each ticket costs at least two subagents. Build on a branch you can throw away. Ignore build output in `.gitignore`, because the conductor stops if the tree is dirty after a ticket. The gate and the smoke test stop after 30 minutes (`FLOW_GATE_TIMEOUT`). The conductor
|
|
314
|
+
The builder subagent has edit and shell access and commits after each ticket. Each ticket costs at least two subagents. Build on a branch you can throw away. Ignore build output in `.gitignore`, because the conductor stops if the tree is dirty after a ticket. The gate and the smoke test stop after 30 minutes (`FLOW_GATE_TIMEOUT`). The session runs every conductor call through `scripts/flow-trust.py`, which sits beside the conductor it runs (`scripts/` in the repo, or `~/.feature-flow/scripts/` for a `--user` install). The skill pins the runner's sha256 and starts it with a one-line loader that checks and runs those same bytes, so a subagent cannot change the check. The runner prints `TRUST <code>.<state>` before the conductor's output, and the session passes that digest to the next call as `FLOW_TRUST`.
|
|
315
|
+
|
|
316
|
+
**Protected:** edits by the builder, reviewer or planner subagents during a session to feature-flow's own files (its `feature_flow` package, its files in `scripts/` (`flow.py`, `flow-status.py`, `gate.py`, `floor-guard.py`, `flow-view.py`, `flow-trust.py`), the guides, the roles and the skill folders, in the repo and in the install folders) and to the run state in `.feature-flow/state/`. That includes edits made by code the conductor runs during a step, such as tests, `check` commands and git hooks. Any of these makes the runner print a `STOP` instead of `TRUST`, naming the changed files where it can, and the session stops. It also holds between sessions when the `HANDOFF` line carries its digest (`HANDOFF /feature-flow <feature> <digest>`). `Floor: allow flow-edit` on a ticket allows its build to change the flow's code (never the run state, and never `scripts/flow-trust.py`, whose hash the skill pins: change the runner outside the flow, or build it with a conductor from another install, then update the pinned sha in both skills); the runner then prints `FLOW-EDIT <files>`, and a session run without `auto` tells you which flow files changed.
|
|
317
|
+
|
|
318
|
+
**Not protected:** a subagent that changes `python3` on PATH or the system Python, the agent itself, or anything outside the repo and the install folders. The session's very first conductor call: whatever is on disk then is trusted. A background process left running by a step's own commands: it can swap a flow file in the instant between the runner's check and the conductor loading it, then put it back. A session that stops passing its digest, or a resume from a `HANDOFF` line without one (the session warns). The conductor's own code check (`STOP flow code changed while building <ticket>: <files>`) stays as a second line that catches accidental edits; on its own it cannot stop deliberate tampering.
|
|
319
|
+
|
|
320
|
+
The floor guard is pattern matching: it can miss things and it can raise false alarms, and `Floor: allow` is the release valve. The run stops on its own when a ticket stays unresolved, when the smoke test or the gate keeps failing, when a ticket keeps failing review, or when it reaches its phase limit.
|
|
305
321
|
|
|
306
322
|
## License
|
|
307
323
|
|
|
@@ -9,7 +9,23 @@ Plan or build the feature in `<arguments>`.
|
|
|
9
9
|
|
|
10
10
|
The first word is the **feature**. `show` means draw the graph. `auto` means ask nothing and go. If no feature was given and the conversation so far describes work to build, plan it (below) and propose the feature's name in the brief. If no feature was given otherwise, list `plans/*/` and ask which one.
|
|
11
11
|
|
|
12
|
-
The conductor is `python3 scripts/flow.py <feature> <command>`. It decides the order, runs the gate and the floor guard, and keeps its state in `.feature-flow/state/`, a folder git ignores. You ask it, and you do what it says. The guides it uses are in `.feature-flow/guides/` (in a feature-flow checkout, `guides/`). Read the project's `AGENTS.md` for its conventions.
|
|
12
|
+
The conductor is `python3 scripts/flow.py <feature> <command>`. If `scripts/flow.py` exists in the repo and the feature-flow package is beside it (`.feature-flow/feature_flow/`, or `feature_flow/` in a feature-flow checkout), use it (the repo's own install wins; an unrelated `scripts/flow.py` does not count); otherwise use `python3 "${FEATURE_FLOW_HOME:-$HOME/.feature-flow}/scripts/flow.py"` (`~/.feature-flow/scripts/flow.py`). Wherever this skill or a message names `scripts/flow.py`, `scripts/flow-status.py` or `scripts/flow-view.py`, it means the same folder, and the guides are then in that folder's sibling `guides/` (`~/.feature-flow/guides/`, else `.feature-flow/guides/` in the repo). Run the conductor with the working directory in the repo: state and plans stay there. It decides the order, runs the gate and the floor guard, and keeps its state in `.feature-flow/state/`, a folder git ignores. You ask it, and you do what it says. The guides it uses are in `.feature-flow/guides/` (in a feature-flow checkout, `guides/`). Read the project's `AGENTS.md` for its conventions.
|
|
13
|
+
|
|
14
|
+
## Calling the conductor
|
|
15
|
+
|
|
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
|
+
|
|
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 ec4e2430b3ed4dae4fa5d9190c3e1726294a0f854bf92584c0c756a07b9029d9
|
|
19
|
+
|
|
20
|
+
A full conductor command, here `next` (any other command and its arguments go where `next` is):
|
|
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 ec4e2430b3ed4dae4fa5d9190c3e1726294a0f854bf92584c0c756a07b9029d9 <feature> next
|
|
23
|
+
|
|
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
|
+
- 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 ec4e2430b3ed4dae4fa5d9190c3e1726294a0f854bf92584c0c756a07b9029d9 --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
|
+
- 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
|
+
- 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.
|
|
13
29
|
|
|
14
30
|
## Show
|
|
15
31
|
|
|
@@ -17,7 +33,7 @@ With `show`, follow `.feature-flow/guides/show.md` for the feature and stop.
|
|
|
17
33
|
|
|
18
34
|
## Plan
|
|
19
35
|
|
|
20
|
-
When `plans/<feature>/` does not exist, or you are planning from the conversation, follow `.feature-flow/guides/brief.md` with the user. You write the brief and ask for both yeses (unless `auto`); a child agent plans, another reviews, and you never draft the plan yourself. Once the brief names the feature, run `FLOW_INVOKE='$feature-flow'
|
|
36
|
+
When `plans/<feature>/` does not exist, or you are planning from the conversation, follow `.feature-flow/guides/brief.md` with the user. You write the brief and ask for both yeses (unless `auto`); a child agent plans, another reviews, and you never draft the plan yourself. Once the brief names the feature, run `start` through `flow-trust.py` (see Calling the conductor) with `FLOW_INVOKE='$feature-flow'`, and check that it prints `PLAN` after the `TRUST` line, and check that `feature-planner.md` and `plan-reviewer.md` exist in `.agents/flow-roles/` or in `${FEATURE_FLOW_HOME:-$HOME/.feature-flow}/agents/` (see below).
|
|
21
37
|
|
|
22
38
|
- `plan-prompt <brief>`: start the planner with `spawn_agent(task_name="feature_planner", fork_turns="none", message=...)`. The message is "Work only in <repo>." followed by the whole output. Get its final reply with `wait_agent`. A child cannot be limited to the draft folder, so it works from its instructions, and `plan-accept` is the only way its draft reaches `plans/`. If web search is off in this Codex, the planner plans from the codebase and says so.
|
|
23
39
|
- `plan-review-prompt`: start a new reviewer the same way, with `task_name="plan_reviewer"` and `fork_turns="none"`, never with the planner's reply. Get its final reply with `wait_agent` and save it with the shell into `.feature-flow/state/plan-review-<feature>.txt`.
|
|
@@ -33,9 +49,9 @@ With a plan present, check these before `start`, and stop at the first that fail
|
|
|
33
49
|
|
|
34
50
|
- `git status --porcelain` is empty, apart from untracked Python bytecode (`__pycache__/`, `*.pyc`) and files listed in `.feature-flow/installed.txt` and that list itself: those are feature-flow's own install or upgrade, so never stop for them (`next` still stops if one of them was edited). Suggest committing them (`git add --pathspec-from-file=.feature-flow/installed.txt && git commit -m "chore: install feature-flow"`) and carry on.
|
|
35
51
|
- `python3 scripts/flow-status.py <feature> --check` prints `OK`.
|
|
36
|
-
-
|
|
52
|
+
- `ticket-builder.md` and `ticket-reviewer.md` are in `.agents/flow-roles/` in the repo or in `${FEATURE_FLOW_HOME:-$HOME/.feature-flow}/agents/` (`~/.feature-flow/agents/` by default). If not, say to run `uvx feature-flow-cli install . --agent codex` in this repo, or `uvx feature-flow-cli install --user --agent codex` (once for every repo) (or `python3 install.py <repo> --agent codex` from a feature-flow clone).
|
|
37
53
|
|
|
38
|
-
Then run `FLOW_INVOKE='$feature-flow'
|
|
54
|
+
Then run `start` through `flow-trust.py` with `FLOW_INVOKE='$feature-flow'`. After the `TRUST` line it prints `OK <token>`. Keep the token and put `FLOW_SESSION=<token>` in front of **every** later conductor command, with `FLOW_INVOKE='$feature-flow'` and the last `FLOW_TRUST`. The conductor keeps its state in `.feature-flow/state/`, a git-ignored folder in the repo, so it never needs to write `.git`. If it prints `STOP cannot write the flow state`, tell the user this session must be allowed to write that folder.
|
|
39
55
|
|
|
40
56
|
If `start` prints `STOP` naming another owner, another session may still be working this feature. Ask the user whether that session is closed. Only on a clear yes, run `start` once more with `FLOW_TAKEOVER=1`. In `auto` mode, never take over: report and stop.
|
|
41
57
|
|
|
@@ -45,17 +61,13 @@ Say what will happen: for each ticket, a builder subagent and then a reviewer su
|
|
|
45
61
|
|
|
46
62
|
## The loop
|
|
47
63
|
|
|
48
|
-
Run `next`, act on its one line, and repeat:
|
|
64
|
+
Run `next`, act on its one line after `TRUST` (and any `FLOW-EDIT` line), and repeat:
|
|
49
65
|
|
|
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
|
|
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.
|
|
51
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 `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
68
|
- `DONE <summary>`: report it and suggest `$feature-flow <feature> show`.
|
|
53
69
|
- `STOP <reason>`: report the reason, run `python3 scripts/flow-status.py <feature>`, show the table, and stop.
|
|
54
|
-
- `HANDOFF <line>`: stop here. Tell the user to open a new session and type exactly `<line
|
|
55
|
-
|
|
56
|
-
A conductor command looks like this:
|
|
57
|
-
|
|
58
|
-
FLOW_SESSION=<token> FLOW_INVOKE='$feature-flow' python3 scripts/flow.py <feature> next
|
|
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.
|
|
59
71
|
|
|
60
72
|
## Rules while building
|
|
61
73
|
|
|
@@ -1,19 +1,69 @@
|
|
|
1
1
|
"""python3 scripts/flow.py <feature> <command>: the conductor's command line."""
|
|
2
2
|
|
|
3
|
+
import os
|
|
3
4
|
import sys
|
|
5
|
+
import traceback
|
|
4
6
|
from pathlib import Path
|
|
5
7
|
|
|
6
|
-
from feature_flow import suggest
|
|
8
|
+
from feature_flow import git, state, suggest
|
|
7
9
|
from feature_flow.conductor import Conductor, NoPhase, Stop
|
|
8
10
|
|
|
9
11
|
USAGE = ("usage: python3 scripts/flow.py <feature> start | next | prompt | verdict <file> | reset [NN]\n"
|
|
10
12
|
" python3 scripts/flow.py <feature> plan-prompt <brief> [findings] | plan-review-prompt | plan-accept")
|
|
11
13
|
COMMANDS = ("start", "next", "prompt", "verdict", "plan-prompt", "plan-review-prompt", "plan-accept", "reset")
|
|
12
14
|
ARGS = {"verdict": (3,), "plan-prompt": (3, 4), "reset": (2, 3)}
|
|
15
|
+
RUNNER_ONLY = ("next", "prompt", "verdict")
|
|
13
16
|
|
|
14
17
|
|
|
15
18
|
def main(argv=None, scripts=None):
|
|
16
|
-
|
|
19
|
+
"""With FLOW_TRUSTED=1 (set by scripts/flow-trust.py) the last stderr line is always
|
|
20
|
+
`flow-state <state sha256 or none> <findings sha256 or none>`, for the bytes the conductor left."""
|
|
21
|
+
trusted = os.environ.get("FLOW_TRUSTED") == "1"
|
|
22
|
+
state.SEEN.clear()
|
|
23
|
+
try:
|
|
24
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
25
|
+
if trusted:
|
|
26
|
+
peek(args)
|
|
27
|
+
return run(args, scripts, trusted)
|
|
28
|
+
except SystemExit as err: # say what the interpreter would say, but before the report
|
|
29
|
+
if not trusted:
|
|
30
|
+
raise
|
|
31
|
+
if err.code is None or isinstance(err.code, int):
|
|
32
|
+
return err.code or 0
|
|
33
|
+
print(err.code, file=sys.stderr)
|
|
34
|
+
return 1
|
|
35
|
+
except BaseException: # the traceback goes before the report, which stays the last stderr line
|
|
36
|
+
if not trusted:
|
|
37
|
+
raise
|
|
38
|
+
traceback.print_exc()
|
|
39
|
+
return 1
|
|
40
|
+
finally:
|
|
41
|
+
if trusted:
|
|
42
|
+
sys.stdout.flush()
|
|
43
|
+
sys.stderr.write("flow-state %s\n" % state.reported())
|
|
44
|
+
sys.stderr.flush()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def peek(args):
|
|
48
|
+
"""Read the feature's state and findings files before anything can return early, so the report
|
|
49
|
+
names the files that are there even after a usage error. Creates nothing."""
|
|
50
|
+
if not args or not plain(args[0]):
|
|
51
|
+
return
|
|
52
|
+
feature = args[0]
|
|
53
|
+
top = git.toplevel()
|
|
54
|
+
if top is not None:
|
|
55
|
+
folder = top / state.STATE_DIR
|
|
56
|
+
state.read_bytes(state.state_path(folder, feature), "state")
|
|
57
|
+
state.read_bytes(state.file_path(folder, feature, "findings"), "findings")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def plain(feature):
|
|
61
|
+
"""True when feature is a plain folder name: not empty, no leading -, no path parts."""
|
|
62
|
+
return (bool(feature) and not feature.startswith("-") and feature not in (".", "..")
|
|
63
|
+
and not any(c in feature for c in "/\\:"))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def run(args, scripts, trusted):
|
|
17
67
|
if len(args) < 2 or args[1] not in COMMANDS or not args[0] or args[0].startswith("-"):
|
|
18
68
|
print(USAGE, file=sys.stderr)
|
|
19
69
|
if len(args) >= 2 and args[0] in COMMANDS:
|
|
@@ -22,12 +72,15 @@ def main(argv=None, scripts=None):
|
|
|
22
72
|
sys.stderr.write(suggest.hint(args[1], COMMANDS))
|
|
23
73
|
return 2
|
|
24
74
|
feature, command = args[0], args[1]
|
|
25
|
-
if
|
|
75
|
+
if not plain(feature):
|
|
26
76
|
print("the feature must be a plain folder name, not %s" % feature, file=sys.stderr)
|
|
27
77
|
return 2
|
|
28
78
|
if len(args) not in ARGS.get(command, (2,)):
|
|
29
79
|
print(USAGE, file=sys.stderr)
|
|
30
80
|
return 2
|
|
81
|
+
if command in RUNNER_ONLY and not trusted:
|
|
82
|
+
print("STOP run the conductor through scripts/flow-trust.py, as the skill says")
|
|
83
|
+
return 1
|
|
31
84
|
scripts = Path(scripts) if scripts else Path.cwd() / "scripts"
|
|
32
85
|
conductor = None
|
|
33
86
|
try:
|
|
@@ -7,11 +7,15 @@ from pathlib import Path
|
|
|
7
7
|
|
|
8
8
|
from feature_flow import git, prompts
|
|
9
9
|
|
|
10
|
+
# feature-flow's own scripts; any other .py file in the same folder is the repo's own code
|
|
11
|
+
SCRIPTS = ("flow.py", "flow-status.py", "gate.py", "floor-guard.py", "flow-view.py", "flow-trust.py")
|
|
12
|
+
|
|
10
13
|
|
|
11
14
|
def _files(scripts):
|
|
12
15
|
package = Path(__file__).resolve().parent
|
|
13
16
|
found = [p for p in package.rglob("*.py") if "__pycache__" not in p.parts]
|
|
14
|
-
|
|
17
|
+
folder = Path(scripts).resolve()
|
|
18
|
+
found += [folder / name for name in SCRIPTS if (folder / name).is_file()]
|
|
15
19
|
for phase in ("build", "review", "review-quality"):
|
|
16
20
|
for folder, names in (("agents", prompts.ROLES), ("guides", prompts.GUIDES)):
|
|
17
21
|
try:
|
|
@@ -9,21 +9,23 @@ import sys
|
|
|
9
9
|
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
|
|
12
|
-
from feature_flow import __version__, cli, install, status, suggest, view
|
|
12
|
+
from feature_flow import __version__, cli, install, 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]
|
|
16
|
+
feature-flow uninstall [target-repo] [--user] [--force] [--dry-run]
|
|
16
17
|
feature-flow status <feature> [--next | --counts | --check | --mermaid [plain] | --json]
|
|
17
18
|
feature-flow view <feature> [--watch] [--no-open] [--out FILE]
|
|
18
19
|
feature-flow reset <feature> [NN]
|
|
19
20
|
feature-flow --version
|
|
20
|
-
install copies the skill, its roles, scripts and guides into a repo; feature-flow install --help says more.
|
|
21
|
+
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
|
+
uninstall removes exactly what install wrote (files you edited are kept); --user removes the personal install in ~/.claude, ~/.agents and ~/.feature-flow.
|
|
21
23
|
status and view read plans/<feature>/ in the current directory, like scripts/flow-status.py and scripts/flow-view.py.
|
|
22
24
|
reset reopens a half-built ticket (or ticket NN), commits that, and clears the run, like scripts/flow.py <feature> reset.
|
|
23
25
|
"""
|
|
24
26
|
|
|
25
27
|
|
|
26
|
-
COMMANDS = ("install", "status", "view", "reset", "--version", "--help")
|
|
28
|
+
COMMANDS = ("install", "uninstall", "status", "view", "reset", "--version", "--help")
|
|
27
29
|
|
|
28
30
|
|
|
29
31
|
def main(argv=None):
|
|
@@ -38,6 +40,8 @@ def main(argv=None):
|
|
|
38
40
|
return 0
|
|
39
41
|
if command == "install":
|
|
40
42
|
return install.main(install.source_root(), rest)
|
|
43
|
+
if command == "uninstall":
|
|
44
|
+
return uninstall.main(rest)
|
|
41
45
|
if command == "status":
|
|
42
46
|
return status.main(rest)
|
|
43
47
|
if command == "view":
|
|
@@ -4,6 +4,7 @@ It sets the limits, retries and review rounds. It judges every phase from the re
|
|
|
4
4
|
caller, and prints exactly one line per command.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import hashlib
|
|
7
8
|
import os
|
|
8
9
|
import re
|
|
9
10
|
import secrets
|
|
@@ -15,6 +16,11 @@ from feature_flow import checks, codehash, floorguard, gate, git, prompts, proof
|
|
|
15
16
|
|
|
16
17
|
NOTE_SEP = "\t" # the state file keeps one line per key, so the review notes are tab separated
|
|
17
18
|
VERDICT = re.compile(r"^REVIEW: (PASS|FAIL)[ \t\r\f\v]*$")
|
|
19
|
+
OLD_OWNER = re.compile(r"^[0-9a-f]{16}$") # a raw token, stored by a run started before 0.5.0
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def owner_hash(token):
|
|
23
|
+
return hashlib.sha256(token.encode("utf-8")).hexdigest()
|
|
18
24
|
|
|
19
25
|
|
|
20
26
|
class Stop(Exception):
|
|
@@ -62,7 +68,11 @@ class Conductor:
|
|
|
62
68
|
self.state_file = state.state_path(folder, feature)
|
|
63
69
|
self.log_file = state.log_path(folder, feature)
|
|
64
70
|
self.findings_file = state.file_path(folder, feature, "findings")
|
|
65
|
-
|
|
71
|
+
state.read_bytes(self.findings_file, "findings") # so a call that never touches it still reports it
|
|
72
|
+
try:
|
|
73
|
+
self.st = state.load(self.state_file, "state")
|
|
74
|
+
except UnicodeDecodeError:
|
|
75
|
+
raise Stop("the state file %s is damaged: it is not UTF-8 text" % self.state_file)
|
|
66
76
|
|
|
67
77
|
# ---- small helpers -------------------------------------------------
|
|
68
78
|
|
|
@@ -76,7 +86,7 @@ class Conductor:
|
|
|
76
86
|
raise Stop("the state file %s is damaged: %s is not a number" % (self.state_file, key))
|
|
77
87
|
|
|
78
88
|
def save(self):
|
|
79
|
-
state.save(self.state_file, self.st)
|
|
89
|
+
state.save(self.state_file, self.st, "state")
|
|
80
90
|
|
|
81
91
|
def log(self, event):
|
|
82
92
|
state.log(self.log_file, self.get("num"), event)
|
|
@@ -321,9 +331,14 @@ class Conductor:
|
|
|
321
331
|
def check_owner(self):
|
|
322
332
|
"""While a session owns the feature, every call must carry its token. Checked before any change."""
|
|
323
333
|
owner = self.get("owner")
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
334
|
+
token = os.environ.get("FLOW_SESSION", "")
|
|
335
|
+
if not owner or owner_hash(token) == owner:
|
|
336
|
+
return
|
|
337
|
+
if OLD_OWNER.match(owner) and token == owner:
|
|
338
|
+
self.st["owner"] = owner_hash(token) # stored as the hash on the next save
|
|
339
|
+
return
|
|
340
|
+
raise Stop("%s is owned by another session (%s). pass its token as FLOW_SESSION, or start this session "
|
|
341
|
+
"with FLOW_TAKEOVER=1 once you are sure the other one is closed" % (self.feature, owner[:8]))
|
|
327
342
|
|
|
328
343
|
def start(self):
|
|
329
344
|
if not self.plan.is_dir():
|
|
@@ -332,9 +347,9 @@ class Conductor:
|
|
|
332
347
|
takeover = bool(owner)
|
|
333
348
|
if owner and os.environ.get("FLOW_TAKEOVER", "0") != "1":
|
|
334
349
|
raise Stop("%s is owned by session %s. if that session is closed or dead, run start with FLOW_TAKEOVER=1"
|
|
335
|
-
% (self.feature, owner))
|
|
350
|
+
% (self.feature, owner[:8]))
|
|
336
351
|
token = secrets.token_hex(8)
|
|
337
|
-
self.st["owner"] = token
|
|
352
|
+
self.st["owner"] = owner_hash(token)
|
|
338
353
|
self.st["session_done"] = 0
|
|
339
354
|
self.save()
|
|
340
355
|
self.log("TAKEOVER" if takeover else "START")
|
|
@@ -374,7 +389,7 @@ class Conductor:
|
|
|
374
389
|
owner = self.get("owner")
|
|
375
390
|
if owner and os.environ.get("FLOW_TAKEOVER", "0") != "1":
|
|
376
391
|
raise Stop("%s is owned by session %s. if that session is closed or dead, run reset with FLOW_TAKEOVER=1"
|
|
377
|
-
% (self.feature, owner))
|
|
392
|
+
% (self.feature, owner[:8]))
|
|
378
393
|
paths = tickets.ticket_files(str(self.plan / (os.environ.get("FLOW_TICKETS") or "tasks")))
|
|
379
394
|
if num is not None:
|
|
380
395
|
chosen = [p for p in paths if tickets.number(p) == num.zfill(2)]
|
|
@@ -393,9 +408,8 @@ class Conductor:
|
|
|
393
408
|
raise Stop("reopened %s but could not commit it. this session must be allowed to run git commit"
|
|
394
409
|
% nums)
|
|
395
410
|
had_run = self.state_file.is_file()
|
|
396
|
-
for path in (self.state_file, self.findings_file):
|
|
397
|
-
|
|
398
|
-
path.unlink()
|
|
411
|
+
for path, kind in ((self.state_file, "state"), (self.findings_file, "findings")):
|
|
412
|
+
state.remove(path, kind)
|
|
399
413
|
if not reopened and not had_run:
|
|
400
414
|
return "OK nothing to reset for %s. run %s %s" % (self.feature, self.invoke, self.feature)
|
|
401
415
|
self.st = {}
|
|
@@ -421,7 +435,7 @@ class Conductor:
|
|
|
421
435
|
self.save()
|
|
422
436
|
return self.pick_ticket()
|
|
423
437
|
if verdict == "fail":
|
|
424
|
-
findings =
|
|
438
|
+
findings = state.read_text(self.findings_file, "findings") or ""
|
|
425
439
|
return self.send_back("quality review" if self.get("review_pass") == "quality" else "spec review",
|
|
426
440
|
findings)
|
|
427
441
|
if self.num("review_attempt") >= self.max_retries:
|
|
@@ -458,7 +472,7 @@ class Conductor:
|
|
|
458
472
|
if match:
|
|
459
473
|
found = match.group(1).lower()
|
|
460
474
|
if found == "fail":
|
|
461
|
-
self.findings_file
|
|
475
|
+
state.write_text(self.findings_file, "\n".join(text.splitlines()[:120]) + "\n", "findings")
|
|
462
476
|
self.st["verdict"] = found or "none"
|
|
463
477
|
self.save()
|
|
464
478
|
self.log("VERDICT-%s" % (found or "none").upper())
|