planrails 0.5.2 → 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 +27 -0
- package/PLANNER.md +11 -3
- package/README.md +15 -1
- package/bin/planrails.mjs +3 -2
- package/package.json +1 -1
- package/tools/check-plans.mjs +28 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
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
|
+
|
|
3
30
|
## 0.5.2 — 2026-09-13
|
|
4
31
|
|
|
5
32
|
A second field test of 0.5.1, four scratch projects and seven fresh sessions, and
|
package/PLANNER.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- planrails 0.
|
|
1
|
+
<!-- planrails 0.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
|
```
|
|
@@ -332,6 +334,12 @@ Each line here was paid for by a real failure in earlier planning systems:
|
|
|
332
334
|
fixes.
|
|
333
335
|
- **History stays out of the plan.** One plan grew a 16,000-word progress section,
|
|
334
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.
|
|
335
343
|
- **Sub-agent findings are leads** because four spot-checked findings were each
|
|
336
344
|
right in direction and wrong in number, and a wrong number becomes a wrong plan.
|
|
337
345
|
Delegation is method, not machinery: a brief of one unit and nothing else worked.
|
package/README.md
CHANGED
|
@@ -164,6 +164,18 @@ structural check, biased toward catching a faked "done":
|
|
|
164
164
|
- an active plan keeps its `RESUME` line, and that line must name a task that
|
|
165
165
|
is still open
|
|
166
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
|
+
```
|
|
178
|
+
|
|
167
179
|
`--verify` goes further and **runs** each proof again, with a timeout, and shows
|
|
168
180
|
the last line a failing proof printed. Because it executes the commands written in
|
|
169
181
|
the plan, use it only on plans you trust — run the default structural check in CI
|
|
@@ -220,6 +232,8 @@ installed files under `.project-management/planrails/`. 0.4.0 makes the plan
|
|
|
220
232
|
carry its own loop, makes the checker demand `exit 0` and check the reload line,
|
|
221
233
|
and teaches the executor to brief sub-agents from the plan. 0.5.0 names the
|
|
222
234
|
session working a plan and has every session check for a live holder before it
|
|
223
|
-
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).
|
|
224
238
|
|
|
225
239
|
MIT.
|
package/bin/planrails.mjs
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
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, isActive } 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;
|
|
@@ -103,7 +103,8 @@ function check(args) {
|
|
|
103
103
|
const root = dirArg(args);
|
|
104
104
|
if (!existsSync(root)) { console.error(`planrails check: --dir path does not exist: ${root}`); return 2; }
|
|
105
105
|
const verify = args.includes("--verify");
|
|
106
|
-
const { plans, problems } = checkPlans({ root, verify });
|
|
106
|
+
const { plans, problems, notes } = checkPlans({ root, verify });
|
|
107
|
+
process.stdout.write(formatNotes(notes));
|
|
107
108
|
if (!plans.length && !problems.length) { console.log("check-plans: no plans under .project-management/plans/ — nothing to check"); return 0; }
|
|
108
109
|
if (problems.length) {
|
|
109
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
|
*
|
|
@@ -15,6 +15,10 @@
|
|
|
15
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, ✅,
|
|
@@ -295,9 +299,27 @@ export function runProof(cmd, root, timeoutMs) {
|
|
|
295
299
|
return { code: r.status ?? 1, last };
|
|
296
300
|
}
|
|
297
301
|
|
|
298
|
-
/**
|
|
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). */
|
|
299
320
|
export function checkPlans({ root = ".", verify = false, verifyTimeoutMs = 10 * 60 * 1000 } = {}) {
|
|
300
321
|
const { plans, problems } = findPlans(root);
|
|
322
|
+
const notes = [];
|
|
301
323
|
const run = verify ? (cmd) => runProof(cmd, root, verifyTimeoutMs) : null;
|
|
302
324
|
const claudeMd = join(root, "CLAUDE.md");
|
|
303
325
|
const reloads = existsSync(claudeMd) ? reloadLines(readFileSync(claudeMd, "utf8")) : null;
|
|
@@ -305,11 +327,12 @@ export function checkPlans({ root = ".", verify = false, verifyTimeoutMs = 10 *
|
|
|
305
327
|
let text = "";
|
|
306
328
|
try { text = readFileSync(path, "utf8"); } catch (e) { problems.push(`${id}: cannot read ${path} (${e.code || e.message})`); continue; }
|
|
307
329
|
problems.push(...checkPlan({ id, text, verify, run }));
|
|
330
|
+
notes.push(...planNotes({ id, text }));
|
|
308
331
|
if (reloads && isActive(text) && !reloads.has(id))
|
|
309
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`);
|
|
310
333
|
}
|
|
311
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`);
|
|
312
|
-
return { plans, problems };
|
|
335
|
+
return { plans, problems, notes };
|
|
313
336
|
}
|
|
314
337
|
|
|
315
338
|
// --- CLI ----------------------------------------------------------------------
|
|
@@ -330,7 +353,8 @@ if (isMain) {
|
|
|
330
353
|
const verify = args.includes("--verify");
|
|
331
354
|
const root = flagValue(args, "--root") || flagValue(args, "--dir") || ".";
|
|
332
355
|
if (!existsSync(root)) { process.stderr.write(`check-plans: --root path does not exist: ${root}\n`); process.exit(2); }
|
|
333
|
-
const { plans, problems } = checkPlans({ root, verify });
|
|
356
|
+
const { plans, problems, notes } = checkPlans({ root, verify });
|
|
357
|
+
process.stdout.write(formatNotes(notes));
|
|
334
358
|
if (!plans.length && !problems.length) { process.stdout.write("check-plans: no plans under .project-management/plans/ — nothing to check\n"); process.exit(0); }
|
|
335
359
|
if (problems.length) {
|
|
336
360
|
process.stderr.write(`check-plans: ${problems.length} problem(s):\n${problems.map((p) => ` - ${p}`).join("\n")}\n`);
|