@difflab/pi 0.7.0-rc.202609271854.6af8600 → 0.7.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.
@@ -8,22 +8,22 @@ run_in_background: true
8
8
  model: openai-codex/gpt-5.6-sol
9
9
  model_fallbacks: meridian/claude-opus-4-8, meridian/claude-opus-5, deepseek/deepseek-v4-pro, qwen-token-plan/qwen3.7-plus
10
10
  thinking: high
11
- tools: read, grep, find, write, edit, Agent, get_subagent_result, steer_subagent, ask_user_question
11
+ tools: read, grep, find, Agent, get_subagent_result, steer_subagent, ask_user_question, plan_context, plan_init, plan_apply_revision, plan_validate, plan_review
12
12
  metadata:
13
13
  model-tier: frontier
14
14
  ---
15
15
 
16
- You are the Diffpi planning agent. Execute the plan skill's direct file workflows to create repository-grounded plans and revise unfinished work. Read the selected workflow reference under `packages/pi/skills/plan/references/workflows/` before acting. Select one unique repository-root plan by its path and plan files, then read it before updating. Preserve the user's request and intent, including visible completed task information. Write authoritative plan files directly through successive normal read/write/edit calls. Do not edit source files or commit Git changes. After each iteration, invoke exactly one independently verified `diffpi-plan-reviewer` with explicit frontier/high selection, repair actionable findings, and rerun it within bounded attempts. If effective model or thinking selection cannot be verified, fail in preflight with the observed selection and required correction rather than using a cheap fallback.
16
+ You are the Diffpi planning agent. Execute the plan skill's direct `plan_*` workflows to create repository-grounded plans and revise unfinished work. Read the selected workflow reference under `packages/pi/skills/plan/references/workflows/` before acting. Preserve incoming request text exactly when it is revision input. Do not emit command or copy-paste routing instructions, edit managed plan artifacts, edit source files, or commit Git changes. Author the entire plan and numbered briefs through one `plan_apply_revision` call per request.
17
17
 
18
18
  ## Plan quality
19
19
 
20
- - 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.
20
+ - Call `plan_context` before changing, annotating, finalizing, or executing a plan.
21
21
  - Inspect the repository before proposing phases. Resolve research during planning; do not leave research tasks for implementation.
22
22
  - Keep stable lowercase phase and task IDs. Give each task explicit dependencies, file scopes, steps, and acceptance criteria.
23
23
  - Keep Design at 300 words or fewer when practical and never finalize it above 800 words.
24
24
  - Preserve completed work and evidence. Amend only draft, pending, or blocked work.
25
- - 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.
26
- - The reviewer verifies the authoritative files after each iteration; do not require an immutable managed review read.
25
+ - Call `plan_validate` after authoring and use strict validation before marking a plan ready.
26
+ - Read the immutable plan review for the current plan revision before applying update instructions.
27
27
 
28
28
  Use `ask_user_question` for every interactive decision. Never ask a question in plain chat. When running in the background, do not ask questions. Record assumptions when safe; otherwise return a concise blocker that identifies the unresolved decision.
29
29
 
package/dist/cli.js CHANGED
@@ -2384,8 +2384,8 @@ function appendBounded(current, next) {
2384
2384
 
2385
2385
  // src/plan/store.ts
2386
2386
  import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
2387
- import { mkdir as mkdir3, readdir, readFile as readFile2, realpath as realpath3, rename as rename2, rm as rm2 } from "node:fs/promises";
2388
- import { basename as basename3, dirname as dirname3, join as join5, resolve as resolve3, sep } from "node:path";
2387
+ import { mkdir as mkdir3, readdir, readFile as readFile3, realpath as realpath3, rename as rename2, rm as rm2 } from "node:fs/promises";
2388
+ import { basename as basename3, dirname as dirname4, join as join7, resolve as resolve3, sep } from "node:path";
2389
2389
 
2390
2390
  // src/extensions/fsx.ts
2391
2391
  import { mkdir, rename, rm, writeFile } from "node:fs/promises";
@@ -2557,6 +2557,57 @@ function projectSlug(repository) {
2557
2557
  return `${readable}-${digest}`;
2558
2558
  }
2559
2559
 
2560
+ // src/templates.ts
2561
+ import { readFile as readFile2 } from "node:fs/promises";
2562
+ import { homedir as homedir2 } from "node:os";
2563
+ import { join as join5, normalize } from "node:path";
2564
+
2565
+ // src/assets.ts
2566
+ import { existsSync } from "node:fs";
2567
+ import { dirname as dirname3, join as join4 } from "node:path";
2568
+ import { fileURLToPath } from "node:url";
2569
+ function resolveBundledAssetDir(name, moduleUrl = import.meta.url) {
2570
+ const moduleDir = dirname3(fileURLToPath(moduleUrl));
2571
+ const candidates = [join4(moduleDir, name), join4(moduleDir, "..", name), join4(moduleDir, "..", "..", name)];
2572
+ return candidates.find((path) => existsSync(path)) ?? candidates[1];
2573
+ }
2574
+ function resolveBundledTemplatesDir(moduleUrl = import.meta.url) {
2575
+ return resolveBundledAssetDir("templates", moduleUrl);
2576
+ }
2577
+
2578
+ // src/templates.ts
2579
+ async function loadTemplate(name, options = {}) {
2580
+ const relative = templateRelativePath(name);
2581
+ const userPath = join5(options.homeDir ?? homedir2(), ".difflab", "diffpi", "templates", relative);
2582
+ const bundledPath = join5(options.bundledDir ?? resolveBundledTemplatesDir(), relative);
2583
+ const user = await readOptionalFile(userPath);
2584
+ if (user !== undefined)
2585
+ return { name, path: userPath, source: "user", content: user };
2586
+ const bundled = await readOptionalFile(bundledPath);
2587
+ if (bundled !== undefined)
2588
+ return { name, path: bundledPath, source: "bundled", content: bundled };
2589
+ throw new Error(`Template "${name}" was not found at ${userPath} or ${bundledPath}.`);
2590
+ }
2591
+ function renderTemplate(content, variables) {
2592
+ return content.replace(/\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g, (match, key) => variables[key] ?? match);
2593
+ }
2594
+ function templateRelativePath(name) {
2595
+ const normalized = normalize(name.replaceAll("\\", "/")).replace(/^\.\//, "");
2596
+ if (!normalized || normalized.startsWith("../") || normalized.includes("/../") || normalized.startsWith("/")) {
2597
+ throw new Error(`Invalid template name: ${name}`);
2598
+ }
2599
+ return normalized.endsWith(".md") ? normalized : `${normalized}.md`;
2600
+ }
2601
+ async function readOptionalFile(path) {
2602
+ try {
2603
+ return await readFile2(path, "utf8");
2604
+ } catch (error) {
2605
+ if (error.code === "ENOENT")
2606
+ return;
2607
+ throw error;
2608
+ }
2609
+ }
2610
+
2560
2611
  // src/plan/markdown.ts
2561
2612
  var import_mdast_builder = __toESM(require_src(), 1);
2562
2613
 
@@ -3903,7 +3954,7 @@ var handle = {
3903
3954
  };
3904
3955
 
3905
3956
  // ../../node_modules/mdast-util-to-markdown/lib/join.js
3906
- var join4 = [joinDefaults];
3957
+ var join6 = [joinDefaults];
3907
3958
  function joinDefaults(left, right, parent, state) {
3908
3959
  if (right.type === "code" && formatCodeAsIndented(right, state) && (left.type === "list" || left.type === right.type && formatCodeAsIndented(left, state))) {
3909
3960
  return false;
@@ -6449,7 +6500,7 @@ function toMarkdown(tree, options) {
6449
6500
  handle: undefined,
6450
6501
  indentLines,
6451
6502
  indexStack: [],
6452
- join: [...join4],
6503
+ join: [...join6],
6453
6504
  options: {},
6454
6505
  safe: safeBound,
6455
6506
  stack: [],
@@ -6753,7 +6804,7 @@ import { default as default2 } from "node:path";
6753
6804
  import { default as default3 } from "node:process";
6754
6805
 
6755
6806
  // ../../node_modules/vfile/lib/minurl.js
6756
- import { fileURLToPath } from "node:url";
6807
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
6757
6808
 
6758
6809
  // ../../node_modules/vfile/lib/minurl.shared.js
6759
6810
  function isUrl(fileUrlOrPath) {
@@ -6839,7 +6890,7 @@ class VFile {
6839
6890
  }
6840
6891
  set path(path) {
6841
6892
  if (isUrl(path)) {
6842
- path = fileURLToPath(path);
6893
+ path = fileURLToPath2(path);
6843
6894
  }
6844
6895
  assertNonEmpty(path, "path");
6845
6896
  if (this.path !== path) {
@@ -7763,7 +7814,7 @@ async function resolvePlan(cwd, query, filters = {}, options = {}) {
7763
7814
  continue;
7764
7815
  if (normalizedQuery && entry.name !== normalizedQuery && stripDate(entry.name) !== normalizedQuery)
7765
7816
  continue;
7766
- const dir = join5(root, entry.name);
7817
+ const dir = join7(root, entry.name);
7767
7818
  await assertContained(root, dir);
7768
7819
  try {
7769
7820
  const record = await readRecord(dir);
@@ -7798,30 +7849,26 @@ function createPlanStore(options = {}) {
7798
7849
  async init(input) {
7799
7850
  const root = await plansDir(input.cwd, options.homeDir);
7800
7851
  const id = planRecordName(input.shortSlug, now());
7801
- const dir = join5(root, id);
7852
+ const dir = join7(root, id);
7802
7853
  await createPlanDirectory(root, dir, id);
7803
7854
  try {
7804
7855
  const timestamp = now().toISOString();
7856
+ const template = await loadTemplate("plan/PLAN", {
7857
+ homeDir: options.homeDir,
7858
+ bundledDir: options.bundledTemplatesDir
7859
+ });
7805
7860
  const title = input.title?.trim() || titleFromSlug(input.shortSlug);
7806
- const document = {
7807
- schemaVersion: 1,
7861
+ const source = renderTemplate(template.content, {
7808
7862
  id,
7809
- revision: 0,
7810
- title,
7811
7863
  branch: input.branch,
7812
- ...input.issueId?.trim() ? { issueId: input.issueId.trim() } : {},
7813
- ...input.issueUrl?.trim() ? { issueUrl: input.issueUrl.trim() } : {},
7864
+ title,
7814
7865
  intent: input.intent?.trim() || "<!-- Describe the intended outcome. -->",
7815
- requirements: [],
7816
- design: { bigIdeas: "", keyApiUpdates: "", consequences: "" },
7817
- phases: [],
7818
- references: [],
7819
- status: "draft",
7820
- createdAt: timestamp,
7821
- updatedAt: timestamp
7822
- };
7823
- const source = renderPlanDocument(document);
7824
- parsePlanDocument(source, join5(dir, "PLAN.md"));
7866
+ issue_id: input.issueId?.trim() || "",
7867
+ issue_url: input.issueUrl?.trim() || "",
7868
+ created_at: timestamp,
7869
+ updated_at: timestamp
7870
+ });
7871
+ const document = parsePlanDocument(source, join7(dir, "PLAN.md"));
7825
7872
  const authoringRequest = input.request ?? { kind: "user", text: input.intent?.trim() || `Initialize phase-less plan ${title}.` };
7826
7873
  await writeRevisionSnapshot(dir, document, source, authoringRequest, []);
7827
7874
  await writeLatestView(dir, document, source, []);
@@ -7836,14 +7883,14 @@ function createPlanStore(options = {}) {
7836
7883
  if (request.mode === "create") {
7837
7884
  const root = await plansDir(cwd, options.homeDir);
7838
7885
  const id = planRecordName(request.shortSlug, now());
7839
- const dir = join5(root, id);
7886
+ const dir = join7(root, id);
7840
7887
  await createPlanDirectory(root, dir, id);
7841
7888
  try {
7842
7889
  const timestamp = now().toISOString();
7843
7890
  const document = normalizeRevision(undefined, request, id, 0, timestamp);
7844
7891
  assertCompleteRevision(document, request.briefs);
7845
7892
  const source = renderPlanDocument(document);
7846
- parsePlanDocument(source, join5(dir, "PLAN.md"));
7893
+ parsePlanDocument(source, join7(dir, "PLAN.md"));
7847
7894
  await writeRevisionSnapshot(dir, document, source, request.request, request.briefs);
7848
7895
  await writeLatestView(dir, document, source, request.briefs);
7849
7896
  return await readRecord(dir);
@@ -7853,7 +7900,7 @@ function createPlanStore(options = {}) {
7853
7900
  }
7854
7901
  }
7855
7902
  const initial = await read(cwd, request.plan);
7856
- return withDirectoryLock(join5(initial.dir, ".lock"), async () => {
7903
+ return withDirectoryLock(join7(initial.dir, ".lock"), async () => {
7857
7904
  const current = await readRecord(initial.dir);
7858
7905
  if (current.document.revision !== request.expectedPlanRevision)
7859
7906
  throw new Error(`Expected plan revision ${request.expectedPlanRevision}, found ${current.document.revision}.`);
@@ -7866,7 +7913,7 @@ function createPlanStore(options = {}) {
7866
7913
  try {
7867
7914
  await writeLatestView(current.dir, document, source, request.briefs);
7868
7915
  } catch (error) {
7869
- await rm2(join5(current.dir, "revisions", String(revision)), { recursive: true, force: true });
7916
+ await rm2(join7(current.dir, "revisions", String(revision)), { recursive: true, force: true });
7870
7917
  throw error;
7871
7918
  }
7872
7919
  return await readRecord(current.dir);
@@ -7874,7 +7921,7 @@ function createPlanStore(options = {}) {
7874
7921
  },
7875
7922
  async mutate(cwd, query, operation, update) {
7876
7923
  const initial = await read(cwd, query);
7877
- return withDirectoryLock(join5(initial.dir, ".lock"), async () => {
7924
+ return withDirectoryLock(join7(initial.dir, ".lock"), async () => {
7878
7925
  const current = await readRecord(initial.dir);
7879
7926
  const next = await update(structuredClone(current.document));
7880
7927
  if (next.id !== current.id)
@@ -7897,22 +7944,22 @@ function createPlanStore(options = {}) {
7897
7944
  };
7898
7945
  }
7899
7946
  async function readRecord(dir) {
7900
- const planPath = join5(dir, "PLAN.md");
7901
- const logPath = join5(dir, "logs.txt");
7902
- const source = await readFile2(planPath, "utf8");
7947
+ const planPath = join7(dir, "PLAN.md");
7948
+ const logPath = join7(dir, "logs.txt");
7949
+ const source = await readFile3(planPath, "utf8");
7903
7950
  const document = parsePlanDocument(source, planPath);
7904
7951
  return {
7905
7952
  id: basename3(dir),
7906
7953
  dir,
7907
7954
  planPath,
7908
7955
  logPath,
7909
- implementationDir: join5(dir, "implementation"),
7956
+ implementationDir: join7(dir, "implementation"),
7910
7957
  document,
7911
7958
  source
7912
7959
  };
7913
7960
  }
7914
7961
  async function createPlanDirectory(root, dir, id) {
7915
- await assertContained(root, dirname3(dir));
7962
+ await assertContained(root, dirname4(dir));
7916
7963
  try {
7917
7964
  await mkdir3(dir);
7918
7965
  } catch (error) {
@@ -7926,17 +7973,17 @@ async function writeRevisionSnapshot(planDir, document, source, request, briefs)
7926
7973
  throw new Error("An exact non-empty authoring request is required.");
7927
7974
  if (request.kind === "annotation" && !request.response?.trim())
7928
7975
  throw new Error("Annotation requests require a non-empty response.");
7929
- const revisionsDir = join5(planDir, "revisions");
7976
+ const revisionsDir = join7(planDir, "revisions");
7930
7977
  await mkdir3(revisionsDir, { recursive: true });
7931
- const target = join5(revisionsDir, String(document.revision));
7978
+ const target = join7(revisionsDir, String(document.revision));
7932
7979
  try {
7933
- await readFile2(join5(target, "metadata.json"), "utf8");
7980
+ await readFile3(join7(target, "metadata.json"), "utf8");
7934
7981
  throw new Error(`Revision ${document.revision} already exists and is immutable.`);
7935
7982
  } catch (error) {
7936
7983
  if (error.code !== "ENOENT")
7937
7984
  throw error;
7938
7985
  }
7939
- const staging = join5(revisionsDir, `.tmp-${document.revision}-${randomUUID2()}`);
7986
+ const staging = join7(revisionsDir, `.tmp-${document.revision}-${randomUUID2()}`);
7940
7987
  await mkdir3(staging);
7941
7988
  try {
7942
7989
  const requestSource = request.kind === "annotation" ? `## Original annotation
@@ -7944,8 +7991,8 @@ ${request.text}
7944
7991
 
7945
7992
  ## LLM response
7946
7993
  ${request.response}` : request.text;
7947
- await atomicWrite(join5(staging, "request.md"), requestSource);
7948
- await atomicWrite(join5(staging, "metadata.json"), `${JSON.stringify({
7994
+ await atomicWrite(join7(staging, "request.md"), requestSource);
7995
+ await atomicWrite(join7(staging, "metadata.json"), `${JSON.stringify({
7949
7996
  schemaVersion: 1,
7950
7997
  id: document.id,
7951
7998
  revision: document.revision,
@@ -7955,7 +8002,7 @@ ${request.response}` : request.text;
7955
8002
  createdAt: document.updatedAt
7956
8003
  }, null, 2)}
7957
8004
  `);
7958
- await atomicWrite(join5(staging, "PLAN.md"), source);
8005
+ await atomicWrite(join7(staging, "PLAN.md"), source);
7959
8006
  await writeImplementationDirectory(staging, document, briefs);
7960
8007
  await rename2(staging, target);
7961
8008
  } catch (error) {
@@ -7964,10 +8011,10 @@ ${request.response}` : request.text;
7964
8011
  }
7965
8012
  }
7966
8013
  async function writeLatestView(planDir, document, source, briefs) {
7967
- const current = join5(planDir, "implementation");
7968
- const stagingRoot = join5(planDir, `.latest-${randomUUID2()}`);
7969
- const stagedImplementation = join5(stagingRoot, "implementation");
7970
- const backup = join5(planDir, `.implementation-backup-${randomUUID2()}`);
8014
+ const current = join7(planDir, "implementation");
8015
+ const stagingRoot = join7(planDir, `.latest-${randomUUID2()}`);
8016
+ const stagedImplementation = join7(stagingRoot, "implementation");
8017
+ const backup = join7(planDir, `.implementation-backup-${randomUUID2()}`);
7971
8018
  await mkdir3(stagingRoot);
7972
8019
  await writeImplementationDirectory(stagingRoot, document, briefs);
7973
8020
  let backedUp = false;
@@ -7980,7 +8027,7 @@ async function writeLatestView(planDir, document, source, briefs) {
7980
8027
  throw error;
7981
8028
  }
7982
8029
  await rename2(stagedImplementation, current);
7983
- await atomicWrite(join5(planDir, "PLAN.md"), source);
8030
+ await atomicWrite(join7(planDir, "PLAN.md"), source);
7984
8031
  if (backedUp)
7985
8032
  await rm2(backup, { recursive: true, force: true });
7986
8033
  } catch (error) {
@@ -7993,13 +8040,13 @@ async function writeLatestView(planDir, document, source, briefs) {
7993
8040
  }
7994
8041
  }
7995
8042
  async function writeImplementationDirectory(baseDir, document, briefs) {
7996
- const implementationDir = join5(baseDir, "implementation");
8043
+ const implementationDir = join7(baseDir, "implementation");
7997
8044
  await mkdir3(implementationDir, { recursive: true });
7998
8045
  for (const [index, phase] of document.phases.entries()) {
7999
8046
  const brief = briefs[index];
8000
8047
  if (!brief)
8001
8048
  throw new Error(`Missing implementation brief for phase ${phase.id}.`);
8002
- await atomicWrite(join5(implementationDir, `phase-${index + 1}.md`), renderImplementationBrief(document.revision, index + 1, phase, brief));
8049
+ await atomicWrite(join7(implementationDir, `phase-${index + 1}.md`), renderImplementationBrief(document.revision, index + 1, phase, brief));
8003
8050
  }
8004
8051
  }
8005
8052
  function normalizeRevision(current, request, id, revision, updatedAt) {
@@ -8151,8 +8198,8 @@ function titleFromSlug(value) {
8151
8198
  });
8152
8199
  }
8153
8200
  // src/plan/reviews.ts
8154
- import { mkdir as mkdir4, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
8155
- import { dirname as dirname4, join as join6 } from "node:path";
8201
+ import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile2 } from "node:fs/promises";
8202
+ import { dirname as dirname5, join as join8 } from "node:path";
8156
8203
  async function createPlanReview(record, runtime = {}) {
8157
8204
  if (await readPlanReview(record))
8158
8205
  throw new Error(`Plan revision ${record.document.revision} already has a review.`);
@@ -8185,7 +8232,7 @@ async function readPlanReview(record) {
8185
8232
  const path = reviewPath(record);
8186
8233
  let source;
8187
8234
  try {
8188
- source = await readFile3(path, "utf8");
8235
+ source = await readFile4(path, "utf8");
8189
8236
  } catch (error) {
8190
8237
  if (error.code === "ENOENT")
8191
8238
  return;
@@ -8202,10 +8249,10 @@ async function readPlanReview(record) {
8202
8249
  return { path, dump };
8203
8250
  }
8204
8251
  function reviewPath(record) {
8205
- return join6(record.dir, "reviews", `${record.document.revision}.json`);
8252
+ return join8(record.dir, "reviews", `${record.document.revision}.json`);
8206
8253
  }
8207
8254
  async function writeReview(review) {
8208
- await mkdir4(dirname4(review.path), { recursive: true });
8255
+ await mkdir4(dirname5(review.path), { recursive: true });
8209
8256
  await writeFile2(review.path, `${JSON.stringify(review.dump, null, 2)}
8210
8257
  `, {
8211
8258
  encoding: "utf8",