@difflab/pi 0.7.0-rc.202609280043.c43aa2b → 0.7.0-rc.202609280945.f13c54b

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/README.md CHANGED
@@ -40,7 +40,7 @@ The skill selects and verifies the Planner or Orchestrator inline profile for fo
40
40
 
41
41
  `diffpi plan annotate <PLAN.md|directory>` opens the live plan in `tuicr --file`; it does not save a managed review. Zed setup installs the pinned `diffpi: annotate plan` task. Override the plan template at `~/.difflab/diffpi/templates/plan/PLAN.md`.
42
42
 
43
- `PLAN.md` keeps Design, numbered phases, phase prerequisites, and flat task checkboxes. Each brief contains ordered steps, nested verification, acceptance criteria, and one action-labeled phase file tree as its sole exact file scope. No per-task or `PLAN.md` file scopes. The Orchestrator alone updates execution status, runs project gates, and owns phase Git/CI actions. `--mode commit` creates one local conventional commit per completed phase. `--mode push` pushes each commit and waits for exact-SHA `watch_ci` results before advancing. Workers only edit source/test scopes derived from their task steps and the phase tree; uncertain or overlapping scopes are serialized.
43
+ `PLAN.md` keeps Design, numbered phases, phase prerequisites, phase constraints (or `None`), and flat task checkboxes. Each brief places a single action-labeled file tree in a fenced `text` block under `## Files Affected` immediately after `## Objective` (not bullet paths); tasks have ordered steps, nested verification, and acceptance criteria. `## Implementation Constraints` accepts free-form guidance with optional Required Libraries & Technology Choices, Key Algorithm Specifications, and Core Invariants headings. No per-task or `PLAN.md` file scopes. Keep phase-wide guardrails in `PLAN.md` and implementation details in briefs without repeating or paraphrasing the guardrails. The Orchestrator alone updates execution status, runs project gates, and owns phase Git/CI actions. `--mode commit` creates one local conventional commit per completed phase. `--mode push` pushes each commit and waits for exact-SHA `watch_ci` results before advancing. Workers only edit source/test scopes derived from their task steps and the Files Affected tree; uncertain or overlapping scopes are serialized.
44
44
 
45
45
  ## Review
46
46
 
@@ -33,7 +33,7 @@ Work as the execution orchestrator for the plan skill. Coordinate live plan file
33
33
 
34
34
  For a background review dispatch, create exactly one named background `reviewer` child for the selected workflow; the child must not recurse or dispatch another review child. Keep review lifecycle decisions in this orchestration turn.
35
35
 
36
- For background plan execution, read the live plan and honor its execution state. Run `plan_verify` on current live files before review or Worker dispatch; its mechanical PASS cannot replace a reviewer. A draft must receive both that PASS and a fresh, attested, **completed** `diffpi-plan-reviewer` PASS before you write READY or dispatch a Worker. If the child is steered/partial or reports BLOCKING findings, repair through Planner and run another fresh review; stop in DRAFT on exhaustion. A repair alone is never approval. Process phases in dependency order. The phase brief's single file tree is the only declared file scope. Derive a bounded Worker scope for each task from its steps and that tree; delegate one task at a time unless all prerequisites are complete and those derived scopes are unambiguously disjoint. Missing or uncertain task-to-file mapping always serializes Workers. Delegated workers never commit or restructure the plan. Reload plan state and persist each result before scheduling more work.
36
+ For background plan execution, read the live plan and honor its execution state. Run `plan_verify` on current live files before review or Worker dispatch; its mechanical PASS cannot replace a reviewer. A draft must receive both that PASS and a fresh, attested, **completed** `diffpi-plan-reviewer` PASS before you write READY or dispatch a Worker. If the child is steered/partial or reports BLOCKING findings, repair through Planner and run another fresh review; stop in DRAFT on exhaustion. A repair alone is never approval. Process phases in dependency order. The phase brief's fenced `Files Affected` tree is the only declared file scope. Derive a bounded Worker scope for each task from its steps and that tree; delegate one task at a time unless all prerequisites are complete and those derived scopes are unambiguously disjoint. Missing or uncertain task-to-file mapping always serializes Workers. Delegated workers never commit or restructure the plan. Reload plan state and persist each result before scheduling more work.
37
37
 
38
38
  After all phase tasks complete or skip, run project gates with bash or ctx_execute. A failure or warning blocks the phase. When the execution policy is `commit` or `push`, only you may invoke `/git commit --yes --no-push`, exactly once after gates pass. In `commit` mode, record the local SHA and do not push. In `push` mode, push the current branch, record the observed SHA and pending CI metadata in the live plan, then call `watch_ci` yourself for that exact SHA and collect its settled result before scheduling or pushing the next phase. Do not delegate CI monitoring to Worker. A failed or timed-out monitor blocks execution; preserve blocked state, attempts, and evidence. Skipped CI is acceptable only when the tool confirms that the forge or commit checks are unavailable.
39
39
 
@@ -23,10 +23,10 @@ Then, in one pass, read the current PLAN.md and every numbered phase brief selec
23
23
 
24
24
  Check all of these together:
25
25
 
26
- - Structure and parity: the live plan is directly under `.diffpi/plan/<YYMMDD[-ticket]-short-slug>/` (never nested `date/slug`), and `PLAN.md` uses `## Phases` and numbered `### Phase N:` headings, phase-only prerequisites, flat task checkboxes without nested task dependencies, ordered stable IDs, and exact task ID/title parity with every numbered brief.
27
- - Action-labeled trees: exactly one `## Phase File Tree` fenced `text` tree per brief; every file leaf starts with `[ADD]`, `[MODIFY]`, `[REMOVE]`, `[MOVE from: path]`, or `[VERIFY]` immediately after its tree branch (suffix labels are invalid). Verification is nested under its task.
28
- - Whole-plan quality: substantive intent, requirements, design, APIs/data flow, consequences, constraints, consistency, risks, and useful references.
29
- - Worker executability: every task has ordered steps, implementation constraints, acceptance criteria, and enough context for a lightweight Worker to act without guessing; the phase-level tree is the sole exact file scope.
26
+ - Structure and parity: the live plan is directly under `.diffpi/plan/<YYMMDD[-ticket]-short-slug>/` (never nested `date/slug`), and `PLAN.md` uses `## Phases` and numbered `### Phase N:` headings, phase-only prerequisites and documented phase constraints (or `None`), flat task checkboxes without nested task dependencies, ordered stable IDs, and exact task ID/title parity with every numbered brief.
27
+ - Files Affected: each brief has exactly one `## Files Affected` immediately after `## Objective` and before `## Tasks`. It contains exactly one fenced `text` file tree with a directory root and tree branches; every file leaf begins with `[ADD]`, `[MODIFY]`, `[REMOVE]`, `[MOVE from: path]`, or `[VERIFY]` immediately after its branch. Reject plain Markdown bullet lists, suffix action labels, the retired `## Phase File Tree` heading, and duplicate scopes under tasks or in `PLAN.md`. Verification is nested under its task.
28
+ - Whole-plan quality: substantive intent, requirements, design, APIs/data flow, consequences, phase constraints in `PLAN.md`, consistency, risks, and useful references.
29
+ - Worker executability: every task has ordered steps, acceptance criteria, and enough free-form `## Implementation Constraints` guidance for a lightweight Worker to act without guessing. Optional subheadings (Required Libraries & Technology Choices, Key Algorithm Specifications, Core Invariants) are never mandatory; phase-wide guardrails in PLAN.md are not restated or paraphrased in briefs, even if no `### Constraints` heading appears. The `Files Affected` tree is the sole exact file scope.
30
30
 
31
31
  Inspect the complete plan set in this single pass; do not substitute a brief sample or separate partial reviews. Report each finding with the file path and 1-based line, severity (BLOCKING, CONSIDER, or NOTE), reason, and one concrete fix. Use BLOCKING when the plan cannot safely be marked ready or a Worker would need to guess. Return an explicit pass only when all checks pass. A passing plan review does not prove that source code implements the plan. This review is read-only policy, not a sandbox; the caller must independently verify runtime status evidence. Never persist a `plan_review` artifact.
32
32
 
@@ -22,7 +22,7 @@ You are the Diffpi planning agent. Execute the plan skill's direct file workflow
22
22
 
23
23
  - For NEW and UPDATE, identify the unique repository-root plan from its path and plan files and read it before changing it; do not use retired managed-plan lookup calls.
24
24
  - Inspect the repository before proposing phases. Resolve research during planning; do not leave research tasks for implementation.
25
- - Keep stable lowercase phase and task IDs. Put prerequisites on phases only; give each task ordered steps and acceptance criteria. Put exact file scopes ONLY in one action-labeled Phase File Tree per implementation brief, never under a task or in PLAN.md.
25
+ - Keep stable lowercase phase and task IDs. Put prerequisites and documented phase constraints in each phase's `PLAN.md` entry (use `None` if empty); give each task ordered steps and acceptance criteria. Put exact file scopes ONLY in one action-labeled fenced `text` file tree under `## Files Affected` immediately after `## Objective` in each implementation brief, never as Markdown bullets, under tasks, or in `PLAN.md`. Keep PLAN.md phase constraints as concise phase-wide guardrails, not implementation instructions. `## Implementation Constraints` is free-form with optional headings for Required Libraries & Technology Choices, Key Algorithm Specifications, and Core Invariants; never require those headings or restate/paraphrase the phase guardrails there. Refer back to the phase ID instead.
26
26
  - Keep Design at 300 words or fewer when practical and never finalize it above 800 words.
27
27
  - Preserve completed work and evidence. Amend only draft, pending, or blocked work.
28
28
  - Read back every file after writing it and use the independent reviewer before marking a plan ready; a reviewer pass is not evidence that source code implements the plan.
@@ -14803,183 +14803,8 @@ import { z as z5 } from "zod";
14803
14803
 
14804
14804
  // src/plan/verify.ts
14805
14805
  import { readFile as readFile6, readdir as readdir2, stat } from "node:fs/promises";
14806
- import { basename as basename4, join as join12, resolve as resolve4 } from "node:path";
14806
+ import { basename as basename4, dirname as dirname5, join as join12, resolve as resolve4 } from "node:path";
14807
14807
  async function verifyLivePlan(requested, cwd = process.cwd()) {
14808
- return verifyLivePlanInternal(requested, cwd);
14809
- }
14810
- function linesOf(source) {
14811
- let fenced = false;
14812
- return source.split(/\r?\n/).map((line) => {
14813
- if (/^\s*```/.test(line)) {
14814
- fenced = !fenced;
14815
- return "";
14816
- }
14817
- return fenced ? "" : line;
14818
- });
14819
- }
14820
- function issue(issues, file, line, code, message) {
14821
- issues.push({ file, line, code, message });
14822
- }
14823
- function field(lines, start, end, name) {
14824
- const label = `- **${name}:**`;
14825
- for (let i = start;i < end; i++) {
14826
- if (lines[i]?.trimStart().startsWith(label)) {
14827
- return { value: lines[i].trimStart().slice(label.length).trim(), line: i + 1 };
14828
- }
14829
- }
14830
- return;
14831
- }
14832
- function heading(lines, text) {
14833
- return lines.flatMap((line, i) => line.trim() === text ? [i] : []);
14834
- }
14835
- function requireSection(lines, file, text, issues) {
14836
- const matches = heading(lines, text);
14837
- if (matches.length !== 1)
14838
- issue(issues, file, matches[0] === undefined ? 0 : matches[0] + 1, "section", `Expected exactly one ${text} section.`);
14839
- return matches[0] ?? -1;
14840
- }
14841
- function parsePlan(source, file, issues) {
14842
- const lines = linesOf(source);
14843
- for (const name of ["Intent", "Requirements", "Design", "Phases", "References"]) {
14844
- requireSection(lines, file, `## ${name}`, issues);
14845
- }
14846
- for (const name of ["Plan ID", "Branch", "Status"]) {
14847
- const entry = field(lines, 0, lines.length, name);
14848
- if (!entry?.value)
14849
- issue(issues, file, entry?.line ?? 0, "metadata", `Missing ${name}.`);
14850
- if (name === "Status" && entry?.value && !/^(?:DRAFT|READY|IN_PROGRESS|BLOCKED|COMPLETED)$/i.test(entry.value)) {
14851
- issue(issues, file, entry.line, "status", "Status must be DRAFT, READY, IN_PROGRESS, BLOCKED, or COMPLETED.");
14852
- }
14853
- }
14854
- const misplacedScope = lines.findIndex((line) => /^\s*(?:-\s+\*\*File scopes:\*\*|#{3,}\s+File Scopes\b)/i.test(line));
14855
- if (misplacedScope >= 0)
14856
- issue(issues, file, misplacedScope + 1, "file-scope", "File scope belongs only in each implementation brief’s Phase File Tree.");
14857
- const phaseHeading = /^### Phase (\d+):\s*(\S.*)$/;
14858
- const phaseLines = lines.flatMap((line, i) => phaseHeading.test(line) ? [i] : []);
14859
- if (!phaseLines.length)
14860
- issue(issues, file, 0, "phases", "Add at least one numbered phase.");
14861
- const phases = [];
14862
- const taskIds = new Set;
14863
- for (const [index, start] of phaseLines.entries()) {
14864
- const match = phaseHeading.exec(lines[start]);
14865
- const end = phaseLines[index + 1] ?? lines.length;
14866
- if (Number(match[1]) !== index + 1)
14867
- issue(issues, file, start + 1, "phase-order", "Phase numbers must start at 1 and be contiguous.");
14868
- const id = field(lines, start + 1, end, "Phase ID");
14869
- const prerequisite = field(lines, start + 1, end, "Prerequisites");
14870
- if (!id?.value)
14871
- issue(issues, file, id?.line ?? start + 1, "phase-id", "Phase ID is required.");
14872
- if (!prerequisite?.value)
14873
- issue(issues, file, prerequisite?.line ?? start + 1, "prerequisites", "Phase prerequisites are required (use None if empty).");
14874
- const prereqs = prerequisite?.value && !/^none$/i.test(prerequisite.value) ? prerequisite.value.split(",").map((value) => value.replaceAll("`", "").trim()) : [];
14875
- for (const required of prereqs) {
14876
- if (!phases.some((phase) => phase.id === required)) {
14877
- issue(issues, file, prerequisite?.line ?? start + 1, "prerequisites", `Prerequisite ${required || "(empty)"} must identify an earlier phase ID.`);
14878
- }
14879
- }
14880
- if (id?.value && phases.some((phase) => phase.id === id.value))
14881
- issue(issues, file, id.line, "phase-id", `Duplicate phase ID ${id.value}.`);
14882
- const tasks = [];
14883
- for (let i = start + 1;i < end; i++) {
14884
- const line = lines[i];
14885
- if (!/^\s*- \[[ xX]\]/.test(line))
14886
- continue;
14887
- const task = /^- \[[ xX]\] \*\*([a-z0-9]+(?:-[a-z0-9]+)*):?\*\* (\S.*)$/.exec(line);
14888
- if (!task) {
14889
- issue(issues, file, i + 1, "task-format", "Use a flat checkbox with a bold stable task ID and a title.");
14890
- continue;
14891
- }
14892
- if (taskIds.has(task[1]))
14893
- issue(issues, file, i + 1, "task-id", `Duplicate task ID ${task[1]}.`);
14894
- taskIds.add(task[1]);
14895
- tasks.push({ id: task[1], title: task[2].trim() });
14896
- }
14897
- if (!tasks.length)
14898
- issue(issues, file, start + 1, "tasks", "Each phase needs at least one task.");
14899
- phases.push({ id: id?.value ?? "", title: match[2].trim(), prerequisites: prereqs, tasks });
14900
- }
14901
- return phases;
14902
- }
14903
- function parseBrief(source, file, phase, number, issues) {
14904
- const lines = linesOf(source);
14905
- const title = lines[0]?.match(/^# Phase (\d+):\s*(\S.*)$/);
14906
- if (!title || Number(title[1]) !== number || title[2]?.trim() !== phase.title) {
14907
- issue(issues, file, 1, "phase-title", `Expected # Phase ${number}: ${phase.title}.`);
14908
- }
14909
- const id = field(lines, 0, lines.length, "Phase ID");
14910
- if (id?.value !== phase.id)
14911
- issue(issues, file, id?.line ?? 0, "phase-id", `Expected Phase ID ${phase.id}.`);
14912
- const prereqs = field(lines, 0, lines.length, "Prerequisites");
14913
- const expected = phase.prerequisites.length ? phase.prerequisites.join(", ") : "None";
14914
- if (prereqs?.value !== expected)
14915
- issue(issues, file, prereqs?.line ?? 0, "prerequisites", `Expected prerequisites ${expected}.`);
14916
- for (const section of ["Objective", "Tasks", "Implementation Constraints", "Phase File Tree"]) {
14917
- requireSection(lines, file, `## ${section}`, issues);
14918
- }
14919
- for (const section of ["Libraries and Algorithms", "Constraints"]) {
14920
- requireSection(lines, file, `### ${section}`, issues);
14921
- }
14922
- const taskHeading = /^### (\d+)\.\s*(\S.*)$/;
14923
- const tasksStart = heading(lines, "## Tasks")[0] ?? 0;
14924
- const tasksEnd = heading(lines, "## Implementation Constraints")[0] ?? lines.length;
14925
- const taskLines = lines.flatMap((line, i) => i > tasksStart && i < tasksEnd && taskHeading.test(line) ? [i] : []);
14926
- if (taskLines.length !== phase.tasks.length)
14927
- issue(issues, file, tasksStart + 1, "task-parity", `Expected ${phase.tasks.length} matching task sections, found ${taskLines.length}.`);
14928
- for (const [index, start] of taskLines.entries()) {
14929
- const end = taskLines[index + 1] ?? tasksEnd;
14930
- const match = taskHeading.exec(lines[start]);
14931
- const planTask = phase.tasks[index];
14932
- const id = field(lines, start + 1, end, "Task ID");
14933
- if (!planTask || Number(match[1]) !== index + 1 || match[2]?.trim() !== planTask.title || id?.value !== planTask.id) {
14934
- issue(issues, file, start + 1, "task-parity", `Task ${index + 1} must match its PLAN.md ID, title, and order.`);
14935
- }
14936
- const steps = field(lines, start + 1, end, "Steps");
14937
- const stepStart = steps ? steps.line : end;
14938
- if (!steps || !lines.slice(stepStart, end).some((line) => /^\s+1\.\s+\S/.test(line))) {
14939
- issue(issues, file, start + 1, "steps", "Add numbered implementation steps starting at 1.");
14940
- }
14941
- const verification = lines.findIndex((line, i) => i > start && i < end && /^\s*- \*\*Verify:\*\*/.test(line));
14942
- const inline = verification >= 0 && /\*\*Verify:\*\*\s*\S/.test(lines[verification]);
14943
- const nested = verification >= 0 && lines.slice(verification + 1, end).some((line) => /^\s{2,}-\s+\S/.test(line));
14944
- if (!inline && !nested) {
14945
- issue(issues, file, verification >= 0 ? verification + 1 : start + 1, "verification", "Nest a nonempty **Verify:** instruction under this task.");
14946
- }
14947
- const acceptance = field(lines, start + 1, end, "Acceptance");
14948
- if (!acceptance?.value)
14949
- issue(issues, file, start + 1, "task-detail", "Add nonempty Acceptance for this task.");
14950
- const misplacedScope = lines.findIndex((line, i) => i > start && i < end && /^\s*(?:-\s+\*\*File scopes:\*\*|#{3,}\s+File Scopes\b)/i.test(line));
14951
- if (misplacedScope >= 0)
14952
- issue(issues, file, misplacedScope + 1, "file-scope", "Put the phase’s file scope only in its Phase File Tree.");
14953
- }
14954
- const treeStart = heading(lines, "## Phase File Tree")[0];
14955
- if (treeStart === undefined)
14956
- return;
14957
- const treeEnd = lines.findIndex((line, i) => i > treeStart && /^## /.test(line));
14958
- const original = source.split(/\r?\n/).slice(treeStart, treeEnd < 0 ? undefined : treeEnd);
14959
- const opens = original.flatMap((line, i) => /^```text\s*$/.test(line) ? [i] : []);
14960
- if (opens.length !== 1) {
14961
- issue(issues, file, treeStart + 1, "tree", "Use exactly one fenced text tree for the phase.");
14962
- return;
14963
- }
14964
- const close = original.findIndex((line, i) => i > opens[0] && /^```\s*$/.test(line));
14965
- if (close < 0) {
14966
- issue(issues, file, treeStart + 1, "tree", "Close the fenced phase file tree.");
14967
- return;
14968
- }
14969
- let leaves = 0;
14970
- for (let i = opens[0] + 1;i < close; i++) {
14971
- const branch = /^\s*[│ ]*(?:├──|└──)\s+(.+)$/.exec(original[i]);
14972
- if (!branch || branch[1].trimEnd().endsWith("/"))
14973
- continue;
14974
- leaves++;
14975
- if (!/^\[(?:ADD|MODIFY|REMOVE|VERIFY|MOVE from: [^\]]+)\]\s+\S/.test(branch[1])) {
14976
- issue(issues, file, treeStart + i + 1, "tree-label", "Every file leaf must start with an action label after the tree branch.");
14977
- }
14978
- }
14979
- if (!leaves)
14980
- issue(issues, file, treeStart + 1, "tree", "Add at least one action-labeled file leaf.");
14981
- }
14982
- async function verifyLivePlanInternal(requested, cwd) {
14983
14808
  const path = resolve4(cwd, requested);
14984
14809
  let planPath = path;
14985
14810
  try {
@@ -14988,42 +14813,80 @@ async function verifyLivePlanInternal(requested, cwd) {
14988
14813
  } catch {}
14989
14814
  const issues = [];
14990
14815
  const briefPaths = [];
14816
+ const planDir = dirname5(planPath);
14991
14817
  if (basename4(planPath) !== "PLAN.md") {
14992
- issue(issues, planPath, 0, "path", "Select a PLAN.md file or its directory.");
14818
+ addIssue(issues, planPath, 0, "path", "Select PLAN.md or its directory.");
14993
14819
  return { planPath, briefPaths, ok: false, issues };
14994
14820
  }
14821
+ if (!/^\d{6}(?:-[a-z0-9]+)+$/.test(basename4(planDir)) || basename4(dirname5(planDir)) !== "plan" || basename4(dirname5(dirname5(planDir))) !== ".diffpi")
14822
+ addIssue(issues, planPath, 0, "layout", "Place PLAN.md under .diffpi/plan/<YYMMDD[-ticket]-short-slug>/.");
14995
14823
  let source;
14996
14824
  try {
14997
14825
  source = await readFile6(planPath, "utf8");
14998
14826
  } catch {
14999
- issue(issues, planPath, 0, "missing-plan", "PLAN.md is not readable.");
14827
+ addIssue(issues, planPath, 0, "missing-plan", "PLAN.md is not readable.");
15000
14828
  return { planPath, briefPaths, ok: false, issues };
15001
14829
  }
15002
- const phases = parsePlan(source, planPath, issues);
15003
- const implementation = join12(resolve4(planPath, ".."), "implementation");
15004
- let entries;
14830
+ const planLines = visibleLines(source);
14831
+ checkHeadings(planLines, planPath, ["Intent", "Requirements", "Design", "Phases", "References"], issues);
14832
+ const phases = planLines.flatMap((line, i) => /^### Phase \d+:\s*\S/.test(line) ? [i] : []);
14833
+ if (!phases.length)
14834
+ addIssue(issues, planPath, 0, "phases", "Add at least one numbered phase heading.");
14835
+ for (const [index, line] of phases.entries()) {
14836
+ if (!planLines[line]?.startsWith(`### Phase ${index + 1}: `))
14837
+ addIssue(issues, planPath, line + 1, "phase-order", "Number phase headings from 1 without gaps.");
14838
+ }
14839
+ const implementation = join12(planDir, "implementation");
14840
+ let rootEntries = [];
14841
+ let briefEntries = [];
15005
14842
  try {
15006
- entries = await readdir2(implementation);
15007
- } catch {
15008
- entries = [];
14843
+ rootEntries = await readdir2(planDir);
14844
+ briefEntries = await readdir2(implementation);
14845
+ } catch {}
14846
+ for (const entry of rootEntries) {
14847
+ if (entry !== "PLAN.md" && entry !== "implementation")
14848
+ addIssue(issues, join12(planDir, entry), 0, "layout", "Only PLAN.md and implementation/ belong in the plan directory.");
15009
14849
  }
15010
- for (const entry of entries) {
15011
- const match = /^phase-(\d+)\.md$/.exec(entry);
15012
- if (match && (Number(match[1]) < 1 || Number(match[1]) > phases.length || entry !== `phase-${Number(match[1])}.md`)) {
15013
- issue(issues, join12(implementation, entry), 0, "extra-brief", "No matching numbered phase exists for this brief.");
15014
- }
14850
+ for (const entry of briefEntries) {
14851
+ const ordinal = /^phase-(\d+)\.md$/.exec(entry)?.[1];
14852
+ if (!ordinal || !Number(ordinal) || Number(ordinal) > phases.length || entry !== `phase-${Number(ordinal)}.md`)
14853
+ addIssue(issues, join12(implementation, entry), 0, "extra-brief", "Only matching numbered phase briefs belong in implementation/.");
15015
14854
  }
15016
- for (const [index, phase] of phases.entries()) {
14855
+ for (let index = 0;index < phases.length; index++) {
15017
14856
  const brief = join12(implementation, `phase-${index + 1}.md`);
15018
14857
  briefPaths.push(brief);
15019
14858
  try {
15020
- parseBrief(await readFile6(brief, "utf8"), brief, phase, index + 1, issues);
14859
+ const lines = visibleLines(await readFile6(brief, "utf8"));
14860
+ if (!/^# Phase \d+:\s*\S/.test(lines[0] ?? "") || !lines[0]?.startsWith(`# Phase ${index + 1}: `))
14861
+ addIssue(issues, brief, 1, "phase-title", `Start with # Phase ${index + 1}: <title>.`);
14862
+ checkHeadings(lines, brief, ["Objective", "Files Affected", "Tasks", "Implementation Constraints"], issues);
15021
14863
  } catch {
15022
- issue(issues, brief, 0, "missing-brief", "Numbered phase brief is not readable.");
14864
+ addIssue(issues, brief, 0, "missing-brief", "Numbered phase brief is not readable.");
15023
14865
  }
15024
14866
  }
15025
14867
  return { planPath, briefPaths, ok: issues.length === 0, issues };
15026
14868
  }
14869
+ function addIssue(issues, file, line, code, message) {
14870
+ issues.push({ file, line, code, message });
14871
+ }
14872
+ function visibleLines(source) {
14873
+ let fenced = false;
14874
+ return source.split(/\r?\n/).map((line) => {
14875
+ if (/^\s*```/.test(line)) {
14876
+ fenced = !fenced;
14877
+ return "";
14878
+ }
14879
+ return fenced ? "" : line;
14880
+ });
14881
+ }
14882
+ function checkHeadings(lines, file, names, issues) {
14883
+ const found = lines.flatMap((line, i) => line.startsWith("## ") ? [{ text: line, line: i + 1 }] : []);
14884
+ const expected = names.map((name) => `## ${name}`);
14885
+ if (found.map(({ text }) => text).join(`
14886
+ `) !== expected.join(`
14887
+ `))
14888
+ addIssue(issues, file, found[0]?.line ?? 0, "headings", `Expected headings, in order: ${expected.join(", ")}.`);
14889
+ }
15027
14890
 
15028
14891
  // src/tools/plan.ts
15029
14892
  var schema8 = z5.object({ plan: z5.string().min(1), cwd: z5.string().optional() }).strict();
@@ -15198,7 +15061,7 @@ import { z as z11 } from "zod";
15198
15061
  // src/extensions/misex.ts
15199
15062
  import { mkdir as mkdir3, readFile as readFile7, writeFile as writeFile2 } from "node:fs/promises";
15200
15063
  import { homedir as homedir6 } from "node:os";
15201
- import { basename as basename5, dirname as dirname5, join as join13 } from "node:path";
15064
+ import { basename as basename5, dirname as dirname6, join as join13 } from "node:path";
15202
15065
  var MISE_HOOK_START = "# >>> @difflab/pi mise >>>";
15203
15066
  var MISE_HOOK_END = "# <<< @difflab/pi mise <<<";
15204
15067
  var mise = {
@@ -15233,7 +15096,7 @@ var mise = {
15233
15096
  const separator = current.length === 0 || current.endsWith(`
15234
15097
  `) ? "" : `
15235
15098
  `;
15236
- await mkdir3(dirname5(hook.path), { recursive: true });
15099
+ await mkdir3(dirname6(hook.path), { recursive: true });
15237
15100
  await writeFile2(hook.path, `${current}${separator}${hook.content}`, "utf8");
15238
15101
  return { path: hook.path, changed: true, planned: false };
15239
15102
  },
@@ -17350,7 +17213,7 @@ import { basename as basename6, join as join19 } from "node:path";
17350
17213
  // src/mcp.ts
17351
17214
  import { mkdir as mkdir5, readFile as readFile13, writeFile as writeFile6 } from "node:fs/promises";
17352
17215
  import { homedir as homedir8 } from "node:os";
17353
- import { dirname as dirname6, join as join17 } from "node:path";
17216
+ import { dirname as dirname7, join as join17 } from "node:path";
17354
17217
  var mcp = {
17355
17218
  globalConfigPath(homeDir = homedir8()) {
17356
17219
  return join17(homeDir, ".config", "mcp", "mcp.json");
@@ -17366,7 +17229,7 @@ var mcp = {
17366
17229
  const next = { ...current, mcpServers: nextServers };
17367
17230
  const changed = JSON.stringify(current) !== JSON.stringify(next);
17368
17231
  if (changed && !options.dryRun) {
17369
- await mkdir5(dirname6(path), { recursive: true });
17232
+ await mkdir5(dirname7(path), { recursive: true });
17370
17233
  await writeFile6(path, `${JSON.stringify(next, null, 2)}
17371
17234
  `, "utf8");
17372
17235
  }
@@ -17410,7 +17273,7 @@ function isRecord(value) {
17410
17273
  // src/pi.ts
17411
17274
  import { mkdir as mkdir6, writeFile as writeFile7 } from "node:fs/promises";
17412
17275
  import { homedir as homedir9 } from "node:os";
17413
- import { dirname as dirname7, join as join18 } from "node:path";
17276
+ import { dirname as dirname8, join as join18 } from "node:path";
17414
17277
  var pi = {
17415
17278
  executableCheck: findPiExecutable,
17416
17279
  packageList: listPiPackages,
@@ -17427,7 +17290,7 @@ async function ensurePiAgent(filename, content, agentDir = resolvePiAgentDir(),
17427
17290
  const currentText = await readTextIfExists(path);
17428
17291
  const changed = currentText !== content;
17429
17292
  if (changed && !dryRun) {
17430
- await mkdir6(dirname7(path), { recursive: true });
17293
+ await mkdir6(dirname8(path), { recursive: true });
17431
17294
  await writeFile7(path, content, "utf8");
17432
17295
  }
17433
17296
  return { path, changed, existed: currentText !== undefined, planned: changed && dryRun };
@@ -17464,7 +17327,7 @@ async function ensurePiConfig(path, update, dryRun = false) {
17464
17327
  const next = update(current);
17465
17328
  const changed = JSON.stringify(current) !== JSON.stringify(next);
17466
17329
  if (changed && !dryRun) {
17467
- await mkdir6(dirname7(path), { recursive: true });
17330
+ await mkdir6(dirname8(path), { recursive: true });
17468
17331
  await writeFile7(path, `${JSON.stringify(next, null, 2)}
17469
17332
  `, "utf8");
17470
17333
  }
package/dist/index.js CHANGED
@@ -2551,183 +2551,8 @@ function parseJsonObject(content, path) {
2551
2551
  }
2552
2552
  // src/plan/verify.ts
2553
2553
  import { readFile as readFile10, readdir as readdir2, stat } from "node:fs/promises";
2554
- import { basename as basename5, join as join13, resolve as resolve4 } from "node:path";
2554
+ import { basename as basename5, dirname as dirname7, join as join13, resolve as resolve4 } from "node:path";
2555
2555
  async function verifyLivePlan(requested, cwd = process.cwd()) {
2556
- return verifyLivePlanInternal(requested, cwd);
2557
- }
2558
- function linesOf(source) {
2559
- let fenced = false;
2560
- return source.split(/\r?\n/).map((line) => {
2561
- if (/^\s*```/.test(line)) {
2562
- fenced = !fenced;
2563
- return "";
2564
- }
2565
- return fenced ? "" : line;
2566
- });
2567
- }
2568
- function issue(issues, file, line, code, message) {
2569
- issues.push({ file, line, code, message });
2570
- }
2571
- function field(lines, start, end, name) {
2572
- const label = `- **${name}:**`;
2573
- for (let i = start;i < end; i++) {
2574
- if (lines[i]?.trimStart().startsWith(label)) {
2575
- return { value: lines[i].trimStart().slice(label.length).trim(), line: i + 1 };
2576
- }
2577
- }
2578
- return;
2579
- }
2580
- function heading(lines, text) {
2581
- return lines.flatMap((line, i) => line.trim() === text ? [i] : []);
2582
- }
2583
- function requireSection(lines, file, text, issues) {
2584
- const matches = heading(lines, text);
2585
- if (matches.length !== 1)
2586
- issue(issues, file, matches[0] === undefined ? 0 : matches[0] + 1, "section", `Expected exactly one ${text} section.`);
2587
- return matches[0] ?? -1;
2588
- }
2589
- function parsePlan(source, file, issues) {
2590
- const lines = linesOf(source);
2591
- for (const name of ["Intent", "Requirements", "Design", "Phases", "References"]) {
2592
- requireSection(lines, file, `## ${name}`, issues);
2593
- }
2594
- for (const name of ["Plan ID", "Branch", "Status"]) {
2595
- const entry = field(lines, 0, lines.length, name);
2596
- if (!entry?.value)
2597
- issue(issues, file, entry?.line ?? 0, "metadata", `Missing ${name}.`);
2598
- if (name === "Status" && entry?.value && !/^(?:DRAFT|READY|IN_PROGRESS|BLOCKED|COMPLETED)$/i.test(entry.value)) {
2599
- issue(issues, file, entry.line, "status", "Status must be DRAFT, READY, IN_PROGRESS, BLOCKED, or COMPLETED.");
2600
- }
2601
- }
2602
- const misplacedScope = lines.findIndex((line) => /^\s*(?:-\s+\*\*File scopes:\*\*|#{3,}\s+File Scopes\b)/i.test(line));
2603
- if (misplacedScope >= 0)
2604
- issue(issues, file, misplacedScope + 1, "file-scope", "File scope belongs only in each implementation brief’s Phase File Tree.");
2605
- const phaseHeading = /^### Phase (\d+):\s*(\S.*)$/;
2606
- const phaseLines = lines.flatMap((line, i) => phaseHeading.test(line) ? [i] : []);
2607
- if (!phaseLines.length)
2608
- issue(issues, file, 0, "phases", "Add at least one numbered phase.");
2609
- const phases = [];
2610
- const taskIds = new Set;
2611
- for (const [index, start] of phaseLines.entries()) {
2612
- const match = phaseHeading.exec(lines[start]);
2613
- const end = phaseLines[index + 1] ?? lines.length;
2614
- if (Number(match[1]) !== index + 1)
2615
- issue(issues, file, start + 1, "phase-order", "Phase numbers must start at 1 and be contiguous.");
2616
- const id = field(lines, start + 1, end, "Phase ID");
2617
- const prerequisite = field(lines, start + 1, end, "Prerequisites");
2618
- if (!id?.value)
2619
- issue(issues, file, id?.line ?? start + 1, "phase-id", "Phase ID is required.");
2620
- if (!prerequisite?.value)
2621
- issue(issues, file, prerequisite?.line ?? start + 1, "prerequisites", "Phase prerequisites are required (use None if empty).");
2622
- const prereqs = prerequisite?.value && !/^none$/i.test(prerequisite.value) ? prerequisite.value.split(",").map((value) => value.replaceAll("`", "").trim()) : [];
2623
- for (const required of prereqs) {
2624
- if (!phases.some((phase) => phase.id === required)) {
2625
- issue(issues, file, prerequisite?.line ?? start + 1, "prerequisites", `Prerequisite ${required || "(empty)"} must identify an earlier phase ID.`);
2626
- }
2627
- }
2628
- if (id?.value && phases.some((phase) => phase.id === id.value))
2629
- issue(issues, file, id.line, "phase-id", `Duplicate phase ID ${id.value}.`);
2630
- const tasks = [];
2631
- for (let i = start + 1;i < end; i++) {
2632
- const line = lines[i];
2633
- if (!/^\s*- \[[ xX]\]/.test(line))
2634
- continue;
2635
- const task = /^- \[[ xX]\] \*\*([a-z0-9]+(?:-[a-z0-9]+)*):?\*\* (\S.*)$/.exec(line);
2636
- if (!task) {
2637
- issue(issues, file, i + 1, "task-format", "Use a flat checkbox with a bold stable task ID and a title.");
2638
- continue;
2639
- }
2640
- if (taskIds.has(task[1]))
2641
- issue(issues, file, i + 1, "task-id", `Duplicate task ID ${task[1]}.`);
2642
- taskIds.add(task[1]);
2643
- tasks.push({ id: task[1], title: task[2].trim() });
2644
- }
2645
- if (!tasks.length)
2646
- issue(issues, file, start + 1, "tasks", "Each phase needs at least one task.");
2647
- phases.push({ id: id?.value ?? "", title: match[2].trim(), prerequisites: prereqs, tasks });
2648
- }
2649
- return phases;
2650
- }
2651
- function parseBrief(source, file, phase, number, issues) {
2652
- const lines = linesOf(source);
2653
- const title = lines[0]?.match(/^# Phase (\d+):\s*(\S.*)$/);
2654
- if (!title || Number(title[1]) !== number || title[2]?.trim() !== phase.title) {
2655
- issue(issues, file, 1, "phase-title", `Expected # Phase ${number}: ${phase.title}.`);
2656
- }
2657
- const id = field(lines, 0, lines.length, "Phase ID");
2658
- if (id?.value !== phase.id)
2659
- issue(issues, file, id?.line ?? 0, "phase-id", `Expected Phase ID ${phase.id}.`);
2660
- const prereqs = field(lines, 0, lines.length, "Prerequisites");
2661
- const expected = phase.prerequisites.length ? phase.prerequisites.join(", ") : "None";
2662
- if (prereqs?.value !== expected)
2663
- issue(issues, file, prereqs?.line ?? 0, "prerequisites", `Expected prerequisites ${expected}.`);
2664
- for (const section of ["Objective", "Tasks", "Implementation Constraints", "Phase File Tree"]) {
2665
- requireSection(lines, file, `## ${section}`, issues);
2666
- }
2667
- for (const section of ["Libraries and Algorithms", "Constraints"]) {
2668
- requireSection(lines, file, `### ${section}`, issues);
2669
- }
2670
- const taskHeading = /^### (\d+)\.\s*(\S.*)$/;
2671
- const tasksStart = heading(lines, "## Tasks")[0] ?? 0;
2672
- const tasksEnd = heading(lines, "## Implementation Constraints")[0] ?? lines.length;
2673
- const taskLines = lines.flatMap((line, i) => i > tasksStart && i < tasksEnd && taskHeading.test(line) ? [i] : []);
2674
- if (taskLines.length !== phase.tasks.length)
2675
- issue(issues, file, tasksStart + 1, "task-parity", `Expected ${phase.tasks.length} matching task sections, found ${taskLines.length}.`);
2676
- for (const [index, start] of taskLines.entries()) {
2677
- const end = taskLines[index + 1] ?? tasksEnd;
2678
- const match = taskHeading.exec(lines[start]);
2679
- const planTask = phase.tasks[index];
2680
- const id = field(lines, start + 1, end, "Task ID");
2681
- if (!planTask || Number(match[1]) !== index + 1 || match[2]?.trim() !== planTask.title || id?.value !== planTask.id) {
2682
- issue(issues, file, start + 1, "task-parity", `Task ${index + 1} must match its PLAN.md ID, title, and order.`);
2683
- }
2684
- const steps = field(lines, start + 1, end, "Steps");
2685
- const stepStart = steps ? steps.line : end;
2686
- if (!steps || !lines.slice(stepStart, end).some((line) => /^\s+1\.\s+\S/.test(line))) {
2687
- issue(issues, file, start + 1, "steps", "Add numbered implementation steps starting at 1.");
2688
- }
2689
- const verification = lines.findIndex((line, i) => i > start && i < end && /^\s*- \*\*Verify:\*\*/.test(line));
2690
- const inline = verification >= 0 && /\*\*Verify:\*\*\s*\S/.test(lines[verification]);
2691
- const nested = verification >= 0 && lines.slice(verification + 1, end).some((line) => /^\s{2,}-\s+\S/.test(line));
2692
- if (!inline && !nested) {
2693
- issue(issues, file, verification >= 0 ? verification + 1 : start + 1, "verification", "Nest a nonempty **Verify:** instruction under this task.");
2694
- }
2695
- const acceptance = field(lines, start + 1, end, "Acceptance");
2696
- if (!acceptance?.value)
2697
- issue(issues, file, start + 1, "task-detail", "Add nonempty Acceptance for this task.");
2698
- const misplacedScope = lines.findIndex((line, i) => i > start && i < end && /^\s*(?:-\s+\*\*File scopes:\*\*|#{3,}\s+File Scopes\b)/i.test(line));
2699
- if (misplacedScope >= 0)
2700
- issue(issues, file, misplacedScope + 1, "file-scope", "Put the phase’s file scope only in its Phase File Tree.");
2701
- }
2702
- const treeStart = heading(lines, "## Phase File Tree")[0];
2703
- if (treeStart === undefined)
2704
- return;
2705
- const treeEnd = lines.findIndex((line, i) => i > treeStart && /^## /.test(line));
2706
- const original = source.split(/\r?\n/).slice(treeStart, treeEnd < 0 ? undefined : treeEnd);
2707
- const opens = original.flatMap((line, i) => /^```text\s*$/.test(line) ? [i] : []);
2708
- if (opens.length !== 1) {
2709
- issue(issues, file, treeStart + 1, "tree", "Use exactly one fenced text tree for the phase.");
2710
- return;
2711
- }
2712
- const close = original.findIndex((line, i) => i > opens[0] && /^```\s*$/.test(line));
2713
- if (close < 0) {
2714
- issue(issues, file, treeStart + 1, "tree", "Close the fenced phase file tree.");
2715
- return;
2716
- }
2717
- let leaves = 0;
2718
- for (let i = opens[0] + 1;i < close; i++) {
2719
- const branch = /^\s*[│ ]*(?:├──|└──)\s+(.+)$/.exec(original[i]);
2720
- if (!branch || branch[1].trimEnd().endsWith("/"))
2721
- continue;
2722
- leaves++;
2723
- if (!/^\[(?:ADD|MODIFY|REMOVE|VERIFY|MOVE from: [^\]]+)\]\s+\S/.test(branch[1])) {
2724
- issue(issues, file, treeStart + i + 1, "tree-label", "Every file leaf must start with an action label after the tree branch.");
2725
- }
2726
- }
2727
- if (!leaves)
2728
- issue(issues, file, treeStart + 1, "tree", "Add at least one action-labeled file leaf.");
2729
- }
2730
- async function verifyLivePlanInternal(requested, cwd) {
2731
2556
  const path = resolve4(cwd, requested);
2732
2557
  let planPath = path;
2733
2558
  try {
@@ -2736,42 +2561,80 @@ async function verifyLivePlanInternal(requested, cwd) {
2736
2561
  } catch {}
2737
2562
  const issues = [];
2738
2563
  const briefPaths = [];
2564
+ const planDir = dirname7(planPath);
2739
2565
  if (basename5(planPath) !== "PLAN.md") {
2740
- issue(issues, planPath, 0, "path", "Select a PLAN.md file or its directory.");
2566
+ addIssue(issues, planPath, 0, "path", "Select PLAN.md or its directory.");
2741
2567
  return { planPath, briefPaths, ok: false, issues };
2742
2568
  }
2569
+ if (!/^\d{6}(?:-[a-z0-9]+)+$/.test(basename5(planDir)) || basename5(dirname7(planDir)) !== "plan" || basename5(dirname7(dirname7(planDir))) !== ".diffpi")
2570
+ addIssue(issues, planPath, 0, "layout", "Place PLAN.md under .diffpi/plan/<YYMMDD[-ticket]-short-slug>/.");
2743
2571
  let source;
2744
2572
  try {
2745
2573
  source = await readFile10(planPath, "utf8");
2746
2574
  } catch {
2747
- issue(issues, planPath, 0, "missing-plan", "PLAN.md is not readable.");
2575
+ addIssue(issues, planPath, 0, "missing-plan", "PLAN.md is not readable.");
2748
2576
  return { planPath, briefPaths, ok: false, issues };
2749
2577
  }
2750
- const phases = parsePlan(source, planPath, issues);
2751
- const implementation = join13(resolve4(planPath, ".."), "implementation");
2752
- let entries;
2578
+ const planLines = visibleLines(source);
2579
+ checkHeadings(planLines, planPath, ["Intent", "Requirements", "Design", "Phases", "References"], issues);
2580
+ const phases = planLines.flatMap((line, i) => /^### Phase \d+:\s*\S/.test(line) ? [i] : []);
2581
+ if (!phases.length)
2582
+ addIssue(issues, planPath, 0, "phases", "Add at least one numbered phase heading.");
2583
+ for (const [index, line] of phases.entries()) {
2584
+ if (!planLines[line]?.startsWith(`### Phase ${index + 1}: `))
2585
+ addIssue(issues, planPath, line + 1, "phase-order", "Number phase headings from 1 without gaps.");
2586
+ }
2587
+ const implementation = join13(planDir, "implementation");
2588
+ let rootEntries = [];
2589
+ let briefEntries = [];
2753
2590
  try {
2754
- entries = await readdir2(implementation);
2755
- } catch {
2756
- entries = [];
2591
+ rootEntries = await readdir2(planDir);
2592
+ briefEntries = await readdir2(implementation);
2593
+ } catch {}
2594
+ for (const entry of rootEntries) {
2595
+ if (entry !== "PLAN.md" && entry !== "implementation")
2596
+ addIssue(issues, join13(planDir, entry), 0, "layout", "Only PLAN.md and implementation/ belong in the plan directory.");
2757
2597
  }
2758
- for (const entry of entries) {
2759
- const match = /^phase-(\d+)\.md$/.exec(entry);
2760
- if (match && (Number(match[1]) < 1 || Number(match[1]) > phases.length || entry !== `phase-${Number(match[1])}.md`)) {
2761
- issue(issues, join13(implementation, entry), 0, "extra-brief", "No matching numbered phase exists for this brief.");
2762
- }
2598
+ for (const entry of briefEntries) {
2599
+ const ordinal = /^phase-(\d+)\.md$/.exec(entry)?.[1];
2600
+ if (!ordinal || !Number(ordinal) || Number(ordinal) > phases.length || entry !== `phase-${Number(ordinal)}.md`)
2601
+ addIssue(issues, join13(implementation, entry), 0, "extra-brief", "Only matching numbered phase briefs belong in implementation/.");
2763
2602
  }
2764
- for (const [index, phase] of phases.entries()) {
2603
+ for (let index = 0;index < phases.length; index++) {
2765
2604
  const brief = join13(implementation, `phase-${index + 1}.md`);
2766
2605
  briefPaths.push(brief);
2767
2606
  try {
2768
- parseBrief(await readFile10(brief, "utf8"), brief, phase, index + 1, issues);
2607
+ const lines = visibleLines(await readFile10(brief, "utf8"));
2608
+ if (!/^# Phase \d+:\s*\S/.test(lines[0] ?? "") || !lines[0]?.startsWith(`# Phase ${index + 1}: `))
2609
+ addIssue(issues, brief, 1, "phase-title", `Start with # Phase ${index + 1}: <title>.`);
2610
+ checkHeadings(lines, brief, ["Objective", "Files Affected", "Tasks", "Implementation Constraints"], issues);
2769
2611
  } catch {
2770
- issue(issues, brief, 0, "missing-brief", "Numbered phase brief is not readable.");
2612
+ addIssue(issues, brief, 0, "missing-brief", "Numbered phase brief is not readable.");
2771
2613
  }
2772
2614
  }
2773
2615
  return { planPath, briefPaths, ok: issues.length === 0, issues };
2774
2616
  }
2617
+ function addIssue(issues, file, line, code, message) {
2618
+ issues.push({ file, line, code, message });
2619
+ }
2620
+ function visibleLines(source) {
2621
+ let fenced = false;
2622
+ return source.split(/\r?\n/).map((line) => {
2623
+ if (/^\s*```/.test(line)) {
2624
+ fenced = !fenced;
2625
+ return "";
2626
+ }
2627
+ return fenced ? "" : line;
2628
+ });
2629
+ }
2630
+ function checkHeadings(lines, file, names, issues) {
2631
+ const found = lines.flatMap((line, i) => line.startsWith("## ") ? [{ text: line, line: i + 1 }] : []);
2632
+ const expected = names.map((name) => `## ${name}`);
2633
+ if (found.map(({ text }) => text).join(`
2634
+ `) !== expected.join(`
2635
+ `))
2636
+ addIssue(issues, file, found[0]?.line ?? 0, "headings", `Expected headings, in order: ${expected.join(", ")}.`);
2637
+ }
2775
2638
  // src/setup.ts
2776
2639
  import { parseFrontmatter as parseFrontmatter3 } from "@earendil-works/pi-coding-agent";
2777
2640
  import { readdir as readdir3, readFile as readFile11 } from "node:fs/promises";
@@ -10,6 +10,6 @@ export interface PlanVerification {
10
10
  ok: boolean;
11
11
  issues: PlanVerificationIssue[];
12
12
  }
13
- /** Read-only, stateless verification of the current live-file plan format. */
13
+ /** Read-only layout check. The independent reviewer judges all plan content. */
14
14
  export declare function verifyLivePlan(requested: string, cwd?: string): Promise<PlanVerification>;
15
15
  //# sourceMappingURL=verify.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../src/plan/verify.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,qBAAqB,EAAE,CAAC;CACjC;AAED,8EAA8E;AAC9E,wBAAsB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,SAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAEtG"}
1
+ {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../src/plan/verify.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,qBAAqB,EAAE,CAAC;CACjC;AAED,gFAAgF;AAChF,wBAAsB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,SAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAiFtG"}
@@ -950,183 +950,8 @@ import { z as z4 } from "zod";
950
950
 
951
951
  // src/plan/verify.ts
952
952
  import { readFile as readFile4, readdir, stat } from "node:fs/promises";
953
- import { basename as basename3, join as join8, resolve as resolve3 } from "node:path";
953
+ import { basename as basename3, dirname as dirname4, join as join8, resolve as resolve3 } from "node:path";
954
954
  async function verifyLivePlan(requested, cwd = process.cwd()) {
955
- return verifyLivePlanInternal(requested, cwd);
956
- }
957
- function linesOf(source) {
958
- let fenced = false;
959
- return source.split(/\r?\n/).map((line) => {
960
- if (/^\s*```/.test(line)) {
961
- fenced = !fenced;
962
- return "";
963
- }
964
- return fenced ? "" : line;
965
- });
966
- }
967
- function issue(issues, file, line, code, message) {
968
- issues.push({ file, line, code, message });
969
- }
970
- function field(lines, start, end, name) {
971
- const label = `- **${name}:**`;
972
- for (let i = start;i < end; i++) {
973
- if (lines[i]?.trimStart().startsWith(label)) {
974
- return { value: lines[i].trimStart().slice(label.length).trim(), line: i + 1 };
975
- }
976
- }
977
- return;
978
- }
979
- function heading(lines, text) {
980
- return lines.flatMap((line, i) => line.trim() === text ? [i] : []);
981
- }
982
- function requireSection(lines, file, text, issues) {
983
- const matches = heading(lines, text);
984
- if (matches.length !== 1)
985
- issue(issues, file, matches[0] === undefined ? 0 : matches[0] + 1, "section", `Expected exactly one ${text} section.`);
986
- return matches[0] ?? -1;
987
- }
988
- function parsePlan(source, file, issues) {
989
- const lines = linesOf(source);
990
- for (const name of ["Intent", "Requirements", "Design", "Phases", "References"]) {
991
- requireSection(lines, file, `## ${name}`, issues);
992
- }
993
- for (const name of ["Plan ID", "Branch", "Status"]) {
994
- const entry = field(lines, 0, lines.length, name);
995
- if (!entry?.value)
996
- issue(issues, file, entry?.line ?? 0, "metadata", `Missing ${name}.`);
997
- if (name === "Status" && entry?.value && !/^(?:DRAFT|READY|IN_PROGRESS|BLOCKED|COMPLETED)$/i.test(entry.value)) {
998
- issue(issues, file, entry.line, "status", "Status must be DRAFT, READY, IN_PROGRESS, BLOCKED, or COMPLETED.");
999
- }
1000
- }
1001
- const misplacedScope = lines.findIndex((line) => /^\s*(?:-\s+\*\*File scopes:\*\*|#{3,}\s+File Scopes\b)/i.test(line));
1002
- if (misplacedScope >= 0)
1003
- issue(issues, file, misplacedScope + 1, "file-scope", "File scope belongs only in each implementation brief’s Phase File Tree.");
1004
- const phaseHeading = /^### Phase (\d+):\s*(\S.*)$/;
1005
- const phaseLines = lines.flatMap((line, i) => phaseHeading.test(line) ? [i] : []);
1006
- if (!phaseLines.length)
1007
- issue(issues, file, 0, "phases", "Add at least one numbered phase.");
1008
- const phases = [];
1009
- const taskIds = new Set;
1010
- for (const [index, start] of phaseLines.entries()) {
1011
- const match = phaseHeading.exec(lines[start]);
1012
- const end = phaseLines[index + 1] ?? lines.length;
1013
- if (Number(match[1]) !== index + 1)
1014
- issue(issues, file, start + 1, "phase-order", "Phase numbers must start at 1 and be contiguous.");
1015
- const id = field(lines, start + 1, end, "Phase ID");
1016
- const prerequisite = field(lines, start + 1, end, "Prerequisites");
1017
- if (!id?.value)
1018
- issue(issues, file, id?.line ?? start + 1, "phase-id", "Phase ID is required.");
1019
- if (!prerequisite?.value)
1020
- issue(issues, file, prerequisite?.line ?? start + 1, "prerequisites", "Phase prerequisites are required (use None if empty).");
1021
- const prereqs = prerequisite?.value && !/^none$/i.test(prerequisite.value) ? prerequisite.value.split(",").map((value) => value.replaceAll("`", "").trim()) : [];
1022
- for (const required of prereqs) {
1023
- if (!phases.some((phase) => phase.id === required)) {
1024
- issue(issues, file, prerequisite?.line ?? start + 1, "prerequisites", `Prerequisite ${required || "(empty)"} must identify an earlier phase ID.`);
1025
- }
1026
- }
1027
- if (id?.value && phases.some((phase) => phase.id === id.value))
1028
- issue(issues, file, id.line, "phase-id", `Duplicate phase ID ${id.value}.`);
1029
- const tasks = [];
1030
- for (let i = start + 1;i < end; i++) {
1031
- const line = lines[i];
1032
- if (!/^\s*- \[[ xX]\]/.test(line))
1033
- continue;
1034
- const task = /^- \[[ xX]\] \*\*([a-z0-9]+(?:-[a-z0-9]+)*):?\*\* (\S.*)$/.exec(line);
1035
- if (!task) {
1036
- issue(issues, file, i + 1, "task-format", "Use a flat checkbox with a bold stable task ID and a title.");
1037
- continue;
1038
- }
1039
- if (taskIds.has(task[1]))
1040
- issue(issues, file, i + 1, "task-id", `Duplicate task ID ${task[1]}.`);
1041
- taskIds.add(task[1]);
1042
- tasks.push({ id: task[1], title: task[2].trim() });
1043
- }
1044
- if (!tasks.length)
1045
- issue(issues, file, start + 1, "tasks", "Each phase needs at least one task.");
1046
- phases.push({ id: id?.value ?? "", title: match[2].trim(), prerequisites: prereqs, tasks });
1047
- }
1048
- return phases;
1049
- }
1050
- function parseBrief(source, file, phase, number, issues) {
1051
- const lines = linesOf(source);
1052
- const title = lines[0]?.match(/^# Phase (\d+):\s*(\S.*)$/);
1053
- if (!title || Number(title[1]) !== number || title[2]?.trim() !== phase.title) {
1054
- issue(issues, file, 1, "phase-title", `Expected # Phase ${number}: ${phase.title}.`);
1055
- }
1056
- const id = field(lines, 0, lines.length, "Phase ID");
1057
- if (id?.value !== phase.id)
1058
- issue(issues, file, id?.line ?? 0, "phase-id", `Expected Phase ID ${phase.id}.`);
1059
- const prereqs = field(lines, 0, lines.length, "Prerequisites");
1060
- const expected = phase.prerequisites.length ? phase.prerequisites.join(", ") : "None";
1061
- if (prereqs?.value !== expected)
1062
- issue(issues, file, prereqs?.line ?? 0, "prerequisites", `Expected prerequisites ${expected}.`);
1063
- for (const section of ["Objective", "Tasks", "Implementation Constraints", "Phase File Tree"]) {
1064
- requireSection(lines, file, `## ${section}`, issues);
1065
- }
1066
- for (const section of ["Libraries and Algorithms", "Constraints"]) {
1067
- requireSection(lines, file, `### ${section}`, issues);
1068
- }
1069
- const taskHeading = /^### (\d+)\.\s*(\S.*)$/;
1070
- const tasksStart = heading(lines, "## Tasks")[0] ?? 0;
1071
- const tasksEnd = heading(lines, "## Implementation Constraints")[0] ?? lines.length;
1072
- const taskLines = lines.flatMap((line, i) => i > tasksStart && i < tasksEnd && taskHeading.test(line) ? [i] : []);
1073
- if (taskLines.length !== phase.tasks.length)
1074
- issue(issues, file, tasksStart + 1, "task-parity", `Expected ${phase.tasks.length} matching task sections, found ${taskLines.length}.`);
1075
- for (const [index, start] of taskLines.entries()) {
1076
- const end = taskLines[index + 1] ?? tasksEnd;
1077
- const match = taskHeading.exec(lines[start]);
1078
- const planTask = phase.tasks[index];
1079
- const id = field(lines, start + 1, end, "Task ID");
1080
- if (!planTask || Number(match[1]) !== index + 1 || match[2]?.trim() !== planTask.title || id?.value !== planTask.id) {
1081
- issue(issues, file, start + 1, "task-parity", `Task ${index + 1} must match its PLAN.md ID, title, and order.`);
1082
- }
1083
- const steps = field(lines, start + 1, end, "Steps");
1084
- const stepStart = steps ? steps.line : end;
1085
- if (!steps || !lines.slice(stepStart, end).some((line) => /^\s+1\.\s+\S/.test(line))) {
1086
- issue(issues, file, start + 1, "steps", "Add numbered implementation steps starting at 1.");
1087
- }
1088
- const verification = lines.findIndex((line, i) => i > start && i < end && /^\s*- \*\*Verify:\*\*/.test(line));
1089
- const inline = verification >= 0 && /\*\*Verify:\*\*\s*\S/.test(lines[verification]);
1090
- const nested = verification >= 0 && lines.slice(verification + 1, end).some((line) => /^\s{2,}-\s+\S/.test(line));
1091
- if (!inline && !nested) {
1092
- issue(issues, file, verification >= 0 ? verification + 1 : start + 1, "verification", "Nest a nonempty **Verify:** instruction under this task.");
1093
- }
1094
- const acceptance = field(lines, start + 1, end, "Acceptance");
1095
- if (!acceptance?.value)
1096
- issue(issues, file, start + 1, "task-detail", "Add nonempty Acceptance for this task.");
1097
- const misplacedScope = lines.findIndex((line, i) => i > start && i < end && /^\s*(?:-\s+\*\*File scopes:\*\*|#{3,}\s+File Scopes\b)/i.test(line));
1098
- if (misplacedScope >= 0)
1099
- issue(issues, file, misplacedScope + 1, "file-scope", "Put the phase’s file scope only in its Phase File Tree.");
1100
- }
1101
- const treeStart = heading(lines, "## Phase File Tree")[0];
1102
- if (treeStart === undefined)
1103
- return;
1104
- const treeEnd = lines.findIndex((line, i) => i > treeStart && /^## /.test(line));
1105
- const original = source.split(/\r?\n/).slice(treeStart, treeEnd < 0 ? undefined : treeEnd);
1106
- const opens = original.flatMap((line, i) => /^```text\s*$/.test(line) ? [i] : []);
1107
- if (opens.length !== 1) {
1108
- issue(issues, file, treeStart + 1, "tree", "Use exactly one fenced text tree for the phase.");
1109
- return;
1110
- }
1111
- const close = original.findIndex((line, i) => i > opens[0] && /^```\s*$/.test(line));
1112
- if (close < 0) {
1113
- issue(issues, file, treeStart + 1, "tree", "Close the fenced phase file tree.");
1114
- return;
1115
- }
1116
- let leaves = 0;
1117
- for (let i = opens[0] + 1;i < close; i++) {
1118
- const branch = /^\s*[│ ]*(?:├──|└──)\s+(.+)$/.exec(original[i]);
1119
- if (!branch || branch[1].trimEnd().endsWith("/"))
1120
- continue;
1121
- leaves++;
1122
- if (!/^\[(?:ADD|MODIFY|REMOVE|VERIFY|MOVE from: [^\]]+)\]\s+\S/.test(branch[1])) {
1123
- issue(issues, file, treeStart + i + 1, "tree-label", "Every file leaf must start with an action label after the tree branch.");
1124
- }
1125
- }
1126
- if (!leaves)
1127
- issue(issues, file, treeStart + 1, "tree", "Add at least one action-labeled file leaf.");
1128
- }
1129
- async function verifyLivePlanInternal(requested, cwd) {
1130
955
  const path = resolve3(cwd, requested);
1131
956
  let planPath = path;
1132
957
  try {
@@ -1135,42 +960,80 @@ async function verifyLivePlanInternal(requested, cwd) {
1135
960
  } catch {}
1136
961
  const issues = [];
1137
962
  const briefPaths = [];
963
+ const planDir = dirname4(planPath);
1138
964
  if (basename3(planPath) !== "PLAN.md") {
1139
- issue(issues, planPath, 0, "path", "Select a PLAN.md file or its directory.");
965
+ addIssue(issues, planPath, 0, "path", "Select PLAN.md or its directory.");
1140
966
  return { planPath, briefPaths, ok: false, issues };
1141
967
  }
968
+ if (!/^\d{6}(?:-[a-z0-9]+)+$/.test(basename3(planDir)) || basename3(dirname4(planDir)) !== "plan" || basename3(dirname4(dirname4(planDir))) !== ".diffpi")
969
+ addIssue(issues, planPath, 0, "layout", "Place PLAN.md under .diffpi/plan/<YYMMDD[-ticket]-short-slug>/.");
1142
970
  let source;
1143
971
  try {
1144
972
  source = await readFile4(planPath, "utf8");
1145
973
  } catch {
1146
- issue(issues, planPath, 0, "missing-plan", "PLAN.md is not readable.");
974
+ addIssue(issues, planPath, 0, "missing-plan", "PLAN.md is not readable.");
1147
975
  return { planPath, briefPaths, ok: false, issues };
1148
976
  }
1149
- const phases = parsePlan(source, planPath, issues);
1150
- const implementation = join8(resolve3(planPath, ".."), "implementation");
1151
- let entries;
977
+ const planLines = visibleLines(source);
978
+ checkHeadings(planLines, planPath, ["Intent", "Requirements", "Design", "Phases", "References"], issues);
979
+ const phases = planLines.flatMap((line, i) => /^### Phase \d+:\s*\S/.test(line) ? [i] : []);
980
+ if (!phases.length)
981
+ addIssue(issues, planPath, 0, "phases", "Add at least one numbered phase heading.");
982
+ for (const [index, line] of phases.entries()) {
983
+ if (!planLines[line]?.startsWith(`### Phase ${index + 1}: `))
984
+ addIssue(issues, planPath, line + 1, "phase-order", "Number phase headings from 1 without gaps.");
985
+ }
986
+ const implementation = join8(planDir, "implementation");
987
+ let rootEntries = [];
988
+ let briefEntries = [];
1152
989
  try {
1153
- entries = await readdir(implementation);
1154
- } catch {
1155
- entries = [];
990
+ rootEntries = await readdir(planDir);
991
+ briefEntries = await readdir(implementation);
992
+ } catch {}
993
+ for (const entry of rootEntries) {
994
+ if (entry !== "PLAN.md" && entry !== "implementation")
995
+ addIssue(issues, join8(planDir, entry), 0, "layout", "Only PLAN.md and implementation/ belong in the plan directory.");
1156
996
  }
1157
- for (const entry of entries) {
1158
- const match = /^phase-(\d+)\.md$/.exec(entry);
1159
- if (match && (Number(match[1]) < 1 || Number(match[1]) > phases.length || entry !== `phase-${Number(match[1])}.md`)) {
1160
- issue(issues, join8(implementation, entry), 0, "extra-brief", "No matching numbered phase exists for this brief.");
1161
- }
997
+ for (const entry of briefEntries) {
998
+ const ordinal = /^phase-(\d+)\.md$/.exec(entry)?.[1];
999
+ if (!ordinal || !Number(ordinal) || Number(ordinal) > phases.length || entry !== `phase-${Number(ordinal)}.md`)
1000
+ addIssue(issues, join8(implementation, entry), 0, "extra-brief", "Only matching numbered phase briefs belong in implementation/.");
1162
1001
  }
1163
- for (const [index, phase] of phases.entries()) {
1002
+ for (let index = 0;index < phases.length; index++) {
1164
1003
  const brief = join8(implementation, `phase-${index + 1}.md`);
1165
1004
  briefPaths.push(brief);
1166
1005
  try {
1167
- parseBrief(await readFile4(brief, "utf8"), brief, phase, index + 1, issues);
1006
+ const lines = visibleLines(await readFile4(brief, "utf8"));
1007
+ if (!/^# Phase \d+:\s*\S/.test(lines[0] ?? "") || !lines[0]?.startsWith(`# Phase ${index + 1}: `))
1008
+ addIssue(issues, brief, 1, "phase-title", `Start with # Phase ${index + 1}: <title>.`);
1009
+ checkHeadings(lines, brief, ["Objective", "Files Affected", "Tasks", "Implementation Constraints"], issues);
1168
1010
  } catch {
1169
- issue(issues, brief, 0, "missing-brief", "Numbered phase brief is not readable.");
1011
+ addIssue(issues, brief, 0, "missing-brief", "Numbered phase brief is not readable.");
1170
1012
  }
1171
1013
  }
1172
1014
  return { planPath, briefPaths, ok: issues.length === 0, issues };
1173
1015
  }
1016
+ function addIssue(issues, file, line, code, message) {
1017
+ issues.push({ file, line, code, message });
1018
+ }
1019
+ function visibleLines(source) {
1020
+ let fenced = false;
1021
+ return source.split(/\r?\n/).map((line) => {
1022
+ if (/^\s*```/.test(line)) {
1023
+ fenced = !fenced;
1024
+ return "";
1025
+ }
1026
+ return fenced ? "" : line;
1027
+ });
1028
+ }
1029
+ function checkHeadings(lines, file, names, issues) {
1030
+ const found = lines.flatMap((line, i) => line.startsWith("## ") ? [{ text: line, line: i + 1 }] : []);
1031
+ const expected = names.map((name) => `## ${name}`);
1032
+ if (found.map(({ text }) => text).join(`
1033
+ `) !== expected.join(`
1034
+ `))
1035
+ addIssue(issues, file, found[0]?.line ?? 0, "headings", `Expected headings, in order: ${expected.join(", ")}.`);
1036
+ }
1174
1037
 
1175
1038
  // src/tools/plan.ts
1176
1039
  var schema2 = z4.object({ plan: z4.string().min(1), cwd: z4.string().optional() }).strict();
@@ -1345,7 +1208,7 @@ import { z as z10 } from "zod";
1345
1208
  // src/extensions/misex.ts
1346
1209
  import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile2 } from "node:fs/promises";
1347
1210
  import { homedir as homedir3 } from "node:os";
1348
- import { basename as basename4, dirname as dirname4, join as join9 } from "node:path";
1211
+ import { basename as basename4, dirname as dirname5, join as join9 } from "node:path";
1349
1212
  var MISE_HOOK_START = "# >>> @difflab/pi mise >>>";
1350
1213
  var MISE_HOOK_END = "# <<< @difflab/pi mise <<<";
1351
1214
  var mise = {
@@ -1380,7 +1243,7 @@ var mise = {
1380
1243
  const separator = current.length === 0 || current.endsWith(`
1381
1244
  `) ? "" : `
1382
1245
  `;
1383
- await mkdir3(dirname4(hook.path), { recursive: true });
1246
+ await mkdir3(dirname5(hook.path), { recursive: true });
1384
1247
  await writeFile2(hook.path, `${current}${separator}${hook.content}`, "utf8");
1385
1248
  return { path: hook.path, changed: true, planned: false };
1386
1249
  },
@@ -3585,7 +3448,7 @@ function normalizeModelReference(value) {
3585
3448
  // src/mcp.ts
3586
3449
  import { mkdir as mkdir5, readFile as readFile12, writeFile as writeFile6 } from "node:fs/promises";
3587
3450
  import { homedir as homedir6 } from "node:os";
3588
- import { dirname as dirname5, join as join14 } from "node:path";
3451
+ import { dirname as dirname6, join as join14 } from "node:path";
3589
3452
  var mcp = {
3590
3453
  globalConfigPath(homeDir = homedir6()) {
3591
3454
  return join14(homeDir, ".config", "mcp", "mcp.json");
@@ -3601,7 +3464,7 @@ var mcp = {
3601
3464
  const next = { ...current, mcpServers: nextServers };
3602
3465
  const changed = JSON.stringify(current) !== JSON.stringify(next);
3603
3466
  if (changed && !options.dryRun) {
3604
- await mkdir5(dirname5(path), { recursive: true });
3467
+ await mkdir5(dirname6(path), { recursive: true });
3605
3468
  await writeFile6(path, `${JSON.stringify(next, null, 2)}
3606
3469
  `, "utf8");
3607
3470
  }
@@ -3645,7 +3508,7 @@ function isRecord(value) {
3645
3508
  // src/pi.ts
3646
3509
  import { mkdir as mkdir6, writeFile as writeFile7 } from "node:fs/promises";
3647
3510
  import { homedir as homedir7 } from "node:os";
3648
- import { dirname as dirname6, join as join15 } from "node:path";
3511
+ import { dirname as dirname7, join as join15 } from "node:path";
3649
3512
  var pi = {
3650
3513
  executableCheck: findPiExecutable,
3651
3514
  packageList: listPiPackages,
@@ -3662,7 +3525,7 @@ async function ensurePiAgent(filename, content, agentDir = resolvePiAgentDir(),
3662
3525
  const currentText = await readTextIfExists(path);
3663
3526
  const changed = currentText !== content;
3664
3527
  if (changed && !dryRun) {
3665
- await mkdir6(dirname6(path), { recursive: true });
3528
+ await mkdir6(dirname7(path), { recursive: true });
3666
3529
  await writeFile7(path, content, "utf8");
3667
3530
  }
3668
3531
  return { path, changed, existed: currentText !== undefined, planned: changed && dryRun };
@@ -3699,7 +3562,7 @@ async function ensurePiConfig(path, update, dryRun = false) {
3699
3562
  const next = update(current);
3700
3563
  const changed = JSON.stringify(current) !== JSON.stringify(next);
3701
3564
  if (changed && !dryRun) {
3702
- await mkdir6(dirname6(path), { recursive: true });
3565
+ await mkdir6(dirname7(path), { recursive: true });
3703
3566
  await writeFile7(path, `${JSON.stringify(next, null, 2)}
3704
3567
  `, "utf8");
3705
3568
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@difflab/pi",
3
- "version": "0.7.0-rc.202609280043.c43aa2b",
3
+ "version": "0.7.0-rc.202609280945.f13c54b",
4
4
  "description": "Tools and skills for the pi coding agent",
5
5
  "repository": {
6
6
  "type": "git",
@@ -18,13 +18,13 @@ For foreground authoring, call `diffpi_modes_set({agent: "planner"})` using the
18
18
 
19
19
  ## Live-file contract
20
20
 
21
- Plans live under the initiating nested Git root in one flat `.diffpi/plan/<YYMMDD[-ticket]-short-slug>/` directory. The date and slug are parts of ONE directory name, never nested `date/slug` directories. Resolve collisions before writing. Use ordinary `read`/`write`/`edit` calls, read every file back, and preserve intent, IDs, completed evidence, order, dependencies, scopes, and constraints. Edit only live plan files; never managed records, plugin source, tests, or profiles. `INCOMPLETE` is a visible marker inside an open `DRAFT`, not a status enum.
21
+ Plans live under the initiating nested Git root in one flat `.diffpi/plan/<YYMMDD[-ticket]-short-slug>/` directory. The date and slug are parts of ONE directory name, never nested `date/slug` directories. Resolve collisions before writing. Use ordinary `read`/`write`/`edit` calls, read every file back, and preserve intent, IDs, completed evidence, order, dependencies, scopes, and constraints. Document phase constraints in each phase's `PLAN.md` entry (use `None` if empty). Each brief puts its sole action-labeled, fenced `text` file tree under `## Files Affected` immediately after `## Objective` (never a bullet list); `## Implementation Constraints` is free-form with optional subheadings. Edit only live plan files; never managed records, plugin source, tests, or profiles. `INCOMPLETE` is a visible marker inside an open `DRAFT`, not a status enum.
22
22
 
23
23
  `new` and `update` run the stateless read-only `plan_verify` on the current files, then review the complete current draft with one reviewer profile. Repair actionable findings, rerun mechanical verification after each repair, and invoke the same named reviewer again in a fresh `Agent` call (never resume a finished child) within bounded attempts. They remain `DRAFT` after a successful review. Require the child to complete with an attested explicit PASS; partial, steered, stopped, missing, and BLOCKING reports never count. `finalize` and draft `go` write `READY` only after both a passing `plan_verify` on current files and that completed reviewer PASS; do not dispatch Workers or write ready after repairing a failed review without a new completed review. An already-ready plan is reread and mechanically verified too. `plan_verify` does not judge quality, change status, freeze files, or replace the reviewer. No retired managed-plan tools.
24
24
 
25
25
  ## Execution
26
26
 
27
- `go --mode no-commit|commit|push` defaults to `no-commit`. The Orchestrator is the only writer of plan status/checkboxes and coordinates Workers. A draft gets one mechanical `plan_verify` check and one reviewer per pass; both must pass on current files before ready. Schedule a phase only after all prerequisites complete; derive each bounded Worker's task scope from its steps and the phase's sole file tree. Serialize tasks with overlapping or uncertain derived scopes; independent tasks may run concurrently only when scopes are unambiguously disjoint and prerequisites are satisfied. Run project format, lint, test, and CI checks via `bash`/`ctx_execute` as appropriate. On any failure, preserve the exact command/output, attempts, and affected role and block.
27
+ `go --mode no-commit|commit|push` defaults to `no-commit`. The Orchestrator is the only writer of plan status/checkboxes and coordinates Workers. A draft gets one mechanical `plan_verify` check and one reviewer per pass; both must pass on current files before ready. Schedule a phase only after all prerequisites complete; derive each bounded Worker's task scope from its steps and the phase's sole Files Affected tree. Serialize tasks with overlapping or uncertain derived scopes; independent tasks may run concurrently only when scopes are unambiguously disjoint and prerequisites are satisfied. Run project format, lint, test, and CI checks via `bash`/`ctx_execute` as appropriate. On any failure, preserve the exact command/output, attempts, and affected role and block.
28
28
 
29
29
  With `commit`, after gates pass make one phase commit. With `push`, push it and call `watch_ci` for that exact SHA; do not push the next phase until settled. `--bg` has one child only and that child does not redispatch. No stateful plan tools.
30
30
 
@@ -5,21 +5,20 @@ Build from repository evidence. Keep the result compact and useful to a Worker.
5
5
  ## PLAN.md
6
6
 
7
7
  - Keep intent, requirements, design, references, and numbered phases in `PLAN.md`.
8
- - Give each phase a stable ID, title, objective, and prerequisites. Prerequisites belong to phases only.
8
+ - Give each phase a stable ID, title, objective, prerequisites, and documented phase-wide guardrails (use `None` if there are none). Prerequisites and phase constraints belong in `PLAN.md` only; keep detailed technology, algorithm, and invariant instructions for the brief instead of copying them into both places.
9
9
  - Put each discrete implementation action in a flat task checkbox with a stable task ID and title. Do not put dependency metadata, steps, file scopes, or acceptance criteria under the checkbox.
10
10
  - Preserve phase/task ID and order parity in every implementation brief.
11
11
 
12
12
  ## Implementation brief
13
13
 
14
14
  - Give tasks ordered implementation steps. Add nested `Verify` bullets under the relevant task; verification is part of the task, not a separate task.
15
- - Put all exact file scopes in the single phase-level file tree; do not add per-task file scopes. Include acceptance criteria for every task.
16
- - Put libraries and algorithms under **Implementation Constraints**.
17
- - Include exactly one fenced `text` phase-level directory tree. Label every file leaf with one of `[ADD]`, `[MODIFY]`, `[REMOVE]`, `[MOVE from: path]`, or `[VERIFY]`.
15
+ - Put `## Files Affected` immediately after `## Objective` and before `## Tasks`. It is the sole phase-level file scope: include exactly one fenced `text` file tree with a directory root and branches whose file leaves start with `[ADD]`, `[MODIFY]`, `[REMOVE]`, `[MOVE from: old/path]`, or `[VERIFY]`. Do not replace the tree with Markdown bullets or add per-task file scopes. Include acceptance criteria for every task.
16
+ - Write `## Implementation Constraints` as useful free-form implementation guidance. Add `### Required Libraries & Technology Choices`, `### Key Algorithm Specifications`, or `### Core Invariants` only when useful; do not require any of them. Do not restate or paraphrase the phase-wide guardrails in `PLAN.md` here; refer to the phase ID when a connection is needed.
18
17
  - Read back each file after writing it. Normal successive write/edit calls are intentional: incomplete drafts remain visible until the plan is ready.
19
18
 
20
19
  ## Review and repair
21
20
 
22
- Template metadata such as revision snapshots and managed markers is not an authoring requirement. Treat their removal as a legacy compatibility change: the existing managed parser will stop recognizing these templates until the follow-on source migration adds a replacement contract. Report that break explicitly; do not preserve metadata in the templates solely to hide it.
21
+ Template metadata such as managed revisions or snapshots is not part of the live-file contract. Leave historical plan files untouched; current plans use the direct-file format.
23
22
 
24
23
  After writing the current `PLAN.md` and all numbered briefs, independently invoke exactly one VERIFIED frontier/high-thinking `diffpi-plan-reviewer` in read-only mode. The reviewer checks structure and parity, action labels, whole-plan quality, consistency and risk, and whether every task is executable by a lightweight Worker without guessing, in one pass. Do not replace this with cheap separate reviewers or tiered checks.
25
24
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  1. Parse `--mode no-commit|commit|push`; default is `no-commit`. Select the shared inline profile with `diffpi_modes_set({agent: "orchestrator"})` and verify `diffpi_modes_status` on the next turn. If switching or evidence fails, stop with the exact error. Git/project tasks and gates run through `bash` (or `ctx_execute` for large output); there are no `task`, `format`, `lint`, `test`, `ci`, `git`, or `push` tools.
6
6
  2. Select and reread the current live `PLAN.md` and all numbered briefs. Run `plan_verify` on that exact plan directory; if it reports issues, stop in its current state and repair through Planner before trying again. If `DRAFT`, invoke exactly one named `diffpi-plan-reviewer` per pass with `Agent(run_in_background:false)`; do not impose a short turn limit. Independently verify runtime attestation and require a **completed** child result with an explicit PASS and no BLOCKING findings. A `steered`, partial, stopped, missing, or failed result is NOT a pass. On findings, hand repairs to Planner, reread live files, rerun `plan_verify`, then invoke the same named reviewer profile in a fresh `Agent` call (not `resume`) within bounded attempts. Write `READY` and dispatch Workers ONLY after a current mechanical PASS and a completed attested reviewer PASS; otherwise leave `DRAFT` and stop with exact evidence. Do not launch a duplicate background child or recursively dispatch. If already `READY`, reread and verify current files as-is.
7
- 3. The Orchestrator alone writes live plan checkboxes/status. Schedule a phase only after every prerequisite is complete. Derive a bounded Worker scope per task from that phase brief's sole file tree and the task's steps; never invent separate per-task scope fields. Serialize when task-to-file ownership is uncertain or scopes overlap. Dispatch disjoint Workers concurrently only when prerequisites are complete and ownership is unambiguous. The Worker never edits plan files, commits, redispatches, asks questions, or falls back inline.
7
+ 3. The Orchestrator alone writes live plan checkboxes/status. Schedule a phase only after every prerequisite is complete. Derive a bounded Worker scope per task from that phase brief's sole fenced Files Affected tree and the task's steps; never invent separate per-task scope fields. Serialize when task-to-file ownership is uncertain or scopes overlap. Dispatch disjoint Workers concurrently only when prerequisites are complete and ownership is unambiguous. The Worker never edits plan files, commits, redispatches, asks questions, or falls back inline.
8
8
  4. Run the declared project format, lint, test, and CI gates through `bash`/`ctx_execute`. On Worker, gate, preflight, reviewer, or command failure, block; preserve the exact command/output, attempts, and affected role.
9
9
  5. In `no-commit`, continue only with verified gates. In `commit`, after all phase gates pass, make exactly one commit for that phase. In `push`, push that phase commit, obtain its exact SHA, and call `watch_ci` for that SHA; do not push or schedule the next phase until CI settles successfully.
10
10
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  1. Select the shared inline profile with `diffpi_modes_set({agent: "planner"})` (not `plan:planner`) and verify `diffpi_modes_status` on the next turn; stop on unavailable switch or unverifiable evidence.
6
6
  2. Resolve the initiating nested Git root and a flat `.diffpi/plan/<YYMMDD[-ticket]-short-slug>/` directory (one date-prefixed directory, not `date/slug`). Check collisions and stop before writing an existing match. Research only after selection.
7
- 3. Successively write `PLAN.md` and one numbered brief per phase. Preserve exact phase/task parity, dependencies, scopes, constraints, acceptance criteria, and one action-labeled tree per brief; read every file back.
7
+ 3. Successively write `PLAN.md` and one numbered brief per phase. Document each phase's high-level constraints in `PLAN.md` (use `None` if empty) without copying implementation details there. Do not repeat or paraphrase those phase constraints in a brief; reference the phase ID instead. Give each brief a `## Files Affected` section immediately after `## Objective` and before `## Tasks`, containing exactly one fenced `text` action-labeled file tree rather than Markdown bullets, with free-form `## Implementation Constraints` after tasks; optional implementation subheadings are not mandatory. Preserve phase/task parity, dependencies, scopes, and acceptance criteria; read every file back.
8
8
  4. Run read-only `plan_verify` against the live directory and repair structural issues before invoking exactly one reviewer. Verify its attestation and review the complete current draft. Repair actionable findings, rerun `plan_verify`, then invoke the same named reviewer profile in a fresh `Agent` call (not `resume`) within bounded attempts.
9
9
  5. Leave the plan marked `DRAFT` after a successful review. Do not write `READY`; only `/plan finalize` or draft `/plan go` may do that.
10
10
 
@@ -37,6 +37,7 @@
37
37
  - **Phase ID:** <!-- assign a stable phase ID -->
38
38
  - **Prerequisites:** <!-- list phase IDs, or “None” -->
39
39
  - **Objective:** <!-- state the outcome -->
40
+ - **Constraints:** <!-- phase guardrails or None; do not repeat them in briefs -->
40
41
 
41
42
  - [ ] <!-- assign a stable task ID --> Task title
42
43
  - [ ] <!-- assign a stable task ID --> Another task title
@@ -7,6 +7,17 @@
7
7
 
8
8
  {{phase_objective}}
9
9
 
10
+ ## Files Affected
11
+
12
+ <!-- This is the sole file scope for this phase. Use one fenced text tree; label each file leaf [ADD], [MODIFY], [REMOVE], [MOVE from: path], or [VERIFY]. Do not repeat file scopes in tasks or PLAN.md. -->
13
+
14
+ ```text
15
+ Phase {{phase_ordinal}}/
16
+ ├── [MODIFY] <!-- exact/path/to/implementation-file -->
17
+ └── [VERIFY] <!-- exact/path/to/test-or-check -->
18
+ {{phase_files_affected}}
19
+ ```
20
+
10
21
  ## Tasks
11
22
 
12
23
  <!-- Keep tasks flat and in phase order. Repeat this shape for each task. -->
@@ -25,21 +36,6 @@
25
36
 
26
37
  ## Implementation Constraints
27
38
 
28
- ### Libraries and Algorithms
29
-
30
- {{phase_libraries}}
39
+ <!-- Free-form implementation details, not a restatement of PLAN.md phase constraints. Add only useful optional headings: Required Libraries & Technology Choices, Key Algorithm Specifications, Core Invariants. Refer to the PLAN.md phase ID instead of repeating a phase guardrail. -->
31
40
 
32
- ### Constraints
33
-
34
- {{phase_constraints}}
35
-
36
- ## Phase File Tree
37
-
38
- <!-- This is the only file scope for the phase; do not repeat file scopes inside tasks or PLAN.md. Include exactly one tree. Label every file leaf with [ADD], [MODIFY], [REMOVE], [MOVE from: path], or [VERIFY]. -->
39
-
40
- ```text
41
- Phase {{phase_ordinal}}/
42
- ├── [MODIFY] <!-- exact/path/to/implementation-file -->
43
- └── [VERIFY] <!-- exact/path/to/test-or-check -->
44
- {{phase_file_tree}}
45
- ```
41
+ {{phase_implementation_constraints}}