planrails 0.1.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE +21 -0
  3. package/README.md +225 -0
  4. package/bin/planrails.mjs +7 -0
  5. package/docs/PLANNING_GUIDE.md +632 -0
  6. package/package.json +51 -0
  7. package/src/hooks/_lib.mjs +32 -0
  8. package/src/hooks/guard-never-delete.sh +29 -0
  9. package/src/hooks/install.mjs +173 -0
  10. package/src/hooks/plan-pre-tool.mjs +71 -0
  11. package/src/hooks/plan-session-start.mjs +46 -0
  12. package/src/hooks/plan-stop.mjs +60 -0
  13. package/src/hooks/plan-subagent-start.mjs +27 -0
  14. package/src/hooks/postcompact-journal.mjs +35 -0
  15. package/src/hooks/precompact-journal.mjs +128 -0
  16. package/src/hooks/selftest.mjs +128 -0
  17. package/src/init.mjs +155 -0
  18. package/src/issue.mjs +55 -0
  19. package/src/plan/fixtures/README.md +7 -0
  20. package/src/plan/fixtures/broken-cli.mjs +27 -0
  21. package/src/plan/fixtures/broken-hooks-root/.claude/settings.json +83 -0
  22. package/src/plan/fixtures/broken-hooks-root/.project-management/plans/.gitkeep +0 -0
  23. package/src/plan/fixtures/broken-hooks-root/CLAUDE.md +9 -0
  24. package/src/plan/fixtures/broken-root/.project-management/plans/broken/PLAN.md +4 -0
  25. package/src/plan/fixtures/broken-root/.project-management/plans/broken/gates.json +1 -0
  26. package/src/plan/fixtures/broken-root/.project-management/plans/broken/rules.json +1 -0
  27. package/src/plan/fixtures/broken-root/.project-management/plans/broken/state.json +67 -0
  28. package/src/plan/fixtures/broken-root/CLAUDE.md +3 -0
  29. package/src/plan/fixtures/broken-trial.mjs +25 -0
  30. package/src/plan/lib/brief.mjs +116 -0
  31. package/src/plan/lib/claude-md.mjs +66 -0
  32. package/src/plan/lib/glob.mjs +81 -0
  33. package/src/plan/lib/judgment.mjs +19 -0
  34. package/src/plan/lib/paths.mjs +65 -0
  35. package/src/plan/lib/schema.mjs +199 -0
  36. package/src/plan/lib/store.mjs +338 -0
  37. package/src/plan/lib/time.mjs +21 -0
  38. package/src/plan/plan.mjs +843 -0
  39. package/src/plan/run.mjs +88 -0
  40. package/src/plan/skill/SKILL.md +15 -0
@@ -0,0 +1,88 @@
1
+ /**
2
+ * ONE FRESH SESSION PER TASK — the honest answer to "clear the context after
3
+ * every task".
4
+ *
5
+ * Claude Code offers no way for the model, a hook or a skill to run /compact
6
+ * or /clear (measured against the docs, 2026-09-12; decision D6 of the
7
+ * planning-system plan). What it does offer is `claude -p`: a session that
8
+ * starts empty, gets the plan's brief from the SessionStart hook, and ends.
9
+ * This driver starts one such session per task. The session's only memory of
10
+ * the work is the plan's files — exactly what a /clear would leave.
11
+ *
12
+ * Each session is told to: task start → work → plan log → task check →
13
+ * task done with every condition answered. The driver then reads state.json
14
+ * (never the session's words) to decide what happened:
15
+ * done → next task
16
+ * blocked → stop and say why (a person decides)
17
+ * not done → ONE retry with the RESUME line, then stop
18
+ * A task with no gate (a manualCheck) is a person's to close; the driver stops.
19
+ *
20
+ * Sessions run with --dangerously-skip-permissions, as unattended work must;
21
+ * the project's hooks (rules, guards) still apply inside
22
+ * them. Use --max-tasks to bound a run and --dry-run to see the prompt.
23
+ */
24
+ import { spawnSync } from "node:child_process";
25
+ import { mkdirSync, appendFileSync } from "node:fs";
26
+ import { join } from "node:path";
27
+ import { projectRoot, runsRoot, cliName } from "./lib/paths.mjs";
28
+ import { loadPlan } from "./lib/store.mjs";
29
+ import { nowIso } from "./lib/time.mjs";
30
+ import { JUDGMENT } from "./lib/judgment.mjs";
31
+
32
+ const CLI = cliName();
33
+
34
+ export function nextTask(plan) {
35
+ const t = plan.state.tasks;
36
+ const doneOrDropped = (id) => ["done", "dropped"].includes(t.find((x) => x.id === id)?.status);
37
+ return t.find((x) => x.status === "doing") || t.find((x) => x.status === "todo" && x.dependsOn.every(doneOrDropped)) || null;
38
+ }
39
+
40
+ export function promptFor(plan, task, { retry = false } = {}) {
41
+ const id = plan.state.id;
42
+ const last = plan.log.at(-1);
43
+ return [
44
+ `You are executing ONE task of plan "${id}" in a fresh session. The plan's brief was injected at session start; if you do not see it, run: ${CLI} brief ${id}`,
45
+ retry ? `A previous session started this task and did not finish. Its RESUME line: "${last?.next || "(none)"}". Check git status and the files before redoing anything.` : "",
46
+ `TASK ${task.id}: ${task.title}${task.effort ? ` (think at effort ${task.effort})` : ""}${task.notes ? `\nNotes: ${task.notes}` : ""}`,
47
+ `Steps, in order:`,
48
+ `1. ${CLI} task start ${id} ${task.id}`,
49
+ `2. Read .project-management/plans/${id}/PLAN.md § Method and § Rules for this plan. Rules for specific files arrive automatically when you touch them.`,
50
+ `3. Do the work — only this task. Files it lists: ${task.files.join(", ") || "(see PLAN.md § Map)"}.`,
51
+ `4. ${CLI} log ${id} --task ${task.id} --what "<what landed, with numbers and paths>" --next "<the exact next action for a stranger>"`,
52
+ `5. ${CLI} task check ${id} ${task.id} (shows every condition of done)`,
53
+ `6. ${CLI} task done ${id} ${task.id} --answer "C4: …" … --answer "C7: …" (one --answer per manual condition that step 5 lists, saying what you checked; it runs the gate)`,
54
+ `If the gate fails: fix the CAUSE and run step 6 again. Never edit the gate, the test or the data so that it passes. If you believe the gate is wrong, or you cannot fix the cause: ${CLI} task block ${id} ${task.id} --reason "<what the gate computed, and what is true>" --needs owner — then stop.`,
55
+ ...JUDGMENT,
56
+ `Record anything learned with ${CLI} learn, any choice with ${CLI} decide. Do not start any other task. Stop when ${task.id} is done or blocked.`,
57
+ ].filter(Boolean).join("\n");
58
+ }
59
+
60
+ export async function runPlan({ id, maxTasks = 1, model = "sonnet", maxTurns = 60, dryRun = false, log = console.log }) {
61
+ const journal = runsRoot();
62
+ mkdirSync(journal, { recursive: true });
63
+ for (let n = 0; n < maxTasks; n++) {
64
+ let plan = loadPlan(id);
65
+ if (plan.state.status !== "active") { log(`${id} is ${plan.state.status}; nothing to run`); return 0; }
66
+ const task = nextTask(plan);
67
+ if (!task) { log(`${id}: no task is ready (all done, dropped, blocked, or waiting on a dependency)`); return 0; }
68
+ if (!task.gate) { log(`${id}: ${task.id} has no gate — "${task.manualCheck}" — a person closes it; stopping`); return 0; }
69
+ for (let attempt = 0; attempt < 2; attempt++) {
70
+ const prompt = promptFor(plan, task, { retry: attempt > 0 || task.status === "doing" });
71
+ if (dryRun) { log(`--- would run (task ${task.id}, attempt ${attempt + 1}) ---\n${prompt}`); return 0; }
72
+ log(`▶ ${task.id} ${task.title} — session ${attempt + 1} (${model}, ≤ ${maxTurns} turns)`);
73
+ const t0 = Date.now();
74
+ const r = spawnSync("claude", ["-p", prompt, "--output-format", "json", "--model", model, "--max-turns", String(maxTurns), "--dangerously-skip-permissions"],
75
+ { cwd: projectRoot(), encoding: "utf8", timeout: 3_600_000, maxBuffer: 64 * 1024 * 1024 });
76
+ let parsed = null; try { parsed = JSON.parse(r.stdout); } catch { /* raw */ }
77
+ plan = loadPlan(id);
78
+ const after = plan.state.tasks.find((x) => x.id === task.id);
79
+ const rec = { at: nowIso(), plan: id, task: task.id, attempt: attempt + 1, session_id: parsed?.session_id || null, exit: r.status, is_error: Boolean(parsed?.is_error), turns: parsed?.num_turns ?? null, ms: Date.now() - t0, statusAfter: after?.status || null };
80
+ appendFileSync(join(journal, `${id}.jsonl`), JSON.stringify(rec) + "\n");
81
+ log(` session ${rec.session_id || "?"}: exit ${rec.exit}${rec.is_error ? " (error)" : ""}, ${rec.turns} turns, ${Math.round(rec.ms / 1000)} s → ${task.id} is ${rec.statusAfter}`);
82
+ if (rec.statusAfter === "done") break;
83
+ if (rec.statusAfter === "blocked") { log(` ${task.id} is blocked: ${after.blocked?.reason} (needs ${after.blocked?.needs}) — stopping`); return 2; }
84
+ if (attempt === 1) { log(` ${task.id} still ${rec.statusAfter} after two sessions — stopping; read the log and .planrails/runs/${id}.jsonl`); return 1; }
85
+ }
86
+ }
87
+ return 0;
88
+ }
@@ -0,0 +1,15 @@
1
+ ---
2
+ name: plan
3
+ description: Create a self-sufficient plan for work that spans sessions — gates that prove "done", schema-checked tracking JSON, rules injected by hooks at the step they apply to, and one pointer in CLAUDE.md. Use when the user gives a raw plan or asks to plan a feature, a programme, a migration or a research task.
4
+ argument-hint: <raw plan text, or a path to a file holding it>
5
+ ---
6
+ You are creating a plan with this project's planning system. You are NOT doing the engineering. The session that executes the plan will not see this conversation, so everything it needs must be in the plan's files.
7
+
8
+ 1. Read `docs/PLANNING_GUIDE.md` in full. § Creating a plan is the procedure; follow its steps in order and do not skip the research step.
9
+ 2. The raw plan is below. If it is a path, read that file first.
10
+ 3. Ask the user only the questions in the guide's list that the raw plan does not answer, in ONE message. Then proceed.
11
+ 4. Produce the plan directory, run `npx planrails validate <id>` until it is clean, verify every gate can fail (`gate verify <id> --all`), activate it, and end your reply with the brief and one line saying what changed in CLAUDE.md.
12
+
13
+ Raw plan:
14
+
15
+ $ARGUMENTS