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