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 +71 -0
- package/PLANNER.md +56 -32
- package/README.md +54 -17
- package/bin/planrails.mjs +14 -6
- package/package.json +1 -1
- package/tools/check-plans.mjs +68 -15
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.
|
|
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.
|
|
@@ -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 ~
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
|
128
|
-
|
|
129
|
-
back on its own. Under the same heading, one
|
|
130
|
-
*Each plan below is held by one session — see
|
|
131
|
-
only when asked in this session, after the
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
155
|
-
reload line, if its NOW lost its RESUME line, or if NOW points at
|
|
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.
|
|
210
|
-
|
|
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
|
|
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
|
|
302
|
-
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.
|
|
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`),
|
|
305
|
-
|
|
306
|
-
|
|
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
|
|
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
|
|
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,
|
|
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,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
|
-
-
|
|
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
|
+
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
|
-
- **
|
|
179
|
-
|
|
180
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
|
@@ -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,
|
|
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
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,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
|
-
* -
|
|
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.
|
|
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
|
-
/**
|
|
299
|
-
|
|
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
|
|
303
|
-
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();
|
|
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
|
-
|
|
309
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
}
|