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 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.5.2 -->
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 ~2,000 words.** Trim prose before Learnings or Decisions.
120
- History goes in LOG.md, not here.
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. See [`CHANGELOG.md`](CHANGELOG.md).
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "planrails",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "Plans that survive a lost session, and \"done\" that means done. A planner prompt and a tiny checker.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // planrails 0.5.2
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
- /** Check every plan under root. Returns { plans, problems }. `--verify` re-runs proofs, each with a timeout (10 min by default). */
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`);