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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +76 -0
- data/LICENSE.txt +21 -0
- data/README.md +823 -0
- data/exe/plan-driven +7 -0
- data/lib/generators/plan_driven/install_generator.rb +37 -0
- data/lib/generators/plan_driven/templates/create_plan_driven_tables.rb.tt +73 -0
- data/lib/generators/plan_driven/templates/plan_driven.rb +39 -0
- data/lib/plan_driven/agent_prompt.rb +86 -0
- data/lib/plan_driven/cli/plan_commands.rb +158 -0
- data/lib/plan_driven/cli/setup_commands.rb +97 -0
- data/lib/plan_driven/cli/ticket_commands.rb +155 -0
- data/lib/plan_driven/cli/ui.rb +101 -0
- data/lib/plan_driven/cli.rb +150 -0
- data/lib/plan_driven/configuration.rb +114 -0
- data/lib/plan_driven/credentials.rb +90 -0
- data/lib/plan_driven/cursor_agents.rb +82 -0
- data/lib/plan_driven/cursor_llm.mjs +70 -0
- data/lib/plan_driven/cursor_llm.rb +79 -0
- data/lib/plan_driven/delivery.rb +362 -0
- data/lib/plan_driven/drafter.rb +122 -0
- data/lib/plan_driven/errors.rb +22 -0
- data/lib/plan_driven/evidence.rb +108 -0
- data/lib/plan_driven/gherkin.rb +42 -0
- data/lib/plan_driven/github.rb +118 -0
- data/lib/plan_driven/guards/migration_guard.rb +94 -0
- data/lib/plan_driven/guards/plan_guard.rb +84 -0
- data/lib/plan_driven/guards/pr_guard.rb +140 -0
- data/lib/plan_driven/guards/ticket_guard.rb +178 -0
- data/lib/plan_driven/guards/ticket_normalizer.rb +90 -0
- data/lib/plan_driven/guards.rb +57 -0
- data/lib/plan_driven/http.rb +60 -0
- data/lib/plan_driven/json_reply.rb +26 -0
- data/lib/plan_driven/llm.rb +80 -0
- data/lib/plan_driven/models/approval.rb +12 -0
- data/lib/plan_driven/models/event.rb +13 -0
- data/lib/plan_driven/models/evidence_run.rb +17 -0
- data/lib/plan_driven/models/plan.rb +98 -0
- data/lib/plan_driven/models/record.rb +8 -0
- data/lib/plan_driven/models/ticket.rb +58 -0
- data/lib/plan_driven/models.rb +8 -0
- data/lib/plan_driven/railtie.rb +9 -0
- data/lib/plan_driven/renderer/html.rb +106 -0
- data/lib/plan_driven/renderer/markdown.rb +145 -0
- data/lib/plan_driven/renderer/pdf.rb +39 -0
- data/lib/plan_driven/renderer/style.css +22 -0
- data/lib/plan_driven/renderer.rb +41 -0
- data/lib/plan_driven/repository.rb +35 -0
- data/lib/plan_driven/schema_context.rb +98 -0
- data/lib/plan_driven/template.rb +113 -0
- data/lib/plan_driven/ticket_generator.rb +98 -0
- data/lib/plan_driven/version.rb +5 -0
- data/lib/plan_driven/workflow.rb +59 -0
- data/lib/plan_driven.rb +72 -0
- metadata +146 -0
data/README.md
ADDED
|
@@ -0,0 +1,823 @@
|
|
|
1
|
+
# plan_driven
|
|
2
|
+
|
|
3
|
+
[](https://github.com/blaz1988/plan-driven/actions/workflows/main.yml)
|
|
4
|
+
[](https://rubygems.org/gems/plan_driven)
|
|
5
|
+
[](LICENSE.txt)
|
|
6
|
+
[](#rails-and-ruby-support)
|
|
7
|
+
[](#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
|
+
[](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
|
+

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

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

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

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

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

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

|
|
513
|
+
|
|
514
|
+
### The result
|
|
515
|
+
|
|
516
|
+

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