plan_driven 0.1.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 (55) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +76 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +823 -0
  5. data/exe/plan-driven +7 -0
  6. data/lib/generators/plan_driven/install_generator.rb +37 -0
  7. data/lib/generators/plan_driven/templates/create_plan_driven_tables.rb.tt +73 -0
  8. data/lib/generators/plan_driven/templates/plan_driven.rb +39 -0
  9. data/lib/plan_driven/agent_prompt.rb +86 -0
  10. data/lib/plan_driven/cli/plan_commands.rb +158 -0
  11. data/lib/plan_driven/cli/setup_commands.rb +97 -0
  12. data/lib/plan_driven/cli/ticket_commands.rb +155 -0
  13. data/lib/plan_driven/cli/ui.rb +101 -0
  14. data/lib/plan_driven/cli.rb +150 -0
  15. data/lib/plan_driven/configuration.rb +114 -0
  16. data/lib/plan_driven/credentials.rb +90 -0
  17. data/lib/plan_driven/cursor_agents.rb +82 -0
  18. data/lib/plan_driven/cursor_llm.mjs +70 -0
  19. data/lib/plan_driven/cursor_llm.rb +79 -0
  20. data/lib/plan_driven/delivery.rb +362 -0
  21. data/lib/plan_driven/drafter.rb +122 -0
  22. data/lib/plan_driven/errors.rb +22 -0
  23. data/lib/plan_driven/evidence.rb +108 -0
  24. data/lib/plan_driven/gherkin.rb +42 -0
  25. data/lib/plan_driven/github.rb +118 -0
  26. data/lib/plan_driven/guards/migration_guard.rb +94 -0
  27. data/lib/plan_driven/guards/plan_guard.rb +84 -0
  28. data/lib/plan_driven/guards/pr_guard.rb +140 -0
  29. data/lib/plan_driven/guards/ticket_guard.rb +178 -0
  30. data/lib/plan_driven/guards/ticket_normalizer.rb +90 -0
  31. data/lib/plan_driven/guards.rb +57 -0
  32. data/lib/plan_driven/http.rb +60 -0
  33. data/lib/plan_driven/json_reply.rb +26 -0
  34. data/lib/plan_driven/llm.rb +80 -0
  35. data/lib/plan_driven/models/approval.rb +12 -0
  36. data/lib/plan_driven/models/event.rb +13 -0
  37. data/lib/plan_driven/models/evidence_run.rb +17 -0
  38. data/lib/plan_driven/models/plan.rb +98 -0
  39. data/lib/plan_driven/models/record.rb +8 -0
  40. data/lib/plan_driven/models/ticket.rb +58 -0
  41. data/lib/plan_driven/models.rb +8 -0
  42. data/lib/plan_driven/railtie.rb +9 -0
  43. data/lib/plan_driven/renderer/html.rb +106 -0
  44. data/lib/plan_driven/renderer/markdown.rb +145 -0
  45. data/lib/plan_driven/renderer/pdf.rb +39 -0
  46. data/lib/plan_driven/renderer/style.css +22 -0
  47. data/lib/plan_driven/renderer.rb +41 -0
  48. data/lib/plan_driven/repository.rb +35 -0
  49. data/lib/plan_driven/schema_context.rb +98 -0
  50. data/lib/plan_driven/template.rb +113 -0
  51. data/lib/plan_driven/ticket_generator.rb +98 -0
  52. data/lib/plan_driven/version.rb +5 -0
  53. data/lib/plan_driven/workflow.rb +59 -0
  54. data/lib/plan_driven.rb +72 -0
  55. metadata +146 -0
data/README.md ADDED
@@ -0,0 +1,823 @@
1
+ # plan_driven
2
+
3
+ [![CI](https://github.com/blaz1988/plan-driven/actions/workflows/main.yml/badge.svg)](https://github.com/blaz1988/plan-driven/actions/workflows/main.yml)
4
+ [![Gem Version](https://img.shields.io/gem/v/plan_driven.svg)](https://rubygems.org/gems/plan_driven)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE.txt)
6
+ [![Ruby](https://img.shields.io/badge/Ruby-3.1%20to%203.4-CC342D.svg)](#rails-and-ruby-support)
7
+ [![Rails](https://img.shields.io/badge/Rails-7.0%20to%208.1-D30001.svg)](#rails-and-ruby-support)
8
+
9
+ **From implementation plan to merged, tested pull requests, driven from the terminal.**
10
+
11
+ `plan_driven` runs a Rails team's delivery process from the command line, with AI agents doing
12
+ the writing and your team making the decisions. A short interview in the terminal becomes an
13
+ implementation plan grounded in your real schema and code. Guards written in Ruby check the
14
+ plan, you read it and approve it. The approved plan becomes tickets, each ticket goes to a
15
+ Cursor cloud agent that opens a pull request, and only the pull requests you approve are
16
+ merged. Acceptance criteria map to Cucumber scenarios, so the delivery report shows which
17
+ criterion is proven by which passing test.
18
+
19
+ Every phase leaves documentation behind in `docs/plans/`: the plan, the tickets, who approved
20
+ what, and the delivery report.
21
+
22
+ Built and maintained by [Rubycode](https://rubycode.co), a Ruby on Rails company from
23
+ Zagreb. [Need Rails engineers?](#about-rubycode)
24
+
25
+ ## Watch it deliver a feature
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)
28
+
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
31
+ idea to production code in a new Rails 8 app,
32
+ [Gather](https://github.com/blaz1988/gather). Nothing in it is staged: the plan, the five
33
+ tickets, the five pull requests ([#8](https://github.com/blaz1988/gather/pull/8) to
34
+ [#12](https://github.com/blaz1988/gather/pull/12)) and the delivery report are all in that
35
+ repository. The planner and the five agents ran on Claude Opus 5.5 through Cursor.
36
+
37
+ <details>
38
+ <summary>Chapters</summary>
39
+
40
+ | Time | Chapter |
41
+ | ---: | --- |
42
+ | 0:00 | Why plan_driven, the flow, and the guards |
43
+ | 2:10 | The app before the feature |
44
+ | 2:49 | `doctor`: keys, repository and the Cursor connection |
45
+ | 3:15 | The interview |
46
+ | 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 |
59
+
60
+ </details>
61
+
62
+ ## Contents
63
+
64
+ - [How it works](#how-it-works)
65
+ - [Requirements](#requirements)
66
+ - [Installation](#installation)
67
+ - [Getting your app ready](#getting-your-app-ready)
68
+ - [Walkthrough: one feature from idea to merged](#walkthrough-one-feature-from-idea-to-merged)
69
+ - [Guards](#guards)
70
+ - [What the agent is told](#what-the-agent-is-told)
71
+ - [Commands](#commands)
72
+ - [Configuration](#configuration)
73
+ - [Keys](#keys)
74
+ - [Working as a team](#working-as-a-team)
75
+ - [Troubleshooting](#troubleshooting)
76
+ - [Rails and Ruby support](#rails-and-ruby-support)
77
+ - [Development](#development)
78
+ - [Contributing](#contributing)
79
+ - [About Rubycode](#about-rubycode)
80
+ - [License](#license)
81
+
82
+ ## How it works
83
+
84
+ ```
85
+ plan-driven new interview in the terminal, the model drafts the rest
86
+ │ PlanGuard + MigrationGuard, repaired until they pass
87
+ ▼
88
+ plan-driven submit draft ─▶ in review docs/plans/pd-1-…/plan.pdf
89
+ plan-driven approve in review ─▶ approved every configured role signs off
90
+ │
91
+ ▼
92
+ plan-driven tickets approved ─▶ ticketed TicketNormalizer + TicketGuard
93
+ plan-driven approve-tickets ─▶ tickets approved GitHub issues created
94
+ │
95
+ ▼
96
+ plan-driven develop one Cursor cloud agent per ready ticket, one PR each
97
+ plan-driven status agent finished ─▶ PR open
98
+ plan-driven review PrGuard: scope, specs, Cucumber scenarios, CI, up to date
99
+ plan-driven feedback the same agent pushes a fix to the same PR
100
+ plan-driven approve-pr PR approved
101
+ plan-driven merge merged; dependent tickets become ready
102
+ │
103
+ ▼
104
+ plan-driven evidence Cucumber results mapped to acceptance criteria
105
+ plan-driven report docs/plans/pd-1-…/delivery-report.pdf
106
+ ```
107
+
108
+ Phases are stored in your application's database, so a plan can't skip a step. Tickets can't
109
+ be drafted before the plan is approved, agents can't start before the tickets are approved,
110
+ and a ticket starts only once every ticket it depends on is merged. Editing an approved plan
111
+ creates a new revision and asks for approval again.
112
+
113
+ The model does the writing and Ruby does the checking. Rules that have one right answer, like
114
+ title prefixes, estimates on the team's scale and the order of expand and contract, are
115
+ enforced or corrected in code rather than asked for in a prompt. Guard errors go back to the
116
+ model as a list to fix, and a plan that still fails isn't accepted.
117
+
118
+ ## Requirements
119
+
120
+ - Ruby 3.1+ and Rails 7.0+ (see [support](#rails-and-ruby-support)).
121
+ - 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.
125
+ - A model for drafting plans and tickets, one of:
126
+ - **Cursor** (`llm_provider :cursor`): any model on your Cursor account, Claude Opus 5.5 by
127
+ default. Needs Node 22.13+ and the Cursor SDK. No other LLM key.
128
+ - **OpenAI** (`:openai`, GPT-4.1 by default) or any OpenAI-compatible gateway.
129
+ - **Anthropic** (`:anthropic`, Claude Sonnet 4.5 by default).
130
+ - A GitHub token that can read and write issues and pull requests (see [Keys](#keys)).
131
+ - Optional: Google Chrome or Chromium for PDFs, and Cucumber for the evidence step.
132
+
133
+ ## Installation
134
+
135
+ ### 1. Add the gem
136
+
137
+ ```ruby
138
+ # Gemfile
139
+ gem "plan_driven", group: :development
140
+ ```
141
+
142
+ ```bash
143
+ bundle install
144
+ bundle binstubs plan_driven # bin/plan-driven, so you don't type bundle exec
145
+ ```
146
+
147
+ ### 2. Generate the tables and the initializer
148
+
149
+ ```bash
150
+ bin/rails generate plan_driven:install
151
+ bin/rails db:migrate
152
+ ```
153
+
154
+ The generator adds five tables (`plan_driven_plans`, `_tickets`, `_approvals`, `_events` and
155
+ `_evidence_runs`), `config/initializers/plan_driven.rb` and `docs/plans/`. Nothing else in
156
+ your application changes. The tables live in your development database, next to your app,
157
+ so the plan's phase and its audit trail travel with the code you're working on.
158
+
159
+ ### 3. Choose the model that drafts
160
+
161
+ Planning is where a stronger model pays for itself. The demo drafts with Claude Opus 5.5 on a
162
+ Cursor account, which reads the application's code (read-only) while it writes:
163
+
164
+ ```ruby
165
+ # config/initializers/plan_driven.rb
166
+ PlanDriven.configure do |config|
167
+ config.llm_provider = :cursor
168
+ config.llm_model = "claude-opus-5-5"
169
+ config.request_timeout = 600 # a full plan from a large model can take a few minutes
170
+
171
+ config.agent_model = "claude-opus-5-5" # the model the cloud agents use
172
+ end
173
+ ```
174
+
175
+ `:cursor` needs Node 22.13+ and the Cursor SDK, installed outside your app:
176
+
177
+ ```bash
178
+ npm install --prefix ~/.plan_driven/node @cursor/sdk
179
+ ```
180
+
181
+ If `node` on your `PATH` is older, point the gem at a newer one with
182
+ `config.node_command = "/path/to/node"` or `PLAN_DRIVEN_NODE`.
183
+
184
+ With OpenAI or Anthropic instead:
185
+
186
+ ```ruby
187
+ config.llm_provider = :openai # gpt-4.1 by default
188
+ config.llm_provider = :anthropic # claude-sonnet-4-5 by default
189
+ config.llm_api_base = "https://gateway.example.com/v1" # optional, OpenAI-compatible
190
+ ```
191
+
192
+ ### 4. Add your keys
193
+
194
+ ```bash
195
+ bin/plan-driven configure
196
+ ```
197
+
198
+ It asks for each key, stores it in `~/.plan_driven/config` (mode 0600) and never echoes it. Keys
199
+ already in the environment are used as they are and never copied to disk. Nothing is written
200
+ into your application. See [Keys](#keys).
201
+
202
+ ### 5. Check everything
203
+
204
+ ```
205
+ $ bin/plan-driven doctor
206
+
207
+ plan-driven 0.1.0
208
+ ✓ Rails application: plan_driven tables present
209
+ LLM: cursor/claude-opus-5-5
210
+ ✓ Cursor SDK: Node v24.21.0, @cursor/sdk found
211
+ ✓ cursor_api_key: /Users/ivan/.plan_driven/config
212
+ ✓ github_token: /Users/ivan/.plan_driven/config
213
+ ✓ GitHub repository: blaz1988/gather
214
+ ✓ PDF: Google Chrome
215
+ ✓ Cursor API: ivan@example.com
216
+ ✓ Agent model: claude-opus-5-5
217
+ ```
218
+
219
+ `doctor` checks the tables, the drafting model, Node and the SDK, each key, the GitHub
220
+ repository (from `config.github_repository` or the `origin` remote), the PDF browser, the Cursor
221
+ API and that your key can start agents with `agent_model`.
222
+
223
+ ## Getting your app ready
224
+
225
+ The agents and the review guards rely on a few things in your repository. Set them up once.
226
+
227
+ **CI on pull requests.** `review` and `merge` read the checks on the pull request, and nothing
228
+ is merged while a check fails or is still running. Run your linters and your whole suite,
229
+ including Cucumber:
230
+
231
+ ```yaml
232
+ # .github/workflows/ci.yml (the test job)
233
+ - name: Prepare the database
234
+ run: bin/rails db:create db:schema:load
235
+ - name: RSpec
236
+ run: bundle exec rspec
237
+ - name: Cucumber
238
+ run: bundle exec cucumber --publish-quiet
239
+ ```
240
+
241
+ **Cucumber.** Every acceptance criterion needs a scenario tagged with its ticket and number,
242
+ which is how `evidence` proves it. Add `cucumber-rails` to the test group and run
243
+ `bin/rails generate cucumber:install`, or let the first agent do it: the prompt tells it to if
244
+ the app has no Cucumber setup. Shared steps, such as signing in, keep the agents' features
245
+ short; point them out in `team_rules`.
246
+
247
+ **Your conventions.** `team_rules` and `extra_context` go into every agent prompt, and the
248
+ drafting model reads them too. Write them the way you'd brief a new engineer:
249
+
250
+ ```ruby
251
+ config.team_rules = [
252
+ "Views are ERB and reuse the classes in app/assets/stylesheets/application.css.",
253
+ "Authentication is Rails 8's: Current.user, `allow_unauthenticated_access`, `authenticated?` in views.",
254
+ "Keep controllers thin; put a multi-step change in a model method or a PORO in app/models.",
255
+ "Tests are RSpec request and model specs with FactoryBot, and Cucumber features that reuse " \
256
+ "features/step_definitions/common_steps.rb (e.g. `Given I am signed in as \"Ana Kovač\"`).",
257
+ "bin/rubocop, bundle exec rspec and bundle exec cucumber must pass; CI runs all three."
258
+ ]
259
+ config.extra_context = "Gather lists community events. An event has an organizer (a User) and a " \
260
+ "capacity in seats. Anyone can browse; signing in is needed to act."
261
+ ```
262
+
263
+ **Who approves.** `plan_approvals` and `ticket_approvals` list the roles that must sign off,
264
+ for example `%w[review qa devops director]`. One person can hold every role on a small team.
265
+
266
+ ## Walkthrough: one feature from idea to merged
267
+
268
+ This is the run from the demo video, in [Gather](https://github.com/blaz1988/gather), with the
269
+ real output. The plan it produced is in
270
+ [`docs/plans/pd-1-rsvps-with-a-waitlist`](https://github.com/blaz1988/gather/tree/main/docs/plans/pd-1-rsvps-with-a-waitlist).
271
+
272
+ ### 1. The interview
273
+
274
+ `new` asks the questions only people can answer: what, why, where, who, when, background and
275
+ what's out of scope. Each answer ends with an empty line.
276
+
277
+ ```
278
+ $ bin/plan-driven new "RSVPs with a waitlist"
279
+
280
+ Plan: RSVPs with a waitlist
281
+ A few questions first. The rest of the plan is drafted from your answers and the schema.
282
+ What: What are we building? Describe the change as the user will see it.
283
+ > Signed-in people can RSVP to an event and cancel their RSVP. The event page shows how many seats are left.
284
+ > When an event is full, RSVPing puts you on a waitlist; when someone cancels, the first person waiting gets the seat.
285
+ > The organizer sees who is going and who is waiting.
286
+ >
287
+ Why: Why now? What problem or gap does it close?
288
+ > Events have a capacity, but nothing counts seats. Organizers collect names in chat and the small rooms overflow.
289
+ >
290
+ ...
291
+ Out of Scope: What is explicitly out of scope? (optional)
292
+ > Email notifications, paid tickets, guests (+1), and changing capacity after people have RSVPed.
293
+ >
294
+
295
+ Drafting with cursor/claude-opus-5-5...
296
+ ✓ PD-1 drafted (1 attempt)
297
+ assumed: Cancelling deletes the `rsvps` row, so there is no cancelled status. Someone who RSVPs again joins the back of the waitlist.
298
+ assumed: The waitlist is first come, first served by `created_at`, then `id`. Promotion is automatic and immediate, with no confirmation step.
299
+ assumed: The organizer's attendee list shows `users.name` only. `users.email_address` is not shown.
300
+ ...
301
+ ✓ All checks passed
302
+ docs/plans/pd-1-rsvps-with-a-waitlist/plan.md
303
+ docs/plans/pd-1-rsvps-with-a-waitlist/plan.html
304
+ docs/plans/pd-1-rsvps-with-a-waitlist/plan.pdf
305
+ Next: read PD-1 (`plan-driven show PD-1` or the PDF), then `plan-driven submit PD-1`.
306
+ ```
307
+
308
+ The model drafts everything else from your answers, the schema and the code: the existing data
309
+ structure, the architectural, database, application and infrastructure changes, risks,
310
+ performance, security with a risk level, monitoring, outstanding questions and testing. The
311
+ sections follow the implementation plan template teams commonly keep in Confluence. Every
312
+ assumption it made is listed, so you know what to check first.
313
+
314
+ ### 2. Read the plan
315
+
316
+ Open `plan.pdf` or `plan.html`, or print it with `show PD-1` (`--section database_changes` for
317
+ one section). Existing Data Structure is checked against the app, so every model, file and
318
+ column it cites exists.
319
+
320
+ ![The plan, grounded in the real schema](docs/images/03-plan.png)
321
+
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:
324
+
325
+ ```
326
+ $ bin/plan-driven redraft PD-1 outstanding_questions "Record my answers under Decided and keep only
327
+ the non-blocking questions open. The organizer cannot RSVP to their own event. RSVP and cancel
328
+ close when the event starts ..."
329
+ Redrafting outstanding_questions...
330
+ ### Decided
331
+ - **Organizer RSVPs:** the organizer cannot RSVP to their own event. `Event#rsvp` rejects the call
332
+ when `organized_by?(user)` is true. ...
333
+ ### Still open (not blocking)
334
+ - Where does the production SQLite database live? ...
335
+ ✓ All checks passed
336
+ ```
337
+
338
+ ![The decisions, recorded in the plan](docs/images/04-plan-decided.png)
339
+
340
+ ### 3. Submit and approve
341
+
342
+ ```
343
+ $ bin/plan-driven submit PD-1
344
+ ✓ PD-1 revision 3 is in review
345
+ Approvals needed: review. `plan-driven approve PD-1 --as ROLE`
346
+
347
+ $ bin/plan-driven approve PD-1 --note "Read it end to end. Decisions recorded under Outstanding questions."
348
+ ✓ PD-1 approved as review by Ivan Blažević <ivan.blazevic@rubycode.co>
349
+ ✓ Every approval is in. PD-1 is approved; `plan-driven tickets PD-1` drafts the tickets.
350
+ ```
351
+
352
+ `submit` runs the guards again and refuses a plan that fails them. `reject PD-1 --note "..."`
353
+ sends it back to draft. An approval belongs to a revision: edit the plan afterwards and it
354
+ needs approving again.
355
+
356
+ ### 4. Tickets
357
+
358
+ `tickets` splits the approved plan into tickets. Warnings show where the breakdown could be
359
+ better. Pass an instruction to redraft the whole set:
360
+
361
+ ```
362
+ $ bin/plan-driven tickets PD-1
363
+ ! T3 has 10 acceptance criteria; consider splitting it
364
+ # Title Kind Pts Status
365
+ T1 Migration: Create rsvps table migration 2 draft
366
+ T2 Show seats left on the event page code 3 draft
367
+ ...
368
+ T9 Docs: RSVP launch runbook and invariant checks docs 1 draft
369
+
370
+ $ bin/plan-driven tickets PD-1 "Make it five tickets with at most 7 acceptance criteria each: the
371
+ rsvps migration; the Rsvp model and Event rules ...; seats left on the events index. Drop the docs ticket."
372
+ ✓ Tickets pass every check
373
+ # Title Kind Pts Status
374
+ T1 Migration: Create rsvps table migration 1 draft
375
+ T2 Add Rsvp model and Event rules for RSVP, canc... code 5 draft
376
+ T3 Let signed-in people RSVP, join the waitlist ... code 5 draft
377
+ T4 Show the organizer who is going and who is wa... code 3 draft
378
+ T5 Show seats left on the events index code 2 draft
379
+ ```
380
+
381
+ Every ticket has a type, a kind, a description, testable acceptance criteria, an estimate, its
382
+ dependencies and the tables it touches. They're added to the plan's PDF as a work overview, so
383
+ you read them where you read the plan.
384
+
385
+ ![Tickets in the plan's work overview](docs/images/06-work-overview.png)
386
+
387
+ ```
388
+ $ bin/plan-driven approve-tickets PD-1
389
+ ✓ 5 tickets approved
390
+ T1 -> issue #3
391
+ ...
392
+ T5 -> issue #7
393
+ ```
394
+
395
+ Approving creates a GitHub issue for each ticket, with the story, the acceptance criteria and
396
+ the implementation notes, unless `sync_issues` is off.
397
+
398
+ ![A ticket as a GitHub issue](docs/images/07-issue.png)
399
+
400
+ ### 5. Development
401
+
402
+ `prompt PD-1/T1` prints exactly what the agent will be told ([see below](#what-the-agent-is-told)).
403
+ `develop` starts one Cursor cloud agent for each ready ticket:
404
+
405
+ ```
406
+ $ bin/plan-driven develop PD-1
407
+ T1 Migration: Create rsvps table
408
+ Start 1 Cursor cloud agent(s)? [y/N] y
409
+ ✓ T1 agent started: https://cursor.com/agents/bc-…
410
+ Each agent opens a pull request when it finishes. `plan-driven status PD-1` checks on them.
411
+
412
+ $ bin/plan-driven status PD-1
413
+ PD-1 RSVPs with a waitlist: in development
414
+ # Title Kind Pts Status PR
415
+ T1 Migration: Create rsvps table migration 1 pr open https://github.com/blaz1988/gather/pull/8
416
+ T2 Add Rsvp model and Event rules for RSVP, canc... code 5 approved
417
+ ...
418
+ Next: `plan-driven review PD-1/T1`
419
+ ```
420
+
421
+ Only tickets whose dependencies are merged start, up to `max_parallel_agents` at a time, so
422
+ each agent begins from a `main` that already has the work it builds on. Run `develop PD-1`
423
+ again after each merge; `develop PD-1 T4` starts one ticket.
424
+
425
+ ### 6. Review the pull request
426
+
427
+ Read the pull request on GitHub as you would any other. Then run the guards:
428
+
429
+ ![The agent's pull request](docs/images/09-pull-request-files.png)
430
+
431
+ ```
432
+ $ bin/plan-driven review PD-1/T1
433
+ Reviewing https://github.com/blaz1988/gather/pull/8
434
+ ✓ Refers to PD-1/T1 and closes #3
435
+ ✓ 7 files, 309 changed lines (limit 800)
436
+ ✓ 5 spec and feature files changed
437
+ ✓ A migration ticket, and it only changes db/ and tests
438
+ ✓ All 6 acceptance criteria have a scenario tagged @pd-1-t1 @ac-N
439
+ ✓ CI is green: lint, scan_js, test, scan_ruby
440
+ ✓ The pull request passes every check
441
+ ```
442
+
443
+ For UI work, check out the branch and try it. The guards prove the criteria have tests; you
444
+ decide whether the feature is right.
445
+
446
+ ![Trying the RSVP card on the branch](docs/images/12-try-branch.png)
447
+
448
+ ### 7. Feedback
449
+
450
+ When something isn't right, send it to the same agent. It pushes to the same pull request, and
451
+ you review again:
452
+
453
+ ```
454
+ $ bin/plan-driven review PD-1/T5
455
+ ...
456
+ ! The branch is 2 commit(s) behind main, so CI ran without them. Ask the agent to merge main and
457
+ run the checks again (`plan-driven feedback`).
458
+
459
+ $ bin/plan-driven feedback PD-1/T5 "Two things. T3 is merged, so merge main into this branch and
460
+ run RuboCop, RSpec and Cucumber again. And seats_left_label repeats the clamp in Event#seats_left:
461
+ let Event#seats_left take a preloaded going count, and have the helper use it."
462
+ ✓ Sent to PD-1/T5's agent; it will push to the same pull request
463
+ ```
464
+
465
+ ### 8. Approve and merge
466
+
467
+ ```
468
+ $ bin/plan-driven approve-pr PD-1/T1 --note "Read the migration and the feature. Matches the plan."
469
+ ✓ All checks passed
470
+ ✓ PD-1/T1 pull request approved by Ivan Blažević <ivan.blazevic@rubycode.co>
471
+
472
+ $ bin/plan-driven merge PD-1/T1
473
+ PD-1/T1 Migration: Create rsvps table
474
+ https://github.com/blaz1988/gather/pull/8, approved by Ivan Blažević <ivan.blazevic@rubycode.co>
475
+ This merges into main. Type T1 to continue: T1
476
+ ✓ PD-1/T1 merged (bafbdf1)
477
+ Now ready: T2. `plan-driven develop PD-1`
478
+ ```
479
+
480
+ `approve-pr` runs the guards first, records the approval, and posts it on the pull request as
481
+ a review comment with your note. `merge` runs them
482
+ again, marks the agent's draft pull request ready, and merges with `merge_method` once you type
483
+ the ticket key. It refuses while a guard fails or CI is still running. A pull request merged
484
+ directly on GitHub is picked up by `status`.
485
+
486
+ ### 9. Evidence and the delivery report
487
+
488
+ ```
489
+ $ bin/plan-driven evidence PD-1
490
+ Running cucumber --tags "@pd-1-t1 or @pd-1-t2 or @pd-1-t3 or @pd-1-t4 or @pd-1-t5"
491
+ AC Result Criterion
492
+ T1.1 passed Running bin/rails db:migrate creates the rsvps table with event_id,...
493
+ T2.4 passed When several threads race for the last seat of an event, exactly on...
494
+ T3.3 passed When 3 people are going, a fourth person sees "Full" and a "Join wa...
495
+ T4.6 passed No attendee's email address appears anywhere on the event page, inc...
496
+ ...
497
+ ✓ 33 of 33 acceptance criteria are proven by a passing scenario.
498
+
499
+ $ bin/plan-driven report PD-1
500
+ ✓ Delivery report for PD-1 written
501
+ docs/plans/pd-1-rsvps-with-a-waitlist/delivery-report.md
502
+ docs/plans/pd-1-rsvps-with-a-waitlist/delivery-report.html
503
+ docs/plans/pd-1-rsvps-with-a-waitlist/delivery-report.pdf
504
+ ```
505
+
506
+ `evidence` runs the plan's scenarios on your machine and stores the result with the commit it
507
+ ran on; `--from cucumber.json` imports a run from CI instead. The delivery report lists each
508
+ ticket with its pull request, merge commit and approver, then every acceptance criterion with
509
+ the scenario that proves it, the guard findings, every approval and the full timeline. Commit
510
+ `docs/plans/` with it, and the plan and its proof stay next to the code.
511
+
512
+ ![The delivery report](docs/images/15-delivery-report.png)
513
+
514
+ ### The result
515
+
516
+ ![Maja, promoted from the waitlist when a seat opened](docs/images/17-app-promoted.png)
517
+
518
+ ## Guards
519
+
520
+ Guards are plain Ruby classes that return errors, warnings, fixes and the checks that passed.
521
+ An error blocks the next step and a warning is shown and recorded.
522
+
523
+ **PlanGuard** runs on every draft and before `submit`:
524
+
525
+ - every required section is written, and long enough to be useful;
526
+ - Existing Data Structure describes only what exists: every model, `app/models` path and
527
+ `table.column` it mentions is checked against the application;
528
+ - Security ends with a risk level (LOW, MEDIUM or HIGH);
529
+ - placeholders such as TBD and TODO are flagged.
530
+
531
+ **MigrationGuard** reads the database changes:
532
+
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;
535
+ - NOT NULL on an existing table without a default or backfill is a warning;
536
+ - on PostgreSQL, an index that isn't built concurrently is a warning.
537
+
538
+ **TicketNormalizer** fixes, and reports, anything with one right answer: keys in order,
539
+ dependencies renumbered, kinds and types normalized, "Migration:" and "Data migration:"
540
+ title prefixes, estimates rounded up to the team's scale, and lists cleaned up.
541
+
542
+ **TicketGuard** checks the breakdown:
543
+
544
+ - each ticket has a title, a description and at least one acceptance criterion long enough to
545
+ test, and a story says "so that";
546
+ - estimates are within `max_estimate`, so a ticket too big for one pull request is split;
547
+ - dependencies exist and have no cycles;
548
+ - on each table, expand and contract happens in order: migration, dual write, backfill,
549
+ switch, then cleanup. A migration that removes a column counts as cleanup;
550
+ - a table the plan changes that no ticket touches is flagged, and so is a table no ticket
551
+ should touch.
552
+
553
+ **PrGuard** runs on `review`, before `approve-pr` and again before `merge`:
554
+
555
+ - the pull request names the ticket and closes its issue;
556
+ - it stays within `max_pr_changed_lines`;
557
+ - it contains specs or features;
558
+ - only migration and backfill tickets add migrations, and a new migration comes with a
559
+ `db/schema.rb` change;
560
+ - every acceptance criterion has a Cucumber scenario tagged with the ticket and the criterion,
561
+ for example `@pd-1-t3 @ac-2`;
562
+ - CI checks passed, with failures an error and pending checks a warning;
563
+ - the branch isn't behind the base branch, so CI ran against the code it will merge into.
564
+
565
+ ## What the agent is told
566
+
567
+ `plan-driven prompt PD-1/T1` prints the exact prompt. It contains the ticket, the parts of the
568
+ approved plan it needs, and the rules the pull request will be checked against afterwards:
569
+
570
+ ```
571
+ # Ticket PD-1/T1: Migration: Create rsvps table
572
+ ...
573
+ # Definition of done
574
+ - Specs cover the change (spec/, test/, features/), and the existing suite still passes.
575
+ - Every acceptance criterion has a Cucumber scenario in `features/pd-1-rsvps-with-a-waitlist/t1.feature`.
576
+ Tag the feature `@pd-1-t1` and each scenario `@ac-N`, where N is the criterion's number above.
577
+ - If the app has no Cucumber setup yet, add `cucumber-rails` to the test group and run
578
+ `bin/rails generate cucumber:install` in this pull request.
579
+ - Keep the change within 800 changed lines.
580
+
581
+ # Rules
582
+ - Follow the conventions already used in this codebase.
583
+ - Schema changes only in migration tickets; this ticket is a migration ticket.
584
+ - Migrations are additive and reversible. Never remove or rename a column that code still reads.
585
+ - Don't edit files under docs/plans; they are the approved plan.
586
+ - Views are ERB and reuse the classes in app/assets/stylesheets/application.css ... ← config.team_rules
587
+ - bin/rubocop, bundle exec rspec and bundle exec cucumber must pass; CI runs all three.
588
+
589
+ # Pull request
590
+ Title it exactly: [PD-1/T1] Migration: Create rsvps table
591
+ In the description include:
592
+ - `PD-1/T1`
593
+ - a line `Closes #3`
594
+ - each acceptance criterion as a checklist, with the spec or scenario that proves it
595
+ ```
596
+
597
+ ## Commands
598
+
599
+ Run them as `bin/plan-driven COMMAND` (or `bundle exec plan-driven COMMAND`). `PLAN` is a plan
600
+ key such as `PD-1`, and `PLAN/TICKET` is a ticket such as `PD-1/T3`.
601
+
602
+ | Command | What it does |
603
+ | --- | --- |
604
+ | `new TITLE` | Interview, then draft the plan from the answers, the schema and the code |
605
+ | `list` | Every plan and its phase |
606
+ | `show PLAN [--section KEY]` | Print the plan or one section |
607
+ | `edit PLAN SECTION` | Edit a section in `$EDITOR` |
608
+ | `redraft PLAN SECTION "instruction"` | Have the model rewrite one section |
609
+ | `check PLAN` | Run the plan guards |
610
+ | `submit PLAN` | Send the plan for approval; the guards must pass |
611
+ | `approve PLAN [--as ROLE] [--note TEXT]` | Approve the current revision |
612
+ | `reject PLAN --note TEXT [--as ROLE]` | Send the plan back to draft |
613
+ | `pdf PLAN` | Write the plan as Markdown, HTML and PDF |
614
+ | `tickets PLAN ["instruction"]` | Draft tickets from the approved plan, or redraft them |
615
+ | `approve-tickets PLAN [--as ROLE]` | Approve the tickets and create GitHub issues |
616
+ | `prompt PLAN/TICKET` | Show what the agent will be told |
617
+ | `develop PLAN [TICKET...]` | Start agents for ready tickets |
618
+ | `status PLAN` | Poll agents and pull requests, then show every ticket and the next step |
619
+ | `review PLAN/TICKET` | Run the pull request guards |
620
+ | `feedback PLAN/TICKET "text"` | Send review feedback to the ticket's agent |
621
+ | `approve-pr PLAN/TICKET [--note TEXT]` | Approve the pull request; the guards must pass |
622
+ | `merge PLAN/TICKET` | Merge an approved pull request, after typing the ticket key |
623
+ | `evidence PLAN [--from FILE]` | Run or import Cucumber results |
624
+ | `report PLAN` | Write the delivery report |
625
+ | `log PLAN` | The audit trail |
626
+ | `configure` | Store keys in `~/.plan_driven/config` |
627
+ | `doctor` | Check keys, repository, PDF browser and the Cursor connection |
628
+
629
+ Section keys for `show --section`, `edit` and `redraft`: `what`, `why`, `where`, `who`,
630
+ `when`, `background`, `existing_data_structure`, `architecture`, `database_changes`,
631
+ `application_changes`, `infrastructure_changes`, `out_of_scope`, `risks`, `performance`,
632
+ `security`, `monitoring`, `outstanding_questions` and `testing`. `edit PLAN` without a section
633
+ lists them.
634
+
635
+ `--yes` skips confirmations, for scripts. `merge` still needs the ticket key typed unless
636
+ `--yes` is given.
637
+
638
+ ## Configuration
639
+
640
+ Everything has a default, so only keys are required. The generated initializer lists the
641
+ settings you're most likely to change:
642
+
643
+ ```ruby
644
+ # config/initializers/plan_driven.rb
645
+ PlanDriven.configure do |config|
646
+ # Drafting plans and tickets
647
+ config.llm_provider = :cursor # :openai (default), :anthropic or :cursor
648
+ config.llm_model = "claude-opus-5-5" # default: gpt-4.1, claude-sonnet-4-5, claude-opus-5-5
649
+ config.llm_api_base = nil # an OpenAI-compatible gateway
650
+ config.temperature = 0.2
651
+ config.request_timeout = 180 # seconds per model call
652
+ config.max_repair_attempts = 2 # redrafts when a guard fails
653
+ config.node_command = "node" # :cursor only; Node 22.13+ (or PLAN_DRIVEN_NODE)
654
+ config.cursor_sdk_path = nil # where @cursor/sdk is, if not ~/.plan_driven/node
655
+
656
+ # Approvals
657
+ config.plan_approvals = %w[review] # e.g. %w[review qa devops director]
658
+ config.ticket_approvals = %w[review]
659
+
660
+ # Cursor cloud agents
661
+ config.agent_model = nil # nil uses your Cursor default
662
+ config.base_branch = "main"
663
+ config.max_parallel_agents = 3
664
+ config.skip_reviewer_request = false # true: the agent doesn't request you as reviewer
665
+
666
+ # GitHub
667
+ config.github_repository = nil # "owner/name"; read from the origin remote when nil
668
+ config.sync_issues = true
669
+ config.issue_labels = %w[plan-driven] # plus the plan key and the ticket kind
670
+ config.merge_method = "squash" # or "merge", "rebase"
671
+
672
+ # Guards
673
+ config.estimate_scale = [1, 2, 3, 5, 8]
674
+ config.max_estimate = 5
675
+ config.max_pr_changed_lines = 800
676
+ config.require_specs_in_pr = true
677
+ config.spec_paths = %w[spec/ test/ features/]
678
+ config.cucumber = true
679
+ config.features_path = "features"
680
+
681
+ # What the model and the agents should know
682
+ config.team_rules = ["Authorization goes through Pundit policies, never in controllers."]
683
+ config.extra_context = "Tenancy is by Account; every table has account_id."
684
+
685
+ # Output
686
+ config.docs_path = "docs/plans"
687
+ config.pdf_renderer = nil # a callable (html_path, pdf_path); nil uses Chrome
688
+ end
689
+ ```
690
+
691
+ `llm_provider :cursor` drafts with any model on your Cursor account through the
692
+ [Cursor SDK](https://cursor.com/docs/sdk/typescript). The agent runs on your machine with
693
+ read-only tools (read, grep, glob, ls), so it reads the application's code while it writes the
694
+ plan and can't change a file.
695
+
696
+ `config.template` replaces the plan's sections if your template differs.
697
+
698
+ ## Keys
699
+
700
+ | Key | Used for | Environment |
701
+ | --- | --- | --- |
702
+ | `cursor_api_key` | cloud agents, and drafting with `llm_provider :cursor` (Cursor dashboard, Integrations) | `CURSOR_API_KEY` |
703
+ | `github_token` | issues, pull requests, reviews, checks, merge | `GITHUB_TOKEN`, or `gh auth token` |
704
+ | `openai_api_key` or `anthropic_api_key` | drafting with `:openai` or `:anthropic` | `OPENAI_API_KEY`, `ANTHROPIC_API_KEY` |
705
+
706
+ A fine-grained GitHub token needs, on the application's repository: Contents, Issues and Pull
707
+ requests (read and write), Checks and Commit statuses (read), and Metadata (read).
708
+
709
+ Keys are read from the environment first, then from `~/.plan_driven/config` (mode 0600 in a
710
+ 0700 directory), which `plan-driven configure` writes. A key from the environment is never
711
+ copied to disk, and no key is ever written into your application or its configuration.
712
+
713
+ ## Working as a team
714
+
715
+ The plan's state lives in the database of whoever runs the commands, and its documents live in
716
+ `docs/plans/`. Most teams have one person drive a plan (a lead or the feature's owner) and
717
+ commit `docs/plans/` so everyone reads the same plan, tickets and report in the repository and
718
+ on GitHub.
719
+
720
+ Approvals and every other action are recorded with who did them, taken from
721
+ `PLAN_DRIVEN_ACTOR` or from `git config user.name` and `user.email`. To record a colleague's
722
+ sign-off from the driver's machine:
723
+
724
+ ```bash
725
+ PLAN_DRIVEN_ACTOR="Petra Novak <petra@example.com>" bin/plan-driven approve PD-1 --as qa --note "Test plan is fine"
726
+ ```
727
+
728
+ `log PD-1` prints the full audit trail, and the delivery report includes it.
729
+
730
+ ## Troubleshooting
731
+
732
+ **`cursor: no answer within 180s`.** Large models can take a few minutes on a full plan. Raise
733
+ `config.request_timeout`, for example to 600.
734
+
735
+ **`doctor` says the Cursor SDK is missing, or Node is too old.** Install the SDK with
736
+ `npm install --prefix ~/.plan_driven/node @cursor/sdk`, and point `config.node_command` or
737
+ `PLAN_DRIVEN_NODE` at Node 22.13 or newer.
738
+
739
+ **The agent can't open the repository.** Connect GitHub in the Cursor dashboard and give it
740
+ access to the application's repository. The agents clone it and push their branches there.
741
+
742
+ **`review` says the branch is behind main.** CI ran without the latest merges. Send
743
+ `feedback` asking the agent to merge main and run the checks again, then review once CI is
744
+ green.
745
+
746
+ **`review` finds a missing scenario.** The criterion needs a scenario tagged `@pd-1-tN @ac-M`
747
+ in `features/<plan>/tN.feature`. Send `feedback` naming the criterion.
748
+
749
+ **No PDF.** Install Google Chrome or Chromium, or set `config.pdf_renderer`. The Markdown and
750
+ HTML versions are always written.
751
+
752
+ **Names or answers look garbled.** Run the commands under a UTF-8 locale
753
+ (`LANG=en_US.UTF-8`).
754
+
755
+ ## Rails and Ruby support
756
+
757
+ Ruby 3.1 or newer and Rails 7.0 or newer. CI runs the suite on every supported combination:
758
+
759
+ | | Rails 7.0 | Rails 7.1 | Rails 7.2 | Rails 8.0 | Rails 8.1 |
760
+ | --- | :---: | :---: | :---: | :---: | :---: |
761
+ | Ruby 3.1 | ✓ | ✓ | ✓ | | |
762
+ | Ruby 3.2 | ✓ | ✓ | ✓ | ✓ | ✓ |
763
+ | Ruby 3.3 | ✓ | ✓ | ✓ | ✓ | ✓ |
764
+ | Ruby 3.4 | | | ✓ | ✓ | ✓ |
765
+
766
+ The OpenAI, Anthropic, Cursor and GitHub APIs are called over `net/http`, so the gem depends
767
+ on nothing beyond Rails. `:cursor` drafting also needs Node and `@cursor/sdk`, outside your
768
+ bundle.
769
+
770
+ ## Development
771
+
772
+ ```bash
773
+ bin/setup
774
+ bundle exec rake # specs and RuboCop
775
+ bin/matrix # the specs on every supported Ruby and Rails combination
776
+ ```
777
+
778
+ The specs run against an in-memory SQLite schema, with a fake LLM and a fake HTTP transport,
779
+ so nothing reaches the network. One spec takes a plan from the interview to delivered through
780
+ every guard, agent run, review and merge.
781
+
782
+ ## Contributing
783
+
784
+ Contributions are very welcome: bug reports, a guard your team relies on, a plan the drafter
785
+ got wrong, or clearer docs. You don't need permission to start.
786
+
787
+ **Found a problem?** [Open an issue](https://github.com/blaz1988/plan-driven/issues/new) with
788
+ your Ruby, Rails and gem versions, the command you ran and what happened. Leave out keys and
789
+ anything confidential from your plans.
790
+
791
+ **Want to send a fix?** Contributions go through a fork and a pull request:
792
+
793
+ 1. [Fork the repository](https://github.com/blaz1988/plan-driven/fork) and clone your fork.
794
+ 2. Create a branch for your change: `git checkout -b guard-for-enum-changes`.
795
+ 3. Run `bin/setup`, make the change, and add a spec for it.
796
+ 4. Run `bundle exec rake` and make sure specs and RuboCop pass.
797
+ 5. Add a line to the `Unreleased` section of [CHANGELOG.md](CHANGELOG.md).
798
+ 6. Push the branch to your fork and
799
+ [open a pull request](https://github.com/blaz1988/plan-driven/compare) against `main`.
800
+
801
+ For a larger change, such as a new agent backend or a new phase, open an issue first so we can
802
+ agree on the approach. See [CONTRIBUTING.md](CONTRIBUTING.md) for the details, and report
803
+ security issues privately as described in [SECURITY.md](SECURITY.md).
804
+
805
+ ## About Rubycode
806
+
807
+ `plan_driven` is written and maintained by [Rubycode](https://rubycode.co). We build and rescue
808
+ Ruby on Rails products: new applications, upgrades, performance work, and senior Ruby and Rails
809
+ engineers who join your team. We also help teams put AI agents to work safely.
810
+
811
+ **Need Ruby or Ruby on Rails engineers? Get in touch.**
812
+
813
+ **Ivan Blažević**, creator of the gem
814
+
815
+ - Email: [ivan.blazevic@rubycode.co](mailto:ivan.blazevic@rubycode.co)
816
+ - Phone: [+385 99 351 3642](tel:+385993513642)
817
+ - LinkedIn: [linkedin.com/in/blazevic-ivan](https://www.linkedin.com/in/blazevic-ivan/)
818
+ - Web: [rubycode.co](https://rubycode.co)
819
+
820
+ ## License
821
+
822
+ Released under the [MIT License](LICENSE.txt). Copyright © 2026 Ivan Blažević,
823
+ [Rubycode](https://rubycode.co).