planrails 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 (40) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE +21 -0
  3. package/README.md +225 -0
  4. package/bin/planrails.mjs +7 -0
  5. package/docs/PLANNING_GUIDE.md +632 -0
  6. package/package.json +51 -0
  7. package/src/hooks/_lib.mjs +32 -0
  8. package/src/hooks/guard-never-delete.sh +29 -0
  9. package/src/hooks/install.mjs +173 -0
  10. package/src/hooks/plan-pre-tool.mjs +71 -0
  11. package/src/hooks/plan-session-start.mjs +46 -0
  12. package/src/hooks/plan-stop.mjs +60 -0
  13. package/src/hooks/plan-subagent-start.mjs +27 -0
  14. package/src/hooks/postcompact-journal.mjs +35 -0
  15. package/src/hooks/precompact-journal.mjs +128 -0
  16. package/src/hooks/selftest.mjs +128 -0
  17. package/src/init.mjs +155 -0
  18. package/src/issue.mjs +55 -0
  19. package/src/plan/fixtures/README.md +7 -0
  20. package/src/plan/fixtures/broken-cli.mjs +27 -0
  21. package/src/plan/fixtures/broken-hooks-root/.claude/settings.json +83 -0
  22. package/src/plan/fixtures/broken-hooks-root/.project-management/plans/.gitkeep +0 -0
  23. package/src/plan/fixtures/broken-hooks-root/CLAUDE.md +9 -0
  24. package/src/plan/fixtures/broken-root/.project-management/plans/broken/PLAN.md +4 -0
  25. package/src/plan/fixtures/broken-root/.project-management/plans/broken/gates.json +1 -0
  26. package/src/plan/fixtures/broken-root/.project-management/plans/broken/rules.json +1 -0
  27. package/src/plan/fixtures/broken-root/.project-management/plans/broken/state.json +67 -0
  28. package/src/plan/fixtures/broken-root/CLAUDE.md +3 -0
  29. package/src/plan/fixtures/broken-trial.mjs +25 -0
  30. package/src/plan/lib/brief.mjs +116 -0
  31. package/src/plan/lib/claude-md.mjs +66 -0
  32. package/src/plan/lib/glob.mjs +81 -0
  33. package/src/plan/lib/judgment.mjs +19 -0
  34. package/src/plan/lib/paths.mjs +65 -0
  35. package/src/plan/lib/schema.mjs +199 -0
  36. package/src/plan/lib/store.mjs +338 -0
  37. package/src/plan/lib/time.mjs +21 -0
  38. package/src/plan/plan.mjs +843 -0
  39. package/src/plan/run.mjs +88 -0
  40. package/src/plan/skill/SKILL.md +15 -0
@@ -0,0 +1,632 @@
1
+ # Planning guide — plans that survive compaction
2
+
3
+ How to plan work that spans many sessions, and how to execute it without
4
+ losing accuracy when the chat is compacted or the session ends. The guide, the
5
+ CLI (`src/plan/plan.mjs`), the hooks (`src/hooks/`) and the `/plan`
6
+ skill are one system. This document is the reference for all four.
7
+
8
+ **Words used in this guide**
9
+
10
+ | word | meaning |
11
+ |---|---|
12
+ | **plan** | one directory of files that says what the work is, what proves it done, and where it stands |
13
+ | **planner** | whoever writes the plan (you, when `/plan` is invoked). The planner does not do the work. |
14
+ | **executor** | the later session that does the work. It sees only the plan's files, never the planner's chat. |
15
+ | **session** | one conversation with Claude Code. It ends, and its chat is gone. |
16
+ | **compaction** | Claude Code shrinking a long chat: your messages stay, every tool result is dropped |
17
+ | **hook** | a small script Claude Code runs by itself at a fixed moment: session start, before a tool call, at the end of a turn |
18
+ | **brief** | a short summary of a plan (≤ 6,000 characters) rendered from its files: status, what to do now, the last log entries, the rules |
19
+ | **gate** | a command that answers one question about the work with its exit code; "done" means the gate passed |
20
+ | **condition of done** | a statement that must hold before a task can be marked done; three are checked by the tool, the rest are answered in words |
21
+
22
+ **Entry points**
23
+
24
+ - *"Plan X"* or `/plan <raw plan>` → § Creating a plan.
25
+ - A session that starts with a plan active → the brief is injected for you;
26
+ § Executing a plan says what to do with it.
27
+ - *"Close the plan"* → § Closing a plan.
28
+
29
+ ## The three problems, and the idea
30
+
31
+ | problem | what used to happen | what the system does instead |
32
+ |---|---|---|
33
+ | **State is lost at compaction.** Compaction keeps your messages and drops every tool result. Doctrine survives; *what you already did* does not. | In the project this was built in, progress lived in a prose section rewritten from memory at the end of a session. It was stale three times in a week, each time saying "done" about work that was not. | Progress lives in append-only JSON written **as it happens** through a CLI. A `SessionStart` hook renders a brief from disk at every start and after every compaction. A `Stop` hook refuses to let a turn end with unlogged edits. |
34
+ | **Context bloat.** Every rule learned went into CLAUDE.md, because trimming it made the same mistakes come back. | That project's CLAUDE.md grew to 18,533 tokens plus a 9,232-token imported guide, both re-read on every call, and its two active plans were 135 KB and 257 KB: 34k and 64k tokens to open. | A rule is attached to the **step it applies to** and a `PreToolUse` hook injects it at that step (≤ 1,200 chars, once per session). The brief is ≤ 6,000 chars. Everything else is queried, not loaded. |
35
+ | **Instruments trusted as verdicts.** A script that succeeds is believed. | A `complete: true` flag shipped 31 times over data a third full, because the script answered a narrower question than its name. Four review passes each re-checked the same records because nobody recorded the checks. | A task is `done` only through `plan task done`, which **runs the gate and records the run**. Every gate states the question it *literally* answers and how it could pass while wrong. Every check made is recorded on the plan, so it is never made twice. |
36
+
37
+ The idea in one line: **put each fact in the cheapest place that still cannot
38
+ be forgotten.**
39
+
40
+ ```mermaid
41
+ flowchart TB
42
+ A["ALWAYS in context<br/>CLAUDE.md: one generated block<br/>= id, title, path of each active plan"]
43
+ B["AT SESSION START and AFTER COMPACTION<br/>the brief (≤ 6k chars): status, RESUME,<br/>NOW, NEXT, last log, leads, rules"]
44
+ C["AT THE STEP (PreToolUse hook)<br/>rules.json: one rule, ≤ 1,200 chars,<br/>when Edit/Bash/Agent matches"]
45
+ D["QUERIED, never loaded<br/>state.json · log.jsonl · learnings.jsonl<br/>decisions.jsonl · gate-runs.jsonl"]
46
+ A --> B --> C --> D
47
+ ```
48
+
49
+ ## The files of a plan
50
+
51
+ One directory per plan: `.project-management/plans/<id>/`. Each file has one
52
+ job and one writer.
53
+
54
+ | file | job | written by |
55
+ |---|---|---|
56
+ | `PLAN.md` | the stable narrative: why, done-means, non-goals, method, rules that cannot be automated, owner-approval points, map, sources | the planner, by hand; rarely changed |
57
+ | `state.json` | manifest (id, title, status, watched `paths`) and the tasks | the CLI, after creation |
58
+ | `gates.json` | what proves each task done: question, command, known-fail case, how it could lie | the planner, with `plan gate add` or by hand; validated |
59
+ | `rules.json` | rules a hook injects at the tool call they apply to | the planner, by hand (there is no `rule add`); validated |
60
+ | `log.jsonl` | progress: what landed, what is next, refs | `plan log` (append-only) |
61
+ | `learnings.jsonl` | what went wrong or surprised, the rule it teaches, how it is enforced | `plan learn` (append-only) |
62
+ | `decisions.jsonl` | what was chosen, why, what was rejected, by whom | `plan decide` (append-only) |
63
+ | `gate-runs.jsonl` | every gate execution: command, exit, tail, result | `plan gate run/verify`, `plan task done` (append-only) |
64
+
65
+ The shape of every record is declared once, in `src/plan/lib/schema.mjs`,
66
+ as strict objects: an unregistered key is refused. The validator
67
+ (`src/plan/lib/store.mjs`) runs inside `the project's check script`.
68
+
69
+ ## The rules
70
+
71
+ 1. **Status is derived, never typed.** The brief is rendered from the files
72
+ every time. There is no "current status" paragraph to go stale. *Case:*
73
+ the "Phase 1 is done" claim that was false three times.
74
+ 2. **Done needs evidence.** `status: done` is valid only with a gate run the CLI
75
+ recorded, or a manual check with a reason of 20+ characters and who gave it.
76
+ The validator refuses anything else, whoever wrote the file.
77
+ 3. **A gate names its question, and the question it is not.** `question` says
78
+ what the command literally answers; `notTheSameAs` names the nearby question a
79
+ reader will assume; `couldPassWhileWrongIf` is the honest proxy risk. *Case:*
80
+ a coverage script answered "has every chapter been started?" and was read
81
+ as "is the book complete?".
82
+ 4. **A gate must have been seen to fail.** `knownFail` is a case that must fail.
83
+ `plan gate verify` runs it and records the result. Until then the brief says
84
+ `never verified`. *Case:* the first run of a new diff tool reported three
85
+ kinds of error that were in the tool, not in the data.
86
+ 5. **Every rule is attached to a trigger.** A rule that applies "always" goes
87
+ in CLAUDE.md, and CLAUDE.md is full. A rule that applies *before editing a
88
+ migration file* goes in `rules.json` with `when: { tool: "Edit|Write", path:
89
+ "db/migrations/**" }` and arrives exactly then.
90
+ 6. **Record as you go, through the CLI.** After each landed piece of work:
91
+ `plan log`. After a surprise: `plan learn`. After a choice: `plan decide`.
92
+ The Stop hook reminds you once if files under the plan's `paths` changed and
93
+ nothing was logged. *Case:* four review passes re-read the same records.
94
+ 7. **The RESUME line is written before you stop, not after you start.** The
95
+ `--next` of the last log entry is what the next session reads first. Write it
96
+ as an instruction a stranger can execute.
97
+ 8. **A learning says how it is enforced.** `prose` (must be read), `rule`
98
+ (injected by the hook), `gate` (machine-checked) or `docs` (written into a
99
+ guide). Closing a plan lists the prose-only ones and asks whether each can be
100
+ promoted. That is how a learnings guide is built one line at a time instead
101
+ of in one 9,000-token sitting.
102
+ 9. **A finding is a lead until a second reader confirms it.** `plan learn
103
+ --lead` marks it; the brief lists leads separately. *Case:* on 2026-09-07 four
104
+ subagent findings were each directionally right and numerically wrong.
105
+ 10. **The owner decides irreversible things.** `PLAN.md § Owner decides` lists
106
+ them; `plan close` and `plan abandon` require `--confirmed-by-owner`, which
107
+ means you asked in chat and were told yes. The CLI cannot check that; the
108
+ guide says it, and the log records who.
109
+ 11. **CLAUDE.md gets one generated block and nothing else.** `plan activate`
110
+ adds the plan's line; `plan close` removes it; `plan validate --all` fails if
111
+ the block and the active plans disagree. Never hand-edit the block.
112
+ 12. **Never delete.** Hard rule 11 applies to plans too: a wrong plan is
113
+ `abandoned`, not removed; a stale file moves to `.planrails/trash/`.
114
+
115
+ ## Creating a plan
116
+
117
+ Whoever writes the plan will not carry it out. The executing session reads
118
+ only the plan's files; it never sees the planner's chat. If a fact is not in
119
+ the files, it does not exist for the executor. So the planner produces files,
120
+ not engineering.
121
+
122
+ ### 1. Read, then answer these before writing
123
+
124
+ Read the raw plan. Then check what already exists:
125
+
126
+ ```bash
127
+ npx planrails list # is there already a plan for this?
128
+ npx planrails learnings --search <term> # what earlier plans learned about this area
129
+ ```
130
+
131
+ Answer these seven questions from the raw plan and the repo. **Ask the user only
132
+ the ones you cannot answer, in one message.** Record every answer in
133
+ `PLAN.md` or as a `plan decide` entry with `--by owner`.
134
+
135
+ 1. What does *done* look like, stated so that a command can check it? (→ gates)
136
+ 2. What must not change? (→ § Non-goals, and `paths` for what may)
137
+ 3. Which steps are irreversible or outward-facing, and who approves them? (→ § Owner decides)
138
+ 4. What already exists — files, plans, scripts, sources, earlier learnings?
139
+ 5. Which steps need maximum thinking (judgement, adjudication, mass edits) and
140
+ which are routine? (→ `effort` on each task)
141
+ 6. What are the limits — parallel workflows, tokens, time?
142
+ 7. Who uses the result, and what would make it useless to them? (→ § Why)
143
+
144
+ ### 2. Research with subagents
145
+
146
+ Subagents return summaries; you write the summaries into `PLAN.md § Sources`
147
+ and `§ Map`. Cover: the `docs/*_GUIDE.md` files that govern the paths this plan
148
+ touches (find them in CLAUDE.md's pointer table); the nearest in-repo example;
149
+ the exact files to be changed; existing scripts that already answer part of a
150
+ gate's question; external docs only if a new library or API is involved.
151
+
152
+ ### 3. Scaffold and write PLAN.md
153
+
154
+ ```bash
155
+ npx planrails new <id> --title "…" --paths "features/x/**,docs/X_GUIDE.md"
156
+ ```
157
+
158
+ `paths` are repo-relative globs where the work lands. The Stop hook watches
159
+ them. Fill each section of `PLAN.md` (the template's comments say what goes
160
+ where). Two sections matter most:
161
+
162
+ - **§ Method** — the recipe per kind of task, with real commands and one worked
163
+ example with real numbers. This is what stops the third session rediscovering
164
+ what the first one learned.
165
+ - **§ Rules for this plan** — only rules that cannot be a gate or a hook rule.
166
+ Numbered. Each one: the rule in a sentence, then the case that taught it. The
167
+ brief shows the first 14 lines.
168
+
169
+ ### 4. Write the gates — one that cannot lie
170
+
171
+ This is the planning-system plan's own first gate, and it runs as written:
172
+
173
+ ```bash
174
+ npx planrails gate add <id> \
175
+ --question "does every refusal in the plan validator's selftest fire on its case, and does every valid case pass" \
176
+ --not "whether the validator refuses every WRONG state a person could write — only the cases somebody imagined are tested" \
177
+ --command "npx planrails selftest" \
178
+ --wrong "the selftest and the validator were written by the same hand, so a refusal nobody thought of is not exercised" \
179
+ --known-fail "PLAN_PROJECT_ROOT=node_modules/planrails/src/plan/fixtures/broken-root npx planrails validate --all --quiet" \
180
+ --known-fail-why "the fixture's task T1 is done citing a gate run that never happened; validate must exit 1" --kind runtime
181
+ ```
182
+
183
+ `kind`: `static` reads files · `runtime` executes code · `reality` observes the
184
+ live system · `report` informs. A `report` gate cannot be a task's gate: the
185
+ validator and `task done` refuse it, because a report never proves anything.
186
+ It can back a spike whose result goes into `plan learn`.
187
+
188
+ **Then prove each gate can fail.** `npx planrails gate verify <id> --all`
189
+ runs every known-fail command. You *want* those commands to fail: that shows
190
+ the gate can see a real problem. `gate verify` prints VERIFIED when the
191
+ known-fail command failed with the expected exit code (`expectExit`, 1 by
192
+ default for `gate add`), and NOT VERIFIED when it passed, or exited with some
193
+ other code (a missing file or a usage error is not the failure the gate is
194
+ for). If it is NOT VERIFIED, fix the gate, not the case.
195
+
196
+ Reuse the project's own gates where they exist: its check script, its test
197
+ runner on one file (`npx vitest run <file>`, `pytest tests/x.py`), its end-to-end
198
+ suite, a script that hits the route. Ask of each one "how could this pass while
199
+ the work is wrong?" and write the answer into `couldPassWhileWrongIf`.
200
+
201
+ **Gates come before the tasks that name them.** `task add --gate G2` is
202
+ refused while G2 is undefined, because every write is validated. The order is:
203
+ gates, then tasks, then learnings, then rules that cite learnings. The
204
+ planning-system plan lost a task to each of the two wrong orders on 2026-09-12.
205
+
206
+ ### 5. Write the tasks
207
+
208
+ A task is one session of work or less, and names what proves it done:
209
+
210
+ ```bash
211
+ npx planrails task add <id> --title "waitlist join awards XP: manifest rule + actorId + tests" \
212
+ --gate G2 --files "features/waitlist/feature.ts,tests/waitlist.test.ts" --effort high --after T1
213
+ npx planrails task add <id> --title "owner reads the new guide" \
214
+ --manual "the owner opens docs/X_GUIDE.md and says in chat that it reads well"
215
+ ```
216
+
217
+ "Implement backend" is not a task. A task with no gate must say in
218
+ `--manual` who checks it and how; it is closed later with
219
+ `task done <id> T6 --manual "<what was checked and how, 20+ chars>" --by owner`.
220
+ Add a task-specific condition of done with `--done-when "the row count
221
+ matches the source export, not the cache"` (repeatable); the plan's own conditions
222
+ (C1–C7, see § Marking a task done) apply to every task without being named. Always include a task for the docs that
223
+ ship with the change (CLAUDE.md rule 9); `activate` warns if none does. Add a
224
+ spike task ("read X, confirm Y is feasible") when a later task depends on
225
+ something unknown — its gate is `report` kind, its result goes in `plan learn`.
226
+
227
+ ### 6. Write the rules — what goes where
228
+
229
+ | the rule applies… | put it in | how it reaches the executor |
230
+ |---|---|---|
231
+ | at one tool call (editing a file kind, running a script, spawning an agent) | `rules.json` | the PreToolUse hook injects it at that call, once per session |
232
+ | to a judgement no trigger can catch (how to weigh witnesses) | `PLAN.md § Rules for this plan` | the brief, at every session start |
233
+ | to every session of every plan, forever | CLAUDE.md — and think twice | always loaded; this is the expensive place |
234
+ | as a check a machine can make | a gate | it cannot be forgotten |
235
+
236
+ A `rules.json` entry:
237
+
238
+ ```json
239
+ { "id": "R1",
240
+ "when": { "tool": "Edit|Write", "path": "db/migrations/**" },
241
+ "text": "A migration is generated, never hand-written: run `npm run db:generate` after the schema change, and restart the dev server afterwards, or the old process keeps the old schema.",
242
+ "repeat": "once", "why": "two hand-written migrations drifted from the schema in one week", "learning": "L2" }
243
+ ```
244
+
245
+ `when.tool` is a regex over tool names; `path` is a glob (Edit, Write, Read,
246
+ NotebookEdit); `command` a regex (Bash); `prompt` a regex (Agent, Workflow).
247
+ `repeat`: `once` per session and per agent (the default; reset after compaction), `always`, or `every:N`.
248
+ Globs here let `**` cross dot-directories (`**/ch*.json` matches
249
+ `.cache/build/x/ch01.json`), unlike a shell.
250
+ Keep `text` under 1,200 characters; longer rules go in a file next to the plan
251
+ (`"file": "rules/R1.md"`) and should still be short.
252
+
253
+ **Order matters when a rule cites a learning.** Record the learning first
254
+ (`plan learn … `), then put its id in the rule's `learning` field. The
255
+ validator refuses a rule that cites a learning that does not exist yet, and
256
+ because every write is validated, *every later write to the plan fails until
257
+ the reference resolves*. The planning-system plan lost its first task this way
258
+ on 2026-09-12: `rules.json` cited L1 and L4 before either existed, so the
259
+ first `task add` was refused and the numbering of every task after it shifted.
260
+ Leave `"learning": null` until the learning is recorded.
261
+
262
+ ### 7. Validate, verify, activate
263
+
264
+ ```bash
265
+ npx planrails validate <id> # schema + every cross-reference; errors block
266
+ npx planrails gate verify <id> --all # every knownFail case must fail
267
+ npx planrails activate <id> # adds the CLAUDE.md line, prints the brief
268
+ ```
269
+
270
+ End your reply with the brief and one line saying what changed in CLAUDE.md.
271
+ Activation is reversible (`plan pause`), so it does not need permission;
272
+ closing does.
273
+
274
+ ## Executing a plan
275
+
276
+ ### Session start
277
+
278
+ The SessionStart hook injects each active plan's brief (`npx planrails
279
+ brief` prints the same thing). Read it in order: **RESUME** (the exact next
280
+ action), **NOW** (the task in flight and its gate's last result), **BLOCKED**,
281
+ **LEADS**, **RULES**. Then:
282
+
283
+ 1. `git status` and `git log --oneline -5`. The repo outranks the brief: if
284
+ RESUME says a file was written and it is not there, log the correction first.
285
+ 2. If this is your first task on this plan this session, read `PLAN.md § Method`
286
+ and `§ Rules for this plan` in full. The brief shows only the first 14 lines
287
+ of the rules.
288
+ 3. After a compaction, also read `.planrails/journal/<sessionId>.md` — the
289
+ PreCompact hook wrote what this session already did (files, gates, commands),
290
+ and the PostCompact hook tells you the path.
291
+
292
+ ### The task loop
293
+
294
+ ```bash
295
+ npx planrails task start <id> T3 # write-ahead: the brief now says NOW T3
296
+ # … work. Rules for the steps you take arrive from the hook. …
297
+ npx planrails log <id> --task T3 --what "digest job enqueues one email per subscriber (12 in the seed) → lib/jobs/digest.ts" --next "wire the Monday schedule in lib/jobs/schedule.ts, then task done T3"
298
+ npx planrails learn <id> --task T3 --what "the job runner retries a failed send 3 times with the same idempotency key, so a flaky SMTP sends nothing twice" --rule "always pass an idempotency key to enqueue()" --when "adding any job that sends" [--lead]
299
+ npx planrails decide <id> --task T3 --what "send the digest at 07:00 in the subscriber's timezone" --why "opens are 3× higher before 09:00 in the newsletter's own stats" --rejected "one global time: wrong for half the list" --by owner
300
+ npx planrails task check <id> T3 # every condition of done, with the automatic ones already evaluated
301
+ npx planrails task done <id> T3 --answer "C4: docs/JOBS_GUIDE.md § Digest updated in this change" --answer "C5: the 12 in the log was re-derived from the seed file" --answer "C6: L2 and D1 recorded" --answer "C7: read digest.ts and schedule.ts whole; the gate cannot see a wrong timezone, I checked the offset math by hand" # runs G3; refuses if anything fails
302
+ ```
303
+
304
+ The order matters: log first, then close. `task done` checks that a log
305
+ entry for the task exists (condition C2) before it runs the gate.
306
+
307
+ - **Log after every landed piece of work**, not at the end. "Landed" means: a
308
+ number a gate reports moved, what is left changed, or a finding someone must
309
+ re-check appeared. The Stop hook enforces the minimum: it blocks a turn's end
310
+ once when edits under `paths` are newer than the last log entry.
311
+ - **`--next` is the RESUME line.** Write it for a stranger: the command, the
312
+ file, the state ("half-done: schema written, migration NOT generated because
313
+ the dev server was up — stop dev, `npm run db:generate`, restart, then T3's
314
+ tests").
315
+ - **Subagent reports die with the session.** Put the path of the saved report
316
+ (a file under the plan directory's `reports/`) in `--refs`.
317
+ - **A gate that fails is information, not an obstacle.** Log where it stands
318
+ and why, fix the cause, run again. Never widen the gate.
319
+ - **Commit at task boundaries** and put the sha in `--refs`.
320
+
321
+ **Never hand-edit `state.json`, `gates.json` or a `.jsonl` file.** The CLI is the one
322
+ writer. If a command crashes or refuses and you cannot see why, log what happened and block
323
+ the task with `--needs owner`; do not repair the file by hand. In the black-box trial on
324
+ 2026-09-12 a session met a crash on an older `state.json` and back-filled the missing keys
325
+ itself. It happened to write the right thing; the next one might not. The CLI now loads
326
+ older files with defaults, so the crash is gone, and the rule stands.
327
+
328
+ ### Marking a task done
329
+
330
+ "Done" is a process, not a word. Every task carries conditions of done: the
331
+ plan's six (set when the plan is created, editable in `state.json`), plus any
332
+ the planner added to the task with `--done-when`. `task check` shows them;
333
+ `task done` refuses until all hold; `review` shows what was answered.
334
+
335
+ | id | condition | how it is settled |
336
+ |---|---|---|
337
+ | C1 | the gate ran in this very command and passed (or the owner's reason) | by the tool |
338
+ | C2 | a log entry names this task, written after it started | by the tool |
339
+ | C3 | every file the task lists exists on disk | by the tool |
340
+ | C4 | the docs for this change shipped with it — name them, or say why none | `--answer "C4: …"` |
341
+ | C5 | every number in the log and any report has a locator, or was re-derived | `--answer "C5: …"` |
342
+ | C6 | what was learned or decided is recorded — name the ids, or say nothing was | `--answer "C6: …"` |
343
+ | C7 | you read the changed files whole, against the task's purpose and the app, and judged the result right yourself — what you read, what you looked for, what the gate could not see | `--answer "C7: …"` |
344
+ | C8+ | the task's own conditions, and any the planner adds to the plan later | `--answer "C8: …"` |
345
+
346
+ C7 is the judgment condition. A green gate proves only what the gate checks;
347
+ C7 asks the person or agent closing the task to read the result as a whole and
348
+ say so in words. "Looks fine" does not pass. "Read greet.mjs and test.mjs whole;
349
+ looked for an argv with spaces, which the gate never tries; it prints the name
350
+ unquoted, and that is fine for a greeting" does.
351
+
352
+ **The plan's conditions can change while the plan runs.** `plan condition list <id>`
353
+ shows them. `plan condition add <id> --statement "…"` adds one; it gets the next
354
+ free id and records when it arrived, so a task closed earlier is not held to it.
355
+ `plan condition drop <id> C9` removes one the planner added. The three the tool
356
+ checks (C1–C3) cannot be dropped.
357
+
358
+ An answer is at least 10 characters and says what was checked, not "yes".
359
+ The answers are stored on the task next to the gate run, so a reviewer runs:
360
+
361
+ ```bash
362
+ npx planrails review <id> # every done task: its evidence and each condition with its answer
363
+ npx planrails review <id> T3 # one task
364
+ ```
365
+
366
+ The validator ties the evidence to the work, not just to the gate: the cited
367
+ run must exist, must have passed, must carry the same timestamp as the
368
+ evidence, must postdate the task's start, and may prove one task only. A
369
+ gated task closed by hand is accepted only from the owner
370
+ (`--manual "…" --by owner --confirmed-by-owner`), never from the agent.
371
+ A task closed before its plan had conditions shows `checklist: NONE` in
372
+ `review` and a warning in `validate`; it is not silently promoted.
373
+
374
+ ### Judgment outranks the gate
375
+
376
+ A gate is a script. It cannot read, cannot see, and answers only the one
377
+ question it was written for. Every gate here has cases where it is wrong, in
378
+ both directions, and so does every gate anywhere. So at every moment an agent
379
+ decides whether work is done (`task check`, the `task done` refusal, the brief,
380
+ the fresh-session prompt) it sees the same three lines:
381
+
382
+ - **Gate red, work right.** Never make the gate pass. Write down what it
383
+ literally computed and what is true. If the gate is wrong, fix the gate and
384
+ run `gate verify` again; its known-fail case must still fail. Otherwise
385
+ `task block <id> T --reason "…" --needs owner` and stop. The owner's word
386
+ closes it (`task done … --manual "…" --by owner --confirmed-by-owner`); the
387
+ agent's does not.
388
+ - **Gate green, work wrong.** Refuse to close. Say what the gate cannot see, and
389
+ fix the work. C7 exists for this.
390
+ - **Anything crashed.** Never edit a plan file by hand. Log it, block, stop.
391
+
392
+ Why the agent gets no "force pass with a reason" of its own: the agent's reason
393
+ is the least reliable signal in the system. In the project this system was built in, agents' claims
394
+ about their own evidence were wrong about one time in four, always in their
395
+ own favour, and "Phase 1 is done" was asserted in prose three times and false
396
+ each time. An override that lives with the agent becomes the path of least
397
+ resistance under pressure. Routing it through the owner keeps the agent's
398
+ judgment (it can say "this gate is wrong, here is why") without making it
399
+ judge and party at once.
400
+
401
+ Two rails back the words. A gate whose command changed after it was last
402
+ verified cannot close a task until `gate verify` runs again, and `validate`
403
+ fails on it; so a gate edited to pass must still fail its known-fail case. And
404
+ a gate that declares a known-fail case but was never verified cannot close a
405
+ task at all. What no rail prevents: an agent can still edit the code under
406
+ test, or a fixture. The append-only log and `plan review` make that visible;
407
+ the owner's reading of them is the last line.
408
+
409
+ ### Blocked and paused
410
+
411
+ `plan task block <id> T3 --reason "…" --needs owner|external|self` puts the
412
+ reason in the brief. Switch to another task if one is independent. `plan pause
413
+ <id>` takes a whole plan out of CLAUDE.md without closing it.
414
+
415
+ ## Closing a plan
416
+
417
+ ```bash
418
+ npx planrails close <id> # runs EVERY gate fresh; lists prose-only learnings
419
+ npx planrails close <id> --confirmed-by-owner # after the owner said yes in chat
420
+ ```
421
+
422
+ A plan closes on what its gates say now, not on their last recorded run. Before
423
+ closing, look at each prose-only learning and ask: can it be a gate here? a
424
+ `rules.json` rule in the next plan that touches this area? a line in the guide
425
+ that governs this area (docs ship with the change)? Promote what can be
426
+ promoted (`plan learn … --enforcement gate --ref G4`, or edit the learning's
427
+ line) and leave the rest as prose with a clear `appliesWhen`. Closed plans stay
428
+ on disk; their learnings stay searchable with `plan learnings --search`.
429
+
430
+ ## Hooks
431
+
432
+ Eight hooks, committed in `src/hooks/`; installed into this machine's gitignored
433
+ `.claude/settings.json` by `npm run hooks:install`; checked by
434
+ `npm run plan:doctor`; tested by `npm run hooks:selftest` against payloads
435
+ captured from a real run. Measured on Claude Code 2.1.269 on 2026-09-12:
436
+ SessionStart and PreToolUse `additionalContext` reach the model; a Stop hook's
437
+ `decision: block` makes the model continue with the reason, and the retry
438
+ carries `stop_hook_active: true`.
439
+
440
+ | hook | event | does | cost |
441
+ |---|---|---|---|
442
+ | `plan-session-start.mjs` | SessionStart | injects every active brief; records the session id; on `compact` resets once-per-session rules | one brief per active plan, ≤ 6k chars |
443
+ | `plan-pre-tool.mjs` | PreToolUse | injects matching `rules.json` rules; records edits under `paths` | ~40 ms per tool call; a rule ≤ 1,200 chars, once |
444
+ | `plan-subagent-start.mjs` | SubagentStart | puts the subagent note (report path, never delete, never close a task) in front of every subagent — measured to reach the subagent only | < 700 chars, once per agent |
445
+ | `plan-stop.mjs` | Stop | blocks once when edits are newer than the last log | only when it fires |
446
+ | `guard-never-delete.sh` | PreToolUse(Bash) | optional (`init --with-never-delete`): no delete commands, move aside instead (needs `jq`) | ~8 ms |
447
+ | `precompact-journal.mjs` | PreCompact | writes `.planrails/journal/<sid>.md`: files edited, gates run, images read | — |
448
+ | `postcompact-journal.mjs` | PostCompact | hands the journal path back | — |
449
+
450
+ Hook state lives in `.planrails/hooks/<sessionId>/` (`injected.json`,
451
+ `edits.jsonl`, `reminded.json`, `starts.jsonl`). `starts.jsonl` shows when the
452
+ SessionStart hook fired and with which `source` — the way to confirm it ran
453
+ after a compaction.
454
+
455
+ Two limits, stated plainly. The Stop hook sees edits made through Edit/Write
456
+ tools, not through a Bash `sed` or a script; log those yourself. And
457
+ `current-session.json` holds the *last* session that started, so a log entry's
458
+ `session` stamp can name a sibling session when two run at once — it is
459
+ informational.
460
+
461
+ ## The CLI
462
+
463
+ `npm run plan -- <command>` or `npx planrails <command>`.
464
+
465
+ | command | does |
466
+ |---|---|
467
+ | `new <id> --title … [--paths a,b]` | scaffold a draft plan |
468
+ | `list` · `brief [id]` · `status <id>` | what exists · the injected brief · everything, with warnings |
469
+ | `agent-brief <id> <T> [--what …] [--label …]` | what a subagent gets instead of the plan (§ Workstreams and subagents) |
470
+ | `validate [id\|--all] [--quiet]` | schema, cross-references, done-needs-evidence, CLAUDE.md drift (inside `the project's check script`) |
471
+ | `task add\|check\|start\|done\|block\|unblock\|drop <id> [T]` | the task lifecycle; `add … --done-when "…"` adds a condition; `check` shows the conditions; `done … --answer "C4: …"` runs the gate and needs every condition; `drop … --reason` |
472
+ | `review <id> [T]` | every done task with its evidence and answered conditions |
473
+ | `condition list\|add\|drop <id> [--statement "…"] [C]` | the plan-level conditions of done; `add` records when, so earlier tasks are exempt |
474
+ | `run <id> [--max-tasks N] [--model m] [--dry-run]` | one fresh `claude -p` session per task (§ Fresh context per task) |
475
+ | `log` · `learn` · `decide` | the three append-only records |
476
+ | `gate list\|run\|verify\|add <id> [G\|--all]` | run a gate; prove it can fail; add one |
477
+ | `activate` · `pause` · `close` · `abandon --reason` | plan lifecycle; the last two need `--confirmed-by-owner` |
478
+ | `learnings --search <term> [--plan id]` | every learning, across plans |
479
+ | `hooks install\|status\|selftest` · `doctor` | the machine side |
480
+ | `selftest` · `schema --write` | prove the validator can fail; export JSON Schema files |
481
+
482
+ ## Adopting an existing prose plan
483
+
484
+ A prose plan you already have keeps working the old way until it is adopted.
485
+ To adopt one: `plan new` with the same intent; copy its Mission/Why and the still-true
486
+ parts of its method into `PLAN.md`; turn its open task rows into `task add` calls
487
+ with gates (the project's own scripts are the gates); turn its Gotchas into `plan learn`
488
+ entries and its Decisions into `plan decide --by owner`; put its route-level
489
+ warnings that apply at a tool call into `rules.json`; activate; then replace the
490
+ old file's pointer in CLAUDE.md with a line saying it was adopted into
491
+ `plans/<id>/`. Do not delete the old file.
492
+
493
+ ## Workstreams and subagents
494
+
495
+ A subagent is a fresh context that sees CLAUDE.md, its prompt, and whatever
496
+ hooks inject. It does not see the chat, the brief, or PLAN.md. Two measured
497
+ facts shape everything below (CLAUDE.md § TOKEN DISCIPLINE): the fixed prompt
498
+ is re-sent on every turn, so what you hand an agent is paid for again on each
499
+ step it takes; and an agent's own context grows with every unit it holds, so
500
+ cost per unit is quadratic in batch size (790,822 tokens per page at 16 pages
501
+ per agent; 306,426 at 1).
502
+
503
+ **What an agent gets: its slice, not the plan.**
504
+
505
+ ```bash
506
+ npx planrails agent-brief <id> T7 --what "read leaf 12 of manasagari-1904" --label leaf-12
507
+ ```
508
+
509
+ prints the brief to paste into the Agent prompt (≤ 3,000 chars): the unit to
510
+ do, the files it may write, what "done" means and who checks it, where to write
511
+ its report, the do-nots, then the plan's rules for those files, clipped. The
512
+ contract comes first so a long rule can never push it off the end. Two hooks
513
+ add the rest without anyone remembering: `plan-subagent-start.mjs` puts a
514
+ short note in front of every subagent (report path, never delete, never close
515
+ a task — measured to reach the subagent only), and `plan-pre-tool.mjs`
516
+ injects the matching `rules.json` rule when the agent touches a file, once
517
+ per agent (subagents share the parent's session id, so the dedupe is keyed by
518
+ `agent_id`).
519
+
520
+ **The rules, each with its case.**
521
+
522
+ 1. **One writer per file.** Two agents that can write the same file will, and
523
+ one will win. Give each task disjoint `files`; the validator warns when two
524
+ tasks in flight list the same file. *Case:* an agent transcribing leaves
525
+ 99–112 overwrote every other leaf's `layoutRule` in that directory
526
+ (2026-09-05); `page_0060.json` still carries the banner.
527
+ 2. **Small units, passed in.** 1–2 units per agent; derive the shared rule
528
+ (layout, format, convention) once and pass it in the brief (~500 tokens)
529
+ rather than letting each agent rediscover it from a dozen pages in view.
530
+ 3. **The report is a file, the reply is a pointer.** An agent writes
531
+ `plans/<id>/reports/<T>-<label>.md`; its reply is ≤ 10 lines naming that
532
+ file; the main session's `plan log --refs` names it too. *Case:* a reviewer
533
+ who rejected a printed number by measuring the glyph against a known one on
534
+ the same page: that measurement fits nowhere on a data record, and without
535
+ the report the next reviewer repeats the work.
536
+ 4. **An agent's finding is a lead.** Record it with `plan learn --lead`; the
537
+ main session or a second reader confirms it before anyone acts on it. A
538
+ number without a locator is dropped, not recorded. *Case:* on 2026-09-07
539
+ four spot-checked agent findings were each directionally right and
540
+ numerically wrong (4 → 1, 4,934 → 4,553).
541
+ 5. **Only the main session closes tasks.** An agent never runs `plan task
542
+ done` or `plan close`; it cannot see the whole plan and the gate is the
543
+ main session's to run. Agents may write under their files and their
544
+ report; the Stop hook counts their edits as unlogged work for the main
545
+ session, which is what you want.
546
+ 6. **Resumable by construction.** An agent skips any unit whose output already
547
+ exists and parses, and says so in its report. Session limits kill workflows
548
+ mid-flight; this has saved several runs.
549
+ 7. **Sizing that was measured.** 8–10 agents per workflow (a workflow runs
550
+ about 9 at once whatever you ask for), 1–2 units per agent, and **at most
551
+ 1–2 workflows at once** on this machine (three parallel reads hit the
552
+ session limit and starved other projects). Effort per agent: `max` for
553
+ judgement, `xhigh` for routine.
554
+ 8. **Fresh over fork.** A forked agent inherits the whole conversation and
555
+ pays for it on every turn; use it only when the agent truly needs the
556
+ chat. Default: a fresh agent with the agent brief.
557
+ 9. **Never optimise the reading; always shrink the carrier.** Cost tracks how
558
+ hard the unit is inside the 0.1% the model writes; the 99.8% is the prompt
559
+ and the accumulated context being re-sent. Cut the second, never the first.
560
+ 10. **Bulk, uniform work is not an agent job at all.** One plain API call per
561
+ unit (an image to transcribe, a record to classify) was measured at 465×
562
+ cheaper than an agent doing the same, at equal or better accuracy; the
563
+ agent's work is the judgement on the result afterwards.
564
+
565
+ **Parallel workstreams in one plan.** A workstream is a set of tasks whose
566
+ `files` are disjoint from every other stream's. Mark each task's stream in its
567
+ title (`[stream A] …`), give them disjoint `files`, start them with `plan
568
+ task start` (several may be `doing` at once), and hand each one to its own
569
+ agent or workflow with its agent brief. The main session merges: it reads the
570
+ reports, runs each task's gate with `plan task done`, logs once per landed
571
+ task. Where two streams must touch the same file, that file gets its own task
572
+ that both depend on (`--after`), so it is written once.
573
+
574
+ ## Installing planrails in a project
575
+
576
+ ```bash
577
+ npx planrails init # a new directory or an existing project; idempotent
578
+ npx planrails init --with-never-delete # also block rm/rmdir/unlink/shred in Bash (needs jq)
579
+ npx planrails doctor # must say healthy
580
+ ```
581
+
582
+ `init` installs planrails as a devDependency, copies this guide to
583
+ `docs/PLANNING_GUIDE.md`, creates `.project-management/plans/`, adds a
584
+ "Project management" section and the generated plans block to CLAUDE.md, adds
585
+ the npm scripts `plan`, `plan:doctor`, `plan:brief` and `plan:validate` (and
586
+ appends the validator to an existing `check` script), ignores `.planrails/`, and
587
+ writes the hooks into `.claude/settings.json` next to whatever hooks were
588
+ already there. The hook commands use `$CLAUDE_PROJECT_DIR`, which Claude Code
589
+ sets for every hook, so the settings file can be committed and works on every
590
+ teammate's machine after `npm install`. Run `npx planrails update` after
591
+ upgrading the package; `npx planrails uninstall` removes the hooks and the skill
592
+ and leaves the plans alone.
593
+
594
+ **Plan files written by an older version keep loading.** A `state.json` that
595
+ predates the conditions of done gets the default conditions when it is read,
596
+ and its tasks get empty checklists; the next write stores them. `validate`
597
+ warns about tasks that were closed before conditions existed, and `review`
598
+ shows `checklist: NONE` for them. No migration is needed.
599
+
600
+ ## Fresh context per task
601
+
602
+ Claude Code cannot wipe its own memory between tasks: nothing the model, a
603
+ hook or a skill can do runs `/compact` or `/clear` (checked against the
604
+ documentation on 2026-09-12; decision D6 of the planning-system plan). The
605
+ nearest thing exists and is better: **a new session for every task.**
606
+
607
+ ```bash
608
+ npx planrails run <id> --max-tasks 3 # three tasks, three fresh sessions
609
+ npx planrails run <id> --dry-run # print the prompt the next session would get
610
+ ```
611
+
612
+ `run` starts one `claude -p` session per task from the project root. The
613
+ session starts empty, the SessionStart hook hands it the brief, it does one
614
+ task, logs, answers the conditions, closes the task with `task done`, and
615
+ exits. The driver then reads `state.json`, never the session's words: done →
616
+ next task; blocked → stop and say why; not done → one retry with the RESUME
617
+ line, then stop. A task with no gate is a person's to close, so the driver
618
+ stops there. Sessions run unattended (`--dangerously-skip-permissions`); the
619
+ project's hooks still apply inside them. Auto-compaction stays on as the safety
620
+ net for a long task, and the PreCompact journal plus the SessionStart
621
+ re-injection make a compaction survivable when it happens mid-task. The
622
+ black-box trial (`scripts/trial/run.mjs`) is this pattern with snapshots
623
+ between sessions.
624
+
625
+ ## Worked example: this system's own plan
626
+
627
+ `.project-management/plans/planning-system/` tracks the build of the planning
628
+ system with the planning system. Open it to see a real `PLAN.md`, gates with
629
+ known-fail cases (one runs the validator against a deliberately broken plan in
630
+ `node_modules/planrails/src/plan/fixtures/`, one runs a real `claude -p` session and checks the
631
+ brief reached the model), tasks closed by recorded gate runs, and learnings
632
+ recorded while building it.