planrails 0.6.0 → 0.8.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 CHANGED
@@ -1,5 +1,102 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.0 — 2026-10-04
4
+
5
+ Plans that run unattended, compact between tasks, and end instead of lingering.
6
+ Every change below came from a test run or a real project's plans.
7
+
8
+ - **An optional Claude Code plugin lets the model compact its own session
9
+ between tasks.** `claude plugin marketplace add vivmagarwal/planrails`, then
10
+ `claude plugin install planrails@planrails`. It gives the model two tools,
11
+ `context_usage` and `compact_after_turn`, and decides nothing: when a task is
12
+ recorded as done it puts the context reading (and the growth since the session
13
+ started or last compacted) beside the result, and step 6 of the plan's loop has
14
+ the model decide. The compaction runs once the turn ends, waits while a
15
+ sub-agent is still going (ask again later), keeps the session id, then resumes with "Continue the plan.";
16
+ a failure is reported to the model as text. Measured in CourseGen Lab on an
17
+ 11-task plan run unattended on Opus, two pairs of runs: compactions only at
18
+ recorded boundaries; inside each plugin run its compactions saved about 30% and
19
+ 50% of its bill; across pairs the plugin runs read 20% and 50% fewer tokens and
20
+ cost 1–32% less (runs vary: the two controls differed by 45%); blind reviews of
21
+ both pairs found no quality loss attributable to it.
22
+ Without the plugin, or headless, nothing changes. Installed per user (or per
23
+ project) from this repo's marketplace into `~/.claude/plugins/`; it adds
24
+ nothing measurable to a normal chat's start (3.3 s against 3.0 s). If an
25
+ `allowedMcpServers` list hides its tools, it stays idle (an earlier build
26
+ stalled every session start by ~8 s), and `npx planrails init` and the planner
27
+ warn and offer three choices: one entry in `~/.claude/settings.json`, the same
28
+ in a project's `.claude/settings.local.json`, or removing the list, with the
29
+ count of MCP servers that would also turn on. The plugin uses Claude Code's mods
30
+ API, which its own types call early access.
31
+ - **Go on until the plan stops you.** Nothing told an unattended session to start
32
+ the next task. The loop now does, works around a blocked task, and lists when
33
+ to stop.
34
+ - **Plans that only wait on the owner stop reloading.** A real project's three
35
+ active plans were 97–99% done, held open by one or two owner rows each, and
36
+ reloaded ~80,000 tokens into every call for days. New status `waiting`: §5
37
+ hands such a plan over (its reload line out, one line under "Waiting on you"
38
+ in `CLAUDE.local.md`); the checker notes an active plan in that state.
39
+ - **Only the plans being worked reload.** The planner reports what reloads and
40
+ its size, and clears it before adding a line, with the person's yes.
41
+ - **A plan is one feature of ~10–30 tasks**, ended at a phase boundary and
42
+ continued in a new plan that carries every Rule, Decision and Learning
43
+ forward. The size note says so when finished rows are most of the plan (the
44
+ 0.6 note asked for a trim the method forbade, and was ignored for weeks), and
45
+ no longer counts the loop block every plan carries.
46
+ - **Slices a stranger can pick up cold**: each row names its files and `after
47
+ Tn`; a proof fails until its task is done (a project-wide check goes after the
48
+ task's own test); irreversible steps are owner rows; Context is the living
49
+ "what exists"; before compacting, what the next task needs goes in the plan.
50
+ - **The stamp is taken in the same command as the proof.** Typed timestamps were
51
+ the slip real plans recorded most.
52
+
53
+ To update: `npx planrails@latest init`. Active plans keep working; to give one
54
+ the new loop, replace its "How to work this plan" block with the template's.
55
+
56
+ ## 0.7.0 — 2026-10-03
57
+
58
+ The reload line moves from `CLAUDE.md` to `CLAUDE.local.md`, which git ignores.
59
+ A plan now reloads only for whoever works it, and a team's `CLAUDE.md` is left
60
+ alone.
61
+
62
+ - **Why.** Field feedback from teams: a team shares `CLAUDE.md`, so every plan
63
+ opened or closed changed the team's file, two branches' lines conflicted, and
64
+ every teammate's sessions reloaded everyone's plans.
65
+ - **The line goes in `CLAUDE.local.md`**, and the agent adds that file to
66
+ `.gitignore` if git does not already ignore it. Claude Code loads it beside
67
+ `CLAUDE.md` and re-reads it, with its imports, after `/compact`. The docs say
68
+ so for `CLAUDE.md` only, so it was tested on Claude Code 2.1.288: a word
69
+ swapped on disk, never shown in the chat, came back after compaction, the same
70
+ as through `CLAUDE.md`. A teammate who takes a plan over adds the line to
71
+ their own file.
72
+ - **The checker asks the holder.** Before, when a `CLAUDE.md` existed, every
73
+ active plan needed its line there. With the line in an ignored file, CI and a
74
+ teammate's checkout cannot see it, so that rule would fail them. Now the
75
+ active plan whose NOW `session:` id starts `$CLAUDE_CODE_SESSION_ID` must be
76
+ reloaded by a `CLAUDE.local.md` or `CLAUDE.md` in the project root or a folder
77
+ above it, which is what Claude Code loads; in CI, at a terminal, and for a
78
+ teammate's plan nothing is asked. The id is read in any case, from six hex
79
+ characters to a whole session id, never from a code fence. This also catches
80
+ a worktree outside the checkout, which has no copy of the ignored file. A `CLAUDE.md` line to a plan that does not exist
81
+ still fails; a `CLAUDE.local.md` one does not, because that file outlives a
82
+ checkout of a branch where the plan does not exist. The ok line says "this
83
+ session's plan reloads" in place of "active plans reload".
84
+ - **`AGENTS.md` projects.** Once any `CLAUDE` file exists, Claude Code stops
85
+ reading `AGENTS.md` by itself. In a project with `AGENTS.md` and no
86
+ `CLAUDE.md`, `CLAUDE.local.md` starts with `@AGENTS.md`. Before 0.7 the
87
+ method wrote a `CLAUDE.md` in such a project, with the same silent effect.
88
+ - **The close deletes the line.** A lasting learning still graduates to the
89
+ shared `CLAUDE.md`: it is the team's knowledge, added as a reviewed change.
90
+ - **`init`** says the agent writes `CLAUDE.local.md`, and points out reload
91
+ lines still in `CLAUDE.md`. It writes neither file, nor `.gitignore`.
92
+
93
+ To update: `npx planrails@latest init`. A reload line in `CLAUDE.md` keeps
94
+ working. To move it, cut it from `CLAUDE.md`, paste it into `CLAUDE.local.md`,
95
+ and add `CLAUDE.local.md` to `.gitignore`. Once it has moved, an active plan's
96
+ block can say "the reload line deleted from CLAUDE.local.md" in place of "the
97
+ reload line in CLAUDE.md wrapped in backticks"; until then, leave the block
98
+ alone, or the close will look for the line in the wrong file.
99
+
3
100
  ## 0.6.0 — 2026-09-19
4
101
 
5
102
  The checker can now advise without blocking.
package/PLANNER.md CHANGED
@@ -1,4 +1,4 @@
1
- <!-- planrails 0.6.0 -->
1
+ <!-- planrails 0.8.0 -->
2
2
  # The Planner
3
3
 
4
4
  You are about to plan a piece of work with a person, then help execute it so the
@@ -11,7 +11,7 @@ project runs Node — copy in one small checker.
11
11
  **How a person starts you.** They say something like *"Follow
12
12
  `.project-management/planrails/PLANNER.md` and tell me when you are ready to plan
13
13
  the next feature with me."* When they do, run **§1 Get ready**, give the
14
- eight-line report, and stop.
14
+ nine-line report, and stop.
15
15
  Do not start planning until they answer.
16
16
 
17
17
  ---
@@ -30,8 +30,10 @@ the plan, including how to work it. If it is not in the plan, it does not exist.
30
30
 
31
31
  Three rules make the plan trustworthy. They are the whole point of this system:
32
32
 
33
- 1. **The reload line.** One line in the project-root `CLAUDE.md` re-opens the
34
- plan after every compaction, so the state is never lost.
33
+ 1. **The reload line.** One line in the project-root `CLAUDE.local.md` re-opens
34
+ the plan after every compaction, so the state is never lost. Git ignores that
35
+ file, so the plan reloads for whoever works it, and the team's `CLAUDE.md` is
36
+ left alone.
35
37
  2. **Proof before work.** Every task names the command that will prove it done
36
38
  *before* the work starts. No command, no way to fake it later.
37
39
  3. **Evidence at close.** A task is done only when its proof was run and its exit
@@ -53,21 +55,25 @@ Do this before you ask the person anything. Read the project; do not make them
53
55
  tell you what the repo already says.
54
56
 
55
57
  1. **Read the always-loaded file** — the project-root `CLAUDE.md`, else
56
- `AGENTS.md`, else `README.md`. Find the stack, the package manager, the
57
- **check command** (the one run before every commit), and the **test command**.
58
+ `AGENTS.md`, else `README.md` — and `CLAUDE.local.md` if there is one. Find the
59
+ stack, the package manager, the **check command** (the one run before every
60
+ commit), and the **test command**.
58
61
  2. **List `docs/`** and read the ones this feature touches. Use sub-agents for
59
62
  long files so your own context stays clear. Record each relevant doc's path
60
63
  and one line of what it holds.
61
64
  3. **Read `.project-management/`.** Read in full the plan this session is
62
65
  assigned — the person names it; if one plan is active, that one — and list
63
- the others by id, with who holds each (NOW's `session:` line).
66
+ the others by id, with who holds each (NOW's `session:` line). Note what
67
+ reloads: each bare `@` plan line in `CLAUDE.local.md` and `CLAUDE.md`, and
68
+ its size (`wc -w`); and what waits on the person (the "Waiting on you" lines),
69
+ to remind them.
64
70
  4. **Read the last ~20 commits** (`git log --oneline -20`) for how the code moves.
65
71
 
66
72
  **A sub-agent's finding is a lead, not a fact.** If it names a file and line, you
67
73
  can check it. If it is only a number, re-derive it yourself or drop it. A wrong
68
74
  number in a plan becomes a wrong decision later.
69
75
 
70
- Then **report ready in eight lines** and stop:
76
+ Then **report ready in nine lines** and stop:
71
77
 
72
78
  ```
73
79
  READY — <project name>
@@ -75,6 +81,7 @@ Stack: <language, framework, package manager>
75
81
  Always-on: <CLAUDE.md | AGENTS.md | README.md>
76
82
  Check: <the check command> Test: <the test command>
77
83
  Plans: <existing plan ids; which are active and who holds each, or "none">
84
+ Reloading: <the plans on @ lines, with their words; or "none">
78
85
  Docs: <relevant doc paths, or "none">
79
86
  Touches: <the folders this feature will change>
80
87
  Questions: <up to 4 things the repo did not answer, or "none">
@@ -104,12 +111,21 @@ templates at the end of this file (§ Template — PLAN.md, § Template — LOG.
104
111
  Pick a short kebab-case `<id>` (`weekly-digest`). Then:
105
112
 
106
113
  - **Write for a senior engineer.** Decisions and context, not obvious steps.
107
- - **Name the proof for every task before any work.** The proof is the command
108
- that shows the task is done: a test, a build, a check. A task that no command
114
+ - **Name the proof for every task before any work.** The proof is a command
115
+ that would fail if this task were not done. A project-wide check (`npm run
116
+ check`) shows only that nothing broke; when you use it, put the task's own test
117
+ first in the same cell: `npx vitest run tests/digest.test.ts && npm run check`.
118
+ Anything that cannot be undone or reaches beyond the repo (a deploy, a publish,
119
+ a push, a migration on shared data, a message to people) is its own task with
120
+ proof `owner`. A task that no command
109
121
  can prove is proven by the person's word — write `owner` in the proof column
110
122
  and record their words and the date in evidence when they give it (the checker
111
123
  wants the date there). The proof cell is exactly one `command`, or `owner`.
112
- - **A task is one sitting's work with one proof.** Longer than that is two tasks.
124
+ - **A task is a slice a stranger can pick up cold.** Its row says what changes,
125
+ the files, and `after T3` when it needs an earlier task; the why is in
126
+ Decisions, the facts in Context. It fits in one stretch between compactions:
127
+ if it means reading more than a handful of files or changing two areas, it is
128
+ two tasks. One task, one proof.
113
129
  Task ids are permanent: never renumber, and a task you will not do keeps its
114
130
  row with status `dropped`.
115
131
  - **A plan with phases ends each phase with a task** whose proof is the Done-when
@@ -120,23 +136,50 @@ Pick a short kebab-case `<id>` (`weekly-digest`). Then:
120
136
  word is paid for again and again. Trim prose before Learnings or Decisions.
121
137
  History goes in LOG.md, not here. Past the line, the checker prints a `note:` —
122
138
  advice, not a failure: the run still exits 0, and you decide what to trim.
123
- - **Add the reload line.** In the project-root `CLAUDE.md`, under a short
124
- "Active plans" spot, add:
139
+ - **A plan is one feature: about 10–30 tasks.** Finished rows stay in the plan
140
+ and reload with it, so a plan that keeps growing keeps costing. When the work
141
+ outgrows that, end the plan at a phase boundary (§5) and go on in a new plan.
142
+ Ending a plan must not lose what it knows: the new plan carries forward every
143
+ Rule, Decision and Learning that still holds, and its Context gets a short
144
+ "What exists" (what the last phase built, where) and a link to the old plan,
145
+ whose rows stay on disk to read when a task needs the detail. What stops
146
+ reloading is the finished rows' narrative, which is most of a big plan's
147
+ words and little of its guidance. An evidence cell holds the stamp, the exit
148
+ code and the last line; the story goes in LOG.md.
149
+ - **Clear what reloads before you add to it.** Every plan on a bare `@` line
150
+ in `CLAUDE.local.md` or `CLAUDE.md` rides on every call of every session in
151
+ this checkout, whether that session works it or not. A session needs only the
152
+ plan it works: the hold check reads the others from disk. So, for each plan
153
+ that reloads: finished, close it (§5); only the owner can move it, hand it over
154
+ (§5); nobody works it now, pause it (`status: paused`, its line out); held by
155
+ a live session (§4's check), leave it. A line for someone else's plan belongs
156
+ in their own `CLAUDE.local.md`; one in the shared `CLAUDE.md` predates 0.7.
157
+ Say what you found, and change a plan only with the person's yes. The aim:
158
+ only the plans being worked right now reload.
159
+ - **Add the reload line.** In the project-root `CLAUDE.local.md` (create it if
160
+ needed), under a short "Active plans" heading, add:
125
161
  ```
126
162
  @.project-management/plans/<id>/PLAN.md
127
163
  ```
128
164
  Put it on its own line, outside any code block — an `@` import wrapped in
129
- backticks does not load. Claude Code re-reads that file, and everything it
130
- imports, at every session start and after every compaction, so the plan comes
131
- back on its own. Under the same heading, one line says how the plans are held:
132
- *Each plan below is held by one session — see its NOW `session:` line. Work one
133
- only when asked in this session, after the check in its block.* (For a tool that
134
- does not do `@`-imports, put the plan's path in `AGENTS.md` and open it by hand
135
- at the start of each session.)
165
+ backticks does not load. Claude Code reads that file beside `CLAUDE.md`, and
166
+ re-reads both, with everything they import, at every session start and after
167
+ every compaction, so the plan comes back on its own. Under the same heading, one
168
+ line says how the plans are held: *Each plan below is held by one session — see
169
+ its NOW `session:` line. Work one only when asked in this session, after the
170
+ check in its block.*
171
+ The file is yours, not the team's: if `git check-ignore -q CLAUDE.local.md`
172
+ fails, add `CLAUDE.local.md` to `.gitignore` on its own line. Do not write the
173
+ reload line in `CLAUDE.md`; a team shares that file, and every teammate would
174
+ reload your plan. Someone who takes the plan over adds the line to their own
175
+ `CLAUDE.local.md`. If the project has `AGENTS.md` and no `CLAUDE.md`, make
176
+ `@AGENTS.md` the first line of `CLAUDE.local.md`: once any `CLAUDE` file
177
+ exists, Claude Code stops reading `AGENTS.md` by itself. (A tool that does not
178
+ do `@`-imports does not reload; open the plan by hand at each session start.)
136
179
  - **One plan per session, one session per plan.** A repo may hold several active
137
180
  plans, each with its own reload line and each held by one session, named on its
138
- NOW `session:` line. Every session reloads all of them, so keep them few; a plan
139
- nobody is working is paused (`status: paused`, its line in backticks). A session
181
+ NOW `session:` line. Every session in this checkout reloads all of them, so keep them few; a plan
182
+ nobody is working is paused (`status: paused`, its line out). A session
140
183
  works only the plan the person in that session assigned; the other reloaded
141
184
  plans are context, not work orders. Naming the session after the plan
142
185
  (`claude -n <id>`) shows the holder in every listing and in the terminal title;
@@ -153,12 +196,25 @@ Pick a short kebab-case `<id>` (`weekly-digest`). Then:
153
196
  if you did not run init, copy `tools/check-plans.mjs` from
154
197
  https://github.com/vivmagarwal/planrails there. Add
155
198
  `node .project-management/planrails/check-plans.mjs` to the check command. Now the
156
- build fails if a task is marked done with no evidence, if an active plan has no
157
- reload line, if its NOW lost its RESUME line, or if NOW points at a finished
158
- task. If the project is not Node,
199
+ build fails if a task is marked done with no evidence, if the plan your session
200
+ holds has no reload line, if its NOW lost its RESUME line, or if NOW points at
201
+ a finished task. If the project is not Node,
159
202
  skip this; the plan still works, and you enforce the gate yourself.
203
+ - **Check the compaction tools, if the planrails plugin is installed.** If you
204
+ lack `context_usage` although `claude plugin list` shows `planrails`, an
205
+ `allowedMcpServers` list in the settings hides them (the plugin then stays
206
+ idle and costs nothing). Tell the person, and offer the choices
207
+ (`npx planrails init` prints them too): `{ "serverName": "planrails" }` in
208
+ `allowedMcpServers` in `~/.claude/settings.json` (every project, every other
209
+ server still off); the same entry in `.claude/settings.local.json` (this
210
+ project only; make sure git ignores it); or removing the list, which also turns
211
+ on every MCP server it keeps off (count them in `~/.claude.json` and
212
+ `.mcp.json` first). Do what they pick; it applies from the next session. A
213
+ managed policy that locks the list (`allowManagedMcpServersOnly`) cannot be
214
+ widened: say who can, and carry on without.
160
215
  - **Check the plan before the first task.** You, or a fresh sub-agent with no
161
- chat context: open every path the plan names, start every proof command,
216
+ chat context: open every path the plan names, start every proof command and
217
+ see it fail on work not yet built,
162
218
  confirm the reload line loads and the checker passes, and log what you changed.
163
219
  A plan can name a file that does not exist, or a proof that proves nothing;
164
220
  ten minutes here saves an hour later.
@@ -208,8 +264,9 @@ decide:
208
264
  no listing to consult. Git is the record: pull first, read a `doing` row with a
209
265
  fresh `updated:` stamp as someone's work in flight, and ask.
210
266
  - A worktree is another checkout with its own copy of the plan: a merge concern
211
- later, not a clobber now. A sub-agent you briefed never runs this check; it
212
- never writes the plan.
267
+ later, not a clobber now. Git does not copy `CLAUDE.local.md` into it, so add
268
+ the plan's reload line there too. A sub-agent you briefed never runs this
269
+ check; it never writes the plan.
213
270
 
214
271
  Stopping means: report the session's name, status and start, what NOW says and
215
272
  what `git status` shows, then ask — `AskUserQuestion` in Claude Code; a `claude -p`
@@ -227,8 +284,10 @@ The loop for each task:
227
284
  2. **Do the work.** Fix the cause, not the symptom. The simplest change that
228
285
  works, end to end.
229
286
  3. **Run the proof.** Right now, not from memory. Copy the exit code and the last
230
- line of output. Take the time from the shell clock (`date`; on Windows
231
- `Get-Date`), never from memory.
287
+ line of output. Take the stamp in the same command, so it cannot be typed:
288
+ `npx vitest run x > /tmp/proof.out 2>&1; echo "exit $?"; date "+%Y-%m-%d %H:%M"; tail -1 /tmp/proof.out`
289
+ (on Windows, `Get-Date -Format "yyyy-MM-dd HH:mm"`). A stamp typed from memory
290
+ is the slip real plans recorded most.
232
291
  4. **Paste the evidence.** Into the task's evidence cell:
233
292
  `2026-09-12 14:20 · exit 0 · "6 passed"`. Exit 0, or the task is not done.
234
293
  5. **Set it done.** Only now. A proof of `owner` is closed only by the person's
@@ -243,12 +302,48 @@ The loop for each task:
243
302
  "trap → rule", with the real case. LOG.md holds what happened; Learnings holds
244
303
  the rule, because Learnings reloads every session and the log does not.
245
304
 
305
+ **Go on until the plan stops you.** When the person asked you to work the plan,
306
+ start the next task without ending your turn to report. A blocked task does not
307
+ end the run: take the next one that does not wait on it. Stop only when every
308
+ row is done or dropped (close, §5), only owner rows and the close are open (hand
309
+ over, §5), every open task is blocked or waits on one, the hold check says stop,
310
+ or something breaks that you cannot diagnose. Write NOW first, then report.
311
+
246
312
  **Update NOW before you end any turn.** NOW is the first thing a fresh session
247
313
  reads, so write it for a stranger. On a long task, note the sub-step in NOW at
248
314
  each checkpoint. If a task cannot proceed, set it `blocked`, put the reason in its
249
315
  evidence cell, and say so in NOW. If NOW is stale, the next session repeats your
250
316
  work or starts in the wrong place.
251
317
 
318
+ **Compact between tasks, if you have the tools.** The planrails plugin gives you
319
+ `context_usage` and `compact_after_turn` (`mcp__planrails__…`; Claude Code may
320
+ list them as deferred: load them with ToolSearch); without them, skip this, and Claude
321
+ Code compacts on its own when the context fills. Every call re-reads the whole
322
+ context, so a big one costs on every step that follows. When a task is done and
323
+ recorded (evidence pasted, NOW updated, LOG appended, and in the plan whatever
324
+ the next task needs: a person's words in Decisions with the date, a trap in
325
+ Learnings, what you built in Context), call `context_usage` and decide:
326
+
327
+ - Compact when the context has grown past about 50,000 tokens since the session
328
+ started or last compacted (`context_usage` reports it; a real project starts a
329
+ session at tens of thousands, which no compaction can drop), or past the line
330
+ the plan's Rules set, and tasks remain. The saving is roughly the tokens dropped
331
+ times the calls still to come.
332
+ - Do not compact mid-task, near the end of the plan, or when the next task works
333
+ in the files you just read: re-reading them costs more than keeping them.
334
+
335
+ To compact, first write into the plan what the next task needs: what you built,
336
+ in Context; a trap, in Learnings; a person's words, in Decisions. The summary is
337
+ rewritten at every compaction, so a fact that lives only there is lost at the
338
+ next one; in a real run, facts left in the instructions ("not yet written down
339
+ anywhere") were exactly that. Then call `compact_after_turn` with
340
+ `instructions` naming the plan and the next task, and `resume: "Continue the
341
+ plan."`, and end your turn. It compacts once the turn
342
+ ends (it refuses while a sub-agent is still going: ask again once it is done),
343
+ and the plan reloads with your session's
344
+ claim intact. If `context_usage` reports the last compaction failed, carry on
345
+ without it.
346
+
252
347
  **A red test starts an investigation**, not an edit: is the product wrong, the
253
348
  test stale, or the environment wrong? Decide which before changing anything, and
254
349
  never weaken an assertion to get green.
@@ -282,13 +377,25 @@ cannot see the screen. You can.
282
377
  - Gate green, work wrong: refuse to close it. Say what the gate cannot see.
283
378
  - A green check is not proof the work is good. It is proof one command exited 0.
284
379
 
285
- **Never edit a plan the way a script would.** If something crashes, log it, set
286
- the task `blocked` with the reason, and stop. Do not hand-fix state to look done.
380
+ **Never edit a plan the way a script would.** If something crashes, log it and set
381
+ the task `blocked` with the reason; go on only with tasks that do not wait on it. Do not hand-fix state to look done.
287
382
 
288
383
  ---
289
384
 
290
385
  ## §5 Close
291
386
 
387
+ **When only the owner can move the plan.** If every open row is an `owner` row
388
+ or the close itself, the work is done and only the person's word is missing. Do
389
+ not leave the plan `active`: it reloads into every session for nothing, and real
390
+ plans have sat that way for days. Run steps 1–3 below now, then hand it over: set
391
+ `status: waiting`, NOW's `session:` to `none` and RESUME to the owner rows; take
392
+ its reload line out of `CLAUDE.local.md`, and add one line there under a
393
+ "Waiting on you" heading: `<id> — T7: the owner tries the export and says it
394
+ works (.project-management/plans/<id>/PLAN.md)`. Tell the person. When they give
395
+ their word, a session sets the plan `active`, puts its reload line back, records
396
+ their words and the date, re-runs every proof, and closes by steps 4–5. The checker notes an active plan
397
+ in this state.
398
+
292
399
  1. **Re-run every proof.** A plan closes on what the checks say now, not on their
293
400
  last recorded run. Paste fresh evidence.
294
401
  2. **Fresh-context review.** A sub-agent, or a new session, with the plan and
@@ -298,14 +405,17 @@ the task `blocked` with the reason, and stop. Do not hand-fix state to look done
298
405
  3. **Update the docs** the work changed — in the same step, per the project's
299
406
  documentation guide if it has one.
300
407
  4. **Retire the plan.** Set its header to `status: done` and NOW's `session:` to
301
- `none`, then wrap its reload line in backticks, or delete it. Do not move a
408
+ `none`, then delete its reload line (or wrap it in backticks). Do not move a
302
409
  bare `@` line under a "Finished" heading: it still imports, and every retired
303
- plan would reload forever. The status comes first: the checker holds an `active` plan to its
304
- reload line. The plan files stay on disk; they are the record.
410
+ plan would reload forever. The status comes first: the checker holds the
411
+ `active` plan your session holds to its reload line. The plan files stay on
412
+ disk; they are the record.
305
413
  5. **Graduate any lasting learning.** A learning that is true beyond this feature
306
- moves to the always-loaded file (`CLAUDE.md` / `AGENTS.md`), so it outlives the
307
- plan you are retiring. A learning that a check could enforce becomes a test.
308
- One that was only about this work retires with it.
414
+ moves to the shared always-loaded file (`CLAUDE.md` / `AGENTS.md`), not
415
+ `CLAUDE.local.md`: it is the team's knowledge, so it goes in as a change they
416
+ review, and it outlives the plan you are retiring. A learning that a check
417
+ could enforce becomes a test. One that was only about this work retires with
418
+ it.
309
419
 
310
420
  ---
311
421
 
@@ -317,14 +427,16 @@ Each line here was paid for by a real failure in earlier planning systems:
317
427
  always-loaded file after compaction is something the tool already does. Hooks
318
428
  that tried to do more misfired: a crashing pre-tool hook blocks the very call,
319
429
  a read guard was wrong twice about sub-agents, a compaction journal came out as
320
- command stubs.
430
+ command stubs. It lives in `CLAUDE.local.md` because teams share `CLAUDE.md`:
431
+ there, every plan opened or closed changed the team's file, and every
432
+ teammate's sessions reloaded everyone's plans.
321
433
  - **Proof before work** exists because a checkbox lies. One project marked a phase
322
434
  "done" three times while it was not; nothing in a status column could catch it.
323
435
  A named command that must be run and pasted can.
324
436
  - **Evidence at close** is the one machine-checkable rail worth keeping. The
325
437
  checker enforces it — exit 0, not a word — plus two integrity checks that keep
326
- the reload honest: the reload line exists, and NOW names a live task. Both
327
- failures were silent in real plans.
438
+ the reload honest: the plan your session holds has its reload line, and NOW
439
+ names a live task. Both failures were silent in real plans.
328
440
  - **The plan carries its own loop** because a session that never saw this file
329
441
  gets only PLAN.md back. It knew where it was; it did not know how to work.
330
442
  - **Learnings live in the plan, not only the log.** The log is history a fresh
@@ -336,8 +448,11 @@ Each line here was paid for by a real failure in earlier planning systems:
336
448
  stamped two hours behind its own log. NOW is four lines; LOG.md is the history.
337
449
  - **The checker's notes advise; they never block.** A script can verify an exit
338
450
  code, so that is a problem and fails the build. It can only suspect that a plan
339
- is too long, so that is a note, and you judge. Four of four real plans had
340
- outgrown the word line with nothing saying so. The check command already runs
451
+ is too long, that only its owner can move it, or that a finished plan still
452
+ reloads, so those are notes, and you judge. Four of four real plans had
453
+ outgrown the word line with nothing saying so; three of three active plans in
454
+ one project were held open by owner rows. A note names an action the method
455
+ allows, or it is ignored. The check command already runs
341
456
  before every commit and its output is already read, so a reminder there needs
342
457
  no hook. A note needs a real failure behind it, or it is noise.
343
458
  - **Sub-agent findings are leads** because four spot-checked findings were each
@@ -385,14 +500,15 @@ updated: <YYYY-MM-DD HH:MM, from the shell clock>
385
500
  session: <none, or the session working this plan: name · the first 8 characters of its session id · since YYYY-MM-DD HH:MM>
386
501
 
387
502
  ## How to work this plan
388
- Read NOW, then Rules and Learnings; do not re-read Context. One task at a time:
503
+ Read NOW, then Rules and Learnings; open a file Context lists only when your task touches it. One task at a time:
389
504
  1. Check who holds the plan (below); set it `doing`; point NOW at it; put your session on its `session:` line.
390
505
  2. Do the work: fix causes, not symptoms; the simplest change that works end to end.
391
- 3. Run the proof now. Paste `YYYY-MM-DD HH:MM · exit N · "last line"` into evidence. Every stamp comes from the shell clock (`date`; on Windows `Get-Date`), never typed from memory.
506
+ 3. Run the proof now, with the stamp taken in the same command (`cmd > /tmp/proof.out 2>&1; echo "exit $?"; date "+%Y-%m-%d %H:%M"; tail -1 /tmp/proof.out`), never typed: `date`; in PowerShell `$LASTEXITCODE` and `Get-Date`. Paste `YYYY-MM-DD HH:MM · exit N · "last line"` into evidence, and only that; the story goes in LOG.md.
392
507
  4. `done` only if N is 0. A proof of `owner` is closed only by the person's words with the date, never by you: until then the row is `blocked`, the reason in its evidence cell, and RESUME says so. Point NOW at the next task; append an entry to LOG.md: did, files, proof, next, learned.
393
- 5. If it fought back, add a Learning: the trap, then the rule. A verified fact goes in Context, a choice in Decisions.
508
+ 5. If it fought back, add a Learning: the trap, then the rule. A verified fact, or what this task built that a later one uses, goes in Context (path — what it is); a choice, or a person's words with the date, in Decisions.
509
+ 6. If you have the planrails plugin's tools (`mcp__planrails__context_usage`; load it with ToolSearch if it is listed as deferred), call `context_usage` now: grown past ~50,000 tokens since the session started or last compacted, with tasks left and the next one in other files, first write into the plan what the next task needs (what you built, in Context; a trap, in Learnings), then call `compact_after_turn` (instructions: the plan and the next task; resume: "Continue the plan.") and end your turn. Otherwise go on to the next task without stopping to report; a blocked task does not end the run: take the next one that does not wait on it. Stop when no task can go ahead, the plan is ready to close or hand over, or the hold check says stop; write NOW first.
394
510
 
395
- NOW is exactly these four lines, RESUME, NEXT, updated and session, written for a stranger; on a long task note the sub-step in RESUME, and update NOW before any turn ends. Blocked: say so in RESUME, reason in the evidence cell. A task you will not do is `dropped`; its row stays. After a compaction, `git status --short` shows the in-flight work. Edit this file with the editor tool; an unquoted shell string eats backticks. A self-contained task may go to a sub-agent briefed with its row, Rules, Decisions, Learnings and Context; it writes to a named file and reports a few lines, which are leads; you run the proof before pasting evidence. If the project runs Node, `node .project-management/planrails/check-plans.mjs` must pass. When no row is left open, done or dropped, close by PLANNER.md §5: proofs re-run, a fresh-context review, `status: done`, `session: none`, the reload line in CLAUDE.md wrapped in backticks. Full method: `.project-management/planrails/PLANNER.md` §4.
511
+ NOW is exactly these four lines, RESUME, NEXT, updated and session, written for a stranger; on a long task note the sub-step in RESUME, and update NOW before any turn ends. Blocked: say so in RESUME, reason in the evidence cell. A task you will not do is `dropped`; its row stays. After a compaction, `git status --short` and the last LOG.md entry show where you are. Edit this file with the editor tool; an unquoted shell string eats backticks. A self-contained task may go to a sub-agent briefed with its row, Rules, Decisions, Learnings and Context; it writes to a named file and reports a few lines, which are leads; you run the proof before pasting evidence. If the project runs Node, `node .project-management/planrails/check-plans.mjs` must pass. When no row is left open, done or dropped, close by PLANNER.md §5: proofs re-run, a fresh-context review, `status: done`, `session: none`, the reload line deleted from CLAUDE.local.md. When only owner rows and the close are open, hand it over by §5: `status: waiting`, `session: none`, its reload line out, one line under "Waiting on you" in CLAUDE.local.md. Full method: `.project-management/planrails/PLANNER.md` §4.
396
512
 
397
513
  Who holds the plan. `session:` names the one session working it: `name · first 8 characters of its session id · since YYYY-MM-DD HH:MM`. Match by id, never by name: the line's 8 characters start a `sessionId` in the listing. Before any task goes `doing`, even when asked to continue: re-read NOW from disk, run `claude agents --json` (your own id is `$CLAUDE_CODE_SESSION_ID`) and `git status --short`, then:
398
514
  - the id is yours: go on.
@@ -414,9 +530,10 @@ Work only the plan assigned in this session; other reloaded plans are context. W
414
530
  ## Tasks
415
531
  | id | task | status | proof | evidence |
416
532
  |----|------|--------|-------|----------|
417
- | T1 | <what to do> (<path>) | todo | `<command that proves it>` | |
418
- | T2 | <what to do> (<path>) | todo | `<command>` | |
419
- | T3 | update the docs this work changed | todo | owner | |
533
+ | T1 | <what changes> (<paths>) | todo | `<a command that fails until T1 is done>` | |
534
+ | T2 | <what changes> (<paths>; after T1) | todo | `<its own test> && <the project check>` | |
535
+ | T3 | the owner tries it on staging and says it works | todo | owner | |
536
+ | T4 | close the plan by PLANNER.md §5 (after T1–T3) | todo | `<the Done-when checks>` | |
420
537
 
421
538
  ## Rules for this plan
422
539
  - <a rule that governs this area — e.g. "every email goes through lib/email, never a bare send">
@@ -429,8 +546,9 @@ Work only the plan assigned in this session; other reloaded plans are context. W
429
546
  ## Learnings
430
547
  - <the trap you hit> → <the rule that avoids it> (<the real case, one line>)
431
548
 
432
- ## Context (read during planning — do not re-read)
549
+ ## Context (facts and what exists; open a listed file only when your task touches it)
433
550
  - <path> — <one line of what it holds; mark a sub-agent's unverified finding as a lead>
551
+ - <path> — <what a task built here that a later task uses: the function, the table, the route>
434
552
  ````
435
553
 
436
554
  ## Template — LOG.md
package/README.md CHANGED
@@ -7,8 +7,10 @@ Long tasks lose their thread. A coding session compacts or ends, and the next on
7
7
  starts blind: it repeats work, or it trusts a status line that says "done" over
8
8
  work that is not. planrails fixes that with three plain rules and almost no code.
9
9
 
10
- 1. **The plan reloads itself.** One line in your project's root `CLAUDE.md`
11
- re-opens the plan after every compaction, and the plan carries its own
10
+ 1. **The plan reloads itself.** One line in your project's root `CLAUDE.local.md`
11
+ re-opens the plan after every compaction. Git ignores that file, so the plan
12
+ reloads for whoever works it and your team's `CLAUDE.md` is never edited. The
13
+ plan carries its own
12
14
  operating loop and the name of the session working it, so a session that
13
15
  never saw the planner prompt still works it correctly — and a second session
14
16
  in the same checkout checks for the first before it touches the plan. (Claude
@@ -18,8 +20,8 @@ work that is not. planrails fixes that with three plain rules and almost no code
18
20
  3. **A task is done only when its proof was run and its exit code pasted in.** An
19
21
  empty evidence cell, a bare word, or a recorded `exit 1` is not done, whatever
20
22
  the status says. A small, dependency-free checker enforces this in your build,
21
- and also checks that every active plan has its reload line and that NOW points
22
- at a task that is still open.
23
+ and also checks that the plan your session holds has its reload line and that
24
+ NOW points at a task that is still open.
23
25
 
24
26
  It works in any project — Node, Python, Go, a monorepo, Windows — because it adds
25
27
  two files and changes nothing else.
@@ -36,10 +38,10 @@ npx planrails init
36
38
 
37
39
  That copies two files into `.project-management/planrails/` and makes the
38
40
  `plans/` folder. It writes **nothing else** — no `package.json`, no `npm install`,
39
- no hooks, no edits to your `CLAUDE.md`. Run it again any time to update: it brings
40
- the two planner files up to the package version and tells you what it replaced,
41
- keeps a same-version copy you edited unless you pass `--force`, and never touches
42
- your plans. What lands:
41
+ no hooks, no edits to your `CLAUDE.md`, `CLAUDE.local.md` or `.gitignore`. Run it
42
+ again any time to update: it brings the two planner files up to the package
43
+ version and tells you what it replaced, keeps a same-version copy you edited
44
+ unless you pass `--force`, and never touches your plans. What lands:
43
45
 
44
46
  ```
45
47
  .project-management/
@@ -56,7 +58,7 @@ Then, two steps:
56
58
  > Follow `.project-management/planrails/PLANNER.md` and tell me when you are
57
59
  > ready to plan the next feature with me.
58
60
 
59
- It reads your repo, reports what it found in eight lines, and waits. Then you
61
+ It reads your repo, reports what it found in nine lines, and waits. Then you
60
62
  plan together, and it writes the plan and wires the reload line.
61
63
 
62
64
  2. **Wire the checker into your build.** Add this to the command you run before
@@ -101,6 +103,73 @@ the same flow in any project that has run `npx planrails init` — the skill rea
101
103
  the project's own copy of `PLANNER.md`, so every project follows the version it
102
104
  has. (It is not called `/plan`, because Claude Code has a `/plan` of its own.)
103
105
 
106
+ ### Optional: let the model compact between tasks (Claude Code plugin)
107
+
108
+ Every model call re-reads the whole conversation, so a long plan pays for its
109
+ early tasks again on every later step. Claude Code compacts on its own only when
110
+ the context is nearly full, wherever that falls, mid-task included. The
111
+ planrails plugin lets the model do it at a better moment: right after a task is
112
+ done and recorded, when the plan already holds everything that matters.
113
+
114
+ ```bash
115
+ claude plugin marketplace add vivmagarwal/planrails
116
+ claude plugin install planrails@planrails
117
+ ```
118
+
119
+ It adds two tools and decides nothing: `context_usage` (how full the context is,
120
+ how much it has grown since the session started or last compacted, how many
121
+ sub-agents run, how the last compaction went) and `compact_after_turn` (compact
122
+ once this turn ends, then go on with "Continue the plan."). When a task is
123
+ recorded as done in the plan, by whatever tool, the plugin puts that reading
124
+ beside the result, and step 6 of the plan's loop has the model decide: after
125
+ growth of about 50,000 tokens, with tasks left and the next one in other files,
126
+ it writes what the next task needs into the plan and compacts. The compaction
127
+ refuses while a sub-agent is still going, keeps the session's id (so the plan's `session:`
128
+ claim holds), and the plan reloads afterwards. Without the plugin, or in a
129
+ headless `claude -p` run, nothing changes.
130
+
131
+ Measured on a real app (CourseGen Lab, an 11-task plan, unattended Opus
132
+ sessions, two pairs of runs with and without the plugin): the sessions with it
133
+ compacted only at recorded task boundaries; each run's own compactions saved
134
+ about 30% and 50% of its bill, against the same run replayed without them. Across
135
+ the pairs the plugin runs read 20% and 50% fewer tokens and cost 1–32% less, a
136
+ wide range because runs vary: the two runs without the plugin differed by 45%.
137
+ Blind reviews of both pairs found no quality loss attributable to compaction
138
+ (one pair favoured each side, on the same bug). The runs, the method and the
139
+ numbers are in `.project-management/research/auto-compact/`; the final wording
140
+ of step 6 came after them.
141
+
142
+ **Where it lives.** Not in the npm package: `claude plugin install` copies it
143
+ into `~/.claude/plugins/cache/planrails/` and switches it on in the scope you
144
+ choose (`--scope user`, the default: every project, for you; `project`: the
145
+ shared `.claude/settings.json`, for the whole team; `local`: this project, for
146
+ you). `claude plugin uninstall planrails@planrails` removes it.
147
+
148
+ **What it costs a normal chat.** Nothing measurable. Sessions with it started as
149
+ fast as without (median 3.3 s against 3.0 s, three runs each). Its two tools
150
+ are listed by name only (Claude Code defers MCP tools until a model loads one),
151
+ and it does nothing unless a plan under `.project-management/plans/` gains a
152
+ done row or the model calls one of its tools. It never compacts on its own.
153
+
154
+ **If an `allowedMcpServers` list hides it.** Such a list keeps every MCP server
155
+ it does not name off, the plugin's included; the plugin notices, stays idle and
156
+ costs nothing (an earlier build stalled every session start by ~8 s here).
157
+ `npx planrails init` and the planner both say so, and offer three choices,
158
+ each applied from the next session:
159
+
160
+ | Choice | Effect |
161
+ |---|---|
162
+ | add `{ "serverName": "planrails" }` to `allowedMcpServers` in `~/.claude/settings.json` | every project; every other server stays off |
163
+ | the same entry in a project's `.claude/settings.local.json` | that project only, for you |
164
+ | remove the `allowedMcpServers` line | also turns on every MCP server it was keeping off (`init` counts them) |
165
+
166
+ If managed settings set `allowManagedMcpServersOnly`, only whoever manages
167
+ Claude Code can add the entry. Without the tools, planrails works as before.
168
+
169
+ The plugin uses Claude Code's mods API, which its own types call early access:
170
+ if a Claude Code update changes it, the tools go missing or report an error,
171
+ and plans run as they do without the plugin.
172
+
104
173
  ## How a plan works
105
174
 
106
175
  Each plan is two files: `PLAN.md` (the map and tracker) and `LOG.md` (append-only
@@ -111,7 +180,7 @@ as the rule that avoids it). Because the reload line brings `PLAN.md` back at th
111
180
  start of every session, a lesson from one chat is read by the next one before it
112
181
  repeats the struggle. The top of `PLAN.md` is what a fresh session reads first. Abridged from
113
182
  [`examples/weekly-digest/`](examples/weekly-digest), which also shows the
114
- `CLAUDE.md` line that reloads it:
183
+ `CLAUDE.local.md` line that reloads it:
115
184
 
116
185
  ```
117
186
  # Weekly digest email — plan
@@ -158,22 +227,37 @@ structural check, biased toward catching a faked "done":
158
227
  fails. An owner-closed task records the owner's words with the date
159
228
  - no spelling of "done" slips past it, and a table row it cannot read (a stray
160
229
  pipe or backtick) fails closed instead of passing
161
- - when the project has a `CLAUDE.md`, every active plan must be reloaded by
162
- `@.project-management/plans/<id>/PLAN.md` on its own line, and no such line may
163
- point at a plan that does not exist
230
+ - the plan your session holds (its NOW `session:` id starts
231
+ `$CLAUDE_CODE_SESSION_ID`) must be reloaded by
232
+ `@.project-management/plans/<id>/PLAN.md` on its own line in `CLAUDE.local.md`
233
+ (or `CLAUDE.md`, where plans before 0.7 put it), in the project root or a
234
+ folder above it, which is what Claude Code loads. In CI, at a terminal, and for
235
+ a teammate's plan it asks nothing: there the line lives in the holder's own
236
+ file. A `CLAUDE.md` line may not point at a plan that does not exist
164
237
  - an active plan keeps its `RESUME` line, and that line must name a task that
165
238
  is still open
166
239
 
167
240
  The checker also **advises without blocking**. What a script can only suspect, it
168
241
  prints as a `note:` before its final line; the run still exits 0, and the agent
169
- that ran the check decides. There is one today: an active `PLAN.md` over ~3,000
170
- words, because the plan reloads into every session and nothing else says when it
171
- has grown. Reminders ride on the check your agent already runs before every
172
- commit, so they need no hook.
242
+ that ran the check decides. Each one names an action the method allows:
243
+
244
+ - an active `PLAN.md` over ~3,000 words. When most of the words are finished
245
+ task rows, trimming prose cannot help, so the note says to end the plan at a
246
+ phase boundary and go on in a new one that carries the Rules, Decisions and
247
+ Learnings forward;
248
+ - an active plan that only the owner can move: every open row is an `owner`
249
+ row or the close. It says to hand the plan over (`status: waiting`, its reload
250
+ line out, one line under "Waiting on you" in `CLAUDE.local.md`), so it stops
251
+ reloading until the owner's word reopens it.
252
+
253
+ Both come from a real project's plans: three active plans there were 97–99%
254
+ done, held open by one or two owner rows each, and reloaded about 59,000 words
255
+ into every session. Reminders ride on the check your agent already runs before
256
+ every commit, so they need no hook.
173
257
 
174
258
  ```
175
- check-plans: note: weekly-digest: PLAN.md is 3,588 words, over the ~3,000 line — it reloads into every session; move history to LOG.md and trim prose before Learnings or Decisions
176
- check-plans: 1 plan(s) ok — every completion claim has a proof and exit 0 evidence, active plans reload, NOW is current
259
+ check-plans: note: weekly-digest: PLAN.md is 3,588 words besides its loop block, over the ~3,000 line — it reloads into every session; move history to LOG.md and trim prose before Learnings or Decisions
260
+ check-plans: 1 plan(s) ok — every completion claim has a proof and exit 0 evidence, this session's plan reloads, NOW is current
177
261
  ```
178
262
 
179
263
  `--verify` goes further and **runs** each proof again, with a timeout, and shows
@@ -187,9 +271,23 @@ trusted pipeline.
187
271
  - **It will not break teammates' builds.** The checker has no dependencies and
188
272
  exits 0 when there are no plans, so a teammate who never uses planrails is
189
273
  unaffected.
190
- - **The reload line is a plain file include.** `@.project-management/plans/…` in
191
- the root `CLAUDE.md` just tells Claude Code to load that file; it commits like
192
- any doc. Other agents do not import it: open the plan by hand at session start.
274
+ - **Your plans stay out of the team's `CLAUDE.md`.** The reload line goes in
275
+ `CLAUDE.local.md`, which Claude Code loads beside `CLAUDE.md` and git ignores,
276
+ so opening or closing a plan changes no shared file, and a teammate's sessions
277
+ do not reload your plans. A teammate who takes a plan over adds its line to
278
+ their own `CLAUDE.local.md`. The one shared edit is a lasting learning, which
279
+ a plan's close graduates to `CLAUDE.md` as a normal, reviewed change. Other
280
+ agents do not import the line: open the plan by hand at session start.
281
+ - **Moving from 0.6 or earlier.** A reload line already in `CLAUDE.md` keeps
282
+ working, and `npx planrails init` points it out. To move it, cut it from
283
+ `CLAUDE.md`, paste it into `CLAUDE.local.md`, and add `CLAUDE.local.md` to
284
+ `.gitignore`.
285
+ - **Worktrees.** A git worktree has no copy of the ignored file. One under
286
+ `.claude/worktrees/`, where Claude Code makes them, still loads the main
287
+ checkout's `CLAUDE.local.md` from the folder above, but that line imports the
288
+ main checkout's copy of the plan. A session that works the plan in a worktree
289
+ adds the line to the worktree's own `CLAUDE.local.md`, or lists
290
+ `CLAUDE.local.md` in a `.worktreeinclude` file so Claude Code copies it in.
193
291
  - **Decide whether to commit `.project-management/`.** Committing it shares plans
194
292
  and lets CI run the checker. If your repo gitignores it, the checker still runs
195
293
  locally and the plan still reloads for whoever has the files.
@@ -215,7 +313,8 @@ peer in this checkout holds no plan, so two sessions can work two plans side by
215
313
  side. A plan
216
314
  written before 0.5 has no `session:` line; `init` points it out, and the
217
315
  changelog says what to add by hand. The claim is a lead, not a lock: a line left by a closed terminal blocks
218
- nobody, and the checker ignores the line. Field-tested with headless sessions: a
316
+ nobody, and the checker never judges it; it reads the line only to know which
317
+ plan must reload for the session running the check. Field-tested with headless sessions: a
219
318
  second session told "Continue the active plan." stopped and asked while the first
220
319
  was live — by its id, after the first had been renamed — took over when it was
221
320
  gone, and touched only the plan it was given when two were active.
@@ -234,6 +333,11 @@ and teaches the executor to brief sub-agents from the plan. 0.5.0 names the
234
333
  session working a plan and has every session check for a live holder before it
235
334
  touches the plan. 0.6.0 lets the checker advise without blocking: a `note:` for
236
335
  what a script can only suspect, starting with a plan that has outgrown its word
237
- line. See [`CHANGELOG.md`](CHANGELOG.md).
336
+ line. 0.7.0 moves the reload line to `CLAUDE.local.md`, so a plan reloads
337
+ only for whoever works it and a team's `CLAUDE.md` is left alone. 0.8.0 adds an
338
+ optional Claude Code plugin that lets the model compact its session between
339
+ tasks, a `waiting` state for plans only the owner can move, and plans that end
340
+ at phase boundaries without losing what they know. See
341
+ [`CHANGELOG.md`](CHANGELOG.md).
238
342
 
239
343
  MIT.
package/bin/planrails.mjs CHANGED
@@ -5,7 +5,8 @@
5
5
  * npx planrails init [--dir DIR] [--force]
6
6
  * Puts PLANNER.md and the checker in <project>/.project-management/planrails/
7
7
  * and makes the plans/ folder. It writes nothing else — no package.json, no npm
8
- * install, no hooks, no edits to your CLAUDE.md. Run it again to update: a copy
8
+ * install, no hooks, no edits to your CLAUDE.md, CLAUDE.local.md or .gitignore.
9
+ * Run it again to update: a copy
9
10
  * older than this package (or unstamped, 0.3.x) is replaced and you are told;
10
11
  * a copy of this version is left alone unless --force; plans/ is never touched.
11
12
  *
@@ -17,8 +18,9 @@
17
18
  */
18
19
  import { readFileSync, copyFileSync, mkdirSync, existsSync, writeFileSync, realpathSync, readdirSync } from "node:fs";
19
20
  import { join, dirname, resolve } from "node:path";
21
+ import { homedir } from "node:os";
20
22
  import { fileURLToPath } from "node:url";
21
- import { checkPlans, isActive, formatNotes } from "../tools/check-plans.mjs";
23
+ import { checkPlans, isActive, formatNotes, reloadLines } from "../tools/check-plans.mjs";
22
24
 
23
25
  const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
24
26
  const version = () => JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8")).version;
@@ -48,6 +50,41 @@ export function stampOf(text) {
48
50
  }
49
51
  const newer = (a, b) => { const [x, y] = [a, b].map((v) => v.split(".").map(Number)); for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] > y[i]; return false; };
50
52
 
53
+ /**
54
+ * Whether Claude Code's settings hide the planrails plugin's tools. Reads the
55
+ * settings files only (user, project, local, managed); writes nothing. Returns
56
+ * null when the plugin is not installed or the tools are allowed, else what hides
57
+ * them and the choices, as lines for init to print.
58
+ */
59
+ export function pluginToolsCheck(project, { configDir = process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude"), platform = process.platform, managedPath } = {}) {
60
+ const read = (p) => { try { return JSON.parse(readFileSync(p, "utf8")); } catch { return null; } };
61
+ const managed = managedPath ?? ({ darwin: "/Library/Application Support/ClaudeCode/managed-settings.json", win32: "C:\\Program Files\\ClaudeCode\\managed-settings.json" }[platform] || "/etc/claude-code/managed-settings.json");
62
+ const sources = [
63
+ ["~/.claude/settings.json", join(configDir, "settings.json")],
64
+ [".claude/settings.json", join(project, ".claude", "settings.json")],
65
+ [".claude/settings.local.json", join(project, ".claude", "settings.local.json")],
66
+ ["managed settings", managed],
67
+ ].map(([label, path]) => ({ label, s: read(path) })).filter((x) => x.s);
68
+ const installed = sources.some(({ s }) => Object.entries(s.enabledPlugins || {}).some(([k, v]) => k.startsWith("planrails@") && v));
69
+ if (!installed) return null;
70
+ const lists = sources.filter(({ s }) => Array.isArray(s.allowedMcpServers));
71
+ if (!lists.length) return null;
72
+ const opens = (x) => x?.serverName === "planrails" || x?.serverUrl !== undefined || x?.serverCommand !== undefined;
73
+ const policy = sources.find((x) => x.label === "managed settings")?.s;
74
+ const locked = policy?.allowManagedMcpServersOnly === true;
75
+ const counted = locked ? [policy.allowedMcpServers || []] : lists.map(({ s }) => s.allowedMcpServers);
76
+ if (counted.some((l) => l.some(opens))) return null;
77
+ const where = lists.map((x) => x.label).join(" and ");
78
+ if (locked) return [`the planrails plugin is installed, but your organisation's managed settings lock allowedMcpServers (allowManagedMcpServersOnly), so its tools stay hidden and the model cannot compact between tasks. Ask whoever manages Claude Code to add { "serverName": "planrails" }. planrails works without it; the plugin stays idle.`];
79
+ const servers = Object.keys(read(existsSync(join(configDir, ".claude.json")) ? join(configDir, ".claude.json") : join(dirname(configDir), ".claude.json"))?.mcpServers || {}).length + Object.keys(read(join(project, ".mcp.json"))?.mcpServers || {}).length;
80
+ return [
81
+ `the planrails plugin is installed, but allowedMcpServers in ${where} hides its tools, so the model cannot compact between tasks. planrails works without them; the plugin stays idle and costs nothing. To turn them on, pick one; it applies from the next session:`,
82
+ ` a) every project, every other server still off: add { "serverName": "planrails" } to allowedMcpServers in ~/.claude/settings.json`,
83
+ ` b) this project only: add the same entry to .claude/settings.local.json (keep it out of git)`,
84
+ ` c) remove the allowedMcpServers line${servers ? `: it also turns on the ${servers} MCP server(s) configured in ~/.claude.json and .mcp.json that it now keeps off` : ""}`,
85
+ ];
86
+ }
87
+
51
88
  function init(args) {
52
89
  const target = dirArg(args);
53
90
  const force = args.includes("--force");
@@ -85,6 +122,13 @@ function init(args) {
85
122
  const text = readFileSync(pf, "utf8");
86
123
  if (isActive(text) && !/^session:/m.test(text)) console.log(` ! plan ${name} predates 0.5: add a session: line to its NOW and replace its "How to work this plan" block with the template's in .project-management/planrails/PLANNER.md; until then a second session cannot tell who holds it`);
87
124
  }
125
+ // Before 0.7 the reload line went in CLAUDE.md, which a team shares. It still
126
+ // works there; say where it goes now, and never move it.
127
+ const claudeMd = join(target, "CLAUDE.md");
128
+ const shared = existsSync(claudeMd) ? [...reloadLines(readFileSync(claudeMd, "utf8"))] : [];
129
+ if (shared.length) console.log(` ! CLAUDE.md reloads ${shared.join(", ")}: since 0.7 the reload line goes in CLAUDE.local.md, which git ignores, so a plan reloads only for whoever works it and CLAUDE.md stays the team's. The line still works where it is; to move it, cut it from CLAUDE.md, paste it into CLAUDE.local.md, and add CLAUDE.local.md to .gitignore`);
130
+ const tools = pluginToolsCheck(target, process.env.PLANRAILS_MANAGED_SETTINGS ? { managedPath: process.env.PLANRAILS_MANAGED_SETTINGS } : {});
131
+ if (tools) console.log(` ! ${tools.join("\n ")}`);
88
132
  const loose = ["PLANNER.md", "check-plans.mjs"].filter((f) => existsSync(join(pm, f)));
89
133
  if (loose.length) console.log(` ! 0.2.x files at .project-management/ root: ${loose.join(", ")} — the copies now live in planrails/; delete the loose ones and point your check command at .project-management/planrails/check-plans.mjs`);
90
134
 
@@ -94,8 +138,12 @@ Next:
94
138
  (Or, in Claude Code, use /planrails if you installed the skill.)
95
139
  2. Add to the command you run before every commit:
96
140
  node .project-management/planrails/check-plans.mjs
97
- 3. When the agent writes a plan, it adds one line to your CLAUDE.md so the plan
98
- reloads after every compaction: @.project-management/plans/<id>/PLAN.md`);
141
+ 3. When the agent writes a plan, it adds one line to CLAUDE.local.md, and that file
142
+ to .gitignore, so the plan reloads after every compaction, for you alone:
143
+ @.project-management/plans/<id>/PLAN.md
144
+ 4. Optional, in Claude Code: let the model compact its session between tasks.
145
+ claude plugin marketplace add vivmagarwal/planrails
146
+ claude plugin install planrails@planrails`);
99
147
  return 0;
100
148
  }
101
149
 
@@ -110,7 +158,7 @@ function check(args) {
110
158
  console.error(`check-plans: ${problems.length} problem(s):\n${problems.map((p) => ` - ${p}`).join("\n")}`);
111
159
  return 1;
112
160
  }
113
- console.log(`check-plans: ${plans.length} plan(s) ok — every completion claim has a proof and exit 0 evidence, active plans reload, NOW is current${verify ? " (proofs re-run)" : ""}`);
161
+ console.log(`check-plans: ${plans.length} plan(s) ok — every completion claim has a proof and exit 0 evidence, this session's plan reloads, NOW is current${verify ? " (proofs re-run)" : ""}`);
114
162
  return 0;
115
163
  }
116
164
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "planrails",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Plans that survive a lost session, and \"done\" that means done. A planner prompt and a tiny checker.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // planrails 0.6.0
2
+ // planrails 0.8.0
3
3
  /**
4
4
  * check-plans — the machine-checked rails of the planner.
5
5
  *
@@ -8,16 +8,23 @@
8
8
  * backticks, or the word owner — and carry evidence that the proof was run.
9
9
  * For a command that means its exit code, pasted, and the code must be 0. An
10
10
  * empty evidence cell, a bare word, or a recorded failure is not done.
11
- * - an active plan must reload: when the project has a CLAUDE.md, it must carry
12
- * `@.project-management/plans/<id>/PLAN.md` on its own line, and no such line
13
- * may point at a plan that does not exist. (Skipped when there is no CLAUDE.md.)
11
+ * - the plan this session works must reload: when $CLAUDE_CODE_SESSION_ID starts
12
+ * with the id on an active plan's NOW `session:` line, a CLAUDE.local.md or
13
+ * CLAUDE.md in the root or a folder above it (what Claude Code loads) must carry `@.project-management/plans/<id>/PLAN.md` on its own
14
+ * line. Elsewhere (CI, a terminal, a teammate's session) the line legitimately
15
+ * lives in someone else's CLAUDE.local.md, so nothing is asked. A CLAUDE.md line
16
+ * may not point at a plan that does not exist; a CLAUDE.local.md line is not
17
+ * judged, because that file is untracked and outlives a branch switch.
14
18
  * - NOW must be current: an active plan with a NOW section keeps its RESUME
15
- * line, and that line may not name only finished tasks. A retired plan says `status: done` (or paused) and is exempt
16
- * from both; a plan with no status line counts as active.
19
+ * line, and that line may not name only finished tasks. A plan that says
20
+ * `status: done`, `paused` or `waiting` is exempt from both; a plan with no
21
+ * status line counts as active.
17
22
  *
18
- * It also prints notes: what a script can only suspect (an active PLAN.md over
19
- * ~3,000 words, which reloads into every session). A note goes to stdout before
20
- * the final line and never changes the exit code; whoever ran the check decides.
23
+ * It also prints notes: what a script can only suspect. An active PLAN.md over
24
+ * ~3,000 words of its own (the loop block not counted); an active plan only the
25
+ * owner can move; a plan that is no longer active but still reloads. A note goes
26
+ * to stdout before the final line and never changes the exit code; whoever ran
27
+ * the check decides.
21
28
  *
22
29
  * The rule is biased toward catching a faked "done": a task counts as a
23
30
  * completion claim UNLESS its status is blank or an explicit not-done word
@@ -40,7 +47,7 @@
40
47
  * them without a real project.
41
48
  */
42
49
  import { readdirSync, readFileSync, existsSync, statSync, realpathSync } from "node:fs";
43
- import { join } from "node:path";
50
+ import { join, resolve, dirname } from "node:path";
44
51
  import { spawnSync } from "node:child_process";
45
52
  import { fileURLToPath } from "node:url";
46
53
 
@@ -250,7 +257,7 @@ export function checkPlan({ id, text, verify = false, run = null }) {
250
257
  * finished word exempts a plan from the reload and NOW rules. A plan with no
251
258
  * status line counts as active — the safe direction for a gate.
252
259
  */
253
- const NOT_ACTIVE = new Set(["done", "paused", "retired", "closed", "shipped", "finished", "complete", "completed", "archived", "dropped", "cancelled", "canceled", "abandoned", "superseded", "onhold", "hold"]);
260
+ const NOT_ACTIVE = new Set(["done", "paused", "retired", "closed", "shipped", "finished", "complete", "completed", "archived", "dropped", "cancelled", "canceled", "abandoned", "superseded", "onhold", "hold", "waiting"]);
254
261
  export function isActive(text) {
255
262
  const m = text.match(/^status:\s*([^\s·|]+)/im);
256
263
  return !(m && NOT_ACTIVE.has(norm(m[1])));
@@ -274,6 +281,19 @@ export function reloadLines(text) {
274
281
  return ids;
275
282
  }
276
283
 
284
+ /**
285
+ * The session id on NOW's `session:` line (`name · id · since …`), lowercased, or
286
+ * null for none or a missing line. The id is the first field that is hex: six or
287
+ * more characters, or a whole session id. A line inside a code fence does not count.
288
+ */
289
+ export function holderId(text) {
290
+ const lines = text.split(/\r?\n/), fenced = fenceMask(lines);
291
+ const k = lines.findIndex((l, i) => !fenced[i] && /^\s*session:/.test(l));
292
+ if (k < 0) return null;
293
+ const id = lines[k].replace(/^\s*session:/, "").split(/[·•|]/).map((f) => f.trim()).find((f) => /^[0-9a-f]{6,}(?:-[0-9a-f]+)*$/i.test(f));
294
+ return id ? id.toLowerCase() : null;
295
+ }
296
+
277
297
  /** Plan folders under .project-management/plans/. A folder with no PLAN.md is a problem. */
278
298
  export function findPlans(root) {
279
299
  const dir = join(root, ".project-management", "plans");
@@ -306,32 +326,68 @@ export function runProof(cmd, root, timeoutMs) {
306
326
  */
307
327
  export const WORD_LINE = 3000;
308
328
  const commas = (n) => String(n).replace(/\B(?=(\d{3})+$)/g, ","); // no Intl: a Node built without it would drop the comma
329
+ const countWords = (s) => s.split(/\s+/).filter(Boolean).length;
330
+ // A row no one will finish: not a completion claim, and not waiting to be done.
331
+ const SET_ASIDE = new Set(["dropped", "cancelled", "canceled", "wontfix", "wontdo", "abandoned", "skip", "skipped", "na"]);
332
+ const isOpen = (t) => (EMPTY.test(t.status) || NOT_DONE.has(norm(t.status))) && !SET_ASIDE.has(norm(t.status));
333
+ const isOwnerRow = (t) => norm(t.proof) === "owner";
334
+ // The plan's own close: "close by PLANNER.md §5", "close the plan", "retire the plan" — not "close the dialog".
335
+ const isCloseRow = (t) => /§\s*5|\bclose (?:by|per) planner\b|\b(?:close|retire) (?:the|this) plan\b/i.test(t.task);
309
336
  export function planNotes({ id, text }) {
310
337
  const notes = [];
311
338
  if (!isActive(text)) return notes;
312
- const words = text.split(/\s+/).filter(Boolean).length;
313
- if (words > WORD_LINE) notes.push(`${id}: PLAN.md is ${commas(words)} words, over the ~${commas(WORD_LINE)} line — it reloads into every session; move history to LOG.md and trim prose before Learnings or Decisions`);
339
+ // The "How to work this plan" block is the method's, the same in every plan:
340
+ // count the plan's own words, which its author can trim.
341
+ const block = text.match(/^## How to work this plan[\s\S]*?(?=^## |(?![\s\S]))/m);
342
+ const words = countWords((block ? text.replace(block[0], "") : text).replace(/\|/g, " "));
343
+ const { tasks } = parseTasks(text);
344
+ if (words > WORD_LINE) {
345
+ // When the task rows are most of the plan, trimming prose cannot help, and the
346
+ // method keeps every row: ending the plan is the way to stop reloading them.
347
+ const rows = tasks.reduce((n, t) => n + countWords(`${t.id} ${t.task} ${t.status} ${t.proof} ${t.evidence}`), 0);
348
+ notes.push(rows * 2 > words
349
+ ? `${id}: PLAN.md is ${commas(words)} words besides its loop block, over the ~${commas(WORD_LINE)} line, and ${commas(rows)} are its task rows — it reloads into every session; end it at a phase boundary and go on in a new plan (its rows stay on disk as the record), and keep each evidence cell to the stamp, the exit code and the last line`
350
+ : `${id}: PLAN.md is ${commas(words)} words besides its loop block, over the ~${commas(WORD_LINE)} line — it reloads into every session; move history to LOG.md and trim prose before Learnings or Decisions`);
351
+ }
352
+ const open = tasks.filter(isOpen);
353
+ const owner = open.filter(isOwnerRow);
354
+ if (owner.length && open.every((t) => isOwnerRow(t) || isCloseRow(t)))
355
+ notes.push(`${id}: only the owner can move it now (${owner.map((t) => t.id).join(", ")}) — hand it over by PLANNER.md §5: status: waiting, session: none, its reload line out, and one line naming those rows under "Waiting on you" in CLAUDE.local.md; it stops reloading until their word reopens it`);
314
356
  return notes;
315
357
  }
316
358
  /** The notes as printed: one "note:" line each, newline-terminated, or "" when there are none. */
317
359
  export const formatNotes = (notes) => notes.map((n) => `check-plans: note: ${n}\n`).join("");
318
360
 
319
- /** Check every plan under root. Returns { plans, problems, notes }. `--verify` re-runs proofs, each with a timeout (10 min by default). */
320
- export function checkPlans({ root = ".", verify = false, verifyTimeoutMs = 10 * 60 * 1000 } = {}) {
361
+ /**
362
+ * Check every plan under root. Returns { plans, problems, notes }. `--verify` re-runs proofs, each with a timeout (10 min by default).
363
+ * `session` is the id of the session running the check; the plan it holds must reload.
364
+ */
365
+ export function checkPlans({ root = ".", verify = false, verifyTimeoutMs = 10 * 60 * 1000, session = process.env.CLAUDE_CODE_SESSION_ID } = {}) {
321
366
  const { plans, problems } = findPlans(root);
322
367
  const notes = [];
323
368
  const run = verify ? (cmd) => runProof(cmd, root, verifyTimeoutMs) : null;
324
- const claudeMd = join(root, "CLAUDE.md");
325
- const reloads = existsSync(claudeMd) ? reloadLines(readFileSync(claudeMd, "utf8")) : null;
369
+ const reloadsIn = (dir, name) => { const p = join(dir, name); return existsSync(p) ? reloadLines(readFileSync(p, "utf8")) : new Set(); };
370
+ const shared = reloadsIn(root, "CLAUDE.md");
371
+ // Claude Code loads both files from the root and every folder above it, so a
372
+ // worktree under .claude/worktrees/ also gets its checkout's. Read what it reads.
373
+ const loaded = new Set();
374
+ for (let d = resolve(root); ; d = dirname(d)) {
375
+ for (const name of ["CLAUDE.md", "CLAUDE.local.md"]) for (const id of reloadsIn(d, name)) loaded.add(id);
376
+ if (dirname(d) === d) break;
377
+ }
378
+ const me = (session || "").toLowerCase();
326
379
  for (const { id, path } of plans) {
327
380
  let text = "";
328
381
  try { text = readFileSync(path, "utf8"); } catch (e) { problems.push(`${id}: cannot read ${path} (${e.code || e.message})`); continue; }
329
382
  problems.push(...checkPlan({ id, text, verify, run }));
330
383
  notes.push(...planNotes({ id, text }));
331
- if (reloads && isActive(text) && !reloads.has(id))
332
- problems.push(`${id}: the plan is active but CLAUDE.md has no reload line — add "@.project-management/plans/${id}/PLAN.md" on its own line, outside backticks, or the plan will not survive a compaction`);
384
+ if (!isActive(text) && loaded.has(id))
385
+ notes.push(`${id}: it is no longer active but still reloads (a bare @ line in CLAUDE.local.md or CLAUDE.md) — take the line out, or wrap it in backticks; it costs every call of every session here`);
386
+ const holder = holderId(text);
387
+ if (me && holder && me.startsWith(holder) && isActive(text) && !loaded.has(id))
388
+ problems.push(`${id}: this session holds the plan but nothing reloads it here — add "@.project-management/plans/${id}/PLAN.md" on its own line, outside backticks, to CLAUDE.local.md, or the plan will not survive a compaction`);
333
389
  }
334
- if (reloads) for (const id of reloads) if (!plans.some((p) => p.id === id)) problems.push(`CLAUDE.md reloads "${id}" but .project-management/plans/${id}/PLAN.md does not exist`);
390
+ for (const id of shared) if (!plans.some((p) => p.id === id)) problems.push(`CLAUDE.md reloads "${id}" but .project-management/plans/${id}/PLAN.md does not exist`);
335
391
  return { plans, problems, notes };
336
392
  }
337
393
 
@@ -360,6 +416,6 @@ if (isMain) {
360
416
  process.stderr.write(`check-plans: ${problems.length} problem(s):\n${problems.map((p) => ` - ${p}`).join("\n")}\n`);
361
417
  process.exit(1);
362
418
  }
363
- process.stdout.write(`check-plans: ${plans.length} plan(s) ok — every completion claim has a proof and exit 0 evidence, active plans reload, NOW is current${verify ? " (proofs re-run)" : ""}\n`);
419
+ process.stdout.write(`check-plans: ${plans.length} plan(s) ok — every completion claim has a proof and exit 0 evidence, this session's plan reloads, NOW is current${verify ? " (proofs re-run)" : ""}\n`);
364
420
  process.exit(0);
365
421
  }