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.
Files changed (69) hide show
  1. {feature_flow_cli-0.3.0/feature_flow_cli.egg-info → feature_flow_cli-0.5.0}/PKG-INFO +22 -6
  2. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/README.md +21 -5
  3. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/adapters/codex/feature-flow/SKILL.md +23 -11
  4. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow/__init__.py +1 -1
  5. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow/cli.py +56 -3
  6. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow/codehash.py +5 -1
  7. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow/command.py +7 -3
  8. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow/conductor.py +27 -13
  9. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow/install.py +72 -45
  10. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow/prompts.py +39 -4
  11. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow/state.py +48 -6
  12. feature_flow_cli-0.5.0/feature_flow/uninstall.py +235 -0
  13. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0/feature_flow_cli.egg-info}/PKG-INFO +22 -6
  14. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow_cli.egg-info/SOURCES.txt +2 -0
  15. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/guides/build.md +1 -1
  16. feature_flow_cli-0.5.0/scripts/floor-guard.py +29 -0
  17. feature_flow_cli-0.5.0/scripts/flow-status.py +29 -0
  18. feature_flow_cli-0.5.0/scripts/flow-trust.py +367 -0
  19. feature_flow_cli-0.5.0/scripts/flow-view.py +29 -0
  20. feature_flow_cli-0.5.0/scripts/flow.py +29 -0
  21. feature_flow_cli-0.5.0/scripts/gate.py +29 -0
  22. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/skills/feature-flow/SKILL.md +23 -11
  23. feature_flow_cli-0.3.0/scripts/floor-guard.py +0 -14
  24. feature_flow_cli-0.3.0/scripts/flow-status.py +0 -14
  25. feature_flow_cli-0.3.0/scripts/flow-view.py +0 -14
  26. feature_flow_cli-0.3.0/scripts/flow.py +0 -14
  27. feature_flow_cli-0.3.0/scripts/gate.py +0 -14
  28. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/LICENSE +0 -0
  29. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/MANIFEST.in +0 -0
  30. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/adapters/codex/feature-flow/agents/openai.yaml +0 -0
  31. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/agents/feature-planner.md +0 -0
  32. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/agents/plan-reviewer.md +0 -0
  33. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/agents/ticket-builder.md +0 -0
  34. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/agents/ticket-reviewer.md +0 -0
  35. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow/__main__.py +0 -0
  36. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow/checks.py +0 -0
  37. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow/floorguard.py +0 -0
  38. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow/gate.py +0 -0
  39. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow/git.py +0 -0
  40. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow/proc.py +0 -0
  41. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow/proof.py +0 -0
  42. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow/released.py +0 -0
  43. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow/status.py +0 -0
  44. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow/suggest.py +0 -0
  45. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow/tickets.py +0 -0
  46. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow/view.py +0 -0
  47. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow_cli.egg-info/dependency_links.txt +0 -0
  48. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow_cli.egg-info/entry_points.txt +0 -0
  49. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/feature_flow_cli.egg-info/top_level.txt +0 -0
  50. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/guides/brief.md +0 -0
  51. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/guides/debug.md +0 -0
  52. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/guides/plan-review.md +0 -0
  53. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/guides/plan.md +0 -0
  54. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/guides/review-quality.md +0 -0
  55. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/guides/review.md +0 -0
  56. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/guides/show.md +0 -0
  57. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/guides/templates/commands.md +0 -0
  58. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/guides/templates/learnings.md +0 -0
  59. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/guides/templates/map.md +0 -0
  60. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/guides/templates/spec.md +0 -0
  61. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/guides/templates/ticket.md +0 -0
  62. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/guides/templates/ui-mockup.md +0 -0
  63. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/install.py +0 -0
  64. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/install.sh +0 -0
  65. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/pyproject.toml +0 -0
  66. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/scripts/flow-view.html +0 -0
  67. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/setup.cfg +0 -0
  68. {feature_flow_cli-0.3.0 → feature_flow_cli-0.5.0}/skills/architect-review/SKILL.md +0 -0
  69. {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.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 goes to the user folder, but the scripts and `.feature-flow/` stay in the repo, because the skill runs them from there.
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`. 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.
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|next|prompt|verdict <file> the conductor (the skill runs it)
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`. `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.
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 also checks that the code it runs (the `feature_flow` package, `scripts/*.py` and the build and review role and guide files) did not change during a build or review; if it did, the run stops with `STOP flow code changed while building <ticket>: <files>`, and `Floor: allow flow-edit` on the ticket allows it for the build. That check is a tripwire, not a sandbox. The floor guard is pattern matching: it can miss things and it can raise false alarms, and `Floor: allow` is the release valve. The run stops on its own when a ticket stays unresolved, when the smoke test or the gate keeps failing, when a ticket keeps failing review, or when it reaches its phase limit.
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 goes to the user folder, but the scripts and `.feature-flow/` stay in the repo, because the skill runs them from there.
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`. 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.
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|next|prompt|verdict <file> the conductor (the skill runs it)
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`. `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.
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 also checks that the code it runs (the `feature_flow` package, `scripts/*.py` and the build and review role and guide files) did not change during a build or review; if it did, the run stops with `STOP flow code changed while building <ticket>: <files>`, and `Floor: allow flow-edit` on the ticket allows it for the build. That check is a tripwire, not a sandbox. The floor guard is pattern matching: it can miss things and it can raise false alarms, and `Floor: allow` is the release valve. The run stops on its own when a ticket stays unresolved, when the smoke test or the gate keeps failing, when a ticket keeps failing review, or when it reaches its phase limit.
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' python3 scripts/flow.py <feature> start` and check that it prints `PLAN`, and check that `.agents/flow-roles/feature-planner.md` and `.agents/flow-roles/plan-reviewer.md` exist (see below).
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
- - `.agents/flow-roles/ticket-builder.md` and `.agents/flow-roles/ticket-reviewer.md` exist. If not, say to run `uvx feature-flow-cli install . --agent codex` in this repo (or `python3 install.py <repo> --agent codex` from a feature-flow clone).
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' python3 scripts/flow.py <feature> start`. It prints `OK <token>`. Keep the token and put `FLOW_SESSION=<token>` in front of **every** later conductor command, with `FLOW_INVOKE='$feature-flow'`. 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.
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>`. The new session resumes where this one stopped.
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,3 +1,3 @@
1
1
  """Feature Flow: a ticket-graph workflow for coding agents."""
2
2
 
3
- __version__ = "0.3.0"
3
+ __version__ = "0.5.0"
@@ -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
- args = list(sys.argv[1:] if argv is None else argv)
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 feature in (".", "..") or any(c in feature for c in "/\\:"):
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
- found += Path(scripts).resolve().glob("*.py")
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
- self.st = state.load(self.state_file)
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
- if owner and os.environ.get("FLOW_SESSION", "") != owner:
325
- raise Stop("%s is owned by another session (%s). pass its token as FLOW_SESSION, or start this session "
326
- "with FLOW_TAKEOVER=1 once you are sure the other one is closed" % (self.feature, owner))
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
- if path.is_file():
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 = self.findings_file.read_text(encoding="utf-8") if self.findings_file.is_file() else ""
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.write_text("\n".join(text.splitlines()[:120]) + "\n", encoding="utf-8")
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())