planrails 0.6.0 → 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 +44 -0
- package/PLANNER.md +46 -30
- package/README.md +41 -18
- package/bin/planrails.mjs +12 -5
- package/package.json +1 -1
- package/tools/check-plans.mjs +42 -13
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
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
|
+
|
|
3
47
|
## 0.6.0 — 2026-09-19
|
|
4
48
|
|
|
5
49
|
The checker can now advise without blocking.
|
package/PLANNER.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- planrails 0.
|
|
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
|
|
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
|
|
57
|
-
**check command** (the one run before every
|
|
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.
|
|
@@ -120,22 +123,29 @@ Pick a short kebab-case `<id>` (`weekly-digest`). Then:
|
|
|
120
123
|
word is paid for again and again. Trim prose before Learnings or Decisions.
|
|
121
124
|
History goes in LOG.md, not here. Past the line, the checker prints a `note:` —
|
|
122
125
|
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
|
|
124
|
-
"Active plans"
|
|
126
|
+
- **Add the reload line.** In the project-root `CLAUDE.local.md` (create it if
|
|
127
|
+
needed), under a short "Active plans" heading, add:
|
|
125
128
|
```
|
|
126
129
|
@.project-management/plans/<id>/PLAN.md
|
|
127
130
|
```
|
|
128
131
|
Put it on its own line, outside any code block — an `@` import wrapped in
|
|
129
|
-
backticks does not load. Claude Code
|
|
130
|
-
|
|
131
|
-
back on its own. Under the same heading, one
|
|
132
|
-
*Each plan below is held by one session — see
|
|
133
|
-
only when asked in this session, after the
|
|
134
|
-
|
|
135
|
-
|
|
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.)
|
|
136
146
|
- **One plan per session, one session per plan.** A repo may hold several active
|
|
137
147
|
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
|
|
148
|
+
NOW `session:` line. Every session in this checkout reloads all of them, so keep them few; a plan
|
|
139
149
|
nobody is working is paused (`status: paused`, its line in backticks). A session
|
|
140
150
|
works only the plan the person in that session assigned; the other reloaded
|
|
141
151
|
plans are context, not work orders. Naming the session after the plan
|
|
@@ -153,9 +163,9 @@ Pick a short kebab-case `<id>` (`weekly-digest`). Then:
|
|
|
153
163
|
if you did not run init, copy `tools/check-plans.mjs` from
|
|
154
164
|
https://github.com/vivmagarwal/planrails there. Add
|
|
155
165
|
`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
|
|
157
|
-
reload line, if its NOW lost its RESUME line, or if NOW points at
|
|
158
|
-
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,
|
|
159
169
|
skip this; the plan still works, and you enforce the gate yourself.
|
|
160
170
|
- **Check the plan before the first task.** You, or a fresh sub-agent with no
|
|
161
171
|
chat context: open every path the plan names, start every proof command,
|
|
@@ -208,8 +218,9 @@ decide:
|
|
|
208
218
|
no listing to consult. Git is the record: pull first, read a `doing` row with a
|
|
209
219
|
fresh `updated:` stamp as someone's work in flight, and ask.
|
|
210
220
|
- A worktree is another checkout with its own copy of the plan: a merge concern
|
|
211
|
-
later, not a clobber now.
|
|
212
|
-
|
|
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.
|
|
213
224
|
|
|
214
225
|
Stopping means: report the session's name, status and start, what NOW says and
|
|
215
226
|
what `git status` shows, then ask — `AskUserQuestion` in Claude Code; a `claude -p`
|
|
@@ -298,14 +309,17 @@ the task `blocked` with the reason, and stop. Do not hand-fix state to look done
|
|
|
298
309
|
3. **Update the docs** the work changed — in the same step, per the project's
|
|
299
310
|
documentation guide if it has one.
|
|
300
311
|
4. **Retire the plan.** Set its header to `status: done` and NOW's `session:` to
|
|
301
|
-
`none`, then
|
|
312
|
+
`none`, then delete its reload line (or wrap it in backticks). Do not move a
|
|
302
313
|
bare `@` line under a "Finished" heading: it still imports, and every retired
|
|
303
|
-
plan would reload forever. The status comes first: the checker holds
|
|
304
|
-
reload line. The plan files stay on
|
|
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.
|
|
305
317
|
5. **Graduate any lasting learning.** A learning that is true beyond this feature
|
|
306
|
-
moves to the always-loaded file (`CLAUDE.md` / `AGENTS.md`),
|
|
307
|
-
|
|
308
|
-
|
|
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.
|
|
309
323
|
|
|
310
324
|
---
|
|
311
325
|
|
|
@@ -317,14 +331,16 @@ Each line here was paid for by a real failure in earlier planning systems:
|
|
|
317
331
|
always-loaded file after compaction is something the tool already does. Hooks
|
|
318
332
|
that tried to do more misfired: a crashing pre-tool hook blocks the very call,
|
|
319
333
|
a read guard was wrong twice about sub-agents, a compaction journal came out as
|
|
320
|
-
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.
|
|
321
337
|
- **Proof before work** exists because a checkbox lies. One project marked a phase
|
|
322
338
|
"done" three times while it was not; nothing in a status column could catch it.
|
|
323
339
|
A named command that must be run and pasted can.
|
|
324
340
|
- **Evidence at close** is the one machine-checkable rail worth keeping. The
|
|
325
341
|
checker enforces it — exit 0, not a word — plus two integrity checks that keep
|
|
326
|
-
the reload honest: the
|
|
327
|
-
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.
|
|
328
344
|
- **The plan carries its own loop** because a session that never saw this file
|
|
329
345
|
gets only PLAN.md back. It knew where it was; it did not know how to work.
|
|
330
346
|
- **Learnings live in the plan, not only the log.** The log is history a fresh
|
|
@@ -392,7 +408,7 @@ Read NOW, then Rules and Learnings; do not re-read Context. One task at a time:
|
|
|
392
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.
|
|
393
409
|
5. If it fought back, add a Learning: the trap, then the rule. A verified fact goes in Context, a choice in Decisions.
|
|
394
410
|
|
|
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
|
|
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.
|
|
396
412
|
|
|
397
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:
|
|
398
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,
|
|
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
|
|
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
|
|
40
|
-
the two planner files up to the package
|
|
41
|
-
keeps a same-version copy you edited
|
|
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,9 +160,13 @@ 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
|
-
-
|
|
162
|
-
|
|
163
|
-
|
|
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,7 +179,7 @@ commit, so they need no hook.
|
|
|
173
179
|
|
|
174
180
|
```
|
|
175
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
|
|
176
|
-
check-plans: 1 plan(s) ok — every completion claim has a proof and exit 0 evidence,
|
|
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
|
|
177
183
|
```
|
|
178
184
|
|
|
179
185
|
`--verify` goes further and **runs** each proof again, with a timeout, and shows
|
|
@@ -187,9 +193,23 @@ trusted pipeline.
|
|
|
187
193
|
- **It will not break teammates' builds.** The checker has no dependencies and
|
|
188
194
|
exits 0 when there are no plans, so a teammate who never uses planrails is
|
|
189
195
|
unaffected.
|
|
190
|
-
- **
|
|
191
|
-
|
|
192
|
-
|
|
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.
|
|
193
213
|
- **Decide whether to commit `.project-management/`.** Committing it shares plans
|
|
194
214
|
and lets CI run the checker. If your repo gitignores it, the checker still runs
|
|
195
215
|
locally and the plan still reloads for whoever has the files.
|
|
@@ -215,7 +235,8 @@ peer in this checkout holds no plan, so two sessions can work two plans side by
|
|
|
215
235
|
side. A plan
|
|
216
236
|
written before 0.5 has no `session:` line; `init` points it out, and the
|
|
217
237
|
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
|
|
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
|
|
219
240
|
second session told "Continue the active plan." stopped and asked while the first
|
|
220
241
|
was live — by its id, after the first had been renamed — took over when it was
|
|
221
242
|
gone, and touched only the plan it was given when two were active.
|
|
@@ -234,6 +255,8 @@ and teaches the executor to brief sub-agents from the plan. 0.5.0 names the
|
|
|
234
255
|
session working a plan and has every session check for a live holder before it
|
|
235
256
|
touches the plan. 0.6.0 lets the checker advise without blocking: a `note:` for
|
|
236
257
|
what a script can only suspect, starting with a plan that has outgrown its word
|
|
237
|
-
line.
|
|
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).
|
|
238
261
|
|
|
239
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.
|
|
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, formatNotes } 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
|
|
98
|
-
reloads after every compaction:
|
|
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
|
|
|
@@ -110,7 +117,7 @@ function check(args) {
|
|
|
110
117
|
console.error(`check-plans: ${problems.length} problem(s):\n${problems.map((p) => ` - ${p}`).join("\n")}`);
|
|
111
118
|
return 1;
|
|
112
119
|
}
|
|
113
|
-
console.log(`check-plans: ${plans.length} plan(s) ok — every completion claim has a proof and exit 0 evidence,
|
|
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)" : ""}`);
|
|
114
121
|
return 0;
|
|
115
122
|
}
|
|
116
123
|
|
package/package.json
CHANGED
package/tools/check-plans.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// planrails 0.
|
|
2
|
+
// planrails 0.7.0
|
|
3
3
|
/**
|
|
4
4
|
* check-plans — the machine-checked rails of the planner.
|
|
5
5
|
*
|
|
@@ -8,9 +8,13 @@
|
|
|
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
|
-
* -
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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.
|
|
@@ -40,7 +44,7 @@
|
|
|
40
44
|
* them without a real project.
|
|
41
45
|
*/
|
|
42
46
|
import { readdirSync, readFileSync, existsSync, statSync, realpathSync } from "node:fs";
|
|
43
|
-
import { join } from "node:path";
|
|
47
|
+
import { join, resolve, dirname } from "node:path";
|
|
44
48
|
import { spawnSync } from "node:child_process";
|
|
45
49
|
import { fileURLToPath } from "node:url";
|
|
46
50
|
|
|
@@ -274,6 +278,19 @@ export function reloadLines(text) {
|
|
|
274
278
|
return ids;
|
|
275
279
|
}
|
|
276
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
|
+
|
|
277
294
|
/** Plan folders under .project-management/plans/. A folder with no PLAN.md is a problem. */
|
|
278
295
|
export function findPlans(root) {
|
|
279
296
|
const dir = join(root, ".project-management", "plans");
|
|
@@ -316,22 +333,34 @@ export function planNotes({ id, text }) {
|
|
|
316
333
|
/** The notes as printed: one "note:" line each, newline-terminated, or "" when there are none. */
|
|
317
334
|
export const formatNotes = (notes) => notes.map((n) => `check-plans: note: ${n}\n`).join("");
|
|
318
335
|
|
|
319
|
-
/**
|
|
320
|
-
|
|
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 } = {}) {
|
|
321
341
|
const { plans, problems } = findPlans(root);
|
|
322
342
|
const notes = [];
|
|
323
343
|
const run = verify ? (cmd) => runProof(cmd, root, verifyTimeoutMs) : null;
|
|
324
|
-
const
|
|
325
|
-
const
|
|
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();
|
|
326
354
|
for (const { id, path } of plans) {
|
|
327
355
|
let text = "";
|
|
328
356
|
try { text = readFileSync(path, "utf8"); } catch (e) { problems.push(`${id}: cannot read ${path} (${e.code || e.message})`); continue; }
|
|
329
357
|
problems.push(...checkPlan({ id, text, verify, run }));
|
|
330
358
|
notes.push(...planNotes({ id, text }));
|
|
331
|
-
|
|
332
|
-
|
|
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`);
|
|
333
362
|
}
|
|
334
|
-
|
|
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`);
|
|
335
364
|
return { plans, problems, notes };
|
|
336
365
|
}
|
|
337
366
|
|
|
@@ -360,6 +389,6 @@ if (isMain) {
|
|
|
360
389
|
process.stderr.write(`check-plans: ${problems.length} problem(s):\n${problems.map((p) => ` - ${p}`).join("\n")}\n`);
|
|
361
390
|
process.exit(1);
|
|
362
391
|
}
|
|
363
|
-
process.stdout.write(`check-plans: ${plans.length} plan(s) ok — every completion claim has a proof and exit 0 evidence,
|
|
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`);
|
|
364
393
|
process.exit(0);
|
|
365
394
|
}
|