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.
Files changed (111) hide show
  1. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/PKG-INFO +19 -4
  2. feature_flow_cli-0.26.0/feature_flow_cli.egg-info/PKG-INFO → feature_flow_cli-0.28.0/README.md +16 -22
  3. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/__init__.py +1 -1
  4. feature_flow_cli-0.28.0/feature_flow/codeintel.py +127 -0
  5. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/conductor.py +2 -2
  6. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/evaluate.py +68 -42
  7. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/prompts.py +4 -0
  8. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/scouts.py +7 -2
  9. feature_flow_cli-0.26.0/README.md → feature_flow_cli-0.28.0/feature_flow_cli.egg-info/PKG-INFO +37 -3
  10. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow_cli.egg-info/SOURCES.txt +6 -0
  11. feature_flow_cli-0.28.0/feature_flow_cli.egg-info/requires.txt +3 -0
  12. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/brainstorm.md +14 -1
  13. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/templates/brainstorm.md +7 -1
  14. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/packages/forkboard/forkboard/__init__.py +1 -1
  15. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/packages/forkboard/forkboard/assets/base.css +5 -3
  16. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/packages/forkboard/forkboard/assets/editor.js +58 -1
  17. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/packages/forkboard/forkboard/cli.py +5 -2
  18. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/packages/forkboard/forkboard/page.py +32 -2
  19. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/packages/forkboard/forkboard/themes/blueprint.css +1 -0
  20. feature_flow_cli-0.28.0/packages/forkboard/forkboard/themes/bright.css +29 -0
  21. feature_flow_cli-0.28.0/packages/forkboard/forkboard/themes/paper.css +35 -0
  22. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/packages/forkboard/forkboard/themes/route.css +1 -0
  23. feature_flow_cli-0.28.0/packages/forkboard/forkboard/themes/sketch.css +30 -0
  24. feature_flow_cli-0.28.0/packages/forkboard/forkboard/themes/terminal.css +30 -0
  25. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/pyproject.toml +4 -0
  26. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/LICENSE +0 -0
  27. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/MANIFEST.in +0 -0
  28. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/adapters/codex/feature-flow/SKILL.md +0 -0
  29. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/adapters/codex/feature-flow/agents/openai.yaml +0 -0
  30. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/agents/feature-planner.md +0 -0
  31. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/agents/flow-architect.md +0 -0
  32. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/agents/flow-designer.md +0 -0
  33. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/agents/flow-lens.md +0 -0
  34. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/agents/flow-po.md +0 -0
  35. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/agents/flow-scout.md +0 -0
  36. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/agents/flow-skeptic.md +0 -0
  37. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/agents/flow-tester.md +0 -0
  38. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/agents/flow-web.md +0 -0
  39. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/agents/plan-reviewer.md +0 -0
  40. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/agents/ticket-builder.md +0 -0
  41. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/agents/ticket-reviewer.md +0 -0
  42. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/__main__.py +0 -0
  43. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/brainstorm_page.py +0 -0
  44. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/checks.py +0 -0
  45. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/cli.py +0 -0
  46. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/codehash.py +0 -0
  47. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/command.py +0 -0
  48. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/floorguard.py +0 -0
  49. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/gate.py +0 -0
  50. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/git.py +0 -0
  51. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/importer.py +0 -0
  52. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/install.py +0 -0
  53. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/lanes.py +0 -0
  54. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/proc.py +0 -0
  55. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/profiles.py +0 -0
  56. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/proof.py +0 -0
  57. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/released.py +0 -0
  58. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/report.py +0 -0
  59. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/state.py +0 -0
  60. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/status.py +0 -0
  61. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/studio.py +0 -0
  62. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/suggest.py +0 -0
  63. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/tickets.py +0 -0
  64. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/uninstall.py +0 -0
  65. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow/view.py +0 -0
  66. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow_cli.egg-info/dependency_links.txt +0 -0
  67. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow_cli.egg-info/entry_points.txt +0 -0
  68. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/feature_flow_cli.egg-info/top_level.txt +0 -0
  69. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/brief.md +0 -0
  70. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/build.md +0 -0
  71. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/debug.md +0 -0
  72. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/lenses/artistic.md +0 -0
  73. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/lenses/open-minded.md +0 -0
  74. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/lenses/positive.md +0 -0
  75. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/lenses/researcher.md +0 -0
  76. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/lenses/security.md +0 -0
  77. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/lenses/ux.md +0 -0
  78. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/plan-review.md +0 -0
  79. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/plan.md +0 -0
  80. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/replan.md +0 -0
  81. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/review-feature.md +0 -0
  82. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/review-quality.md +0 -0
  83. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/review-single.md +0 -0
  84. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/review.md +0 -0
  85. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/show.md +0 -0
  86. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/templates/commands.md +0 -0
  87. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/templates/learnings.md +0 -0
  88. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/templates/map.md +0 -0
  89. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/templates/spec.md +0 -0
  90. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/templates/ticket.md +0 -0
  91. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/templates/ui-mockup.md +0 -0
  92. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/guides/test.md +0 -0
  93. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/install.py +0 -0
  94. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/install.sh +0 -0
  95. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/packages/forkboard/forkboard/__main__.py +0 -0
  96. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/packages/forkboard/forkboard/assets/live.css +0 -0
  97. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/packages/forkboard/forkboard/assets/live.js +0 -0
  98. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/packages/forkboard/forkboard/doc.py +0 -0
  99. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/packages/forkboard/forkboard/graph.py +0 -0
  100. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/packages/forkboard/forkboard/live.py +0 -0
  101. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/scripts/floor-guard.py +0 -0
  102. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/scripts/flow-status.py +0 -0
  103. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/scripts/flow-trust.py +0 -0
  104. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/scripts/flow-view.html +0 -0
  105. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/scripts/flow-view.py +0 -0
  106. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/scripts/flow.py +0 -0
  107. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/scripts/gate.py +0 -0
  108. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/setup.cfg +0 -0
  109. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/skills/architect-review/SKILL.md +0 -0
  110. {feature_flow_cli-0.26.0 → feature_flow_cli-0.28.0}/skills/automation-design/SKILL.md +0 -0
  111. {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.26.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 `blueprint`, picked with `--style`, the brainstorm's `Style:` line or `FLOW_STYLE`. A style is one CSS file of colour and font 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.
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
  ![A Forkboard review page for a rate-limits brainstorm: a sidebar with the sections and options, option A's diagram with the API gateway box selected and the editing tools above it, and its pros, cons, size and risky areas below](docs/forkboard.png)
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 `blueprint` (Forkboard's themes) |
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
 
@@ -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 `blueprint`, picked with `--style`, the brainstorm's `Style:` line or `FLOW_STYLE`. A style is one CSS file of colour and font 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.
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
  ![A Forkboard review page for a rate-limits brainstorm: a sidebar with the sections and options, option A's diagram with the API gateway box selected and the editing tools above it, and its pros, cons, size and risky areas below](docs/forkboard.png)
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 `blueprint` (Forkboard's themes) |
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
 
@@ -1,3 +1,3 @@
1
1
  """Feature Flow: a ticket-graph workflow for coding agents."""
2
2
 
3
- __version__ = "0.26.0"
3
+ __version__ = "0.28.0"
@@ -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 tokens(home, top, window, rows):
141
- """Token use of the Claude Code sessions (and their subagents) that worked in this repo during the
142
- run, summed from the transcripts' usage records, each message counted once. Split per ticket by
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 None
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 '"usage"' not in line:
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
- message = item.get("message") if isinstance(item, dict) else None
171
- usage = message.get("usage") if isinstance(message, dict) else None
172
- if not isinstance(usage, dict) or not _inside(item.get("cwd") or "", top):
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
- key = (message.get("id"), item.get("requestId"))
178
- if key != (None, None):
179
- if key in seen:
180
- continue
181
- seen.add(key)
182
- used = True
183
- counts = {
184
- "input": int(usage.get("input_tokens") or 0),
185
- "output": int(usage.get("output_tokens") or 0),
186
- "cache_read": int(usage.get("cache_read_input_tokens") or 0),
187
- "cache_write": int(usage.get("cache_creation_input_tokens") or 0),
188
- }
189
- add(total, counts)
190
- add(models.setdefault(message.get("model") or "unknown", {}), counts)
191
- num = "-"
192
- for t, n in marks:
193
- if t > when:
194
- break
195
- num = n
196
- add(per.setdefault(num, {}), counts)
197
- files += used
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(home, top, window, rows),
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"]), encoding="utf-8")
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"