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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +55 -1
- data/README.md +339 -31
- data/app/controllers/plan_driven/wizard/application_controller.rb +59 -0
- data/app/controllers/plan_driven/wizard/configuration_controller.rb +62 -0
- data/app/controllers/plan_driven/wizard/jobs_controller.rb +16 -0
- data/app/controllers/plan_driven/wizard/plans_controller.rb +102 -0
- data/app/views/layouts/plan_driven/wizard/application.html.erb +139 -0
- data/app/views/plan_driven/wizard/configuration/show.html.erb +104 -0
- data/app/views/plan_driven/wizard/plans/_agents.html.erb +44 -0
- data/app/views/plan_driven/wizard/plans/_approve.html.erb +40 -0
- data/app/views/plan_driven/wizard/plans/_finish.html.erb +24 -0
- data/app/views/plan_driven/wizard/plans/_plan.html.erb +52 -0
- data/app/views/plan_driven/wizard/plans/_tickets.html.erb +42 -0
- data/app/views/plan_driven/wizard/plans/index.html.erb +31 -0
- data/app/views/plan_driven/wizard/plans/new.html.erb +21 -0
- data/app/views/plan_driven/wizard/plans/show.html.erb +36 -0
- data/config/routes.rb +16 -0
- data/exe/plan-driven +1 -0
- data/lib/generators/plan_driven/install_generator.rb +7 -0
- data/lib/generators/plan_driven/templates/plan_driven.rb +10 -1
- data/lib/plan_driven/cli/config_commands.rb +60 -0
- data/lib/plan_driven/cli/plan_commands.rb +6 -2
- data/lib/plan_driven/cli/setup_commands.rb +10 -0
- data/lib/plan_driven/cli/ticket_commands.rb +23 -3
- data/lib/plan_driven/cli/ui.rb +41 -2
- data/lib/plan_driven/cli.rb +20 -4
- data/lib/plan_driven/configuration.rb +31 -3
- data/lib/plan_driven/connections.rb +69 -0
- data/lib/plan_driven/cursor_agents.rb +10 -2
- data/lib/plan_driven/delivery.rb +23 -5
- data/lib/plan_driven/evidence.rb +5 -1
- data/lib/plan_driven/github.rb +4 -0
- data/lib/plan_driven/guards/migration_guard.rb +86 -9
- data/lib/plan_driven/guards/ticket_guard.rb +1 -2
- data/lib/plan_driven/interview.rb +161 -0
- data/lib/plan_driven/local_agents.rb +254 -0
- data/lib/plan_driven/renderer/markdown.rb +26 -1
- data/lib/plan_driven/template.rb +7 -2
- data/lib/plan_driven/usage.rb +114 -0
- data/lib/plan_driven/version.rb +1 -1
- data/lib/plan_driven/wizard/engine.rb +19 -0
- data/lib/plan_driven/wizard.rb +245 -0
- data/lib/plan_driven.rb +5 -0
- metadata +23 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2cad74cf1d61e2bc83cc71e30037048a81a0042a25a355695e97f1ec5ebdd823
|
|
4
|
+
data.tar.gz: f9b7139fbc8cac5475fd0b1429ebf90d8f06884c71d49c122b48c310c180c442
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
-
[](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
|
-
|
|
30
|
-
|
|
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
|
|
48
|
-
|
|
|
49
|
-
| 9:
|
|
50
|
-
| 10:
|
|
51
|
-
|
|
|
52
|
-
| 13:
|
|
53
|
-
|
|
|
54
|
-
|
|
|
55
|
-
|
|
|
56
|
-
|
|
|
57
|
-
| 23:
|
|
58
|
-
| 24:
|
|
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
|
|
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
|
-
-
|
|
123
|
-
|
|
124
|
-
|
|
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.
|
|
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
|
+

|
|
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
|

|
|
321
445
|
|
|
322
|
-
|
|
323
|
-
|
|
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
|

|
|
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
|
+

|
|
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
|
|
534
|
-
|
|
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
|
-
| `
|
|
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
|
|
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
|
-
#
|
|
661
|
-
config.
|
|
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
|