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.
- package/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +225 -0
- package/bin/planrails.mjs +7 -0
- package/docs/PLANNING_GUIDE.md +632 -0
- package/package.json +51 -0
- package/src/hooks/_lib.mjs +32 -0
- package/src/hooks/guard-never-delete.sh +29 -0
- package/src/hooks/install.mjs +173 -0
- package/src/hooks/plan-pre-tool.mjs +71 -0
- package/src/hooks/plan-session-start.mjs +46 -0
- package/src/hooks/plan-stop.mjs +60 -0
- package/src/hooks/plan-subagent-start.mjs +27 -0
- package/src/hooks/postcompact-journal.mjs +35 -0
- package/src/hooks/precompact-journal.mjs +128 -0
- package/src/hooks/selftest.mjs +128 -0
- package/src/init.mjs +155 -0
- package/src/issue.mjs +55 -0
- package/src/plan/fixtures/README.md +7 -0
- package/src/plan/fixtures/broken-cli.mjs +27 -0
- package/src/plan/fixtures/broken-hooks-root/.claude/settings.json +83 -0
- package/src/plan/fixtures/broken-hooks-root/.project-management/plans/.gitkeep +0 -0
- package/src/plan/fixtures/broken-hooks-root/CLAUDE.md +9 -0
- package/src/plan/fixtures/broken-root/.project-management/plans/broken/PLAN.md +4 -0
- package/src/plan/fixtures/broken-root/.project-management/plans/broken/gates.json +1 -0
- package/src/plan/fixtures/broken-root/.project-management/plans/broken/rules.json +1 -0
- package/src/plan/fixtures/broken-root/.project-management/plans/broken/state.json +67 -0
- package/src/plan/fixtures/broken-root/CLAUDE.md +3 -0
- package/src/plan/fixtures/broken-trial.mjs +25 -0
- package/src/plan/lib/brief.mjs +116 -0
- package/src/plan/lib/claude-md.mjs +66 -0
- package/src/plan/lib/glob.mjs +81 -0
- package/src/plan/lib/judgment.mjs +19 -0
- package/src/plan/lib/paths.mjs +65 -0
- package/src/plan/lib/schema.mjs +199 -0
- package/src/plan/lib/store.mjs +338 -0
- package/src/plan/lib/time.mjs +21 -0
- package/src/plan/plan.mjs +843 -0
- package/src/plan/run.mjs +88 -0
- 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.
|