plan_driven 0.1.0 → 0.2.0

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +55 -1
  3. data/README.md +339 -31
  4. data/app/controllers/plan_driven/wizard/application_controller.rb +59 -0
  5. data/app/controllers/plan_driven/wizard/configuration_controller.rb +62 -0
  6. data/app/controllers/plan_driven/wizard/jobs_controller.rb +16 -0
  7. data/app/controllers/plan_driven/wizard/plans_controller.rb +102 -0
  8. data/app/views/layouts/plan_driven/wizard/application.html.erb +139 -0
  9. data/app/views/plan_driven/wizard/configuration/show.html.erb +104 -0
  10. data/app/views/plan_driven/wizard/plans/_agents.html.erb +44 -0
  11. data/app/views/plan_driven/wizard/plans/_approve.html.erb +40 -0
  12. data/app/views/plan_driven/wizard/plans/_finish.html.erb +24 -0
  13. data/app/views/plan_driven/wizard/plans/_plan.html.erb +52 -0
  14. data/app/views/plan_driven/wizard/plans/_tickets.html.erb +42 -0
  15. data/app/views/plan_driven/wizard/plans/index.html.erb +31 -0
  16. data/app/views/plan_driven/wizard/plans/new.html.erb +21 -0
  17. data/app/views/plan_driven/wizard/plans/show.html.erb +36 -0
  18. data/config/routes.rb +16 -0
  19. data/exe/plan-driven +1 -0
  20. data/lib/generators/plan_driven/install_generator.rb +7 -0
  21. data/lib/generators/plan_driven/templates/plan_driven.rb +10 -1
  22. data/lib/plan_driven/cli/config_commands.rb +60 -0
  23. data/lib/plan_driven/cli/plan_commands.rb +6 -2
  24. data/lib/plan_driven/cli/setup_commands.rb +10 -0
  25. data/lib/plan_driven/cli/ticket_commands.rb +23 -3
  26. data/lib/plan_driven/cli/ui.rb +41 -2
  27. data/lib/plan_driven/cli.rb +20 -4
  28. data/lib/plan_driven/configuration.rb +31 -3
  29. data/lib/plan_driven/connections.rb +69 -0
  30. data/lib/plan_driven/cursor_agents.rb +10 -2
  31. data/lib/plan_driven/delivery.rb +23 -5
  32. data/lib/plan_driven/evidence.rb +5 -1
  33. data/lib/plan_driven/github.rb +4 -0
  34. data/lib/plan_driven/guards/migration_guard.rb +86 -9
  35. data/lib/plan_driven/guards/ticket_guard.rb +1 -2
  36. data/lib/plan_driven/interview.rb +161 -0
  37. data/lib/plan_driven/local_agents.rb +254 -0
  38. data/lib/plan_driven/renderer/markdown.rb +26 -1
  39. data/lib/plan_driven/template.rb +7 -2
  40. data/lib/plan_driven/usage.rb +114 -0
  41. data/lib/plan_driven/version.rb +1 -1
  42. data/lib/plan_driven/wizard/engine.rb +19 -0
  43. data/lib/plan_driven/wizard.rb +245 -0
  44. data/lib/plan_driven.rb +5 -0
  45. metadata +23 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 49e95b18ac3191d34d9d526ba2fea2428ea662fab966da32c15239ba39573e5d
4
- data.tar.gz: '099d0a1fae1854e186a894e95354118b086722ecb6b13c72e90e28dfa2a2bc13'
3
+ metadata.gz: 2cad74cf1d61e2bc83cc71e30037048a81a0042a25a355695e97f1ec5ebdd823
4
+ data.tar.gz: f9b7139fbc8cac5475fd0b1429ebf90d8f06884c71d49c122b48c310c180c442
5
5
  SHA512:
6
- metadata.gz: 2bd6c949e802f2f2e7ccc1b98052ebc3c744b5e4bb381666cddbe79f7849e7ff1cf4205bde8a8f00b560bbce70f85ebac584c2ddd76f12156a8adf3f06843520
7
- data.tar.gz: c6605b055a96b9f2779038c2915ae2bf2585dfa28b66cd289a6b9a2dcbf295ed1ebda16d2df4476ea0d71b6dfd26ea05e9da39b596c905bbe18aeda6c6aa98a8
6
+ metadata.gz: 4b1de072864c1a701b49cff10ff0fa25cbe1c8c111c354884dcaaec308637ed1b62f2a12db0dac5be768f77706d9a03cbd24498cfdc93cd10d07611250ee266b
7
+ data.tar.gz: 65eb69253b06990105156e72f2a61a9b8b77e7b39b444414ce4516d065ae5a1eabcd0b2a671e1fd33a2929d86031688d8967f3a4d3a97246bac5ca97b3741641
data/CHANGELOG.md CHANGED
@@ -6,6 +6,59 @@ All notable changes to this project are documented here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.2.0] - 2026-09-30
10
+
11
+ ### Added
12
+
13
+ - The browser wizard: a Rails engine the install generator mounts at `/plan_driven`, in
14
+ development only. It walks a plan through the interview, plan, approval, tickets, agents
15
+ and evidence with Back and Next, and every button runs the matching `plan-driven` command in
16
+ the background, with the command and its live output shown beside the form. Only a fixed
17
+ list of commands can run, for local requests only (`config.wizard_enabled`).
18
+ - The wizard's Configuration page: Connections shows which services the app uses and whether
19
+ each has a key, and connects one from a pasted key; Interview questions changes, adds and
20
+ removes the questions `new` asks.
21
+ - `plan-driven connect SERVICE` (`cursor`, `openai`, `anthropic`, `github`) checks a key with
22
+ the service before storing it, and stores nothing when the service refuses it. The key is
23
+ read from stdin, never from the command line.
24
+ - `plan-driven questions` and `plan-driven question KEY` (`--title`, `--ask`, `--group`,
25
+ `--required`, `--optional`, `--remove`). The changes live in the app, in
26
+ `config/plan_driven/interview.yml`, so the team shares one interview; an added question is
27
+ a section of the plan like the others.
28
+ - `plan-driven edit PLAN SECTION --from FILE` replaces a section with a file's contents.
29
+ - Tokens and cost. Every model call (drafting, redrafts, tickets) and every agent run is
30
+ recorded with its tokens and duration. `plan-driven usage PLAN` prints them, and the delivery
31
+ report has a Tokens and cost table. `config.token_prices` turns tokens into dollars.
32
+ - Local coding agents: `config.agent_provider = :local` runs `config.agent_command` (Claude
33
+ Code, Codex, the Cursor CLI...) in its own git worktree per ticket, then commits, pushes and
34
+ opens the pull request. Feedback runs in the same worktree; the worktree is removed after
35
+ the merge. `{prompt_file}` in the command passes the prompt as a file instead of stdin.
36
+ - `GitHub#create_pull`, and Cursor agent runs carry `duration_ms`.
37
+
38
+ ### Changed
39
+
40
+ - MigrationGuard judges each removal or rename on its own sentence, or migration code line,
41
+ and the step it sits under. Before, the word "expand" or "contract" anywhere in Database
42
+ changes excused every destructive change in the section. Headings, negated sentences,
43
+ rollback notes and tables the plan creates are no longer read as removals.
44
+ - Terminal tables fit the terminal width, cutting the last column short instead of wrapping.
45
+ - When answers are piped into `plan-driven new`, the interview prints each answer after its
46
+ question, so the transcript reads like the terminal session.
47
+
48
+ ### Fixed
49
+
50
+ - Optional questions no longer read "(optional) (optional)" in the New plan form: the
51
+ interview adds "(optional)" itself, in the terminal and in the wizard alike.
52
+ - `plan-driven version` exits with status 0 instead of a TypeError after printing the version.
53
+ - `evidence` always runs Cucumber with `RAILS_ENV=test`. Started from a development server,
54
+ as the wizard does, it inherited `RAILS_ENV=development` and ran the scenarios against the
55
+ development database. The wizard also no longer passes the server's `RAILS_ENV` to commands.
56
+ - `remove_check_constraint` and other `remove_*` methods that drop no data are no longer
57
+ destructive changes; `remove_column(s)`, `remove_reference`, `remove_timestamps`,
58
+ `drop_table`, `rename_*` and `change_column` still are.
59
+ - Ticket coverage asks for tickets only for tables the plan creates or changes. Tables the
60
+ plan names in prose, or that already exist in the schema, no longer need their own ticket.
61
+
9
62
  ## [0.1.0] - 2026-09-30
10
63
 
11
64
  First public release.
@@ -72,5 +125,6 @@ First public release.
72
125
  - `plan-driven doctor` checks keys, the repository, the PDF browser, the Cursor API, the agent
73
126
  model, and Node and the SDK for `:cursor`.
74
127
 
75
- [Unreleased]: https://github.com/blaz1988/plan-driven/compare/v0.1.0...HEAD
128
+ [Unreleased]: https://github.com/blaz1988/plan-driven/compare/v0.2.0...HEAD
129
+ [0.2.0]: https://github.com/blaz1988/plan-driven/compare/v0.1.0...v0.2.0
76
130
  [0.1.0]: https://github.com/blaz1988/plan-driven/releases/tag/v0.1.0
data/README.md CHANGED
@@ -24,10 +24,50 @@ Zagreb. [Need Rails engineers?](#about-rubycode)
24
24
 
25
25
  ## Watch it deliver a feature
26
26
 
27
- [![Watch the plan_driven demo (25 min)](docs/images/demo.png)](https://github.com/blaz1988/plan-driven/releases/download/v0.1.0/plan-driven-demo.mp4)
27
+ [![Watch the plan_driven wizard demo (12 min)](docs/images/wizard-demo.png)](https://github.com/blaz1988/plan-driven/releases/download/v0.1.0/plan-driven-wizard.mp4)
28
+
29
+ **[▶ Watch the wizard demo](https://github.com/blaz1988/plan-driven/releases/download/v0.1.0/plan-driven-wizard.mp4)**
30
+ (12 minutes, narrated, with captions). One feature, comments on events, is delivered from
31
+ the [browser wizard](#the-browser-wizard) in [Gather](https://github.com/blaz1988/gather):
32
+ the plan drafted and one section redrafted, six tickets as issues
33
+ ([#34](https://github.com/blaz1988/gather/issues/34) to
34
+ [#39](https://github.com/blaz1988/gather/issues/39)), six agents and six pull requests
35
+ ([#40](https://github.com/blaz1988/gather/pull/40) to
36
+ [#45](https://github.com/blaz1988/gather/pull/45)), one round of feedback, and 42 of 42
37
+ acceptance criteria proven by a passing scenario. After every click the video zooms into the
38
+ wizard's terminal panel, which shows the `plan-driven` command that ran and its output. The
39
+ wizard ships with 0.2.0.
28
40
 
29
- **[▶ Watch the demo](https://github.com/blaz1988/plan-driven/releases/download/v0.1.0/plan-driven-demo.mp4)**
30
- (25 minutes, narrated, with captions). One feature, RSVPs with a waitlist, goes from an
41
+ <details>
42
+ <summary>Chapters</summary>
43
+
44
+ | Time | Chapter |
45
+ | ---: | --- |
46
+ | 0:00 | What plan_driven is |
47
+ | 0:24 | Installing it, and how the wizard works |
48
+ | 1:13 | `doctor`, from the wizard |
49
+ | 1:48 | The interview as a form, and the draft |
50
+ | 2:43 | Reading the plan, redrafting a section, submitting and approving |
51
+ | 3:51 | Tickets drafted and approved, as GitHub issues |
52
+ | 4:37 | Starting the agents |
53
+ | 5:18 | Refreshing, and `review` |
54
+ | 6:01 | Reading the first pull request |
55
+ | 6:46 | Approving and merging, and the next agent starts |
56
+ | 7:43 | Feedback to the agent on T3 |
57
+ | 8:24 | Every ticket merged |
58
+ | 8:53 | Evidence (42 of 42), tokens, and the delivery report |
59
+ | 9:47 | Reading the delivery report |
60
+ | 10:11 | The feature in the app |
61
+ | 10:46 | The same commands in a terminal |
62
+ | 11:22 | Recap |
63
+
64
+ </details>
65
+
66
+ ### The CLI deep dive
67
+
68
+ **[▶ Watch the CLI demo](https://github.com/blaz1988/plan-driven/releases/download/v0.1.0/plan-driven-demo.mp4)**
69
+ (28 minutes, narrated, with captions), for everything the wizard runs, typed in a terminal.
70
+ One feature, RSVPs with a waitlist, goes from an
31
71
  idea to production code in a new Rails 8 app,
32
72
  [Gather](https://github.com/blaz1988/gather). Nothing in it is staged: the plan, the five
33
73
  tickets, the five pull requests ([#8](https://github.com/blaz1988/gather/pull/8) to
@@ -44,18 +84,20 @@ repository. The planner and the five agents ran on Claude Opus 5.5 through Curso
44
84
  | 2:49 | `doctor`: keys, repository and the Cursor connection |
45
85
  | 3:15 | The interview |
46
86
  | 4:14 | Reading the implementation plan |
47
- | 6:05 | Deciding the open questions, then approving |
48
- | 7:20 | Tickets: drafted, steered to five, read and approved |
49
- | 9:42 | The tickets as GitHub issues |
50
- | 10:12 | What the agent is told, and starting the first agent |
51
- | 11:08 | Reading the first pull request, `review`, `approve-pr` and `merge` |
52
- | 13:18 | The model and its rules (T2) |
53
- | 15:22 | The RSVP card, tried on the branch before merging (T3) |
54
- | 17:41 | Feedback: a behind-main warning and a refactor (T5) |
55
- | 20:12 | The organizer's attendee list (T4) |
56
- | 21:47 | Evidence: 33 of 33 acceptance criteria, and the delivery report |
57
- | 23:01 | The finished feature in the app |
58
- | 24:12 | Recap |
87
+ | 6:05 | Deciding the open questions with `redraft` |
88
+ | 6:50 | Every section can be changed: Database changes redrafted and edited in vim (PD-2) |
89
+ | 9:51 | Submitting and approving the plan |
90
+ | 10:23 | Tickets: drafted, steered to five, read and approved |
91
+ | 12:44 | The tickets as GitHub issues |
92
+ | 13:15 | What the agent is told, and starting the first agent |
93
+ | 14:10 | Reading the first pull request, `review`, `approve-pr` and `merge` |
94
+ | 16:21 | The model and its rules (T2) |
95
+ | 18:25 | The RSVP card, tried on the branch before merging (T3) |
96
+ | 20:44 | Feedback: a behind-main warning and a refactor (T5) |
97
+ | 23:15 | The organizer's attendee list (T4) |
98
+ | 24:50 | Evidence: 33 of 33 acceptance criteria, and the delivery report |
99
+ | 26:04 | The finished feature in the app |
100
+ | 27:15 | Recap |
59
101
 
60
102
  </details>
61
103
 
@@ -65,11 +107,15 @@ repository. The planner and the five agents ran on Claude Opus 5.5 through Curso
65
107
  - [Requirements](#requirements)
66
108
  - [Installation](#installation)
67
109
  - [Getting your app ready](#getting-your-app-ready)
110
+ - [The browser wizard](#the-browser-wizard)
68
111
  - [Walkthrough: one feature from idea to merged](#walkthrough-one-feature-from-idea-to-merged)
69
112
  - [Guards](#guards)
70
113
  - [What the agent is told](#what-the-agent-is-told)
71
114
  - [Commands](#commands)
72
115
  - [Configuration](#configuration)
116
+ - [Choosing the coding agents](#choosing-the-coding-agents)
117
+ - [Tokens and cost](#tokens-and-cost)
118
+ - [How it compares](#how-it-compares)
73
119
  - [Keys](#keys)
74
120
  - [Working as a team](#working-as-a-team)
75
121
  - [Troubleshooting](#troubleshooting)
@@ -93,7 +139,7 @@ repository. The planner and the five agents ran on Claude Opus 5.5 through Curso
93
139
  plan-driven approve-tickets ─▶ tickets approved GitHub issues created
94
140
  │
95
141
  ▼
96
- plan-driven develop one Cursor cloud agent per ready ticket, one PR each
142
+ plan-driven develop one agent per ready ticket (Cursor cloud, or a local CLI), one PR each
97
143
  plan-driven status agent finished ─▶ PR open
98
144
  plan-driven review PrGuard: scope, specs, Cucumber scenarios, CI, up to date
99
145
  plan-driven feedback the same agent pushes a fix to the same PR
@@ -102,7 +148,7 @@ repository. The planner and the five agents ran on Claude Opus 5.5 through Curso
102
148
  │
103
149
  ▼
104
150
  plan-driven evidence Cucumber results mapped to acceptance criteria
105
- plan-driven report docs/plans/pd-1-…/delivery-report.pdf
151
+ plan-driven report docs/plans/pd-1-…/delivery-report.pdf, with tokens and cost
106
152
  ```
107
153
 
108
154
  Phases are stored in your application's database, so a plan can't skip a step. Tickets can't
@@ -119,9 +165,12 @@ model as a list to fix, and a plan that still fails isn't accepted.
119
165
 
120
166
  - Ruby 3.1+ and Rails 7.0+ (see [support](#rails-and-ruby-support)).
121
167
  - The application on GitHub, with CI running on pull requests.
122
- - A [Cursor](https://cursor.com) account with the GitHub integration connected to that
123
- repository, and a Cursor API key (Cursor dashboard, Integrations). The agents run as
124
- Cursor cloud agents.
168
+ - Coding agents, one of (see [Choosing the coding agents](#choosing-the-coding-agents)):
169
+ - **Cursor cloud agents** (the default): a [Cursor](https://cursor.com) account with the
170
+ GitHub integration connected to that repository, and a Cursor API key (Cursor dashboard,
171
+ Integrations).
172
+ - **A local agent CLI** (`agent_provider :local`): Claude Code, Codex, the Cursor CLI or any
173
+ command that edits files in its working directory, plus `git` push access to the repository.
125
174
  - A model for drafting plans and tickets, one of:
126
175
  - **Cursor** (`llm_provider :cursor`): any model on your Cursor account, Claude Opus 5.5 by
127
176
  default. Needs Node 22.13+ and the Cursor SDK. No other LLM key.
@@ -204,7 +253,7 @@ into your application. See [Keys](#keys).
204
253
  ```
205
254
  $ bin/plan-driven doctor
206
255
 
207
- plan-driven 0.1.0
256
+ plan-driven 0.2.0
208
257
  ✓ Rails application: plan_driven tables present
209
258
  LLM: cursor/claude-opus-5-5
210
259
  ✓ Cursor SDK: Node v24.21.0, @cursor/sdk found
@@ -263,6 +312,81 @@ config.extra_context = "Gather lists community events. An event has an organizer
263
312
  **Who approves.** `plan_approvals` and `ticket_approvals` list the roles that must sign off,
264
313
  for example `%w[review qa devops director]`. One person can hold every role on a small team.
265
314
 
315
+ ## The browser wizard
316
+
317
+ Not everyone wants to drive a delivery from the terminal. The install generator mounts a
318
+ wizard in your app, in development only:
319
+
320
+ ```ruby
321
+ # config/routes.rb
322
+ mount PlanDriven::Wizard::Engine, at: "/plan_driven" if Rails.env.development?
323
+ ```
324
+
325
+ Start the app and open `http://localhost:3000/plan_driven`. It walks a plan through the same
326
+ five steps, with Back and Next: **Plan** (the interview, then read, edit or redraft any section
327
+ and submit), **Approve**, **Tickets**, **Agents & PRs** and **Proof & report**. A step opens
328
+ once the plan has reached it.
329
+
330
+ The wizard is a front end for the CLI, not a second implementation. Every button runs one
331
+ `plan-driven` command in the background, and the panel on the right shows that command and its
332
+ output as it runs, exactly as you'd see it in a terminal:
333
+
334
+ ![The wizard's Approve step, with the command it ran and its output](docs/images/18-wizard.png)
335
+
336
+ ```
337
+ $ bin/plan-driven edit PD-3 database_changes --from tmp/plan_driven/wizard/sections/PD-3-database_changes-1f2e.md --yes
338
+ ✓ Database changes updated; PD-3 is now revision 2 (draft)
339
+ ✓ All checks passed
340
+ ```
341
+
342
+ So anything done in the browser can be repeated, scripted or reviewed from the terminal, and
343
+ the audit trail is the same either way. A few things to know:
344
+
345
+ - Only a fixed list of commands can run, built from the form fields as an argument list, never
346
+ through a shell. A key pasted on the Configuration page goes to `plan-driven connect` on
347
+ stdin, so it's never in the command line, the panel or the logs.
348
+ - It answers local requests only, and only in development. `config.wizard_enabled = true`
349
+ turns it on in another environment, still for local requests only.
350
+ - "Acting as" at the top sets `PLAN_DRIVEN_ACTOR` for the commands it runs, so approvals are
351
+ recorded under the name you give; it defaults to your git identity.
352
+ - Each run is kept in `tmp/plan_driven/wizard/`: the command, its output and its exit status.
353
+
354
+ ### Configuration: connections and the interview
355
+
356
+ **Configuration**, at the top of every page, has two parts.
357
+
358
+ **Connections** shows which services this app's configuration uses (Cursor for cloud agents
359
+ or drafting, OpenAI or Anthropic for drafting, GitHub for issues and pull requests), whether
360
+ each one has a key, and where the key comes from. Paste a key and click Connect:
361
+ `plan-driven connect cursor` checks it with the service first (for Cursor, the account it
362
+ belongs to) and only then stores it in `~/.plan_driven/config`. A refused key is never stored.
363
+ **Check every connection** runs `doctor`. The same works in the terminal:
364
+
365
+ ```
366
+ $ bin/plan-driven connect cursor
367
+ Cursor key:
368
+ Checking the key with Cursor...
369
+ ✓ Cursor: connected as ana@example.com
370
+ stored in ~/.plan_driven/config (0600), never in the app
371
+ ```
372
+
373
+ Which model drafts and which agents write the code are still set in the initializer (see
374
+ [Choosing the coding agents](#choosing-the-coding-agents)); the page shows the current choice.
375
+
376
+ **Interview questions** lists what `plan-driven new` and the New plan form ask. Change a
377
+ question's title or wording, make it required or optional, add your own questions, or put one
378
+ back to the default. Each Save runs `plan-driven question`:
379
+
380
+ ```
381
+ $ bin/plan-driven question success_metric --title "Success metric" --ask "How will we know it worked?" --optional
382
+ ✓ Question success_metric added
383
+ ```
384
+
385
+ The changes are written to `config/plan_driven/interview.yml` in your app. Commit it, and the
386
+ whole team gets the same interview, in the wizard and in the terminal. An added question goes
387
+ to the model with the other answers, and it's a section of the plan under the group you pick.
388
+ Drafted sections (Database changes, Risks...) belong to the model and can't be changed here.
389
+
266
390
  ## Walkthrough: one feature from idea to merged
267
391
 
268
392
  This is the run from the demo video, in [Gather](https://github.com/blaz1988/gather), with the
@@ -319,8 +443,22 @@ column it cites exists.
319
443
 
320
444
  ![The plan, grounded in the real schema](docs/images/03-plan.png)
321
445
 
322
- Change what isn't right. `edit` opens a section in `$EDITOR`. `redraft` has the model rewrite
323
- one section from an instruction, which is the quickest way to record decisions:
446
+ **Every section of the plan can be changed**, not only Outstanding questions: What, Why,
447
+ Database changes, Application changes, Risks, Testing, any of them (the keys are listed under
448
+ [Commands](#commands)). The draft is the model's proposal, and your team has the final say.
449
+ There are two ways to change a section:
450
+
451
+ - `edit PLAN SECTION` opens the section in `$VISUAL` or `$EDITOR` as Markdown. Use it for exact
452
+ changes: a column, a name, a step, a test case.
453
+ - `redraft PLAN SECTION "instruction"` has the model rewrite only that section, following your
454
+ instruction. It reads the rest of the plan and the code while it does.
455
+
456
+ Either way the change becomes a new revision, the plan goes back to draft, the guards run again
457
+ and the Markdown, HTML and PDF are written again. `plan-driven log PLAN` lists every change.
458
+ Change the plan through these commands, not by editing `plan.md`: the database is the source,
459
+ and the files are rendered from it.
460
+
461
+ `redraft` is also the quickest way to record decisions:
324
462
 
325
463
  ```
326
464
  $ bin/plan-driven redraft PD-1 outstanding_questions "Record my answers under Decided and keep only
@@ -337,6 +475,80 @@ Redrafting outstanding_questions...
337
475
 
338
476
  ![The decisions, recorded in the plan](docs/images/04-plan-decided.png)
339
477
 
478
+ #### Example: changing the database design
479
+
480
+ The agent's draft is a starting point, and the database design is where teams most often
481
+ disagree with it. In Gather's second plan, PD-2 (event categories), the model suggested a string
482
+ column on `events`:
483
+
484
+ ```
485
+ $ bin/plan-driven show PD-2 --section database_changes
486
+ ### Step 1: Expand (migration `AddCategoryToEvents`)
487
+
488
+ On table `events`:
489
+ - Add column `category`: type `string`, **nullable**, default `'meetup'`.
490
+ - Add composite index `index_events_on_category_and_starts_at` on `[:category, :starts_at]`. ...
491
+ ```
492
+
493
+ The team wanted a table instead. `redraft` rewrites the section to that design, keeping the
494
+ expand and contract steps:
495
+
496
+ ```
497
+ $ bin/plan-driven redraft PD-2 database_changes "Use a categories table instead of a string column:
498
+ name and slug, seeded with meetup, workshop, talk and conference, and a category_id reference on events."
499
+ Redrafting database_changes...
500
+ Categories live in their own `categories` table, and each event points to one of them through
501
+ `events.category_id`. ...
502
+ ### Step 1: Expand
503
+ #### Migration `CreateCategories`
504
+ New table `categories`:
505
+ - `name` string, **not null**, no default. ...
506
+ - `slug` string, **not null**, no default. ...
507
+ ...
508
+ ### Step 5: Contract (migration `EnforceCategoryOnEvents`)
509
+ - `change_column_null :events, :category_id, false`.
510
+ ✓ All checks passed
511
+ ```
512
+
513
+ A small change doesn't need the model. `edit` opens the section in your editor. Here, a `color`
514
+ column is added to the new table, in the column list, the migration and the resulting schema:
515
+
516
+ ```
517
+ $ EDITOR=vim bin/plan-driven edit PD-2 database_changes
518
+ ✓ Database changes updated; PD-2 is now revision 3 (draft)
519
+ ✓ All checks passed
520
+ ```
521
+
522
+ ![Adding a column to the plan in vim](docs/images/04b-edit-section.png)
523
+
524
+ The guards check your edit the same way they check the model's draft (see [Guards](#guards)),
525
+ and anything that fails is listed right after you save. `submit` refuses a plan with errors.
526
+
527
+ Other sections that depend on the change follow the same way. The model reads the whole plan,
528
+ so it picks up the new table and the `color` column:
529
+
530
+ ```
531
+ $ bin/plan-driven redraft PD-2 application_changes "Follow the new Database changes: a Category model,
532
+ events.category_id instead of an enum, and the category colour on the card badge."
533
+ ...
534
+ ✓ All checks passed
535
+
536
+ $ bin/plan-driven submit PD-2
537
+ ✓ PD-2 revision 4 is in review
538
+
539
+ $ bin/plan-driven log PD-2
540
+ When Event Ticket By Details
541
+ 2026-09-30 10:07 plan.drafted Ivan Blažević <ivan...> model=cursor/claude-opus-5-5 ...
542
+ 2026-09-30 10:10 plan.revised Ivan Blažević <ivan...> sections=database_changes
543
+ 2026-09-30 10:16 plan.revised Ivan Blažević <ivan...> sections=database_changes
544
+ ...
545
+ ```
546
+
547
+ You can change a plan in draft, in review and after it's approved. An approval belongs to a
548
+ revision, so a changed plan must be approved again. Once its tickets are drafted, the plan is
549
+ locked, because the tickets and pull requests were built from it. Changes after that go into a
550
+ follow-up plan.
551
+
340
552
  ### 3. Submit and approve
341
553
 
342
554
  ```
@@ -530,8 +742,12 @@ An error blocks the next step and a warning is shown and recorded.
530
742
 
531
743
  **MigrationGuard** reads the database changes:
532
744
 
533
- - removing or renaming a column or table in one step is an error, unless the plan stages it
534
- with `ignored_columns`, expand and contract, or a later release;
745
+ - removing or renaming a column or table in one step is an error. Each change is judged on its
746
+ own, sentence by sentence and line by line in migration code: it's accepted only when that
747
+ sentence stages it (`ignored_columns`, a later release, after the backfill) or when it sits
748
+ under a contract, cleanup or later step. Mentioning "expand" somewhere else in the section
749
+ doesn't excuse it. Headings, negated sentences ("No column is removed"), rollback notes and
750
+ tables the plan itself creates don't count as removals;
535
751
  - NOT NULL on an existing table without a default or backfill is a warning;
536
752
  - on PostgreSQL, an index that isn't built concurrently is a warning.
537
753
 
@@ -623,14 +839,18 @@ key such as `PD-1`, and `PLAN/TICKET` is a ticket such as `PD-1/T3`.
623
839
  | `evidence PLAN [--from FILE]` | Run or import Cucumber results |
624
840
  | `report PLAN` | Write the delivery report |
625
841
  | `log PLAN` | The audit trail |
842
+ | `usage PLAN` | Tokens, time and cost per step and per agent run |
843
+ | `questions` | The interview's questions, and which ones the team changed or added |
844
+ | `question KEY [--title T] [--ask Q] [--group G] [--required \| --optional] [--remove]` | Change or add an interview question, or put it back |
626
845
  | `configure` | Store keys in `~/.plan_driven/config` |
627
- | `doctor` | Check keys, repository, PDF browser and the Cursor connection |
846
+ | `connect SERVICE` | Check a key with `cursor`, `openai`, `anthropic` or `github`, then store it |
847
+ | `doctor` | Check keys, repository, PDF browser, and the Cursor connection or local agent command |
628
848
 
629
849
  Section keys for `show --section`, `edit` and `redraft`: `what`, `why`, `where`, `who`,
630
850
  `when`, `background`, `existing_data_structure`, `architecture`, `database_changes`,
631
851
  `application_changes`, `infrastructure_changes`, `out_of_scope`, `risks`, `performance`,
632
- `security`, `monitoring`, `outstanding_questions` and `testing`. `edit PLAN` without a section
633
- lists them.
852
+ `security`, `monitoring`, `outstanding_questions` and `testing`, plus any question the team
853
+ added. `edit PLAN` without a section lists them.
634
854
 
635
855
  `--yes` skips confirmations, for scripts. `merge` still needs the ticket key typed unless
636
856
  `--yes` is given.
@@ -657,11 +877,17 @@ PlanDriven.configure do |config|
657
877
  config.plan_approvals = %w[review] # e.g. %w[review qa devops director]
658
878
  config.ticket_approvals = %w[review]
659
879
 
660
- # Cursor cloud agents
661
- config.agent_model = nil # nil uses your Cursor default
880
+ # Coding agents
881
+ config.agent_provider = :cursor # or :local, see "Choosing the coding agents"
882
+ config.agent_command = nil # :local only, e.g. "claude -p --permission-mode acceptEdits --output-format json"
883
+ config.agent_timeout = 3600 # :local only; seconds before a run is stopped
884
+ config.agent_model = nil # :cursor; nil uses your Cursor default
662
885
  config.base_branch = "main"
663
886
  config.max_parallel_agents = 3
664
- config.skip_reviewer_request = false # true: the agent doesn't request you as reviewer
887
+ config.skip_reviewer_request = false # :cursor; true: the agent doesn't request you as reviewer
888
+
889
+ # Tokens and cost: dollars per million tokens, by model id. None ship with the gem.
890
+ config.token_prices = {} # { "model-id" => { input: 3.0, output: 15.0, cache_write: 3.75, cache_read: 0.3 } }
665
891
 
666
892
  # GitHub
667
893
  config.github_repository = nil # "owner/name"; read from the origin remote when nil
@@ -695,6 +921,88 @@ plan and can't change a file.
695
921
 
696
922
  `config.template` replaces the plan's sections if your template differs.
697
923
 
924
+ ## Choosing the coding agents
925
+
926
+ Every ticket goes to one agent, which works on its own branch and opens one pull request. The
927
+ rest of the workflow is the same whichever agents you use: `review`, `feedback`, `approve-pr`
928
+ and `merge` see only the pull request.
929
+
930
+ **Cursor cloud agents** (`agent_provider :cursor`, the default) run on Cursor-hosted machines
931
+ against a fresh clone of the repository, so nothing runs on your laptop and several tickets
932
+ can run at once. `config.agent_model` picks the model.
933
+
934
+ **A local agent CLI** (`agent_provider :local`) runs a command on your machine. Each ticket
935
+ gets its own git worktree and branch under `tmp/plan_driven/agents`, so tickets don't touch
936
+ your working copy or each other. The command gets the same prompt a cloud agent gets, on stdin,
937
+ or wherever the command says `{prompt_file}`. When it exits cleanly, plan_driven commits what
938
+ it left, pushes the branch and opens the pull request, using the description the agent wrote
939
+ to `PR_DESCRIPTION.md`. Feedback runs the command again in the same worktree and pushes to the
940
+ same pull request. After the merge, the worktree and the local branch are removed.
941
+
942
+ ```ruby
943
+ config.agent_provider = :local
944
+
945
+ # Claude Code
946
+ config.agent_command = "claude -p --permission-mode acceptEdits --output-format json"
947
+ # Codex
948
+ config.agent_command = "codex exec --full-auto -"
949
+ # The Cursor CLI
950
+ config.agent_command = 'cursor-agent -p --force --output-format json "$(cat {prompt_file})"'
951
+ ```
952
+
953
+ A local agent runs with your permissions and your shell, so give it only the tools it needs to
954
+ edit and to run the test suite, and read the pull request as carefully as a cloud agent's.
955
+ Runs longer than `config.agent_timeout` (an hour by default) are stopped. The Cursor CLI setup
956
+ is the one tested end to end; flags change between CLI versions, so check your CLI's `--help`.
957
+ `plan-driven doctor` checks that the command is on the `PATH`.
958
+
959
+ ## Tokens and cost
960
+
961
+ Every model call and every agent run is recorded with the tokens it used: drafting the plan,
962
+ each `redraft`, drafting the tickets, and each agent run and follow-up, with its duration.
963
+ `plan-driven usage PD-1` prints them, and the delivery report has a Tokens and cost table.
964
+
965
+ Prices change and differ per account, so the gem ships none: put what your provider charges in
966
+ `config.token_prices` (dollars per million tokens, with separate cache prices), and the report
967
+ shows dollars next to the tokens. Without a price you still get the tokens and the time.
968
+
969
+ Cursor reports tokens for every cloud agent run. A local CLI's tokens are recorded when it
970
+ prints them the way Claude Code's `--output-format json` does; otherwise only the time is.
971
+
972
+ For scale, these are the five cloud agents from the demo, read back from Cursor's usage API
973
+ (T4 and T5 include their feedback runs):
974
+
975
+ | Ticket | Runs | Output tokens | Cache reads | Total tokens |
976
+ | --- | ---: | ---: | ---: | ---: |
977
+ | T1 migration | 1 | 13,861 | 738,595 | 791,861 |
978
+ | T2 model rules | 1 | 21,037 | 1,519,826 | 1,596,409 |
979
+ | T3 RSVP card | 1 | 16,611 | 1,161,514 | 1,242,945 |
980
+ | T4 attendee list | 2 | 10,957 | 1,083,671 | 1,149,274 |
981
+ | T5 seats on the index | 2 | 12,725 | 1,154,333 | 1,248,462 |
982
+ | **Total** | 7 | **75,191** | **5,657,939** | **6,028,951** |
983
+
984
+ 94% of the tokens are cache reads, which cost a fraction of fresh input, and only 75 thousand
985
+ are code and text the agents wrote. Most of an agent's tokens go into reading the codebase, so
986
+ a small, conventional one is cheaper to work on.
987
+
988
+ ## How it compares
989
+
990
+ plan_driven sits next to spec-driven tools such as GitHub's Spec Kit and Kiro, which also start
991
+ from a written spec before an agent writes code. As we understand them, those are
992
+ language-agnostic and focus on producing the spec, the design and the task list for an agent to
993
+ follow. plan_driven is narrower and goes further on the Rails side:
994
+
995
+ - the plan is drafted from your Rails schema, models and routes, and Existing Data Structure is
996
+ checked against them;
997
+ - the rules are Ruby code that blocks the next step (expand and contract, ticket size and
998
+ order, pull request scope, CI), rather than guidance in a prompt;
999
+ - phases and approvals are stored in your database, per revision, with an audit trail;
1000
+ - each acceptance criterion is mapped to a Cucumber scenario, and the delivery report shows
1001
+ the proof, the approvals and the cost.
1002
+
1003
+ If your stack isn't Rails, or you only want a spec for a single agent session, a general tool
1004
+ is the better fit.
1005
+
698
1006
  ## Keys
699
1007
 
700
1008
  | Key | Used for | Environment |
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlanDriven
4
+ module Wizard
5
+ # The wizard runs commands on this machine, so it only answers local requests in development
6
+ # (or wherever config.wizard_enabled says).
7
+ class ApplicationController < ActionController::Base
8
+ protect_from_forgery with: :exception
9
+ layout "plan_driven/wizard/application"
10
+ before_action :local_only!
11
+ helper_method :acting_as, :current_job
12
+
13
+ helper do
14
+ # A button that runs one command for the plan on the page, e.g. run_button("Submit", "submit").
15
+ def run_button(label, action, fields = {}, primary: false, disabled: false)
16
+ button_to label, run_plan_path(@plan.key),
17
+ params: fields.merge(do: action, step: @step), disabled: disabled,
18
+ class: primary ? "primary" : nil, form: { style: "display:inline" }
19
+ end
20
+
21
+ def markdown(text)
22
+ PlanDriven::Renderer::HTML.convert(text).html_safe
23
+ end
24
+
25
+ def guard_list(report)
26
+ safe_join([
27
+ (tag.ul(safe_join(report.errors.map { |e| tag.li(e) }), class: "errors") if report.errors.any?),
28
+ (tag.ul(safe_join(report.warnings.map { |w| tag.li(w) }), class: "warnings") if report.warnings.any?),
29
+ (tag.p("#{report.passes.size} checks passed.", class: "muted") if report.passes.any?)
30
+ ].compact)
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def local_only!
37
+ enabled = PlanDriven.configuration.wizard_enabled
38
+ enabled = Rails.env.development? if enabled.nil?
39
+ head :forbidden unless enabled && request.local?
40
+ end
41
+
42
+ def acting_as
43
+ session[:plan_driven_actor].presence || PlanDriven.actor
44
+ end
45
+
46
+ def current_job
47
+ return @current_job if defined?(@current_job)
48
+
49
+ @current_job = params[:job].present? ? Job.find(params[:job]) : nil
50
+ rescue ArgumentError
51
+ @current_job = nil
52
+ end
53
+
54
+ def start(argv, stdin: nil)
55
+ Job.start(argv, stdin: stdin, actor: session[:plan_driven_actor])
56
+ end
57
+ end
58
+ end
59
+ end