planrails 0.5.2 → 0.7.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,76 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.0 — 2026-10-03
4
+
5
+ The reload line moves from `CLAUDE.md` to `CLAUDE.local.md`, which git ignores.
6
+ A plan now reloads only for whoever works it, and a team's `CLAUDE.md` is left
7
+ alone.
8
+
9
+ - **Why.** Field feedback from teams: a team shares `CLAUDE.md`, so every plan
10
+ opened or closed changed the team's file, two branches' lines conflicted, and
11
+ every teammate's sessions reloaded everyone's plans.
12
+ - **The line goes in `CLAUDE.local.md`**, and the agent adds that file to
13
+ `.gitignore` if git does not already ignore it. Claude Code loads it beside
14
+ `CLAUDE.md` and re-reads it, with its imports, after `/compact`. The docs say
15
+ so for `CLAUDE.md` only, so it was tested on Claude Code 2.1.288: a word
16
+ swapped on disk, never shown in the chat, came back after compaction, the same
17
+ as through `CLAUDE.md`. A teammate who takes a plan over adds the line to
18
+ their own file.
19
+ - **The checker asks the holder.** Before, when a `CLAUDE.md` existed, every
20
+ active plan needed its line there. With the line in an ignored file, CI and a
21
+ teammate's checkout cannot see it, so that rule would fail them. Now the
22
+ active plan whose NOW `session:` id starts `$CLAUDE_CODE_SESSION_ID` must be
23
+ reloaded by a `CLAUDE.local.md` or `CLAUDE.md` in the project root or a folder
24
+ above it, which is what Claude Code loads; in CI, at a terminal, and for a
25
+ teammate's plan nothing is asked. The id is read in any case, from six hex
26
+ characters to a whole session id, never from a code fence. This also catches
27
+ a worktree outside the checkout, which has no copy of the ignored file. A `CLAUDE.md` line to a plan that does not exist
28
+ still fails; a `CLAUDE.local.md` one does not, because that file outlives a
29
+ checkout of a branch where the plan does not exist. The ok line says "this
30
+ session's plan reloads" in place of "active plans reload".
31
+ - **`AGENTS.md` projects.** Once any `CLAUDE` file exists, Claude Code stops
32
+ reading `AGENTS.md` by itself. In a project with `AGENTS.md` and no
33
+ `CLAUDE.md`, `CLAUDE.local.md` starts with `@AGENTS.md`. Before 0.7 the
34
+ method wrote a `CLAUDE.md` in such a project, with the same silent effect.
35
+ - **The close deletes the line.** A lasting learning still graduates to the
36
+ shared `CLAUDE.md`: it is the team's knowledge, added as a reviewed change.
37
+ - **`init`** says the agent writes `CLAUDE.local.md`, and points out reload
38
+ lines still in `CLAUDE.md`. It writes neither file, nor `.gitignore`.
39
+
40
+ To update: `npx planrails@latest init`. A reload line in `CLAUDE.md` keeps
41
+ working. To move it, cut it from `CLAUDE.md`, paste it into `CLAUDE.local.md`,
42
+ and add `CLAUDE.local.md` to `.gitignore`. Once it has moved, an active plan's
43
+ block can say "the reload line deleted from CLAUDE.local.md" in place of "the
44
+ reload line in CLAUDE.md wrapped in backticks"; until then, leave the block
45
+ alone, or the close will look for the line in the wrong file.
46
+
47
+ ## 0.6.0 — 2026-09-19
48
+
49
+ The checker can now advise without blocking.
50
+
51
+ - **Notes.** A problem is what a script can verify, and it still fails the build.
52
+ A note is what a script can only suspect: it prints as `check-plans: note: …`
53
+ on stdout before the final line, the exit code and the final line do not
54
+ change, and the agent that ran the check decides. `checkPlans` returns `notes`
55
+ beside `problems`; `npx planrails check` prints the same ones. On a plan with
56
+ no note the output is byte-for-byte what 0.5.2 printed.
57
+ - **One note ships: an active `PLAN.md` over ~3,000 words.** The plan reloads
58
+ into every session, and nothing said when it had grown: four of four real plans
59
+ were past the stated line, one at 9,123 words. A retired plan gets no note; it
60
+ does not reload.
61
+ - **The word line moves from ~2,000 to ~3,000.** The line was written before the
62
+ plan carried its own ~480-word block; two carefully kept plans measure ~2,800.
63
+ The doc was stale, not the plans.
64
+ - **Why not hooks.** The idea began as advisory Claude Code hooks. Checked against
65
+ the hooks reference: a PreCompact hook cannot add context, no event fires at a
66
+ context threshold, a Stop hook's advice forces another turn, and no event knows
67
+ a plan's task closed. The check command already runs before every commit and
68
+ its output is already read, in any agent. `CONTRIBUTING.md` sets the bar for a
69
+ new note: a recorded failure behind it, silent on a healthy plan. A LOG-entry
70
+ reminder was tested on four real plans, never fired, and was not built.
71
+
72
+ To update: `npx planrails@latest init`. No plan needs editing.
73
+
3
74
  ## 0.5.2 — 2026-09-13
4
75
 
5
76
  A second field test of 0.5.1, four scratch projects and seven fresh sessions, and
package/PLANNER.md CHANGED
@@ -1,4 +1,4 @@
1
- <!-- planrails 0.5.2 -->
1
+ <!-- planrails 0.7.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
@@ -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,8 +55,9 @@ 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.
@@ -116,24 +119,33 @@ Pick a short kebab-case `<id>` (`weekly-digest`). Then:
116
119
  checks, run end to end, the way a user would.
117
120
  - **Use repo-relative paths** (`lib/digest/query.ts`), never absolute ones. They
118
121
  are clickable and they survive a move to another machine.
119
- - **Keep PLAN.md under ~2,000 words.** Trim prose before Learnings or Decisions.
120
- History goes in LOG.md, not here.
121
- - **Add the reload line.** In the project-root `CLAUDE.md`, under a short
122
- "Active plans" spot, add:
122
+ - **Keep PLAN.md under ~3,000 words.** It reloads into every session, so every
123
+ word is paid for again and again. Trim prose before Learnings or Decisions.
124
+ History goes in LOG.md, not here. Past the line, the checker prints a `note:` —
125
+ advice, not a failure: the run still exits 0, and you decide what to trim.
126
+ - **Add the reload line.** In the project-root `CLAUDE.local.md` (create it if
127
+ needed), under a short "Active plans" heading, add:
123
128
  ```
124
129
  @.project-management/plans/<id>/PLAN.md
125
130
  ```
126
131
  Put it on its own line, outside any code block — an `@` import wrapped in
127
- backticks does not load. Claude Code re-reads that file, and everything it
128
- imports, at every session start and after every compaction, so the plan comes
129
- back on its own. Under the same heading, one line says how the plans are held:
130
- *Each plan below is held by one session — see its NOW `session:` line. Work one
131
- only when asked in this session, after the check in its block.* (For a tool that
132
- does not do `@`-imports, put the plan's path in `AGENTS.md` and open it by hand
133
- at the start of each session.)
132
+ backticks does not load. Claude Code reads that file beside `CLAUDE.md`, and
133
+ re-reads both, with everything they import, at every session start and after
134
+ every compaction, so the plan comes back on its own. Under the same heading, one
135
+ line says how the plans are held: *Each plan below is held by one session — see
136
+ its NOW `session:` line. Work one only when asked in this session, after the
137
+ check in its block.*
138
+ The file is yours, not the team's: if `git check-ignore -q CLAUDE.local.md`
139
+ fails, add `CLAUDE.local.md` to `.gitignore` on its own line. Do not write the
140
+ reload line in `CLAUDE.md`; a team shares that file, and every teammate would
141
+ reload your plan. Someone who takes the plan over adds the line to their own
142
+ `CLAUDE.local.md`. If the project has `AGENTS.md` and no `CLAUDE.md`, make
143
+ `@AGENTS.md` the first line of `CLAUDE.local.md`: once any `CLAUDE` file
144
+ exists, Claude Code stops reading `AGENTS.md` by itself. (A tool that does not
145
+ do `@`-imports does not reload; open the plan by hand at each session start.)
134
146
  - **One plan per session, one session per plan.** A repo may hold several active
135
147
  plans, each with its own reload line and each held by one session, named on its
136
- NOW `session:` line. Every session reloads all of them, so keep them few; a plan
148
+ NOW `session:` line. Every session in this checkout reloads all of them, so keep them few; a plan
137
149
  nobody is working is paused (`status: paused`, its line in backticks). A session
138
150
  works only the plan the person in that session assigned; the other reloaded
139
151
  plans are context, not work orders. Naming the session after the plan
@@ -151,9 +163,9 @@ Pick a short kebab-case `<id>` (`weekly-digest`). Then:
151
163
  if you did not run init, copy `tools/check-plans.mjs` from
152
164
  https://github.com/vivmagarwal/planrails there. Add
153
165
  `node .project-management/planrails/check-plans.mjs` to the check command. Now the
154
- build fails if a task is marked done with no evidence, if an active plan has no
155
- reload line, if its NOW lost its RESUME line, or if NOW points at a finished
156
- task. If the project is not Node,
166
+ build fails if a task is marked done with no evidence, if the plan your session
167
+ holds has no reload line, if its NOW lost its RESUME line, or if NOW points at
168
+ a finished task. If the project is not Node,
157
169
  skip this; the plan still works, and you enforce the gate yourself.
158
170
  - **Check the plan before the first task.** You, or a fresh sub-agent with no
159
171
  chat context: open every path the plan names, start every proof command,
@@ -206,8 +218,9 @@ decide:
206
218
  no listing to consult. Git is the record: pull first, read a `doing` row with a
207
219
  fresh `updated:` stamp as someone's work in flight, and ask.
208
220
  - A worktree is another checkout with its own copy of the plan: a merge concern
209
- later, not a clobber now. A sub-agent you briefed never runs this check; it
210
- never writes the plan.
221
+ later, not a clobber now. Git does not copy `CLAUDE.local.md` into it, so add
222
+ the plan's reload line there too. A sub-agent you briefed never runs this
223
+ check; it never writes the plan.
211
224
 
212
225
  Stopping means: report the session's name, status and start, what NOW says and
213
226
  what `git status` shows, then ask — `AskUserQuestion` in Claude Code; a `claude -p`
@@ -296,14 +309,17 @@ the task `blocked` with the reason, and stop. Do not hand-fix state to look done
296
309
  3. **Update the docs** the work changed — in the same step, per the project's
297
310
  documentation guide if it has one.
298
311
  4. **Retire the plan.** Set its header to `status: done` and NOW's `session:` to
299
- `none`, then wrap its reload line in backticks, or delete it. Do not move a
312
+ `none`, then delete its reload line (or wrap it in backticks). Do not move a
300
313
  bare `@` line under a "Finished" heading: it still imports, and every retired
301
- plan would reload forever. The status comes first: the checker holds an `active` plan to its
302
- reload line. The plan files stay on disk; they are the record.
314
+ plan would reload forever. The status comes first: the checker holds the
315
+ `active` plan your session holds to its reload line. The plan files stay on
316
+ disk; they are the record.
303
317
  5. **Graduate any lasting learning.** A learning that is true beyond this feature
304
- moves to the always-loaded file (`CLAUDE.md` / `AGENTS.md`), so it outlives the
305
- plan you are retiring. A learning that a check could enforce becomes a test.
306
- One that was only about this work retires with it.
318
+ moves to the shared always-loaded file (`CLAUDE.md` / `AGENTS.md`), not
319
+ `CLAUDE.local.md`: it is the team's knowledge, so it goes in as a change they
320
+ review, and it outlives the plan you are retiring. A learning that a check
321
+ could enforce becomes a test. One that was only about this work retires with
322
+ it.
307
323
 
308
324
  ---
309
325
 
@@ -315,14 +331,16 @@ Each line here was paid for by a real failure in earlier planning systems:
315
331
  always-loaded file after compaction is something the tool already does. Hooks
316
332
  that tried to do more misfired: a crashing pre-tool hook blocks the very call,
317
333
  a read guard was wrong twice about sub-agents, a compaction journal came out as
318
- command stubs.
334
+ command stubs. It lives in `CLAUDE.local.md` because teams share `CLAUDE.md`:
335
+ there, every plan opened or closed changed the team's file, and every
336
+ teammate's sessions reloaded everyone's plans.
319
337
  - **Proof before work** exists because a checkbox lies. One project marked a phase
320
338
  "done" three times while it was not; nothing in a status column could catch it.
321
339
  A named command that must be run and pasted can.
322
340
  - **Evidence at close** is the one machine-checkable rail worth keeping. The
323
341
  checker enforces it — exit 0, not a word — plus two integrity checks that keep
324
- the reload honest: the reload line exists, and NOW names a live task. Both
325
- failures were silent in real plans.
342
+ the reload honest: the plan your session holds has its reload line, and NOW
343
+ names a live task. Both failures were silent in real plans.
326
344
  - **The plan carries its own loop** because a session that never saw this file
327
345
  gets only PLAN.md back. It knew where it was; it did not know how to work.
328
346
  - **Learnings live in the plan, not only the log.** The log is history a fresh
@@ -332,6 +350,12 @@ Each line here was paid for by a real failure in earlier planning systems:
332
350
  fixes.
333
351
  - **History stays out of the plan.** One plan grew a 16,000-word progress section,
334
352
  stamped two hours behind its own log. NOW is four lines; LOG.md is the history.
353
+ - **The checker's notes advise; they never block.** A script can verify an exit
354
+ code, so that is a problem and fails the build. It can only suspect that a plan
355
+ is too long, so that is a note, and you judge. Four of four real plans had
356
+ outgrown the word line with nothing saying so. The check command already runs
357
+ before every commit and its output is already read, so a reminder there needs
358
+ no hook. A note needs a real failure behind it, or it is noise.
335
359
  - **Sub-agent findings are leads** because four spot-checked findings were each
336
360
  right in direction and wrong in number, and a wrong number becomes a wrong plan.
337
361
  Delegation is method, not machinery: a brief of one unit and nothing else worked.
@@ -384,7 +408,7 @@ Read NOW, then Rules and Learnings; do not re-read Context. One task at a time:
384
408
  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.
385
409
  5. If it fought back, add a Learning: the trap, then the rule. A verified fact goes in Context, a choice in Decisions.
386
410
 
387
- 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.
411
+ 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 deleted from CLAUDE.local.md. Full method: `.project-management/planrails/PLANNER.md` §4.
388
412
 
389
413
  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:
390
414
  - the id is yours: go on.
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/
@@ -111,7 +113,7 @@ as the rule that avoids it). Because the reload line brings `PLAN.md` back at th
111
113
  start of every session, a lesson from one chat is read by the next one before it
112
114
  repeats the struggle. The top of `PLAN.md` is what a fresh session reads first. Abridged from
113
115
  [`examples/weekly-digest/`](examples/weekly-digest), which also shows the
114
- `CLAUDE.md` line that reloads it:
116
+ `CLAUDE.local.md` line that reloads it:
115
117
 
116
118
  ```
117
119
  # Weekly digest email — plan
@@ -158,12 +160,28 @@ structural check, biased toward catching a faked "done":
158
160
  fails. An owner-closed task records the owner's words with the date
159
161
  - no spelling of "done" slips past it, and a table row it cannot read (a stray
160
162
  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
163
+ - the plan your session holds (its NOW `session:` id starts
164
+ `$CLAUDE_CODE_SESSION_ID`) must be reloaded by
165
+ `@.project-management/plans/<id>/PLAN.md` on its own line in `CLAUDE.local.md`
166
+ (or `CLAUDE.md`, where plans before 0.7 put it), in the project root or a
167
+ folder above it, which is what Claude Code loads. In CI, at a terminal, and for
168
+ a teammate's plan it asks nothing: there the line lives in the holder's own
169
+ file. A `CLAUDE.md` line may not point at a plan that does not exist
164
170
  - an active plan keeps its `RESUME` line, and that line must name a task that
165
171
  is still open
166
172
 
173
+ The checker also **advises without blocking**. What a script can only suspect, it
174
+ prints as a `note:` before its final line; the run still exits 0, and the agent
175
+ that ran the check decides. There is one today: an active `PLAN.md` over ~3,000
176
+ words, because the plan reloads into every session and nothing else says when it
177
+ has grown. Reminders ride on the check your agent already runs before every
178
+ commit, so they need no hook.
179
+
180
+ ```
181
+ 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
182
+ check-plans: 1 plan(s) ok — every completion claim has a proof and exit 0 evidence, this session's plan reloads, NOW is current
183
+ ```
184
+
167
185
  `--verify` goes further and **runs** each proof again, with a timeout, and shows
168
186
  the last line a failing proof printed. Because it executes the commands written in
169
187
  the plan, use it only on plans you trust — run the default structural check in CI
@@ -175,9 +193,23 @@ trusted pipeline.
175
193
  - **It will not break teammates' builds.** The checker has no dependencies and
176
194
  exits 0 when there are no plans, so a teammate who never uses planrails is
177
195
  unaffected.
178
- - **The reload line is a plain file include.** `@.project-management/plans/…` in
179
- the root `CLAUDE.md` just tells Claude Code to load that file; it commits like
180
- any doc. Other agents do not import it: open the plan by hand at session start.
196
+ - **Your plans stay out of the team's `CLAUDE.md`.** The reload line goes in
197
+ `CLAUDE.local.md`, which Claude Code loads beside `CLAUDE.md` and git ignores,
198
+ so opening or closing a plan changes no shared file, and a teammate's sessions
199
+ do not reload your plans. A teammate who takes a plan over adds its line to
200
+ their own `CLAUDE.local.md`. The one shared edit is a lasting learning, which
201
+ a plan's close graduates to `CLAUDE.md` as a normal, reviewed change. Other
202
+ agents do not import the line: open the plan by hand at session start.
203
+ - **Moving from 0.6 or earlier.** A reload line already in `CLAUDE.md` keeps
204
+ working, and `npx planrails init` points it out. To move it, cut it from
205
+ `CLAUDE.md`, paste it into `CLAUDE.local.md`, and add `CLAUDE.local.md` to
206
+ `.gitignore`.
207
+ - **Worktrees.** A git worktree has no copy of the ignored file. One under
208
+ `.claude/worktrees/`, where Claude Code makes them, still loads the main
209
+ checkout's `CLAUDE.local.md` from the folder above, but that line imports the
210
+ main checkout's copy of the plan. A session that works the plan in a worktree
211
+ adds the line to the worktree's own `CLAUDE.local.md`, or lists
212
+ `CLAUDE.local.md` in a `.worktreeinclude` file so Claude Code copies it in.
181
213
  - **Decide whether to commit `.project-management/`.** Committing it shares plans
182
214
  and lets CI run the checker. If your repo gitignores it, the checker still runs
183
215
  locally and the plan still reloads for whoever has the files.
@@ -203,7 +235,8 @@ peer in this checkout holds no plan, so two sessions can work two plans side by
203
235
  side. A plan
204
236
  written before 0.5 has no `session:` line; `init` points it out, and the
205
237
  changelog says what to add by hand. The claim is a lead, not a lock: a line left by a closed terminal blocks
206
- nobody, and the checker ignores the line. Field-tested with headless sessions: a
238
+ nobody, and the checker never judges it; it reads the line only to know which
239
+ plan must reload for the session running the check. Field-tested with headless sessions: a
207
240
  second session told "Continue the active plan." stopped and asked while the first
208
241
  was live — by its id, after the first had been renamed — took over when it was
209
242
  gone, and touched only the plan it was given when two were active.
@@ -220,6 +253,10 @@ installed files under `.project-management/planrails/`. 0.4.0 makes the plan
220
253
  carry its own loop, makes the checker demand `exit 0` and check the reload line,
221
254
  and teaches the executor to brief sub-agents from the plan. 0.5.0 names the
222
255
  session working a plan and has every session check for a live holder before it
223
- touches the plan. See [`CHANGELOG.md`](CHANGELOG.md).
256
+ touches the plan. 0.6.0 lets the checker advise without blocking: a `note:` for
257
+ what a script can only suspect, starting with a plan that has outgrown its word
258
+ line. 0.7.0 moves the reload line to `CLAUDE.local.md`, so a plan reloads
259
+ only for whoever works it and a team's `CLAUDE.md` is left alone. See
260
+ [`CHANGELOG.md`](CHANGELOG.md).
224
261
 
225
262
  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
  *
@@ -18,7 +19,7 @@
18
19
  import { readFileSync, copyFileSync, mkdirSync, existsSync, writeFileSync, realpathSync, readdirSync } from "node:fs";
19
20
  import { join, dirname, resolve } from "node:path";
20
21
  import { fileURLToPath } from "node:url";
21
- import { checkPlans, isActive } from "../tools/check-plans.mjs";
22
+ import { checkPlans, isActive, formatNotes, reloadLines } from "../tools/check-plans.mjs";
22
23
 
23
24
  const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
24
25
  const version = () => JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8")).version;
@@ -85,6 +86,11 @@ function init(args) {
85
86
  const text = readFileSync(pf, "utf8");
86
87
  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
88
  }
89
+ // Before 0.7 the reload line went in CLAUDE.md, which a team shares. It still
90
+ // works there; say where it goes now, and never move it.
91
+ const claudeMd = join(target, "CLAUDE.md");
92
+ const shared = existsSync(claudeMd) ? [...reloadLines(readFileSync(claudeMd, "utf8"))] : [];
93
+ 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`);
88
94
  const loose = ["PLANNER.md", "check-plans.mjs"].filter((f) => existsSync(join(pm, f)));
89
95
  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
96
 
@@ -94,8 +100,9 @@ Next:
94
100
  (Or, in Claude Code, use /planrails if you installed the skill.)
95
101
  2. Add to the command you run before every commit:
96
102
  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`);
103
+ 3. When the agent writes a plan, it adds one line to CLAUDE.local.md, and that file
104
+ to .gitignore, so the plan reloads after every compaction, for you alone:
105
+ @.project-management/plans/<id>/PLAN.md`);
99
106
  return 0;
100
107
  }
101
108
 
@@ -103,13 +110,14 @@ function check(args) {
103
110
  const root = dirArg(args);
104
111
  if (!existsSync(root)) { console.error(`planrails check: --dir path does not exist: ${root}`); return 2; }
105
112
  const verify = args.includes("--verify");
106
- const { plans, problems } = checkPlans({ root, verify });
113
+ const { plans, problems, notes } = checkPlans({ root, verify });
114
+ process.stdout.write(formatNotes(notes));
107
115
  if (!plans.length && !problems.length) { console.log("check-plans: no plans under .project-management/plans/ — nothing to check"); return 0; }
108
116
  if (problems.length) {
109
117
  console.error(`check-plans: ${problems.length} problem(s):\n${problems.map((p) => ` - ${p}`).join("\n")}`);
110
118
  return 1;
111
119
  }
112
- 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)" : ""}`);
120
+ 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)" : ""}`);
113
121
  return 0;
114
122
  }
115
123
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "planrails",
3
- "version": "0.5.2",
3
+ "version": "0.7.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.5.2
2
+ // planrails 0.7.0
3
3
  /**
4
4
  * check-plans — the machine-checked rails of the planner.
5
5
  *
@@ -8,13 +8,21 @@
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
19
  * line, and that line may not name only finished tasks. A retired plan says `status: done` (or paused) and is exempt
16
20
  * from both; a plan with no status line counts as active.
17
21
  *
22
+ * It also prints notes: what a script can only suspect (an active PLAN.md over
23
+ * ~3,000 words, which reloads into every session). A note goes to stdout before
24
+ * the final line and never changes the exit code; whoever ran the check decides.
25
+ *
18
26
  * The rule is biased toward catching a faked "done": a task counts as a
19
27
  * completion claim UNLESS its status is blank or an explicit not-done word
20
28
  * (todo, doing, blocked, …). So no spelling of "done" — done, completed, ✅,
@@ -36,7 +44,7 @@
36
44
  * them without a real project.
37
45
  */
38
46
  import { readdirSync, readFileSync, existsSync, statSync, realpathSync } from "node:fs";
39
- import { join } from "node:path";
47
+ import { join, resolve, dirname } from "node:path";
40
48
  import { spawnSync } from "node:child_process";
41
49
  import { fileURLToPath } from "node:url";
42
50
 
@@ -270,6 +278,19 @@ export function reloadLines(text) {
270
278
  return ids;
271
279
  }
272
280
 
281
+ /**
282
+ * The session id on NOW's `session:` line (`name · id · since …`), lowercased, or
283
+ * null for none or a missing line. The id is the first field that is hex: six or
284
+ * more characters, or a whole session id. A line inside a code fence does not count.
285
+ */
286
+ export function holderId(text) {
287
+ const lines = text.split(/\r?\n/), fenced = fenceMask(lines);
288
+ const k = lines.findIndex((l, i) => !fenced[i] && /^\s*session:/.test(l));
289
+ if (k < 0) return null;
290
+ const id = lines[k].replace(/^\s*session:/, "").split(/[·•|]/).map((f) => f.trim()).find((f) => /^[0-9a-f]{6,}(?:-[0-9a-f]+)*$/i.test(f));
291
+ return id ? id.toLowerCase() : null;
292
+ }
293
+
273
294
  /** Plan folders under .project-management/plans/. A folder with no PLAN.md is a problem. */
274
295
  export function findPlans(root) {
275
296
  const dir = join(root, ".project-management", "plans");
@@ -295,21 +316,52 @@ export function runProof(cmd, root, timeoutMs) {
295
316
  return { code: r.status ?? 1, last };
296
317
  }
297
318
 
298
- /** Check every plan under root. Returns { plans, problems }. `--verify` re-runs proofs, each with a timeout (10 min by default). */
299
- export function checkPlans({ root = ".", verify = false, verifyTimeoutMs = 10 * 60 * 1000 } = {}) {
319
+ /**
320
+ * Notes on one plan: what a script can only suspect. A note prints and the run
321
+ * still exits 0; whoever ran the check decides. Only an active plan gets one —
322
+ * a retired plan does not reload, so its size costs nothing.
323
+ */
324
+ export const WORD_LINE = 3000;
325
+ const commas = (n) => String(n).replace(/\B(?=(\d{3})+$)/g, ","); // no Intl: a Node built without it would drop the comma
326
+ export function planNotes({ id, text }) {
327
+ const notes = [];
328
+ if (!isActive(text)) return notes;
329
+ const words = text.split(/\s+/).filter(Boolean).length;
330
+ 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`);
331
+ return notes;
332
+ }
333
+ /** The notes as printed: one "note:" line each, newline-terminated, or "" when there are none. */
334
+ export const formatNotes = (notes) => notes.map((n) => `check-plans: note: ${n}\n`).join("");
335
+
336
+ /**
337
+ * Check every plan under root. Returns { plans, problems, notes }. `--verify` re-runs proofs, each with a timeout (10 min by default).
338
+ * `session` is the id of the session running the check; the plan it holds must reload.
339
+ */
340
+ export function checkPlans({ root = ".", verify = false, verifyTimeoutMs = 10 * 60 * 1000, session = process.env.CLAUDE_CODE_SESSION_ID } = {}) {
300
341
  const { plans, problems } = findPlans(root);
342
+ const notes = [];
301
343
  const run = verify ? (cmd) => runProof(cmd, root, verifyTimeoutMs) : null;
302
- const claudeMd = join(root, "CLAUDE.md");
303
- const reloads = existsSync(claudeMd) ? reloadLines(readFileSync(claudeMd, "utf8")) : null;
344
+ const reloadsIn = (dir, name) => { const p = join(dir, name); return existsSync(p) ? reloadLines(readFileSync(p, "utf8")) : new Set(); };
345
+ const shared = reloadsIn(root, "CLAUDE.md");
346
+ // Claude Code loads both files from the root and every folder above it, so a
347
+ // worktree under .claude/worktrees/ also gets its checkout's. Read what it reads.
348
+ const loaded = new Set();
349
+ for (let d = resolve(root); ; d = dirname(d)) {
350
+ for (const name of ["CLAUDE.md", "CLAUDE.local.md"]) for (const id of reloadsIn(d, name)) loaded.add(id);
351
+ if (dirname(d) === d) break;
352
+ }
353
+ const me = (session || "").toLowerCase();
304
354
  for (const { id, path } of plans) {
305
355
  let text = "";
306
356
  try { text = readFileSync(path, "utf8"); } catch (e) { problems.push(`${id}: cannot read ${path} (${e.code || e.message})`); continue; }
307
357
  problems.push(...checkPlan({ id, text, verify, run }));
308
- if (reloads && isActive(text) && !reloads.has(id))
309
- 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`);
358
+ notes.push(...planNotes({ id, text }));
359
+ const holder = holderId(text);
360
+ if (me && holder && me.startsWith(holder) && isActive(text) && !loaded.has(id))
361
+ 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`);
310
362
  }
311
- 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`);
312
- return { plans, problems };
363
+ 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`);
364
+ return { plans, problems, notes };
313
365
  }
314
366
 
315
367
  // --- CLI ----------------------------------------------------------------------
@@ -330,12 +382,13 @@ if (isMain) {
330
382
  const verify = args.includes("--verify");
331
383
  const root = flagValue(args, "--root") || flagValue(args, "--dir") || ".";
332
384
  if (!existsSync(root)) { process.stderr.write(`check-plans: --root path does not exist: ${root}\n`); process.exit(2); }
333
- const { plans, problems } = checkPlans({ root, verify });
385
+ const { plans, problems, notes } = checkPlans({ root, verify });
386
+ process.stdout.write(formatNotes(notes));
334
387
  if (!plans.length && !problems.length) { process.stdout.write("check-plans: no plans under .project-management/plans/ — nothing to check\n"); process.exit(0); }
335
388
  if (problems.length) {
336
389
  process.stderr.write(`check-plans: ${problems.length} problem(s):\n${problems.map((p) => ` - ${p}`).join("\n")}\n`);
337
390
  process.exit(1);
338
391
  }
339
- 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`);
392
+ 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`);
340
393
  process.exit(0);
341
394
  }