planrails 0.5.1 → 0.5.2

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,44 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.2 — 2026-09-13
4
+
5
+ A second field test of 0.5.1, four scratch projects and seven fresh sessions, and
6
+ a newcomer's pass over the docs. The published behaviour held: a stale claim was
7
+ taken over by id, a live holder stopped a second session, a session assigned one
8
+ of two plans touched only that one. Four gaps in the plan's block, and one in the
9
+ worked example, are closed here.
10
+
11
+ - **Two sessions can work two plans in one checkout.** The block's rule for an
12
+ unheld plan (`session: none`, or no line) said stop whenever any session in the
13
+ checkout was busy or any plan file was dirty, so the second of two sessions
14
+ always had to ask. Now a session assigned an unheld plan writes its line and
15
+ goes on, unless *that plan's* files are dirty or a busy session in the checkout
16
+ has its id on no plan's `session:` line. The same-plan rule is unchanged: a
17
+ live holder, matched by id, stops you.
18
+ - **An `owner` proof is closed only by the person.** A field session closed an
19
+ owner task with a check of its own and an invented exit code; the checker cannot
20
+ see that. The block and §4 now say: the row stays `blocked` with the reason in
21
+ its evidence cell, and RESUME says so.
22
+ - **NOW keeps its shape, and the block says how a plan closes.** A session
23
+ renamed RESUME to DONE, wrote the `session:` line twice and left a finished plan
24
+ active. The block says NOW is exactly the four lines, and points at §5 for the
25
+ close. The checker now fails an active plan whose NOW section has no RESUME
26
+ line, the one hole a renamed line opened in the NOW rule.
27
+ - **The session rule is a list.** Its paragraph fell from 197 words to four
28
+ bullets; the whole block is about 480 words, up from 443, with the owner and
29
+ close clauses added.
30
+ - **Existing 0.5.0 and 0.5.1 plans:** replace the block with the template's, so
31
+ they carry the unheld-plan rule and the owner clause; `init` cannot tell them
32
+ from 0.5.2 plans, since they already have a `session:` line.
33
+ - **The worked example carries the template's block, byte for byte**, pinned by a
34
+ test. It had shipped the 0.5.0 paragraph that matched holders by name.
35
+ - **`init` points out plans that predate 0.5**: an active plan with no `session:`
36
+ line is named, with the by-hand steps from 0.5.0. `init` still never touches a
37
+ plan.
38
+ - Timestamps come from the shell clock: `date`, or `Get-Date` on Windows, where
39
+ `date` prompts. The README no longer suggests `ListAgents` can do the id match.
40
+ The 0.4.0 entry no longer overstates the stray-backtick fix. 77 tests.
41
+
3
42
  ## 0.5.1 — 2026-09-13
4
43
 
5
44
  A fresh-context review of 0.5.0, run the same day, found the block — the one copy
@@ -103,8 +142,8 @@ nothing but its two files.
103
142
  Re-run the proof and paste its exit code.
104
143
  - **The checker fails closed on a row it cannot read.** The cell splitter follows
105
144
  the CommonMark rule for backtick runs, so a stray backtick or three backticks in
106
- prose no longer shift the columns; a row whose cell count still differs from the
107
- header's is a problem instead of a silent pass. A task table inside a code fence
145
+ prose are read the way CommonMark reads them; a row whose cell count still
146
+ differs from the header's is a problem instead of a silent pass. A task table inside a code fence
108
147
  is ignored, a table right after the task table with no heading between is no
109
148
  longer read as tasks, and `**id**` in a header is read.
110
149
  - **Two integrity checks keep the reload honest.** When the project has a
package/PLANNER.md CHANGED
@@ -1,4 +1,4 @@
1
- <!-- planrails 0.5.1 -->
1
+ <!-- planrails 0.5.2 -->
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
@@ -152,7 +152,8 @@ Pick a short kebab-case `<id>` (`weekly-digest`). Then:
152
152
  https://github.com/vivmagarwal/planrails there. Add
153
153
  `node .project-management/planrails/check-plans.mjs` to the check command. Now the
154
154
  build fails if a task is marked done with no evidence, if an active plan has no
155
- reload line, or if NOW points at a finished task. If the project is not Node,
155
+ reload line, if its NOW lost its RESUME line, or if NOW points at a finished
156
+ task. If the project is not Node,
156
157
  skip this; the plan still works, and you enforce the gate yourself.
157
158
  - **Check the plan before the first task.** You, or a fresh sub-agent with no
158
159
  chat context: open every path the plan names, start every proof command,
@@ -196,9 +197,11 @@ decide:
196
197
  - Its id is not listed: the claim is stale only when no other session in this
197
198
  checkout is live — then say so, take over, write your claim. When one is live,
198
199
  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): a busy session in this checkout, or plan
200
- files dirty in `git status`: stop. Otherwise say what you saw in one line, write
201
- your claim, go on.
200
+ - `none`, or no line (an older plan): say what you saw in one line, write your
201
+ claim, go on — unless this plan's files are dirty in `git status`, or a busy
202
+ session in this checkout has its id on no plan's `session:` line; then stop
203
+ and ask. A busy peer whose id is on another plan's line is working that plan,
204
+ so two sessions can work two plans in one checkout side by side.
202
205
  - No `claude` command (another agent), or a plan shared across machines: there is
203
206
  no listing to consult. Git is the record: pull first, read a `doing` row with a
204
207
  fresh `updated:` stamp as someone's work in flight, and ask.
@@ -217,14 +220,20 @@ The loop for each task:
217
220
 
218
221
  1. **Check who holds the plan** (above), then **set it doing.** Change the status
219
222
  cell to `doing`. Update **NOW**, and put your session on its `session:` line
220
- (`since` from `date` on a takeover; unchanged when it already names you).
223
+ (`since` from the shell clock on a takeover; unchanged when it already names
224
+ you).
221
225
  2. **Do the work.** Fix the cause, not the symptom. The simplest change that
222
226
  works, end to end.
223
227
  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`, never from memory.
228
+ line of output. Take the time from the shell clock (`date`; on Windows
229
+ `Get-Date`), never from memory.
225
230
  4. **Paste the evidence.** Into the task's evidence cell:
226
231
  `2026-09-12 14:20 · exit 0 · "6 passed"`. Exit 0, or the task is not done.
227
- 5. **Set it done.** Only now. Update **NOW** to point at the next task.
232
+ 5. **Set it done.** Only now. A proof of `owner` is closed only by the person's
233
+ words with the date, never by you: until then the row is `blocked`, the
234
+ reason in its evidence cell, and RESUME says so.
235
+ Update **NOW** to point at the next task; NOW keeps its four lines and their
236
+ names, RESUME included, until the plan is retired.
228
237
  6. **Append an entry to LOG.md** — what landed, what is next, anything learned,
229
238
  any decision made.
230
239
  7. **If the task fought back, record the learning.** An error, a wrong turn, an
@@ -364,20 +373,25 @@ status: active · opened <YYYY-MM-DD> · id: <kebab-id>
364
373
  ## NOW
365
374
  RESUME: <T2 — the one thing to do next, with the file; written for a stranger>
366
375
  NEXT: <T3 · T4 · …>
367
- updated: <YYYY-MM-DD HH:MM, from `date`>
376
+ updated: <YYYY-MM-DD HH:MM, from the shell clock>
368
377
  session: <none, or the session working this plan: name · the first 8 characters of its session id · since YYYY-MM-DD HH:MM>
369
378
 
370
379
  ## How to work this plan
371
380
  Read NOW, then Rules and Learnings; do not re-read Context. One task at a time:
372
- 1. Set it `doing`; point NOW at it; put your session on its `session:` line.
381
+ 1. Check who holds the plan (below); set it `doing`; point NOW at it; put your session on its `session:` line.
373
382
  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` run at that moment, never typed from memory.
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.
383
+ 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.
384
+ 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
385
  5. If it fought back, add a Learning: the trap, then the rule. A verified fact goes in Context, a choice in Decisions.
377
386
 
378
- NOW is four lines for a stranger; on a long task note the sub-step, and update it before any turn ends. Blocked: say so in NOW, 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 or a quoted heredoc; 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. Full method: `.project-management/planrails/PLANNER.md` §4.
387
+ NOW is exactly these four lines, RESUME, NEXT, updated and session, written for a stranger; on a long task note the sub-step in RESUME, and update NOW before any turn ends. Blocked: say so in RESUME, reason in the evidence cell. A task you will not do is `dropped`; its row stays. After a compaction, `git status --short` shows the in-flight work. Edit this file with the editor tool; an unquoted shell string eats backticks. A self-contained task may go to a sub-agent briefed with its row, Rules, Decisions, Learnings and Context; it writes to a named file and reports a few lines, which are leads; you run the proof before pasting evidence. If the project runs Node, `node .project-management/planrails/check-plans.mjs` must pass. When no row is left open, done or dropped, close by PLANNER.md §5: proofs re-run, a fresh-context review, `status: done`, `session: none`, the reload line in CLAUDE.md wrapped in backticks. Full method: `.project-management/planrails/PLANNER.md` §4.
379
388
 
380
- The `session:` line names the one session working this plan: name · first 8 characters of its session id · since. Match by id; names change. `claude agents --json` lists every live session on this machine with `sessionId`, `cwd` and `status`; in Claude Code your id is `$CLAUDE_CODE_SESSION_ID`. Before any task goes `doing`, even when asked to continue: re-read NOW from disk, run that listing and `git status --short`. Stop, report what you found, and ask before writing to the plan, its files, or a commit if the line's id is live and not yours, or if the line is `none` or missing while another session in this checkout is busy or plan files are dirty. If the id is not listed and no other session in this checkout is live, the claim is stale: say so, take over. On `doing` and on a takeover, write your own name · id · since, from `date`. Without a `claude` command, or on another machine, git is the record: pull first, treat a fresh `doing` row as someone's work in flight, and ask. Other reloaded plans are context; work only the one assigned in this session, and say which.
389
+ Who holds the plan. `session:` names the one session working it: `name · first 8 characters of its session id · since YYYY-MM-DD HH:MM`. Match by id, never by name: the line's 8 characters start a `sessionId` in the listing. Before any task goes `doing`, even when asked to continue: re-read NOW from disk, run `claude agents --json` (your own id is `$CLAUDE_CODE_SESSION_ID`) and `git status --short`, then:
390
+ - the id is yours: go on.
391
+ - the id is live and not yours: stop, report what you found, ask; write nothing.
392
+ - 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.
393
+ - `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.
394
+ 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
395
 
382
396
  ## Goal
383
397
  <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,8 @@ 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's `RESUME` line must name a task that is still open
164
+ - an active plan keeps its `RESUME` line, and that line must name a task that
165
+ is still open
165
166
 
166
167
  `--verify` goes further and **runs** each proof again, with a timeout, and shows
167
168
  the last line a failing proof printed. Because it executes the commands written in
@@ -192,11 +193,16 @@ directory, so a second session can read the plan and start the task the first on
192
193
  is on. A plan's NOW names the session working it
193
194
  (`session: app-3f · 7c1d2e9a · since 2026-09-12 14:05`), and the plan's block tells
194
195
  every session, before it sets a task `doing`, to re-read that line, list the live
195
- sessions (`claude agents --json`; `ListAgents` inside Claude Code) and run
196
- `git status --short`, and to stop and ask when another live session holds the plan
197
- — even when you asked it to continue, because you can forget which window owns
198
- it. One plan per session, one session per plan; the other reloaded plans are
199
- context. The claim is a lead, not a lock: a line left by a closed terminal blocks
196
+ sessions (`claude agents --json`, matching the line's id; `ListAgents` inside
197
+ Claude Code names sessions but shows no id) and run `git status --short`, and to
198
+ stop and ask when another live session holds the plan — even when you asked it to
199
+ continue, because you can forget which window owns it. One plan per session, one
200
+ session per plan; the other reloaded plans are context. A session assigned an
201
+ unheld plan claims it and goes on, unless that plan's files are dirty or a busy
202
+ peer in this checkout holds no plan, so two sessions can work two plans side by
203
+ side. A plan
204
+ written before 0.5 has no `session:` line; `init` points it out, and the
205
+ changelog says what to add by hand. The claim is a lead, not a lock: a line left by a closed terminal blocks
200
206
  nobody, and the checker ignores the line. Field-tested with headless sessions: a
201
207
  second session told "Continue the active plan." stopped and asked while the first
202
208
  was live — by its id, after the first had been renamed — took over when it was
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 } 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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "planrails",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
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.1
2
+ // planrails 0.5.2
3
3
  /**
4
4
  * check-plans — the machine-checked rails of the planner.
5
5
  *
@@ -11,8 +11,8 @@
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: the RESUME line of an active plan may not name only
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
18
  * The rule is biased toward catching a faked "done": a task counts as a
@@ -229,8 +229,11 @@ export function checkPlan({ id, text, verify = false, run = null }) {
229
229
  if (code !== 0) problems.push(`${id} ${t.id}: proof re-run failed — \`${cmd}\` exited ${code}${r.last ? ` — ${r.last}` : ""}`);
230
230
  }
231
231
  }
232
- // NOW must point somewhere live: a RESUME line that names only finished tasks is stale.
232
+ // NOW must point somewhere live: an active plan keeps its RESUME line, and a
233
+ // RESUME line that names only finished tasks is stale.
233
234
  if (isActive(text)) {
235
+ const ls = text.split(/\r?\n/), fm = fenceMask(ls);
236
+ 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
237
  const named = resumeIds(text, tasks.map((t) => t.id).filter(Boolean));
235
238
  if (named.length && named.every((n) => tasks.some((t) => t.id === n && isCompletionClaim(t.status))))
236
239
  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`);