feature-flow-cli 0.26.0__tar.gz → 0.28.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.26.0 → feature_flow_cli-0.28.0}/PKG-INFO +19 -4
- feature_flow_cli-0.26.0/feature_flow_cli.egg-info/PKG-INFO → feature_flow_cli-0.28.0/README.md +16 -22
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/__init__.py +1 -1
- feature_flow_cli-0.28.0/feature_flow/codeintel.py +127 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/conductor.py +2 -2
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/evaluate.py +68 -42
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/prompts.py +4 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/scouts.py +7 -2
- feature_flow_cli-0.26.0/README.md → feature_flow_cli-0.28.0/feature_flow_cli.egg-info/PKG-INFO +37 -3
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow_cli.egg-info/SOURCES.txt +6 -0
- feature_flow_cli-0.28.0/feature_flow_cli.egg-info/requires.txt +3 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/brainstorm.md +14 -1
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/templates/brainstorm.md +7 -1
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/packages/forkboard/forkboard/__init__.py +1 -1
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/packages/forkboard/forkboard/assets/base.css +5 -3
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/packages/forkboard/forkboard/assets/editor.js +58 -1
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/packages/forkboard/forkboard/cli.py +5 -2
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/packages/forkboard/forkboard/page.py +32 -2
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/packages/forkboard/forkboard/themes/blueprint.css +1 -0
- feature_flow_cli-0.28.0/packages/forkboard/forkboard/themes/bright.css +29 -0
- feature_flow_cli-0.28.0/packages/forkboard/forkboard/themes/paper.css +35 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/packages/forkboard/forkboard/themes/route.css +1 -0
- feature_flow_cli-0.28.0/packages/forkboard/forkboard/themes/sketch.css +30 -0
- feature_flow_cli-0.28.0/packages/forkboard/forkboard/themes/terminal.css +30 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/pyproject.toml +4 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/LICENSE +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/MANIFEST.in +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/adapters/codex/feature-flow/SKILL.md +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/adapters/codex/feature-flow/agents/openai.yaml +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/agents/feature-planner.md +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/agents/flow-architect.md +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/agents/flow-designer.md +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/agents/flow-lens.md +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/agents/flow-po.md +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/agents/flow-scout.md +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/agents/flow-skeptic.md +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/agents/flow-tester.md +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/agents/flow-web.md +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/agents/plan-reviewer.md +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/agents/ticket-builder.md +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/agents/ticket-reviewer.md +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/__main__.py +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/brainstorm_page.py +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/checks.py +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/cli.py +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/codehash.py +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/command.py +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/floorguard.py +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/gate.py +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/git.py +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/importer.py +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/install.py +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/lanes.py +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/proc.py +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/profiles.py +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/proof.py +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/released.py +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/report.py +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/state.py +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/status.py +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/studio.py +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/suggest.py +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/tickets.py +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/uninstall.py +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/view.py +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow_cli.egg-info/dependency_links.txt +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow_cli.egg-info/entry_points.txt +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow_cli.egg-info/top_level.txt +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/brief.md +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/build.md +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/debug.md +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/lenses/artistic.md +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/lenses/open-minded.md +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/lenses/positive.md +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/lenses/researcher.md +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/lenses/security.md +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/lenses/ux.md +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/plan-review.md +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/plan.md +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/replan.md +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/review-feature.md +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/review-quality.md +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/review-single.md +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/review.md +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/show.md +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/templates/commands.md +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/templates/learnings.md +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/templates/map.md +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/templates/spec.md +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/templates/ticket.md +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/templates/ui-mockup.md +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/test.md +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/install.py +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/install.sh +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/packages/forkboard/forkboard/__main__.py +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/packages/forkboard/forkboard/assets/live.css +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/packages/forkboard/forkboard/assets/live.js +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/packages/forkboard/forkboard/doc.py +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/packages/forkboard/forkboard/graph.py +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/packages/forkboard/forkboard/live.py +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/scripts/floor-guard.py +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/scripts/flow-status.py +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/scripts/flow-trust.py +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/scripts/flow-view.html +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/scripts/flow-view.py +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/scripts/flow.py +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/scripts/gate.py +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/setup.cfg +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/skills/architect-review/SKILL.md +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/skills/automation-design/SKILL.md +0 -0
- {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/skills/feature-flow/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.28.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
|
|
@@ -15,6 +15,8 @@ Classifier: Topic :: Software Development
|
|
|
15
15
|
Requires-Python: >=3.9
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
17
17
|
License-File: LICENSE
|
|
18
|
+
Provides-Extra: codeintel
|
|
19
|
+
Requires-Dist: codecortex>=0.29; extra == "codeintel"
|
|
18
20
|
Dynamic: license-file
|
|
19
21
|
|
|
20
22
|
# Feature Flow
|
|
@@ -42,7 +44,7 @@ One skill, 12 agent roles, a Python conductor, 6 Python scripts (`flow`, `flow-t
|
|
|
42
44
|
[docs/releases.md](docs/releases.md) lists what each release added.
|
|
43
45
|
|
|
44
46
|
- [Quick start](#quick-start) · [How a ticket flows](#how-a-ticket-flows) · [The skill and the roles](#the-skill-and-the-roles) · [Plans and tickets](#plans-and-tickets) · [See the graph](#see-the-graph)
|
|
45
|
-
- [Lanes](#tickets-side-by-side-lanes) · [Scouts](#scouts) · [Questions](#questions-for-the-architect-designer-or-product-owner) · [Acceptance tests](#acceptance-tests-the-tester) · [Fast path](#the-fast-path) · [Time budget](#a-time-budget) · [Eval](#recording-a-run-eval) · [Profiles](#profiles) · [Brainstorm](#brainstorm-before-planning) · [Import](#starting-from-spec-kit-or-openspec)
|
|
47
|
+
- [Lanes](#tickets-side-by-side-lanes) · [Scouts](#scouts) · [codeintel](#codeintel-optional) · [Questions](#questions-for-the-architect-designer-or-product-owner) · [Acceptance tests](#acceptance-tests-the-tester) · [Fast path](#the-fast-path) · [Time budget](#a-time-budget) · [Eval](#recording-a-run-eval) · [Profiles](#profiles) · [Brainstorm](#brainstorm-before-planning) · [Import](#starting-from-spec-kit-or-openspec)
|
|
46
48
|
- [Long features and handoff](#long-features-and-handoff) · [Commands and settings](#commands-and-settings) · [What is tested](#what-is-tested) · [Limits](#limits) · [Upgrading](#upgrading) · [Caution](#caution)
|
|
47
49
|
|
|
48
50
|
## Quick start
|
|
@@ -332,6 +334,17 @@ Before a planner, builder or reviewer starts, the session can spawn cheap helper
|
|
|
332
334
|
|
|
333
335
|
`scouts [lane]` lists `SCOUT <k> <agent> <reply file>` lines (or `NONE`) and clears the last ones' replies; `scout-prompt <k> [lane]` prints one scout's prompt. The session saves each reply into its file, and the phase's `prompt` adds them under `## Scout findings (unverified)`, cut to 60 lines each. A reply saved for one phase never reaches another. Retries and later rounds get no scouts. `FLOW_SCOUTS=off` turns them off.
|
|
334
336
|
|
|
337
|
+
## codeintel (optional)
|
|
338
|
+
|
|
339
|
+
[codeintel](https://github.com/hamilton-sky/codeintel) is a separate code-intelligence tool: one command answers who calls a function, what a change would break, or where the code that does something lives, from a call graph, a language server and semantic search. feature-flow never needs it. When it is installed and has the repository indexed, two roles get a short `## codeintel` section in their prompt:
|
|
340
|
+
|
|
341
|
+
- **Scouts** (`flow-scout`, not `flow-web`) are told to ask it `callers`, `impact` or `search` where grep would take several tries.
|
|
342
|
+
- **Reviewers** (every ticket review and the feature review) are told to run `codeintel query --op changed --target <base>`, which lists each function the change removed, re-signed or rewrote with the callers it did not update.
|
|
343
|
+
|
|
344
|
+
Each of them ends its reply with one line per question, `codeintel: <op> <target>: helped | no answer | wrong: <why>`, and is told to check an answer before relying on it and to fall back to grep. `eval` counts the queries and those lines, and the verdict form asks whether codeintel helped, made no difference or misled the run, with a line for an answer it got wrong: material for a codeintel issue.
|
|
345
|
+
|
|
346
|
+
To set it up: `pip install "feature-flow-cli[codeintel]"` (or `pip install codecortex`), then `codeintel setup --all .` in the repository. feature-flow asks `codeintel doctor --json` which engines are ready and keeps the answer for an hour in `.feature-flow/state/codeintel.json`; with nothing ready, or no `codeintel` on PATH, the prompts are as before. `FLOW_CODEINTEL=off` leaves it out. In a lane, the agents run it in the lane's worktree, where codeintel may not have an index, and then fall back to grep. The two tools are released on their own and neither imports the other.
|
|
347
|
+
|
|
335
348
|
## Questions for the architect, designer or product owner
|
|
336
349
|
|
|
337
350
|
A builder, reviewer or planner that hits a question it should not decide alone asks it instead of guessing, and the flow pauses that job until the right role answers.
|
|
@@ -377,6 +390,7 @@ Every finished run can leave a record of what it cost and what it caught, so a t
|
|
|
377
390
|
- **`plans/<feature>/eval.json`**: tickets, builds, reviews and rework; tickets settled when the plan was approved and never built; every catch by the check that made it (gate, floor guard, Done when, test first, footprint, reviewer, feature review); the follow-ups in `followups.md` and the deferred checks; who drove the run (`driver`: `person` or `agent`); where the run needed its driver (stops, questions, handoffs, resets); active minutes; and tokens.
|
|
378
391
|
- **`plans/<feature>/followups.md`**: what the run left for later, committed as it goes: the `minor` findings of every passing review, the findings a level 2 ticket passed with after its full rounds, and each `needs` check (below) that could not pass on this host.
|
|
379
392
|
- **Tokens** come from Claude Code's own session transcripts (`~/.claude/projects/`, or `CLAUDE_CONFIG_DIR`), never from the agent: every message of the sessions and subagents that worked in this repo during the run, each counted once, by model and, without lanes, split per ticket by time. Codex sessions are not read yet.
|
|
393
|
+
- **codeintel**: when an agent asked [codeintel](#codeintel-optional), `codeintel` in `eval.json` counts the queries by op and what the agents said of the answers (helped, no answer, wrong, and why), and the form below asks whether it helped.
|
|
380
394
|
- **`plans/<feature>/eval-verdict.md`**: a form for you. Was the plan right, how many fixes you made by hand, would you use it again, and for each catch whether it was real or a false alarm. Run `--eval` again and your answers land in `eval.json`. The false-alarm count is the only way to tell whether the checks are worth their cost.
|
|
381
395
|
- **Later:** `feature-flow eval <feature> --later`, a week or two on, lists the commits since the run whose subject says fix, revert, bug or regression on the paths each ticket's `Touches:` names: the defects the run let through.
|
|
382
396
|
|
|
@@ -402,7 +416,7 @@ When an idea is still fuzzy, the session can brainstorm it with you before it wr
|
|
|
402
416
|
|
|
403
417
|
Read only helpers work for the session during a brainstorm: a code scout, a web scout, the architect, designer and product owner, `flow-skeptic`, who attacks each finished option so the session that wrote the options is not the one that judges them, and `flow-lens`, who looks at the options through one lens the session picks for the idea: `positive`, `researcher`, `open-minded`, `artistic`, `ux` or `security` ([guides/lenses/](guides/lenses/)). Each gets its prompt from `brainstorm-prompt <helper> <question file>`, which counts them: `normal` allows 2 scouts, 1 web, 2 consultants, 1 skeptic and 2 lenses per brainstorm, `careful` 3, 2, 3, 1 and 3, and `FLOW_SCOUTS=off` no scouts. Helpers never write the brainstorm and never ask you anything; the session does both.
|
|
404
418
|
|
|
405
|
-
Before you pick, the session shows the brainstorm as a review page: `feature-flow brainstorm-page <feature>` (or `flow-status.py <feature> --brainstorm-page`) writes it beside the brainstorm as one HTML file. It has a sidebar that routes between the sections and shows how far you read, a system, light or dark switch, flowchart diagrams you can edit (drag, add boxes of seven kinds, colour them from 12 colours, rename and delete them, connect them with arrows, undo and redo, with the keyboard too), reset, and copy back as mermaid (the edited diagram once you change it; edits are kept only in that browser), and an arrow animation that stays off until you turn it on. Its look is a style: `route` (the default) or `
|
|
419
|
+
Before you pick, the session shows the brainstorm as a review page: `feature-flow brainstorm-page <feature>` (or `flow-status.py <feature> --brainstorm-page`) writes it beside the brainstorm as one HTML file. It has a sidebar that routes between the sections and shows how far you read, a system, light or dark switch, flowchart diagrams you can edit (arrange them left to right, top to bottom, radial or as a grid, drag, add boxes of seven kinds, colour them from 12 colours, rename and delete them, connect them with arrows, undo and redo, with the keyboard too), reset, and copy back as mermaid (the edited diagram once you change it; edits are kept only in that browser), and an arrow animation that stays off until you turn it on. Its look is a style: `route` (the default), `blueprint`, `sketch`, `paper`, `terminal` or `bright`, picked with `--style`, the brainstorm's `Style:` line or `FLOW_STYLE`; the session picks the one that fits the subject unless you name one, and colours boxes by role. A style is one CSS file of colour, font and box tokens in Forkboard's [themes/](packages/forkboard/forkboard/themes/), so adding one is adding a file. `--fragment` leaves out the html and body tags, for publishing the page as an artifact.
|
|
406
420
|
|
|
407
421
|

|
|
408
422
|
|
|
@@ -493,6 +507,7 @@ The commands in `commands.md` run through `bash -c` on Linux and macOS and throu
|
|
|
493
507
|
| `FLOW_BUDGET` | unset | read by `start`: minutes of active work before fast mode turns on, then a re-plan offer; `off` drops it |
|
|
494
508
|
| `FLOW_BATCH` | `5` | fast mode: most small tickets one builder takes at once; `1` turns batches off |
|
|
495
509
|
| `FLOW_SCOUTS` | `on` | `off` lists no scouts before any phase |
|
|
510
|
+
| `FLOW_CODEINTEL` | `auto` | `off` keeps codeintel out of the scout's and reviewer's prompts; `auto` adds it when `codeintel doctor` reports an engine ready |
|
|
496
511
|
| `FLOW_TAKEOVER` | unset | `1` lets `start` take a feature over from a session that is gone |
|
|
497
512
|
| `FLOW_MAX_RETRIES` | `2` | builds or reviews per step before the run stops |
|
|
498
513
|
| `FLOW_MAX_REVIEW_ROUNDS` | `3` | times a ticket may be sent back before the run stops (a level 1 ticket: once) |
|
|
@@ -505,7 +520,7 @@ The commands in `commands.md` run through `bash -c` on Linux and macOS and throu
|
|
|
505
520
|
| `FLOW_DIR`, `FLOW_TICKETS` | `plans`, `tasks` | where the plans and the ticket folder live |
|
|
506
521
|
| `FLOW_NO_OPEN` | unset | `1` makes `flow-view.py` never open a browser |
|
|
507
522
|
| `FLOW_WATCH_SECONDS` | `3` | how often `flow-view.py --watch` rewrites the page |
|
|
508
|
-
| `FLOW_STYLE` | `route` | the brainstorm page's look when the brainstorm has no `Style:` line: `route` or `
|
|
523
|
+
| `FLOW_STYLE` | `route` | the brainstorm page's look when the brainstorm has no `Style:` line: `route`, `blueprint`, `sketch`, `paper`, `terminal` or `bright` (Forkboard's themes) |
|
|
509
524
|
|
|
510
525
|
The run also stops on its own after a number of phases (`BUILD` and `REVIEW` hand-outs, the spec review and the quality review each counting as one, a single review counting as one) that grows with the ticket count, recomputed at every pick, so a loop of failures cannot go on forever.
|
|
511
526
|
|
feature_flow_cli-0.26.0/feature_flow_cli.egg-info/PKG-INFO → feature_flow_cli-0.28.0/README.md
RENAMED
|
@@ -1,22 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: feature-flow-cli
|
|
3
|
-
Version: 0.26.0
|
|
4
|
-
Summary: A ticket-graph workflow for coding agents: installs the feature-flow skill into a repo
|
|
5
|
-
License-Expression: MIT
|
|
6
|
-
Project-URL: Homepage, https://github.com/hamilton-sky/feature-flow
|
|
7
|
-
Project-URL: Source, https://github.com/hamilton-sky/feature-flow
|
|
8
|
-
Keywords: claude-code,codex,agents,workflow,tickets
|
|
9
|
-
Classifier: Development Status :: 3 - Alpha
|
|
10
|
-
Classifier: Environment :: Console
|
|
11
|
-
Classifier: Intended Audience :: Developers
|
|
12
|
-
Classifier: Operating System :: OS Independent
|
|
13
|
-
Classifier: Programming Language :: Python :: 3
|
|
14
|
-
Classifier: Topic :: Software Development
|
|
15
|
-
Requires-Python: >=3.9
|
|
16
|
-
Description-Content-Type: text/markdown
|
|
17
|
-
License-File: LICENSE
|
|
18
|
-
Dynamic: license-file
|
|
19
|
-
|
|
20
1
|
# Feature Flow
|
|
21
2
|
|
|
22
3
|
**A ticket-graph workflow for coding agents.** Works with Claude Code and Codex.
|
|
@@ -42,7 +23,7 @@ One skill, 12 agent roles, a Python conductor, 6 Python scripts (`flow`, `flow-t
|
|
|
42
23
|
[docs/releases.md](docs/releases.md) lists what each release added.
|
|
43
24
|
|
|
44
25
|
- [Quick start](#quick-start) · [How a ticket flows](#how-a-ticket-flows) · [The skill and the roles](#the-skill-and-the-roles) · [Plans and tickets](#plans-and-tickets) · [See the graph](#see-the-graph)
|
|
45
|
-
- [Lanes](#tickets-side-by-side-lanes) · [Scouts](#scouts) · [Questions](#questions-for-the-architect-designer-or-product-owner) · [Acceptance tests](#acceptance-tests-the-tester) · [Fast path](#the-fast-path) · [Time budget](#a-time-budget) · [Eval](#recording-a-run-eval) · [Profiles](#profiles) · [Brainstorm](#brainstorm-before-planning) · [Import](#starting-from-spec-kit-or-openspec)
|
|
26
|
+
- [Lanes](#tickets-side-by-side-lanes) · [Scouts](#scouts) · [codeintel](#codeintel-optional) · [Questions](#questions-for-the-architect-designer-or-product-owner) · [Acceptance tests](#acceptance-tests-the-tester) · [Fast path](#the-fast-path) · [Time budget](#a-time-budget) · [Eval](#recording-a-run-eval) · [Profiles](#profiles) · [Brainstorm](#brainstorm-before-planning) · [Import](#starting-from-spec-kit-or-openspec)
|
|
46
27
|
- [Long features and handoff](#long-features-and-handoff) · [Commands and settings](#commands-and-settings) · [What is tested](#what-is-tested) · [Limits](#limits) · [Upgrading](#upgrading) · [Caution](#caution)
|
|
47
28
|
|
|
48
29
|
## Quick start
|
|
@@ -332,6 +313,17 @@ Before a planner, builder or reviewer starts, the session can spawn cheap helper
|
|
|
332
313
|
|
|
333
314
|
`scouts [lane]` lists `SCOUT <k> <agent> <reply file>` lines (or `NONE`) and clears the last ones' replies; `scout-prompt <k> [lane]` prints one scout's prompt. The session saves each reply into its file, and the phase's `prompt` adds them under `## Scout findings (unverified)`, cut to 60 lines each. A reply saved for one phase never reaches another. Retries and later rounds get no scouts. `FLOW_SCOUTS=off` turns them off.
|
|
334
315
|
|
|
316
|
+
## codeintel (optional)
|
|
317
|
+
|
|
318
|
+
[codeintel](https://github.com/hamilton-sky/codeintel) is a separate code-intelligence tool: one command answers who calls a function, what a change would break, or where the code that does something lives, from a call graph, a language server and semantic search. feature-flow never needs it. When it is installed and has the repository indexed, two roles get a short `## codeintel` section in their prompt:
|
|
319
|
+
|
|
320
|
+
- **Scouts** (`flow-scout`, not `flow-web`) are told to ask it `callers`, `impact` or `search` where grep would take several tries.
|
|
321
|
+
- **Reviewers** (every ticket review and the feature review) are told to run `codeintel query --op changed --target <base>`, which lists each function the change removed, re-signed or rewrote with the callers it did not update.
|
|
322
|
+
|
|
323
|
+
Each of them ends its reply with one line per question, `codeintel: <op> <target>: helped | no answer | wrong: <why>`, and is told to check an answer before relying on it and to fall back to grep. `eval` counts the queries and those lines, and the verdict form asks whether codeintel helped, made no difference or misled the run, with a line for an answer it got wrong: material for a codeintel issue.
|
|
324
|
+
|
|
325
|
+
To set it up: `pip install "feature-flow-cli[codeintel]"` (or `pip install codecortex`), then `codeintel setup --all .` in the repository. feature-flow asks `codeintel doctor --json` which engines are ready and keeps the answer for an hour in `.feature-flow/state/codeintel.json`; with nothing ready, or no `codeintel` on PATH, the prompts are as before. `FLOW_CODEINTEL=off` leaves it out. In a lane, the agents run it in the lane's worktree, where codeintel may not have an index, and then fall back to grep. The two tools are released on their own and neither imports the other.
|
|
326
|
+
|
|
335
327
|
## Questions for the architect, designer or product owner
|
|
336
328
|
|
|
337
329
|
A builder, reviewer or planner that hits a question it should not decide alone asks it instead of guessing, and the flow pauses that job until the right role answers.
|
|
@@ -377,6 +369,7 @@ Every finished run can leave a record of what it cost and what it caught, so a t
|
|
|
377
369
|
- **`plans/<feature>/eval.json`**: tickets, builds, reviews and rework; tickets settled when the plan was approved and never built; every catch by the check that made it (gate, floor guard, Done when, test first, footprint, reviewer, feature review); the follow-ups in `followups.md` and the deferred checks; who drove the run (`driver`: `person` or `agent`); where the run needed its driver (stops, questions, handoffs, resets); active minutes; and tokens.
|
|
378
370
|
- **`plans/<feature>/followups.md`**: what the run left for later, committed as it goes: the `minor` findings of every passing review, the findings a level 2 ticket passed with after its full rounds, and each `needs` check (below) that could not pass on this host.
|
|
379
371
|
- **Tokens** come from Claude Code's own session transcripts (`~/.claude/projects/`, or `CLAUDE_CONFIG_DIR`), never from the agent: every message of the sessions and subagents that worked in this repo during the run, each counted once, by model and, without lanes, split per ticket by time. Codex sessions are not read yet.
|
|
372
|
+
- **codeintel**: when an agent asked [codeintel](#codeintel-optional), `codeintel` in `eval.json` counts the queries by op and what the agents said of the answers (helped, no answer, wrong, and why), and the form below asks whether it helped.
|
|
380
373
|
- **`plans/<feature>/eval-verdict.md`**: a form for you. Was the plan right, how many fixes you made by hand, would you use it again, and for each catch whether it was real or a false alarm. Run `--eval` again and your answers land in `eval.json`. The false-alarm count is the only way to tell whether the checks are worth their cost.
|
|
381
374
|
- **Later:** `feature-flow eval <feature> --later`, a week or two on, lists the commits since the run whose subject says fix, revert, bug or regression on the paths each ticket's `Touches:` names: the defects the run let through.
|
|
382
375
|
|
|
@@ -402,7 +395,7 @@ When an idea is still fuzzy, the session can brainstorm it with you before it wr
|
|
|
402
395
|
|
|
403
396
|
Read only helpers work for the session during a brainstorm: a code scout, a web scout, the architect, designer and product owner, `flow-skeptic`, who attacks each finished option so the session that wrote the options is not the one that judges them, and `flow-lens`, who looks at the options through one lens the session picks for the idea: `positive`, `researcher`, `open-minded`, `artistic`, `ux` or `security` ([guides/lenses/](guides/lenses/)). Each gets its prompt from `brainstorm-prompt <helper> <question file>`, which counts them: `normal` allows 2 scouts, 1 web, 2 consultants, 1 skeptic and 2 lenses per brainstorm, `careful` 3, 2, 3, 1 and 3, and `FLOW_SCOUTS=off` no scouts. Helpers never write the brainstorm and never ask you anything; the session does both.
|
|
404
397
|
|
|
405
|
-
Before you pick, the session shows the brainstorm as a review page: `feature-flow brainstorm-page <feature>` (or `flow-status.py <feature> --brainstorm-page`) writes it beside the brainstorm as one HTML file. It has a sidebar that routes between the sections and shows how far you read, a system, light or dark switch, flowchart diagrams you can edit (drag, add boxes of seven kinds, colour them from 12 colours, rename and delete them, connect them with arrows, undo and redo, with the keyboard too), reset, and copy back as mermaid (the edited diagram once you change it; edits are kept only in that browser), and an arrow animation that stays off until you turn it on. Its look is a style: `route` (the default) or `
|
|
398
|
+
Before you pick, the session shows the brainstorm as a review page: `feature-flow brainstorm-page <feature>` (or `flow-status.py <feature> --brainstorm-page`) writes it beside the brainstorm as one HTML file. It has a sidebar that routes between the sections and shows how far you read, a system, light or dark switch, flowchart diagrams you can edit (arrange them left to right, top to bottom, radial or as a grid, drag, add boxes of seven kinds, colour them from 12 colours, rename and delete them, connect them with arrows, undo and redo, with the keyboard too), reset, and copy back as mermaid (the edited diagram once you change it; edits are kept only in that browser), and an arrow animation that stays off until you turn it on. Its look is a style: `route` (the default), `blueprint`, `sketch`, `paper`, `terminal` or `bright`, picked with `--style`, the brainstorm's `Style:` line or `FLOW_STYLE`; the session picks the one that fits the subject unless you name one, and colours boxes by role. A style is one CSS file of colour, font and box tokens in Forkboard's [themes/](packages/forkboard/forkboard/themes/), so adding one is adding a file. `--fragment` leaves out the html and body tags, for publishing the page as an artifact.
|
|
406
399
|
|
|
407
400
|

|
|
408
401
|
|
|
@@ -493,6 +486,7 @@ The commands in `commands.md` run through `bash -c` on Linux and macOS and throu
|
|
|
493
486
|
| `FLOW_BUDGET` | unset | read by `start`: minutes of active work before fast mode turns on, then a re-plan offer; `off` drops it |
|
|
494
487
|
| `FLOW_BATCH` | `5` | fast mode: most small tickets one builder takes at once; `1` turns batches off |
|
|
495
488
|
| `FLOW_SCOUTS` | `on` | `off` lists no scouts before any phase |
|
|
489
|
+
| `FLOW_CODEINTEL` | `auto` | `off` keeps codeintel out of the scout's and reviewer's prompts; `auto` adds it when `codeintel doctor` reports an engine ready |
|
|
496
490
|
| `FLOW_TAKEOVER` | unset | `1` lets `start` take a feature over from a session that is gone |
|
|
497
491
|
| `FLOW_MAX_RETRIES` | `2` | builds or reviews per step before the run stops |
|
|
498
492
|
| `FLOW_MAX_REVIEW_ROUNDS` | `3` | times a ticket may be sent back before the run stops (a level 1 ticket: once) |
|
|
@@ -505,7 +499,7 @@ The commands in `commands.md` run through `bash -c` on Linux and macOS and throu
|
|
|
505
499
|
| `FLOW_DIR`, `FLOW_TICKETS` | `plans`, `tasks` | where the plans and the ticket folder live |
|
|
506
500
|
| `FLOW_NO_OPEN` | unset | `1` makes `flow-view.py` never open a browser |
|
|
507
501
|
| `FLOW_WATCH_SECONDS` | `3` | how often `flow-view.py --watch` rewrites the page |
|
|
508
|
-
| `FLOW_STYLE` | `route` | the brainstorm page's look when the brainstorm has no `Style:` line: `route` or `
|
|
502
|
+
| `FLOW_STYLE` | `route` | the brainstorm page's look when the brainstorm has no `Style:` line: `route`, `blueprint`, `sketch`, `paper`, `terminal` or `bright` (Forkboard's themes) |
|
|
509
503
|
|
|
510
504
|
The run also stops on its own after a number of phases (`BUILD` and `REVIEW` hand-outs, the spec review and the quality review each counting as one, a single review counting as one) that grows with the ticket count, recomputed at every pick, so a loop of failures cannot go on forever.
|
|
511
505
|
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""codeintel: an optional code-intelligence tool the scouts and reviewers may ask, when it is installed.
|
|
2
|
+
|
|
3
|
+
feature-flow never imports or installs it (`pip install feature-flow-cli[codeintel]` does). When the `codeintel`
|
|
4
|
+
command is on PATH and its `doctor` says an engine is ready for this repository, the scout's and the reviewer's
|
|
5
|
+
prompts get a short section on when to ask it, and each agent ends its reply with one line per question it asked.
|
|
6
|
+
`feature-flow eval` counts the questions and those lines, so a run says whether codeintel helped. The doctor's
|
|
7
|
+
answer is kept for an hour in the state folder. FLOW_CODEINTEL=off leaves codeintel out.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import shutil
|
|
14
|
+
import subprocess
|
|
15
|
+
import time
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from feature_flow import state
|
|
19
|
+
|
|
20
|
+
CACHE = "codeintel.json"
|
|
21
|
+
FRESH = 3600 # seconds the doctor's answer is kept
|
|
22
|
+
TIMEOUT = 30 # seconds the doctor may take
|
|
23
|
+
OPS = {"graph": "callers, callees, impact, changed", "semantic": "search", "lsp": "symbol"}
|
|
24
|
+
VERDICTS = ("helped", "no answer", "wrong")
|
|
25
|
+
LINE = re.compile(r"^[\s>*-]*codeintel: (?!<)(\S+) (.+?): (helped|no answer|wrong)\b[:,]?\s*(.*)$")
|
|
26
|
+
QUERY = re.compile(r"\bcodeintel\s+query\b.*?--op[=\s]+([\w-]+)")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def ready(repo="."):
|
|
30
|
+
"""The engines codeintel has ready for repo, such as ["graph", "semantic"]; [] when it is off or missing."""
|
|
31
|
+
if os.environ.get("FLOW_CODEINTEL", "auto") == "off":
|
|
32
|
+
return []
|
|
33
|
+
exe = shutil.which("codeintel")
|
|
34
|
+
if exe is None:
|
|
35
|
+
return []
|
|
36
|
+
top = Path(repo).resolve()
|
|
37
|
+
cache = top / state.STATE_DIR / CACHE
|
|
38
|
+
try:
|
|
39
|
+
saved = json.loads(cache.read_text(encoding="utf-8"))
|
|
40
|
+
if saved.get("exe") == exe and 0 <= time.time() - float(saved.get("checked", 0)) < FRESH:
|
|
41
|
+
return [e for e in saved.get("ready") or [] if e in OPS]
|
|
42
|
+
except (OSError, ValueError, TypeError, AttributeError):
|
|
43
|
+
pass
|
|
44
|
+
found = probe(exe, top)
|
|
45
|
+
try:
|
|
46
|
+
cache.parent.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
cache.write_text(json.dumps({"exe": exe, "checked": time.time(), "ready": found}) + "\n", encoding="utf-8")
|
|
48
|
+
except OSError:
|
|
49
|
+
pass
|
|
50
|
+
return found
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def probe(exe, top):
|
|
54
|
+
"""Ask `codeintel doctor --json` which engines are installed, runnable and have top indexed."""
|
|
55
|
+
try:
|
|
56
|
+
result = subprocess.run([exe, "doctor", "--json", str(top)], cwd=str(top), stdin=subprocess.DEVNULL,
|
|
57
|
+
capture_output=True, text=True, timeout=TIMEOUT)
|
|
58
|
+
engines = json.loads(result.stdout).get("engines") or {}
|
|
59
|
+
return sorted(name for name, row in engines.items()
|
|
60
|
+
if name in OPS and isinstance(row, dict) and row.get("status") == "ok")
|
|
61
|
+
except (OSError, subprocess.SubprocessError, ValueError, AttributeError):
|
|
62
|
+
return []
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def note(role, repo=".", base=None):
|
|
66
|
+
"""The section a scout's or reviewer's prompt gets when codeintel is ready, else ""."""
|
|
67
|
+
engines = ready(repo)
|
|
68
|
+
if not engines:
|
|
69
|
+
return ""
|
|
70
|
+
ops = "; ".join("`%s` (%s engine)" % (OPS[e].replace(", ", "`, `"), e) for e in engines)
|
|
71
|
+
if role == "reviewer" and "graph" in engines:
|
|
72
|
+
ask = ("`codeintel query --op changed --target %s` lists each function this change removed, re-signed or "
|
|
73
|
+
"rewrote, with the callers it did not update: check those callers." % (base or "<base commit>"))
|
|
74
|
+
else:
|
|
75
|
+
ask = ("Ask it who calls a function or what a change would break (`--op callers` or `--op impact` with "
|
|
76
|
+
"`--target <name>`), or where the code that does something lives (`--op search --target \"<what it "
|
|
77
|
+
"does>\"`).")
|
|
78
|
+
return ("\n## codeintel\n\ncodeintel, a code-intelligence tool, is installed for this repository. Where grep "
|
|
79
|
+
"would take several tries, ask it once with `codeintel query --op <op> --target <name>`, a read only "
|
|
80
|
+
"command. Ready here: %s. %s Its answers can be wrong, empty or older than the code: check one before you "
|
|
81
|
+
"rely on it, and fall back to grep.\n\nAt the end of your reply, before any last verdict line, add one "
|
|
82
|
+
"line for each codeintel query you ran, so the run can judge the tool:\n\n"
|
|
83
|
+
" codeintel: <op> <target>: helped | no answer | wrong: <what it got wrong>\n" % (ops, ask))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# ---- what a run's transcripts say about codeintel, for eval ----------------------------
|
|
87
|
+
|
|
88
|
+
def use(records):
|
|
89
|
+
"""{"queries": {op: n}, "said": {verdict: n}, "wrong": [...]} from transcript records (dicts), or None
|
|
90
|
+
when codeintel was never asked."""
|
|
91
|
+
queries, said, wrong, seen = {}, {}, [], set()
|
|
92
|
+
for item in records:
|
|
93
|
+
message = item.get("message") if isinstance(item, dict) else None
|
|
94
|
+
blocks = message.get("content") if isinstance(message, dict) else None
|
|
95
|
+
if not isinstance(blocks, list):
|
|
96
|
+
continue
|
|
97
|
+
for block in blocks:
|
|
98
|
+
if not isinstance(block, dict):
|
|
99
|
+
continue
|
|
100
|
+
if block.get("type") == "tool_use":
|
|
101
|
+
op = _asked(block)
|
|
102
|
+
if op and ("tool", block.get("id")) not in seen:
|
|
103
|
+
seen.add(("tool", block.get("id")))
|
|
104
|
+
queries[op] = queries.get(op, 0) + 1
|
|
105
|
+
elif block.get("type") == "text" and "codeintel:" in str(block.get("text")):
|
|
106
|
+
for line in str(block["text"]).splitlines():
|
|
107
|
+
found = LINE.match(line)
|
|
108
|
+
if not found or ("line", line.strip()) in seen:
|
|
109
|
+
continue
|
|
110
|
+
seen.add(("line", line.strip()))
|
|
111
|
+
op, target, verdict, why = found.groups()
|
|
112
|
+
said[verdict] = said.get(verdict, 0) + 1
|
|
113
|
+
if verdict == "wrong" and len(wrong) < 20:
|
|
114
|
+
wrong.append({"op": op, "target": target.strip("`"), "why": why.strip()})
|
|
115
|
+
if not queries and not said:
|
|
116
|
+
return None
|
|
117
|
+
return {"queries": queries, "said": said, "wrong": wrong}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _asked(block):
|
|
121
|
+
"""The op of a codeintel query made by this tool call, "" when it is not one."""
|
|
122
|
+
name = str(block.get("name") or "")
|
|
123
|
+
args = block.get("input") if isinstance(block.get("input"), dict) else {}
|
|
124
|
+
if "codeintel" in name.lower():
|
|
125
|
+
return str(args.get("op") or name.rsplit("__", 1)[-1])
|
|
126
|
+
found = QUERY.search(str(args.get("command") or ""))
|
|
127
|
+
return found.group(1) if found else ""
|
|
@@ -1668,7 +1668,7 @@ class Conductor:
|
|
|
1668
1668
|
about = ("The user's session is brainstorming how to build the feature `%s`, before it is planned: no brief "
|
|
1669
1669
|
"and no plan exist yet." % self.feature)
|
|
1670
1670
|
if group in ("scout", "web"):
|
|
1671
|
-
out = scouts.prompt(role_text, "planner", about + "\n\n" + question)
|
|
1671
|
+
out = scouts.prompt(role_text, "planner", about + "\n\n" + question, codeintel=group == "scout")
|
|
1672
1672
|
elif group == "lens":
|
|
1673
1673
|
out = ("%s\n\n---\n\nYour lens is `%s`. Its guide follows.\n\n%s\n\n---\n\n%s The options it drafted "
|
|
1674
1674
|
"follow. Read the repository.\n\n%s\n\nDo not edit any file. Your final reply must end with exactly "
|
|
@@ -1766,7 +1766,7 @@ class Conductor:
|
|
|
1766
1766
|
role_text = prompts.role(self.scripts, agent + ".md")
|
|
1767
1767
|
except FileNotFoundError as err:
|
|
1768
1768
|
raise Stop(str(err))
|
|
1769
|
-
return scouts.prompt(role_text, role, question, workdir)
|
|
1769
|
+
return scouts.prompt(role_text, role, question, workdir, codeintel=agent == "flow-scout")
|
|
1770
1770
|
|
|
1771
1771
|
def scout_findings(self):
|
|
1772
1772
|
"""The scout replies saved for the phase handed out last, as a section for its prompt, or ""."""
|
|
@@ -5,6 +5,8 @@ Reads the run log and state in .feature-flow/state/, the plan's tickets, and, wh
|
|
|
5
5
|
Claude Code session transcripts of this repo (for tokens). Writes <out>/eval.json, and the first time
|
|
6
6
|
<out>/eval-verdict.md, a short form you fill in: was the plan right, what did you fix by hand, and was
|
|
7
7
|
each catch real or a false alarm. Run it again after filling the form and eval.json carries your answers.
|
|
8
|
+
When the agents asked codeintel (feature_flow/codeintel.py), the record counts their queries and what they said
|
|
9
|
+
of each answer, and the form asks whether it helped.
|
|
8
10
|
--later also looks at the commits made after the run for fixes and reverts on the files each ticket
|
|
9
11
|
touched: run it a week or two after the feature is done. <out> is plans/<feature>/ unless --out is given,
|
|
10
12
|
so the record is committed with the plan and survives a cloud session. Nothing is sent anywhere.
|
|
@@ -18,7 +20,7 @@ import time
|
|
|
18
20
|
from datetime import datetime, timezone
|
|
19
21
|
from pathlib import Path
|
|
20
22
|
|
|
21
|
-
from feature_flow import __version__, git, lanes, report, state, suggest, tickets
|
|
23
|
+
from feature_flow import __version__, codeintel, git, lanes, report, state, suggest, tickets
|
|
22
24
|
|
|
23
25
|
USAGE = "usage: feature-flow eval <feature> [--later] [--out DIR] [--claude-home DIR]"
|
|
24
26
|
CATCHES = report.FAILS + ("FOOTPRINT-FAIL", "FEATURE-FAIL", "ACCEPT-FAIL")
|
|
@@ -137,20 +139,15 @@ def _inside(cwd, top):
|
|
|
137
139
|
return False
|
|
138
140
|
|
|
139
141
|
|
|
140
|
-
def
|
|
141
|
-
"""
|
|
142
|
-
|
|
143
|
-
time slice: a record goes to the ticket of the last log line before it (approximate; with lanes
|
|
144
|
-
the slices overlap, so no split is given). None when there is nothing to read."""
|
|
142
|
+
def records(home, top, window):
|
|
143
|
+
"""(file, record) for each transcript record written in this repo during the run, from Claude Code's session
|
|
144
|
+
transcripts (and their subagents'). Nothing when there is no window or no transcripts."""
|
|
145
145
|
projects = Path(home) / "projects"
|
|
146
146
|
if window is None or not projects.is_dir():
|
|
147
|
-
return
|
|
147
|
+
return
|
|
148
148
|
start, end = window
|
|
149
149
|
since = start.timestamp() - 60
|
|
150
150
|
top = Path(top).resolve()
|
|
151
|
-
marks = sorted((t, num) for t, num, _, _ in rows if t is not None)
|
|
152
|
-
seen, total, models, per = set(), {}, {}, {}
|
|
153
|
-
files = 0
|
|
154
151
|
for path in projects.rglob("*.jsonl"):
|
|
155
152
|
try:
|
|
156
153
|
if path.stat().st_mtime < since:
|
|
@@ -158,46 +155,59 @@ def tokens(home, top, window, rows):
|
|
|
158
155
|
handle = open(path, encoding="utf-8", errors="replace")
|
|
159
156
|
except OSError:
|
|
160
157
|
continue
|
|
161
|
-
used = False
|
|
162
158
|
with handle:
|
|
163
159
|
for line in handle:
|
|
164
|
-
if '"
|
|
160
|
+
if '"message"' not in line:
|
|
165
161
|
continue
|
|
166
162
|
try:
|
|
167
163
|
item = json.loads(line)
|
|
168
164
|
except ValueError:
|
|
169
165
|
continue
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
if not
|
|
166
|
+
if not isinstance(item, dict) or not isinstance(item.get("message"), dict):
|
|
167
|
+
continue
|
|
168
|
+
if not _inside(item.get("cwd") or "", top):
|
|
173
169
|
continue
|
|
174
170
|
when = parse_iso(item.get("timestamp"))
|
|
175
171
|
if when is None or when < start or when > end:
|
|
176
172
|
continue
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
173
|
+
yield path, item
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def tokens(found, rows):
|
|
177
|
+
"""Token use of the transcript records found, summed from their usage records, each message counted once.
|
|
178
|
+
Split per ticket by time slice: a record goes to the ticket of the last log line before it (approximate; with
|
|
179
|
+
lanes the slices overlap, so no split is given). None when there is nothing to read."""
|
|
180
|
+
marks = sorted((t, num) for t, num, _, _ in rows if t is not None)
|
|
181
|
+
seen, total, models, per, files = set(), {}, {}, {}, set()
|
|
182
|
+
for path, item in found:
|
|
183
|
+
message = item["message"]
|
|
184
|
+
usage = message.get("usage")
|
|
185
|
+
if not isinstance(usage, dict):
|
|
186
|
+
continue
|
|
187
|
+
key = (message.get("id"), item.get("requestId"))
|
|
188
|
+
if key != (None, None):
|
|
189
|
+
if key in seen:
|
|
190
|
+
continue
|
|
191
|
+
seen.add(key)
|
|
192
|
+
files.add(path)
|
|
193
|
+
counts = {
|
|
194
|
+
"input": int(usage.get("input_tokens") or 0),
|
|
195
|
+
"output": int(usage.get("output_tokens") or 0),
|
|
196
|
+
"cache_read": int(usage.get("cache_read_input_tokens") or 0),
|
|
197
|
+
"cache_write": int(usage.get("cache_creation_input_tokens") or 0),
|
|
198
|
+
}
|
|
199
|
+
add(total, counts)
|
|
200
|
+
add(models.setdefault(message.get("model") or "unknown", {}), counts)
|
|
201
|
+
when = parse_iso(item.get("timestamp"))
|
|
202
|
+
num = "-"
|
|
203
|
+
for t, n in marks:
|
|
204
|
+
if t > when:
|
|
205
|
+
break
|
|
206
|
+
num = n
|
|
207
|
+
add(per.setdefault(num, {}), counts)
|
|
198
208
|
if not total:
|
|
199
209
|
return None
|
|
200
|
-
return {"source": "claude-code transcripts", "files": files, "total": total, "by_model": models,
|
|
210
|
+
return {"source": "claude-code transcripts", "files": len(files), "total": total, "by_model": models,
|
|
201
211
|
"by_ticket": per, "by_ticket_note": "approximate: split by the time between log lines"}
|
|
202
212
|
|
|
203
213
|
|
|
@@ -217,7 +227,7 @@ def add(into, counts):
|
|
|
217
227
|
|
|
218
228
|
# ---- the human verdict ---------------------------------------------------------------
|
|
219
229
|
|
|
220
|
-
def verdict_template(feature, catches, driven_by="person"):
|
|
230
|
+
def verdict_template(feature, catches, driven_by="person", asked_codeintel=False):
|
|
221
231
|
minutes = ("minutes-of-your-time: (roughly, answering questions and reviewing)" if driven_by != "agent" else
|
|
222
232
|
"minutes-of-your-time: (an agent drove this run: only the minutes you spent yourself, 0 if none)")
|
|
223
233
|
lines = [
|
|
@@ -230,6 +240,11 @@ def verdict_template(feature, catches, driven_by="person"):
|
|
|
230
240
|
"would-use-again: (yes | no)",
|
|
231
241
|
minutes,
|
|
232
242
|
"note: ",
|
|
243
|
+
]
|
|
244
|
+
if asked_codeintel:
|
|
245
|
+
lines += ["codeintel: (helped | no difference | misled: did codeintel's answers help this run?)",
|
|
246
|
+
"codeintel-note: (an answer it got wrong or could not give, for a codeintel issue)"]
|
|
247
|
+
lines += [
|
|
233
248
|
"",
|
|
234
249
|
"## Catches",
|
|
235
250
|
"",
|
|
@@ -256,7 +271,8 @@ def read_verdict(path):
|
|
|
256
271
|
word = re.match(r"[a-z]*", value.lower()).group(0)
|
|
257
272
|
if word in ("real", "false"):
|
|
258
273
|
catches[key] = word
|
|
259
|
-
elif key in ("plan", "hand-fixes", "would-use-again", "minutes-of-your-time", "note"
|
|
274
|
+
elif key in ("plan", "hand-fixes", "would-use-again", "minutes-of-your-time", "note", "codeintel",
|
|
275
|
+
"codeintel-note"):
|
|
260
276
|
answers[key.replace("-", "_")] = value
|
|
261
277
|
if "plan" in answers and answers["plan"].split()[0].lower() not in PLAN_ANSWERS:
|
|
262
278
|
answers["plan_unrecognised"] = answers.pop("plan")
|
|
@@ -320,6 +336,7 @@ def build(feature, top, out_dir, home, with_later):
|
|
|
320
336
|
spent = run_state.get("spent", "")
|
|
321
337
|
plan_dir = Path(top) / os.environ.get("FLOW_DIR", "plans") / feature
|
|
322
338
|
driven_by = driver(rows, run_state)
|
|
339
|
+
found = list(records(home, top, window))
|
|
323
340
|
totals["settled_at_plan"] = settled_at_plan(plan_dir, per)
|
|
324
341
|
totals["followups"] = followups(plan_dir)
|
|
325
342
|
record = {
|
|
@@ -333,7 +350,8 @@ def build(feature, top, out_dir, home, with_later):
|
|
|
333
350
|
"driver": driven_by,
|
|
334
351
|
"brainstorm": brainstorm(plan_dir, rows),
|
|
335
352
|
"you": human,
|
|
336
|
-
"tokens": tokens(
|
|
353
|
+
"tokens": tokens(found, rows),
|
|
354
|
+
"codeintel": codeintel.use(item for _, item in found),
|
|
337
355
|
"verdict": read_verdict(out_dir / VERDICT_FILE),
|
|
338
356
|
}
|
|
339
357
|
if record["tokens"] and totals["lanes"]:
|
|
@@ -369,6 +387,12 @@ def render(record, out_dir, wrote_form):
|
|
|
369
387
|
else:
|
|
370
388
|
lines.append(" tokens: not found (no Claude Code transcripts for this repo in the run window,"
|
|
371
389
|
" or a log from before 0.15.0)")
|
|
390
|
+
ci = record.get("codeintel")
|
|
391
|
+
if ci:
|
|
392
|
+
lines.append(" codeintel: %d quer%s (%s); the agents said %s" % (
|
|
393
|
+
sum(ci["queries"].values()), "y" if sum(ci["queries"].values()) == 1 else "ies",
|
|
394
|
+
", ".join("%s x%d" % kv for kv in sorted(ci["queries"].items())) or "none seen",
|
|
395
|
+
", ".join("%s x%d" % (v, ci["said"][v]) for v in codeintel.VERDICTS if ci["said"].get(v)) or "nothing"))
|
|
372
396
|
you = record["you"]
|
|
373
397
|
lines.append(" %s: %d stop(s), %d question(s), %d handoff(s), %d reset(s)"
|
|
374
398
|
% ("needed the driving agent (not you)" if record.get("driver") == "agent" else "needed you",
|
|
@@ -377,7 +401,8 @@ def render(record, out_dir, wrote_form):
|
|
|
377
401
|
if verdict:
|
|
378
402
|
lines.append(" your verdict: plan %s, %s hand fix(es), %s real catch(es), %s false alarm(s)"
|
|
379
403
|
% (verdict.get("plan", "?"), verdict.get("hand_fixes", "?"),
|
|
380
|
-
verdict.get("real_catches", "?"), verdict.get("false_alarms", "?"))
|
|
404
|
+
verdict.get("real_catches", "?"), verdict.get("false_alarms", "?"))
|
|
405
|
+
+ (", codeintel %s" % verdict["codeintel"] if verdict.get("codeintel") else ""))
|
|
381
406
|
elif wrote_form:
|
|
382
407
|
lines.append(" fill in %s, then run eval again" % (out_dir / VERDICT_FILE))
|
|
383
408
|
else:
|
|
@@ -430,7 +455,8 @@ def main(argv, out=None, err=None):
|
|
|
430
455
|
form = out_dir / VERDICT_FILE
|
|
431
456
|
wrote_form = not form.exists()
|
|
432
457
|
if wrote_form:
|
|
433
|
-
form.write_text(verdict_template(feature, catches, record["driver"]
|
|
458
|
+
form.write_text(verdict_template(feature, catches, record["driver"], bool(record["codeintel"])),
|
|
459
|
+
encoding="utf-8")
|
|
434
460
|
(out_dir / "eval.json").write_text(json.dumps(record, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
435
461
|
out.write(render(record, out_dir, wrote_form))
|
|
436
462
|
return 0
|
|
@@ -7,6 +7,8 @@ import re
|
|
|
7
7
|
import shlex
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
|
|
10
|
+
from feature_flow import codeintel
|
|
11
|
+
|
|
10
12
|
ROLES = {"build": "ticket-builder.md", "review": "ticket-reviewer.md", "review-quality": "ticket-reviewer.md",
|
|
11
13
|
"review-single": "ticket-reviewer.md",
|
|
12
14
|
"review-feature": "ticket-reviewer.md", "plan": "feature-planner.md", "plan-review": "plan-reviewer.md",
|
|
@@ -106,6 +108,7 @@ def build(phase, scripts, feature, ticket, num, sha, notes=(), retry=False, work
|
|
|
106
108
|
fresh))
|
|
107
109
|
for note in notes:
|
|
108
110
|
task += "\nThe conductor notes: %s" % note
|
|
111
|
+
task += codeintel.note("reviewer", REPO, sha)
|
|
109
112
|
intro = "The review guide follows. Follow it exactly."
|
|
110
113
|
if workdir is not None:
|
|
111
114
|
folder, branch = workdir
|
|
@@ -135,6 +138,7 @@ def feature_review(scripts, feature, plan, base, nums, notes=(), tasks="tasks"):
|
|
|
135
138
|
"`REVIEW: PASS` or `REVIEW: FAIL`." % (feature, base, plan, ", ".join(nums) or "none"))
|
|
136
139
|
for note in notes:
|
|
137
140
|
task += "\nThe conductor notes: %s" % note
|
|
141
|
+
task += codeintel.note("reviewer", REPO, base)
|
|
138
142
|
parts = [role_text, "---", "The feature review guide follows. Follow it exactly.", guide, "---", task]
|
|
139
143
|
return "\n\n".join(parts) + "\n"
|
|
140
144
|
|
|
@@ -8,6 +8,8 @@ import hashlib
|
|
|
8
8
|
import shlex
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
|
|
11
|
+
from feature_flow import codeintel as code_intel, prompts
|
|
12
|
+
|
|
11
13
|
MAX = 3 # most scouts for one phase
|
|
12
14
|
LINES = 60 # most lines of one reply that go into the prompt
|
|
13
15
|
|
|
@@ -82,9 +84,12 @@ def listing(folder, feature, slot, found):
|
|
|
82
84
|
for k, (agent, _, _) in enumerate(found, 1))
|
|
83
85
|
|
|
84
86
|
|
|
85
|
-
def prompt(role_text, role, question, workdir=None):
|
|
86
|
-
"""A scout's whole prompt: its role file, then the role it scouts for and the question
|
|
87
|
+
def prompt(role_text, role, question, workdir=None, codeintel=False):
|
|
88
|
+
"""A scout's whole prompt: its role file, then the role it scouts for and the question, and with codeintel
|
|
89
|
+
the section on asking codeintel when it is ready."""
|
|
87
90
|
task = "ROLE: %s\n\n%s\n\nReply in the shape your role gives, and change nothing." % (role, question)
|
|
91
|
+
if codeintel:
|
|
92
|
+
task += code_intel.note("scout", prompts.REPO)
|
|
88
93
|
if workdir is not None:
|
|
89
94
|
task = ("Work in the folder `%s`: it is the worktree the %s will work in. Run `cd %s` before any git "
|
|
90
95
|
"command, and read every path here relative to it, as `%s/<path>`.\n\n%s"
|