planrails 0.2.0 → 0.3.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,49 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0 — 2026-09-12
4
+
5
+ Two changes, both asked for by a user planning a long, multi-session feature:
6
+ carry learnings forward so a mistake is not re-paid in the next chat, and tidy the
7
+ installed files into one folder.
8
+
9
+ - **Learnings are now a first-class, reloaded part of a plan.** `PLAN.md` gains a
10
+ `## Learnings` section: each line is the trap and the rule it taught. It reloads
11
+ with the plan at the start of every session — and after every compaction —
12
+ through the same reload line, so the next session reads the lesson before it
13
+ repeats the struggle. A learning is distinct from a `Rule` (a constraint known
14
+ up front) and a `Decision` (a choice and why). `PLANNER.md` makes capturing a
15
+ learning a step of the execute loop, written the moment a task fights back; tells
16
+ a resuming session to read the Learnings first; and graduates a learning that
17
+ outlives the plan to the always-loaded file. Until now the only place for a
18
+ lesson was a `LOG.md` line, and the log is neither reloaded nor re-read — which
19
+ is exactly why the same struggle returned in each new session.
20
+ - **The two installed files move into `.project-management/planrails/`.** `init`
21
+ now writes `.project-management/planrails/PLANNER.md` and
22
+ `.project-management/planrails/check-plans.mjs`, leaving `.project-management/`
23
+ holding just two folders: `planrails/` (the tool) and `plans/` (your plans). The
24
+ reload line is unchanged — `@.project-management/plans/<id>/PLAN.md`. If you ran
25
+ an older `init`, point your check command at the new path
26
+ (`node .project-management/planrails/check-plans.mjs`) and remove the two loose
27
+ files left at the `.project-management/` root.
28
+ - The checker's behaviour is unchanged: learnings are prose, enforced by the
29
+ method, not the gate, and a plan with no `Learnings` section still validates. A
30
+ test pins that a `Learnings` section does not confuse the parser. 34 tests.
31
+
32
+ ## 0.2.1 — 2026-09-12
33
+
34
+ A careful post-release review, including an end-to-end test of the published
35
+ package, found one robustness gap in the checker and fixed it.
36
+
37
+ - The checker finds a task table by its columns (`id`, `status`, `proof`,
38
+ `evidence`), not by a `## Tasks` heading. So tasks under `## Phase 2 Tasks`,
39
+ `## Backlog`, any heading, or none at all are all gated — a phased plan is no
40
+ longer rejected with a confusing "no readable Tasks table". A table under a
41
+ literal `## Tasks` heading that is missing a column still names which one.
42
+ - Verified against the live 0.2.0 package: `npx planrails init` copies only into
43
+ `.project-management/`, leaves `package.json` and a pnpm lockfile untouched,
44
+ writes no `node_modules` and no `CLAUDE.md`, and the checker catches every
45
+ faked-"done" bypass. 33 tests.
46
+
3
47
  ## 0.2.0 — 2026-09-12
4
48
 
5
49
  Rebuilt as a planner prompt and one checker, with a two-command CLI that only
package/PLANNER.md CHANGED
@@ -36,6 +36,13 @@ Three rules make the plan trustworthy. They are the whole point of this system:
36
36
  code and last line are pasted into the plan. An empty evidence cell is not
37
37
  done, whatever the status column says.
38
38
 
39
+ The plan is also the project's memory. Besides the tasks, PLAN.md carries the
40
+ **Rules** you must not break, the **Decisions** you made and why, and the
41
+ **Learnings** — a mistake or dead end, written as the rule that avoids it next
42
+ time. The reload line brings all of it back at the start of every session, so a
43
+ lesson learned in one chat is read by the next one *before* it repeats the
44
+ struggle. That is how you stop paying for the same mistake twice.
45
+
39
46
  ---
40
47
 
41
48
  ## §1 Get ready
@@ -110,16 +117,21 @@ Pick a short kebab-case `<id>` (`weekly-digest`). Then:
110
117
  back on its own. (For a tool that does not do `@`-imports, put the plan's path
111
118
  in `AGENTS.md` and open it by hand at the start of each session.)
112
119
  - **Wire the checker, if the project runs Node and has a check command.**
113
- `npx planrails init` already put it at `.project-management/check-plans.mjs`; if
114
- you did not run init, copy this repo's `tools/check-plans.mjs` there. Add
115
- `node .project-management/check-plans.mjs` to the check command. Now the build
116
- fails if a task is marked done with no evidence. If the project is not Node,
120
+ `npx planrails init` already put it at `.project-management/planrails/check-plans.mjs`;
121
+ if you did not run init, copy this repo's `tools/check-plans.mjs` there. Add
122
+ `node .project-management/planrails/check-plans.mjs` to the check command. Now the
123
+ build fails if a task is marked done with no evidence. If the project is not Node,
117
124
  skip this; the plan still works, and you enforce the gate yourself.
118
125
 
119
126
  ---
120
127
 
121
128
  ## §4 Execute — one task at a time
122
129
 
130
+ **At the start of every session, read the plan back first.** The reload line has
131
+ already loaded PLAN.md. Read **NOW**, then the **Rules** and the **Learnings**,
132
+ before you touch anything. The Learnings are mistakes a past session already paid
133
+ for — read them and you skip the struggle instead of repeating it.
134
+
123
135
  The loop for each task:
124
136
 
125
137
  1. **Set it doing.** Change the status cell to `doing`. Update **NOW**.
@@ -131,6 +143,10 @@ The loop for each task:
131
143
  5. **Set it done.** Only now. Update **NOW** to point at the next task.
132
144
  6. **Append one line to LOG.md** — what landed, what is next, anything learned,
133
145
  any decision made.
146
+ 7. **If the task fought back, record the learning.** An error, a wrong turn, an
147
+ hour lost before you found the cause — add it to PLAN.md under **Learnings** as
148
+ "trap → rule", with the real case. LOG.md holds what happened; Learnings holds
149
+ the rule, because Learnings reloads every session and the log does not.
134
150
 
135
151
  **Update NOW before you end any turn.** NOW is the first thing a fresh session
136
152
  reads. If it is stale, the next session repeats your work or starts in the wrong
@@ -158,6 +174,9 @@ the task `blocked` with the reason, and stop. Do not hand-fix state to look done
158
174
  3. **Retire the plan.** Move its reload line out of "Active plans" into a
159
175
  "Finished" list (or delete the line). The plan files stay on disk; they are the
160
176
  record.
177
+ 4. **Graduate any lasting learning.** A learning that is true beyond this feature
178
+ moves to the always-loaded file (`CLAUDE.md` / `AGENTS.md`), so it outlives the
179
+ plan you are retiring. One that was only about this work retires with it.
161
180
 
162
181
  ---
163
182
 
@@ -172,6 +191,11 @@ Each line here was paid for by a real failure in earlier planning systems:
172
191
  A named command that must be run and pasted can.
173
192
  - **Evidence at close** is the one machine-checkable rail worth keeping. The
174
193
  checker enforces exactly this and nothing else.
194
+ - **Learnings live in the plan, not only the log.** The log is history a fresh
195
+ session does not re-read; the plan is reloaded every session. A mistake written
196
+ as a rule, where the next session will see it, is the only kind that stops being
197
+ repeated. The same struggle coming back in a new chat is the exact failure this
198
+ fixes.
175
199
  - **Sub-agent findings are leads** because four spot-checked findings were each
176
200
  right in direction and wrong in number, and a wrong number becomes a wrong plan.
177
201
  - **Repo-relative paths** because absolute paths break on the next machine, and a
@@ -181,8 +205,10 @@ Each line here was paid for by a real failure in earlier planning systems:
181
205
  package, and that is what broke on the projects that were not npm — Python,
182
206
  pnpm, bun, monorepos, Windows. A prompt and a copied script work everywhere.
183
207
 
184
- Keep it this simple. If you are tempted to add a config file, a second script, or
185
- a fourth rule, you are rebuilding the thing this replaced.
208
+ Keep it this simple. The three rails reload, proof, evidence are the whole
209
+ machine-checked core. If you are tempted to add a config file, a second script, or
210
+ a fourth rail, you are rebuilding the thing this replaced. (Rules, Decisions and
211
+ Learnings are plain sections of the plan, not new machinery.)
186
212
 
187
213
  ---
188
214
 
@@ -232,6 +258,14 @@ updated: <YYYY-MM-DD HH:MM>
232
258
  |------|----------|-----|
233
259
  | <YYYY-MM-DD> | <what was chosen> | <the reason and what it rules out> |
234
260
 
261
+ ## Learnings
262
+ <!-- Mistakes already paid for, so no later session repeats them. Each line: the
263
+ trap, then the rule it taught, with the real case. Not a Rule (a constraint
264
+ known up front) and not a Decision (a choice between options) — a learning is
265
+ what a failure taught you. Add one the moment a task fights back. This section
266
+ reloads with the plan every session; that is what makes the lesson stick. -->
267
+ - <the trap you hit> → <the rule that avoids it> (<the real case, one line>)
268
+
235
269
  ## Context (read during planning — do not re-read)
236
270
  - <path> — <one line of what it holds>
237
271
  ````
@@ -251,5 +285,6 @@ Append-only. One entry per landed piece of work, newest at the bottom.
251
285
  - files: <repo-relative paths touched>
252
286
  - proof: <the command and its result — e.g. `npx vitest run tests/digest.test.ts` → exit 0, "6 passed">
253
287
  - next: <what comes next; where you stopped if you paused>
254
- - learned / decided: <anything a second reader needs; omit if nothing>
288
+ - learned / decided: <anything a second reader needs; omit if nothing. A durable
289
+ trap also goes to PLAN.md § Learnings, which reloads every session>
255
290
  ````
package/README.md CHANGED
@@ -28,15 +28,16 @@ From the root of your project:
28
28
  npx planrails init
29
29
  ```
30
30
 
31
- That copies two files into `.project-management/` and makes the `plans/` folder.
32
- It writes **nothing else** — no `package.json`, no `npm install`, no hooks, no
33
- edits to your `CLAUDE.md`. Run it again any time; it skips files that already
34
- exist. What lands:
31
+ That copies two files into `.project-management/planrails/` and makes the
32
+ `plans/` folder. It writes **nothing else** — no `package.json`, no `npm install`,
33
+ no hooks, no edits to your `CLAUDE.md`. Run it again any time; it skips files that
34
+ already exist. What lands:
35
35
 
36
36
  ```
37
37
  .project-management/
38
- PLANNER.md the prompt your agent follows to plan and execute
39
- check-plans.mjs the checker (the one machine-enforced rail)
38
+ planrails/
39
+ PLANNER.md the prompt your agent follows to plan and execute
40
+ check-plans.mjs the checker (the one machine-enforced rail)
40
41
  plans/ your plans will live here, one folder each
41
42
  ```
42
43
 
@@ -44,8 +45,8 @@ Then, two steps:
44
45
 
45
46
  1. **Tell your agent to plan with you.** In any coding agent:
46
47
 
47
- > Follow `.project-management/PLANNER.md` and tell me when you are ready to
48
- > plan the next feature with me.
48
+ > Follow `.project-management/planrails/PLANNER.md` and tell me when you are
49
+ > ready to plan the next feature with me.
49
50
 
50
51
  It reads your repo, reports what it found in eight lines, and waits. Then you
51
52
  plan together, and it writes the plan and wires the reload line.
@@ -54,7 +55,7 @@ Then, two steps:
54
55
  every commit (your `check` / `lint` / CI script):
55
56
 
56
57
  ```bash
57
- node .project-management/check-plans.mjs
58
+ node .project-management/planrails/check-plans.mjs
58
59
  ```
59
60
 
60
61
  Now the build fails if any task is marked done without pasted evidence.
@@ -76,9 +77,9 @@ The command is the same; the difference is what your agent sees.
76
77
  ### Without npm, or a non-Node project
77
78
 
78
79
  No npm? Copy the two files by hand from this repo:
79
- [`PLANNER.md`](PLANNER.md) → `.project-management/PLANNER.md`, and
80
+ [`PLANNER.md`](PLANNER.md) → `.project-management/planrails/PLANNER.md`, and
80
81
  [`tools/check-plans.mjs`](tools/check-plans.mjs) →
81
- `.project-management/check-plans.mjs`. Make a `.project-management/plans/`
82
+ `.project-management/planrails/check-plans.mjs`. Make a `.project-management/plans/`
82
83
  folder. Done.
83
84
 
84
85
  The checker needs Node to run. If your project has no Node at all, skip it — the
@@ -93,7 +94,11 @@ beside it in the same folder. Then `/plan` starts the same flow in any project.
93
94
  ## How a plan works
94
95
 
95
96
  Each plan is two files: `PLAN.md` (the map and tracker) and `LOG.md` (append-only
96
- history). The top of `PLAN.md` is what a fresh session reads first:
97
+ history). `PLAN.md` also carries the plan's memory the rules to keep, the
98
+ decisions made, and the **learnings** (a mistake, written as the rule that avoids
99
+ it). Because the reload line brings `PLAN.md` back at the start of every session, a
100
+ lesson from one chat is read by the next one before it repeats the struggle. The
101
+ top of `PLAN.md` is what a fresh session reads first:
97
102
 
98
103
  ```
99
104
  ## NOW
@@ -115,9 +120,9 @@ is in [`examples/weekly-digest/`](examples/weekly-digest).
115
120
  ## The checker
116
121
 
117
122
  ```bash
118
- node .project-management/check-plans.mjs # done tasks must name a proof and carry evidence
119
- node .project-management/check-plans.mjs --verify # also re-run each done task's proof, expect exit 0
120
- npx planrails check # the same, using the latest published checker
123
+ node .project-management/planrails/check-plans.mjs # done tasks must name a proof and carry evidence
124
+ node .project-management/planrails/check-plans.mjs --verify # also re-run each done task's proof, expect exit 0
125
+ npx planrails check # the same, using the latest published checker
121
126
  ```
122
127
 
123
128
  No dependencies. Node 20+, any OS (Windows included). The default is a fast
@@ -147,6 +152,8 @@ review found ten data-loss and silent-failure paths in that surface, and the
147
152
  "works with any project" promise broke on pnpm, bun, non-Node projects,
148
153
  monorepos and Windows. 0.2.0 keeps the idea and drops the weight: the same three
149
154
  rails, as a prompt plus one checker, with a two-command CLI that only copies
150
- files. See [`CHANGELOG.md`](CHANGELOG.md).
155
+ files. 0.3.0 makes learnings a reloaded part of every plan and groups the two
156
+ installed files under `.project-management/planrails/`. See
157
+ [`CHANGELOG.md`](CHANGELOG.md).
151
158
 
152
159
  MIT.
package/bin/planrails.mjs CHANGED
@@ -3,14 +3,14 @@
3
3
  * planrails — a planner prompt and one checker. Two commands, both safe:
4
4
  *
5
5
  * npx planrails init [--dir DIR] [--force]
6
- * Copies PLANNER.md and the checker into <project>/.project-management/ and
7
- * makes the plans/ folder. It writes nothing else — no package.json, no npm
6
+ * Copies PLANNER.md and the checker into <project>/.project-management/planrails/
7
+ * and makes the plans/ folder. It writes nothing else — no package.json, no npm
8
8
  * install, no hooks, no edits to your CLAUDE.md. Idempotent: it skips files
9
9
  * that already exist unless you pass --force.
10
10
  *
11
11
  * npx planrails check [--dir DIR] [--verify]
12
12
  * Runs the checker over the project's plans. --verify re-runs each done
13
- * task's proof. Same as running the copied .project-management/check-plans.mjs.
13
+ * task's proof. Same as running the copied .project-management/planrails/check-plans.mjs.
14
14
  *
15
15
  * planrails --version | --help
16
16
  */
@@ -28,8 +28,8 @@ const HELP = `planrails ${version()} — a planner prompt and one checker.
28
28
  npx planrails check [--dir DIR] [--verify] check the project's plans
29
29
  planrails --version | --help
30
30
 
31
- After init, tell your agent: Follow .project-management/PLANNER.md and plan <the feature> with me.
32
- Add to the command you run before every commit: node .project-management/check-plans.mjs
31
+ After init, tell your agent: Follow .project-management/planrails/PLANNER.md and plan <the feature> with me.
32
+ Add to the command you run before every commit: node .project-management/planrails/check-plans.mjs
33
33
  Full guide: https://github.com/vivmagarwal/planrails#readme`;
34
34
 
35
35
  function flag(args, name) {
@@ -43,7 +43,9 @@ function init(args) {
43
43
  const target = resolve(flag(args, "--dir") || ".");
44
44
  const force = args.includes("--force");
45
45
  const pm = join(target, ".project-management");
46
+ const sys = join(pm, "planrails");
46
47
  const plans = join(pm, "plans");
48
+ mkdirSync(sys, { recursive: true });
47
49
  mkdirSync(plans, { recursive: true });
48
50
 
49
51
  const copy = (from, to, label) => {
@@ -52,18 +54,18 @@ function init(args) {
52
54
  console.log(` + ${label}`);
53
55
  };
54
56
  console.log(`planrails ${version()} → ${target}`);
55
- copy(join(ROOT, "PLANNER.md"), join(pm, "PLANNER.md"), ".project-management/PLANNER.md");
56
- copy(join(ROOT, "tools", "check-plans.mjs"), join(pm, "check-plans.mjs"), ".project-management/check-plans.mjs");
57
+ copy(join(ROOT, "PLANNER.md"), join(sys, "PLANNER.md"), ".project-management/planrails/PLANNER.md");
58
+ copy(join(ROOT, "tools", "check-plans.mjs"), join(sys, "check-plans.mjs"), ".project-management/planrails/check-plans.mjs");
57
59
  const keep = join(plans, ".gitkeep");
58
60
  if (!existsSync(keep)) { writeFileSync(keep, ""); console.log(" + .project-management/plans/"); }
59
61
  else console.log(" · .project-management/plans/ already present");
60
62
 
61
63
  console.log(`
62
64
  Next:
63
- 1. Tell your agent: Follow .project-management/PLANNER.md and plan <the feature> with me.
65
+ 1. Tell your agent: Follow .project-management/planrails/PLANNER.md and plan <the feature> with me.
64
66
  (Or, in Claude Code, use /plan if you installed the skill.)
65
67
  2. Add to the command you run before every commit:
66
- node .project-management/check-plans.mjs
68
+ node .project-management/planrails/check-plans.mjs
67
69
  3. When the agent writes a plan, it adds one line to your CLAUDE.md so the plan
68
70
  reloads after every compaction: @.project-management/plans/<id>/PLAN.md`);
69
71
  return 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "planrails",
3
- "version": "0.2.0",
3
+ "version": "0.3.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",
@@ -11,8 +11,8 @@
11
11
  * (todo, doing, blocked, …). So no spelling of "done" — done, completed, ✅,
12
12
  * shipped, a typo — can slip through unchecked.
13
13
  *
14
- * `npx planrails init` copies this file into a project's .project-management/;
15
- * add `node .project-management/check-plans.mjs` to the command you run before
14
+ * `npx planrails init` copies this file into a project's .project-management/planrails/;
15
+ * add `node .project-management/planrails/check-plans.mjs` to the command you run before
16
16
  * every commit. No dependencies. Runs on Node 20+ on any OS.
17
17
  *
18
18
  * node check-plans.mjs # structural: completion claims need proof + evidence
@@ -80,41 +80,55 @@ const isRow = (l) => l.trim().startsWith("|");
80
80
  const isHeading = (l) => /^#{1,6}\s/.test(l.trim());
81
81
  const isSeparator = (l) => /^\|[\s:|-]+\|?\s*$/.test(l.trim());
82
82
 
83
+ const REQUIRED = ["id", "status", "proof", "evidence"];
84
+ /** The column indices of a row, and which required columns it is missing. */
85
+ function headerCols(rowText) {
86
+ const h = cells(rowText).map((c) => c.toLowerCase());
87
+ const ci = { id: h.indexOf("id"), task: h.indexOf("task"), status: h.indexOf("status"), proof: h.indexOf("proof"), evidence: h.indexOf("evidence") };
88
+ return { ci, missing: REQUIRED.filter((k) => ci[k] === -1) };
89
+ }
90
+
83
91
  /**
84
- * Parse the task rows of a plan. Scans EVERY "## Tasks" section; within a
85
- * section it collects all table rows to the next heading, so a blank line in the
86
- * middle does not end the table and a second table is not lost. Columns are
87
- * matched by name, in any order. Returns { found, tasks, missingCols }.
92
+ * Parse the task rows of a plan. A "task table" is any markdown table whose
93
+ * header carries the four columns id, status, proof, evidence found by its
94
+ * columns, not by a heading, so tasks under "## Tasks", "## Phase 2 Tasks",
95
+ * "## Backlog", or a second table are all read; blank lines inside a table are
96
+ * tolerated. Returns { found, tasks, missingCols }.
88
97
  */
89
98
  export function parseTasks(text) {
90
99
  const lines = text.split(/\r?\n/);
91
- let found = false;
92
100
  const tasks = [];
93
- const missingCols = new Set();
94
- for (let i = 0; i < lines.length; i++) {
95
- if (!/^#{1,6}\s+tasks\b/i.test(lines[i].trim())) continue;
96
- const rows = [];
101
+ let found = false;
102
+ let i = 0;
103
+ while (i < lines.length) {
104
+ if (!isRow(lines[i]) || isSeparator(lines[i]) || headerCols(lines[i]).missing.length) { i++; continue; }
105
+ found = true; // lines[i] is a task-table header (a | row naming all four columns)
106
+ const { ci } = headerCols(lines[i]);
97
107
  let j = i + 1;
98
108
  for (; j < lines.length; j++) {
99
- if (isHeading(lines[j])) break;
100
- if (isRow(lines[j])) rows.push({ text: lines[j], line: j + 1 });
101
- }
102
- i = j - 1;
103
- if (!rows.length) continue;
104
- const header = cells(rows[0].text).map((h) => h.toLowerCase());
105
- const ci = { id: header.indexOf("id"), task: header.indexOf("task"), status: header.indexOf("status"), proof: header.indexOf("proof"), evidence: header.indexOf("evidence") };
106
- const missing = ["id", "status", "proof", "evidence"].filter((k) => ci[k] === -1);
107
- if (missing.length) { missing.forEach((m) => missingCols.add(m)); continue; }
108
- found = true;
109
- for (const row of rows.slice(1)) {
110
- if (isSeparator(row.text)) continue;
111
- const c = cells(row.text);
109
+ const l = lines[j];
110
+ if (isHeading(l)) break; // a heading ends the table
111
+ if (l.trim() === "") continue; // a blank line inside the table does not
112
+ if (!isRow(l)) break; // prose ends the table
113
+ if (isSeparator(l)) continue;
114
+ if (headerCols(l).missing.length === 0) break; // the next table's header — reprocess it
115
+ const c = cells(l);
112
116
  const at = (idx) => (idx >= 0 && idx < c.length ? c[idx] : "");
113
- if (norm(at(ci.status)) === "status" && norm(at(ci.id)) === "id") continue; // a repeated header row
114
- tasks.push({ id: at(ci.id), task: at(ci.task), status: at(ci.status), proof: at(ci.proof), evidence: at(ci.evidence), line: row.line });
117
+ tasks.push({ id: at(ci.id), task: at(ci.task), status: at(ci.status), proof: at(ci.proof), evidence: at(ci.evidence), line: j + 1 });
118
+ }
119
+ i = j;
120
+ }
121
+ // A helpful message for the common slip: a "## Tasks" table missing one column.
122
+ let missingCols = [];
123
+ if (!found) {
124
+ for (let k = 0; k < lines.length && !missingCols.length; k++) {
125
+ if (!/^#{1,6}\s+tasks\b/i.test(lines[k].trim())) continue;
126
+ for (let m = k + 1; m < lines.length && !isHeading(lines[m]); m++) {
127
+ if (isRow(lines[m]) && !isSeparator(lines[m])) { const miss = headerCols(lines[m]).missing; if (miss.length && miss.length < REQUIRED.length) missingCols = miss; break; }
128
+ }
115
129
  }
116
130
  }
117
- return { found, tasks, missingCols: [...missingCols] };
131
+ return { found, tasks, missingCols };
118
132
  }
119
133
 
120
134
  /** The command inside a proof cell (backticks stripped), or null for `owner`/prose/empty. */
@@ -129,7 +143,7 @@ export function checkPlan({ id, text, verify = false, run = null }) {
129
143
  const { found, tasks, missingCols } = parseTasks(text);
130
144
  if (!found) {
131
145
  if (missingCols.length) problems.push(`${id}: the Tasks table is missing the ${missingCols.map((c) => `"${c}"`).join(", ")} column(s)`);
132
- else problems.push(`${id}: no readable Tasks table (needs a | id | task | status | proof | evidence | table under a "## Tasks" heading)`);
146
+ else problems.push(`${id}: no readable Tasks table (needs a | id | task | status | proof | evidence | table)`);
133
147
  return problems;
134
148
  }
135
149
  for (const t of tasks) {