planrails 0.5.1 → 0.6.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 +68 -2
- package/PLANNER.md +38 -16
- package/README.md +27 -7
- package/bin/planrails.mjs +12 -3
- package/package.json +1 -1
- package/tools/check-plans.mjs +34 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,71 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.0 — 2026-09-19
|
|
4
|
+
|
|
5
|
+
The checker can now advise without blocking.
|
|
6
|
+
|
|
7
|
+
- **Notes.** A problem is what a script can verify, and it still fails the build.
|
|
8
|
+
A note is what a script can only suspect: it prints as `check-plans: note: …`
|
|
9
|
+
on stdout before the final line, the exit code and the final line do not
|
|
10
|
+
change, and the agent that ran the check decides. `checkPlans` returns `notes`
|
|
11
|
+
beside `problems`; `npx planrails check` prints the same ones. On a plan with
|
|
12
|
+
no note the output is byte-for-byte what 0.5.2 printed.
|
|
13
|
+
- **One note ships: an active `PLAN.md` over ~3,000 words.** The plan reloads
|
|
14
|
+
into every session, and nothing said when it had grown: four of four real plans
|
|
15
|
+
were past the stated line, one at 9,123 words. A retired plan gets no note; it
|
|
16
|
+
does not reload.
|
|
17
|
+
- **The word line moves from ~2,000 to ~3,000.** The line was written before the
|
|
18
|
+
plan carried its own ~480-word block; two carefully kept plans measure ~2,800.
|
|
19
|
+
The doc was stale, not the plans.
|
|
20
|
+
- **Why not hooks.** The idea began as advisory Claude Code hooks. Checked against
|
|
21
|
+
the hooks reference: a PreCompact hook cannot add context, no event fires at a
|
|
22
|
+
context threshold, a Stop hook's advice forces another turn, and no event knows
|
|
23
|
+
a plan's task closed. The check command already runs before every commit and
|
|
24
|
+
its output is already read, in any agent. `CONTRIBUTING.md` sets the bar for a
|
|
25
|
+
new note: a recorded failure behind it, silent on a healthy plan. A LOG-entry
|
|
26
|
+
reminder was tested on four real plans, never fired, and was not built.
|
|
27
|
+
|
|
28
|
+
To update: `npx planrails@latest init`. No plan needs editing.
|
|
29
|
+
|
|
30
|
+
## 0.5.2 — 2026-09-13
|
|
31
|
+
|
|
32
|
+
A second field test of 0.5.1, four scratch projects and seven fresh sessions, and
|
|
33
|
+
a newcomer's pass over the docs. The published behaviour held: a stale claim was
|
|
34
|
+
taken over by id, a live holder stopped a second session, a session assigned one
|
|
35
|
+
of two plans touched only that one. Four gaps in the plan's block, and one in the
|
|
36
|
+
worked example, are closed here.
|
|
37
|
+
|
|
38
|
+
- **Two sessions can work two plans in one checkout.** The block's rule for an
|
|
39
|
+
unheld plan (`session: none`, or no line) said stop whenever any session in the
|
|
40
|
+
checkout was busy or any plan file was dirty, so the second of two sessions
|
|
41
|
+
always had to ask. Now a session assigned an unheld plan writes its line and
|
|
42
|
+
goes on, unless *that plan's* files are dirty or a busy session in the checkout
|
|
43
|
+
has its id on no plan's `session:` line. The same-plan rule is unchanged: a
|
|
44
|
+
live holder, matched by id, stops you.
|
|
45
|
+
- **An `owner` proof is closed only by the person.** A field session closed an
|
|
46
|
+
owner task with a check of its own and an invented exit code; the checker cannot
|
|
47
|
+
see that. The block and §4 now say: the row stays `blocked` with the reason in
|
|
48
|
+
its evidence cell, and RESUME says so.
|
|
49
|
+
- **NOW keeps its shape, and the block says how a plan closes.** A session
|
|
50
|
+
renamed RESUME to DONE, wrote the `session:` line twice and left a finished plan
|
|
51
|
+
active. The block says NOW is exactly the four lines, and points at §5 for the
|
|
52
|
+
close. The checker now fails an active plan whose NOW section has no RESUME
|
|
53
|
+
line, the one hole a renamed line opened in the NOW rule.
|
|
54
|
+
- **The session rule is a list.** Its paragraph fell from 197 words to four
|
|
55
|
+
bullets; the whole block is about 480 words, up from 443, with the owner and
|
|
56
|
+
close clauses added.
|
|
57
|
+
- **Existing 0.5.0 and 0.5.1 plans:** replace the block with the template's, so
|
|
58
|
+
they carry the unheld-plan rule and the owner clause; `init` cannot tell them
|
|
59
|
+
from 0.5.2 plans, since they already have a `session:` line.
|
|
60
|
+
- **The worked example carries the template's block, byte for byte**, pinned by a
|
|
61
|
+
test. It had shipped the 0.5.0 paragraph that matched holders by name.
|
|
62
|
+
- **`init` points out plans that predate 0.5**: an active plan with no `session:`
|
|
63
|
+
line is named, with the by-hand steps from 0.5.0. `init` still never touches a
|
|
64
|
+
plan.
|
|
65
|
+
- Timestamps come from the shell clock: `date`, or `Get-Date` on Windows, where
|
|
66
|
+
`date` prompts. The README no longer suggests `ListAgents` can do the id match.
|
|
67
|
+
The 0.4.0 entry no longer overstates the stray-backtick fix. 77 tests.
|
|
68
|
+
|
|
3
69
|
## 0.5.1 — 2026-09-13
|
|
4
70
|
|
|
5
71
|
A fresh-context review of 0.5.0, run the same day, found the block — the one copy
|
|
@@ -103,8 +169,8 @@ nothing but its two files.
|
|
|
103
169
|
Re-run the proof and paste its exit code.
|
|
104
170
|
- **The checker fails closed on a row it cannot read.** The cell splitter follows
|
|
105
171
|
the CommonMark rule for backtick runs, so a stray backtick or three backticks in
|
|
106
|
-
prose
|
|
107
|
-
header's is a problem instead of a silent pass. A task table inside a code fence
|
|
172
|
+
prose are read the way CommonMark reads them; a row whose cell count still
|
|
173
|
+
differs from the header's is a problem instead of a silent pass. A task table inside a code fence
|
|
108
174
|
is ignored, a table right after the task table with no heading between is no
|
|
109
175
|
longer read as tasks, and `**id**` in a header is read.
|
|
110
176
|
- **Two integrity checks keep the reload honest.** When the project has a
|
package/PLANNER.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- planrails 0.
|
|
1
|
+
<!-- planrails 0.6.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
|
|
@@ -116,8 +116,10 @@ Pick a short kebab-case `<id>` (`weekly-digest`). Then:
|
|
|
116
116
|
checks, run end to end, the way a user would.
|
|
117
117
|
- **Use repo-relative paths** (`lib/digest/query.ts`), never absolute ones. They
|
|
118
118
|
are clickable and they survive a move to another machine.
|
|
119
|
-
- **Keep PLAN.md under ~
|
|
120
|
-
|
|
119
|
+
- **Keep PLAN.md under ~3,000 words.** It reloads into every session, so every
|
|
120
|
+
word is paid for again and again. Trim prose before Learnings or Decisions.
|
|
121
|
+
History goes in LOG.md, not here. Past the line, the checker prints a `note:` —
|
|
122
|
+
advice, not a failure: the run still exits 0, and you decide what to trim.
|
|
121
123
|
- **Add the reload line.** In the project-root `CLAUDE.md`, under a short
|
|
122
124
|
"Active plans" spot, add:
|
|
123
125
|
```
|
|
@@ -152,7 +154,8 @@ Pick a short kebab-case `<id>` (`weekly-digest`). Then:
|
|
|
152
154
|
https://github.com/vivmagarwal/planrails there. Add
|
|
153
155
|
`node .project-management/planrails/check-plans.mjs` to the check command. Now the
|
|
154
156
|
build fails if a task is marked done with no evidence, if an active plan has no
|
|
155
|
-
reload line,
|
|
157
|
+
reload line, if its NOW lost its RESUME line, or if NOW points at a finished
|
|
158
|
+
task. If the project is not Node,
|
|
156
159
|
skip this; the plan still works, and you enforce the gate yourself.
|
|
157
160
|
- **Check the plan before the first task.** You, or a fresh sub-agent with no
|
|
158
161
|
chat context: open every path the plan names, start every proof command,
|
|
@@ -196,9 +199,11 @@ decide:
|
|
|
196
199
|
- Its id is not listed: the claim is stale only when no other session in this
|
|
197
200
|
checkout is live — then say so, take over, write your claim. When one is live,
|
|
198
201
|
stop and ask; it may be the holder under a new id, after a branch or a fork.
|
|
199
|
-
- `none`, or no line (an older plan):
|
|
200
|
-
|
|
201
|
-
|
|
202
|
+
- `none`, or no line (an older plan): say what you saw in one line, write your
|
|
203
|
+
claim, go on — unless this plan's files are dirty in `git status`, or a busy
|
|
204
|
+
session in this checkout has its id on no plan's `session:` line; then stop
|
|
205
|
+
and ask. A busy peer whose id is on another plan's line is working that plan,
|
|
206
|
+
so two sessions can work two plans in one checkout side by side.
|
|
202
207
|
- No `claude` command (another agent), or a plan shared across machines: there is
|
|
203
208
|
no listing to consult. Git is the record: pull first, read a `doing` row with a
|
|
204
209
|
fresh `updated:` stamp as someone's work in flight, and ask.
|
|
@@ -217,14 +222,20 @@ The loop for each task:
|
|
|
217
222
|
|
|
218
223
|
1. **Check who holds the plan** (above), then **set it doing.** Change the status
|
|
219
224
|
cell to `doing`. Update **NOW**, and put your session on its `session:` line
|
|
220
|
-
(`since` from
|
|
225
|
+
(`since` from the shell clock on a takeover; unchanged when it already names
|
|
226
|
+
you).
|
|
221
227
|
2. **Do the work.** Fix the cause, not the symptom. The simplest change that
|
|
222
228
|
works, end to end.
|
|
223
229
|
3. **Run the proof.** Right now, not from memory. Copy the exit code and the last
|
|
224
|
-
line of output. Take the time from `date
|
|
230
|
+
line of output. Take the time from the shell clock (`date`; on Windows
|
|
231
|
+
`Get-Date`), never from memory.
|
|
225
232
|
4. **Paste the evidence.** Into the task's evidence cell:
|
|
226
233
|
`2026-09-12 14:20 · exit 0 · "6 passed"`. Exit 0, or the task is not done.
|
|
227
|
-
5. **Set it done.** Only now.
|
|
234
|
+
5. **Set it done.** Only now. A proof of `owner` is closed only by the person's
|
|
235
|
+
words with the date, never by you: until then the row is `blocked`, the
|
|
236
|
+
reason in its evidence cell, and RESUME says so.
|
|
237
|
+
Update **NOW** to point at the next task; NOW keeps its four lines and their
|
|
238
|
+
names, RESUME included, until the plan is retired.
|
|
228
239
|
6. **Append an entry to LOG.md** — what landed, what is next, anything learned,
|
|
229
240
|
any decision made.
|
|
230
241
|
7. **If the task fought back, record the learning.** An error, a wrong turn, an
|
|
@@ -323,6 +334,12 @@ Each line here was paid for by a real failure in earlier planning systems:
|
|
|
323
334
|
fixes.
|
|
324
335
|
- **History stays out of the plan.** One plan grew a 16,000-word progress section,
|
|
325
336
|
stamped two hours behind its own log. NOW is four lines; LOG.md is the history.
|
|
337
|
+
- **The checker's notes advise; they never block.** A script can verify an exit
|
|
338
|
+
code, so that is a problem and fails the build. It can only suspect that a plan
|
|
339
|
+
is too long, so that is a note, and you judge. Four of four real plans had
|
|
340
|
+
outgrown the word line with nothing saying so. The check command already runs
|
|
341
|
+
before every commit and its output is already read, so a reminder there needs
|
|
342
|
+
no hook. A note needs a real failure behind it, or it is noise.
|
|
326
343
|
- **Sub-agent findings are leads** because four spot-checked findings were each
|
|
327
344
|
right in direction and wrong in number, and a wrong number becomes a wrong plan.
|
|
328
345
|
Delegation is method, not machinery: a brief of one unit and nothing else worked.
|
|
@@ -364,20 +381,25 @@ status: active · opened <YYYY-MM-DD> · id: <kebab-id>
|
|
|
364
381
|
## NOW
|
|
365
382
|
RESUME: <T2 — the one thing to do next, with the file; written for a stranger>
|
|
366
383
|
NEXT: <T3 · T4 · …>
|
|
367
|
-
updated: <YYYY-MM-DD HH:MM, from
|
|
384
|
+
updated: <YYYY-MM-DD HH:MM, from the shell clock>
|
|
368
385
|
session: <none, or the session working this plan: name · the first 8 characters of its session id · since YYYY-MM-DD HH:MM>
|
|
369
386
|
|
|
370
387
|
## How to work this plan
|
|
371
388
|
Read NOW, then Rules and Learnings; do not re-read Context. One task at a time:
|
|
372
|
-
1.
|
|
389
|
+
1. Check who holds the plan (below); set it `doing`; point NOW at it; put your session on its `session:` line.
|
|
373
390
|
2. Do the work: fix causes, not symptoms; the simplest change that works end to end.
|
|
374
|
-
3. Run the proof now. Paste `YYYY-MM-DD HH:MM · exit N · "last line"` into evidence. Every stamp comes from `date
|
|
375
|
-
4. `done` only if N is 0. Point NOW at the next task; append an entry to LOG.md: did, files, proof, next, learned.
|
|
391
|
+
3. Run the proof now. Paste `YYYY-MM-DD HH:MM · exit N · "last line"` into evidence. Every stamp comes from the shell clock (`date`; on Windows `Get-Date`), never typed from memory.
|
|
392
|
+
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.
|
|
376
393
|
5. If it fought back, add a Learning: the trap, then the rule. A verified fact goes in Context, a choice in Decisions.
|
|
377
394
|
|
|
378
|
-
NOW is four lines for a stranger; on a long task note the sub-step, and update
|
|
395
|
+
NOW is exactly these four lines, RESUME, NEXT, updated and session, written for a stranger; on a long task note the sub-step in RESUME, and update NOW before any turn ends. Blocked: say so in RESUME, reason in the evidence cell. A task you will not do is `dropped`; its row stays. After a compaction, `git status --short` shows the in-flight work. Edit this file with the editor tool; an unquoted shell string eats backticks. A self-contained task may go to a sub-agent briefed with its row, Rules, Decisions, Learnings and Context; it writes to a named file and reports a few lines, which are leads; you run the proof before pasting evidence. If the project runs Node, `node .project-management/planrails/check-plans.mjs` must pass. When no row is left open, done or dropped, close by PLANNER.md §5: proofs re-run, a fresh-context review, `status: done`, `session: none`, the reload line in CLAUDE.md wrapped in backticks. Full method: `.project-management/planrails/PLANNER.md` §4.
|
|
379
396
|
|
|
380
|
-
|
|
397
|
+
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
|
+
- the id is yours: go on.
|
|
399
|
+
- the id is live and not yours: stop, report what you found, ask; write nothing.
|
|
400
|
+
- the id is not listed: stale if no other session in this checkout is live; say so, take over, write your line. Else stop and ask.
|
|
401
|
+
- `none`, or no line: write your line and go on, unless this plan's PLAN.md, LOG.md or the files its tasks name are dirty, or a busy session in this checkout has its id on no plan's `session:` line; then stop and ask.
|
|
402
|
+
Work only the plan assigned in this session; other reloaded plans are context. Without a `claude` command, git is the record: pull first, and ask about a fresh `doing` row.
|
|
381
403
|
|
|
382
404
|
## Goal
|
|
383
405
|
<4–5 sentences: what we are building and why. What is true when it ships. A mermaid diagram only if the architecture is non-trivial.>
|
package/README.md
CHANGED
|
@@ -161,7 +161,20 @@ structural check, biased toward catching a faked "done":
|
|
|
161
161
|
- when the project has a `CLAUDE.md`, every active plan must be reloaded by
|
|
162
162
|
`@.project-management/plans/<id>/PLAN.md` on its own line, and no such line may
|
|
163
163
|
point at a plan that does not exist
|
|
164
|
-
- an active plan
|
|
164
|
+
- an active plan keeps its `RESUME` line, and that line must name a task that
|
|
165
|
+
is still open
|
|
166
|
+
|
|
167
|
+
The checker also **advises without blocking**. What a script can only suspect, it
|
|
168
|
+
prints as a `note:` before its final line; the run still exits 0, and the agent
|
|
169
|
+
that ran the check decides. There is one today: an active `PLAN.md` over ~3,000
|
|
170
|
+
words, because the plan reloads into every session and nothing else says when it
|
|
171
|
+
has grown. Reminders ride on the check your agent already runs before every
|
|
172
|
+
commit, so they need no hook.
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
check-plans: note: weekly-digest: PLAN.md is 3,588 words, over the ~3,000 line — it reloads into every session; move history to LOG.md and trim prose before Learnings or Decisions
|
|
176
|
+
check-plans: 1 plan(s) ok — every completion claim has a proof and exit 0 evidence, active plans reload, NOW is current
|
|
177
|
+
```
|
|
165
178
|
|
|
166
179
|
`--verify` goes further and **runs** each proof again, with a timeout, and shows
|
|
167
180
|
the last line a failing proof printed. Because it executes the commands written in
|
|
@@ -192,11 +205,16 @@ directory, so a second session can read the plan and start the task the first on
|
|
|
192
205
|
is on. A plan's NOW names the session working it
|
|
193
206
|
(`session: app-3f · 7c1d2e9a · since 2026-09-12 14:05`), and the plan's block tells
|
|
194
207
|
every session, before it sets a task `doing`, to re-read that line, list the live
|
|
195
|
-
sessions (`claude agents --json
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
it. One plan per session, one
|
|
199
|
-
|
|
208
|
+
sessions (`claude agents --json`, matching the line's id; `ListAgents` inside
|
|
209
|
+
Claude Code names sessions but shows no id) and run `git status --short`, and to
|
|
210
|
+
stop and ask when another live session holds the plan — even when you asked it to
|
|
211
|
+
continue, because you can forget which window owns it. One plan per session, one
|
|
212
|
+
session per plan; the other reloaded plans are context. A session assigned an
|
|
213
|
+
unheld plan claims it and goes on, unless that plan's files are dirty or a busy
|
|
214
|
+
peer in this checkout holds no plan, so two sessions can work two plans side by
|
|
215
|
+
side. A plan
|
|
216
|
+
written before 0.5 has no `session:` line; `init` points it out, and the
|
|
217
|
+
changelog says what to add by hand. The claim is a lead, not a lock: a line left by a closed terminal blocks
|
|
200
218
|
nobody, and the checker ignores the line. Field-tested with headless sessions: a
|
|
201
219
|
second session told "Continue the active plan." stopped and asked while the first
|
|
202
220
|
was live — by its id, after the first had been renamed — took over when it was
|
|
@@ -214,6 +232,8 @@ installed files under `.project-management/planrails/`. 0.4.0 makes the plan
|
|
|
214
232
|
carry its own loop, makes the checker demand `exit 0` and check the reload line,
|
|
215
233
|
and teaches the executor to brief sub-agents from the plan. 0.5.0 names the
|
|
216
234
|
session working a plan and has every session check for a live holder before it
|
|
217
|
-
touches the plan.
|
|
235
|
+
touches the plan. 0.6.0 lets the checker advise without blocking: a `note:` for
|
|
236
|
+
what a script can only suspect, starting with a plan that has outgrown its word
|
|
237
|
+
line. See [`CHANGELOG.md`](CHANGELOG.md).
|
|
218
238
|
|
|
219
239
|
MIT.
|
package/bin/planrails.mjs
CHANGED
|
@@ -15,10 +15,10 @@
|
|
|
15
15
|
*
|
|
16
16
|
* planrails --version | --help
|
|
17
17
|
*/
|
|
18
|
-
import { readFileSync, copyFileSync, mkdirSync, existsSync, writeFileSync, realpathSync } from "node:fs";
|
|
18
|
+
import { readFileSync, copyFileSync, mkdirSync, existsSync, writeFileSync, realpathSync, readdirSync } from "node:fs";
|
|
19
19
|
import { join, dirname, resolve } from "node:path";
|
|
20
20
|
import { fileURLToPath } from "node:url";
|
|
21
|
-
import { checkPlans } from "../tools/check-plans.mjs";
|
|
21
|
+
import { checkPlans, isActive, formatNotes } from "../tools/check-plans.mjs";
|
|
22
22
|
|
|
23
23
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
24
24
|
const version = () => JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8")).version;
|
|
@@ -77,6 +77,14 @@ function init(args) {
|
|
|
77
77
|
const keep = join(plans, ".gitkeep");
|
|
78
78
|
if (!existsSync(keep)) { writeFileSync(keep, ""); console.log(" + .project-management/plans/"); }
|
|
79
79
|
else console.log(" = .project-management/plans/ (your plans are never touched)");
|
|
80
|
+
// A plan written before 0.5 has no session: line and an older block. init never
|
|
81
|
+
// touches a plan, so say which ones need the by-hand update from CHANGELOG 0.5.0.
|
|
82
|
+
for (const name of existsSync(plans) ? readdirSync(plans) : []) {
|
|
83
|
+
const pf = join(plans, name, "PLAN.md");
|
|
84
|
+
if (!existsSync(pf)) continue;
|
|
85
|
+
const text = readFileSync(pf, "utf8");
|
|
86
|
+
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
|
+
}
|
|
80
88
|
const loose = ["PLANNER.md", "check-plans.mjs"].filter((f) => existsSync(join(pm, f)));
|
|
81
89
|
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`);
|
|
82
90
|
|
|
@@ -95,7 +103,8 @@ function check(args) {
|
|
|
95
103
|
const root = dirArg(args);
|
|
96
104
|
if (!existsSync(root)) { console.error(`planrails check: --dir path does not exist: ${root}`); return 2; }
|
|
97
105
|
const verify = args.includes("--verify");
|
|
98
|
-
const { plans, problems } = checkPlans({ root, verify });
|
|
106
|
+
const { plans, problems, notes } = checkPlans({ root, verify });
|
|
107
|
+
process.stdout.write(formatNotes(notes));
|
|
99
108
|
if (!plans.length && !problems.length) { console.log("check-plans: no plans under .project-management/plans/ — nothing to check"); return 0; }
|
|
100
109
|
if (problems.length) {
|
|
101
110
|
console.error(`check-plans: ${problems.length} problem(s):\n${problems.map((p) => ` - ${p}`).join("\n")}`);
|
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.6.0
|
|
3
3
|
/**
|
|
4
4
|
* check-plans — the machine-checked rails of the planner.
|
|
5
5
|
*
|
|
@@ -11,10 +11,14 @@
|
|
|
11
11
|
* - an active plan must reload: when the project has a CLAUDE.md, it must carry
|
|
12
12
|
* `@.project-management/plans/<id>/PLAN.md` on its own line, and no such line
|
|
13
13
|
* may point at a plan that does not exist. (Skipped when there is no CLAUDE.md.)
|
|
14
|
-
* - NOW must be current:
|
|
15
|
-
* finished tasks. A retired plan says `status: done` (or paused) and is exempt
|
|
14
|
+
* - NOW must be current: an active plan with a NOW section keeps its RESUME
|
|
15
|
+
* line, and that line may not name only finished tasks. A retired plan says `status: done` (or paused) and is exempt
|
|
16
16
|
* from both; a plan with no status line counts as active.
|
|
17
17
|
*
|
|
18
|
+
* It also prints notes: what a script can only suspect (an active PLAN.md over
|
|
19
|
+
* ~3,000 words, which reloads into every session). A note goes to stdout before
|
|
20
|
+
* the final line and never changes the exit code; whoever ran the check decides.
|
|
21
|
+
*
|
|
18
22
|
* The rule is biased toward catching a faked "done": a task counts as a
|
|
19
23
|
* completion claim UNLESS its status is blank or an explicit not-done word
|
|
20
24
|
* (todo, doing, blocked, …). So no spelling of "done" — done, completed, ✅,
|
|
@@ -229,8 +233,11 @@ export function checkPlan({ id, text, verify = false, run = null }) {
|
|
|
229
233
|
if (code !== 0) problems.push(`${id} ${t.id}: proof re-run failed — \`${cmd}\` exited ${code}${r.last ? ` — ${r.last}` : ""}`);
|
|
230
234
|
}
|
|
231
235
|
}
|
|
232
|
-
// NOW must point somewhere live:
|
|
236
|
+
// NOW must point somewhere live: an active plan keeps its RESUME line, and a
|
|
237
|
+
// RESUME line that names only finished tasks is stale.
|
|
233
238
|
if (isActive(text)) {
|
|
239
|
+
const ls = text.split(/\r?\n/), fm = fenceMask(ls);
|
|
240
|
+
if (ls.some((l, k) => !fm[k] && /^#{1,6}\s+NOW\b/.test(l)) && !ls.some((l, k) => !fm[k] && /^\s*RESUME:/.test(l))) problems.push(`${id}: NOW has no RESUME line — an active plan keeps RESUME, NEXT, updated and session; a finished plan says status: done`);
|
|
234
241
|
const named = resumeIds(text, tasks.map((t) => t.id).filter(Boolean));
|
|
235
242
|
if (named.length && named.every((n) => tasks.some((t) => t.id === n && isCompletionClaim(t.status))))
|
|
236
243
|
problems.push(`${id}: NOW is stale — RESUME names ${named.map((n) => n.replace(/[`*_]/g, "")).join(", ")}, which ${named.length === 1 ? "is" : "are all"} done; point it at the next open task, or set the plan's status to done`);
|
|
@@ -292,9 +299,27 @@ export function runProof(cmd, root, timeoutMs) {
|
|
|
292
299
|
return { code: r.status ?? 1, last };
|
|
293
300
|
}
|
|
294
301
|
|
|
295
|
-
/**
|
|
302
|
+
/**
|
|
303
|
+
* Notes on one plan: what a script can only suspect. A note prints and the run
|
|
304
|
+
* still exits 0; whoever ran the check decides. Only an active plan gets one —
|
|
305
|
+
* a retired plan does not reload, so its size costs nothing.
|
|
306
|
+
*/
|
|
307
|
+
export const WORD_LINE = 3000;
|
|
308
|
+
const commas = (n) => String(n).replace(/\B(?=(\d{3})+$)/g, ","); // no Intl: a Node built without it would drop the comma
|
|
309
|
+
export function planNotes({ id, text }) {
|
|
310
|
+
const notes = [];
|
|
311
|
+
if (!isActive(text)) return notes;
|
|
312
|
+
const words = text.split(/\s+/).filter(Boolean).length;
|
|
313
|
+
if (words > WORD_LINE) notes.push(`${id}: PLAN.md is ${commas(words)} words, over the ~${commas(WORD_LINE)} line — it reloads into every session; move history to LOG.md and trim prose before Learnings or Decisions`);
|
|
314
|
+
return notes;
|
|
315
|
+
}
|
|
316
|
+
/** The notes as printed: one "note:" line each, newline-terminated, or "" when there are none. */
|
|
317
|
+
export const formatNotes = (notes) => notes.map((n) => `check-plans: note: ${n}\n`).join("");
|
|
318
|
+
|
|
319
|
+
/** Check every plan under root. Returns { plans, problems, notes }. `--verify` re-runs proofs, each with a timeout (10 min by default). */
|
|
296
320
|
export function checkPlans({ root = ".", verify = false, verifyTimeoutMs = 10 * 60 * 1000 } = {}) {
|
|
297
321
|
const { plans, problems } = findPlans(root);
|
|
322
|
+
const notes = [];
|
|
298
323
|
const run = verify ? (cmd) => runProof(cmd, root, verifyTimeoutMs) : null;
|
|
299
324
|
const claudeMd = join(root, "CLAUDE.md");
|
|
300
325
|
const reloads = existsSync(claudeMd) ? reloadLines(readFileSync(claudeMd, "utf8")) : null;
|
|
@@ -302,11 +327,12 @@ export function checkPlans({ root = ".", verify = false, verifyTimeoutMs = 10 *
|
|
|
302
327
|
let text = "";
|
|
303
328
|
try { text = readFileSync(path, "utf8"); } catch (e) { problems.push(`${id}: cannot read ${path} (${e.code || e.message})`); continue; }
|
|
304
329
|
problems.push(...checkPlan({ id, text, verify, run }));
|
|
330
|
+
notes.push(...planNotes({ id, text }));
|
|
305
331
|
if (reloads && isActive(text) && !reloads.has(id))
|
|
306
332
|
problems.push(`${id}: the plan is active but CLAUDE.md has no reload line — add "@.project-management/plans/${id}/PLAN.md" on its own line, outside backticks, or the plan will not survive a compaction`);
|
|
307
333
|
}
|
|
308
334
|
if (reloads) for (const id of reloads) if (!plans.some((p) => p.id === id)) problems.push(`CLAUDE.md reloads "${id}" but .project-management/plans/${id}/PLAN.md does not exist`);
|
|
309
|
-
return { plans, problems };
|
|
335
|
+
return { plans, problems, notes };
|
|
310
336
|
}
|
|
311
337
|
|
|
312
338
|
// --- CLI ----------------------------------------------------------------------
|
|
@@ -327,7 +353,8 @@ if (isMain) {
|
|
|
327
353
|
const verify = args.includes("--verify");
|
|
328
354
|
const root = flagValue(args, "--root") || flagValue(args, "--dir") || ".";
|
|
329
355
|
if (!existsSync(root)) { process.stderr.write(`check-plans: --root path does not exist: ${root}\n`); process.exit(2); }
|
|
330
|
-
const { plans, problems } = checkPlans({ root, verify });
|
|
356
|
+
const { plans, problems, notes } = checkPlans({ root, verify });
|
|
357
|
+
process.stdout.write(formatNotes(notes));
|
|
331
358
|
if (!plans.length && !problems.length) { process.stdout.write("check-plans: no plans under .project-management/plans/ — nothing to check\n"); process.exit(0); }
|
|
332
359
|
if (problems.length) {
|
|
333
360
|
process.stderr.write(`check-plans: ${problems.length} problem(s):\n${problems.map((p) => ` - ${p}`).join("\n")}\n`);
|