@difflab/pi 0.3.0-rc.202609200047.ac3661a → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +2 -22
  2. package/agents/diffpi-orchestrator.md +0 -10
  3. package/agents/diffpi-worker.md +1 -9
  4. package/dist/commands/index.d.ts +0 -1
  5. package/dist/commands/index.d.ts.map +1 -1
  6. package/dist/commands/review.d.ts.map +1 -1
  7. package/dist/environment.d.ts.map +1 -1
  8. package/dist/extensions/index.js +844 -2773
  9. package/dist/extensions/zedx.d.ts +0 -2
  10. package/dist/extensions/zedx.d.ts.map +1 -1
  11. package/dist/index.d.ts +2 -5
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +128 -1140
  14. package/dist/setup.d.ts.map +1 -1
  15. package/dist/store.d.ts +0 -1
  16. package/dist/store.d.ts.map +1 -1
  17. package/dist/tools/index.d.ts +0 -1
  18. package/dist/tools/index.d.ts.map +1 -1
  19. package/dist/tools/index.js +678 -2400
  20. package/package.json +1 -4
  21. package/agents/diffpi-planner.md +0 -30
  22. package/dist/cli/plan.d.ts +0 -7
  23. package/dist/cli/plan.d.ts.map +0 -1
  24. package/dist/cli.d.ts +0 -3
  25. package/dist/cli.d.ts.map +0 -1
  26. package/dist/cli.js +0 -729
  27. package/dist/commands/background.d.ts +0 -39
  28. package/dist/commands/background.d.ts.map +0 -1
  29. package/dist/commands/plan.d.ts +0 -21
  30. package/dist/commands/plan.d.ts.map +0 -1
  31. package/dist/plan/annotations.d.ts +0 -25
  32. package/dist/plan/annotations.d.ts.map +0 -1
  33. package/dist/plan/escalation.d.ts +0 -4
  34. package/dist/plan/escalation.d.ts.map +0 -1
  35. package/dist/plan/execution.d.ts +0 -19
  36. package/dist/plan/execution.d.ts.map +0 -1
  37. package/dist/plan/index.d.ts +0 -15
  38. package/dist/plan/index.d.ts.map +0 -1
  39. package/dist/plan/lock.d.ts +0 -7
  40. package/dist/plan/lock.d.ts.map +0 -1
  41. package/dist/plan/log.d.ts +0 -5
  42. package/dist/plan/log.d.ts.map +0 -1
  43. package/dist/plan/markdown.d.ts +0 -6
  44. package/dist/plan/markdown.d.ts.map +0 -1
  45. package/dist/plan/store.d.ts +0 -34
  46. package/dist/plan/store.d.ts.map +0 -1
  47. package/dist/plan/transitions.d.ts +0 -9
  48. package/dist/plan/transitions.d.ts.map +0 -1
  49. package/dist/plan/types.d.ts +0 -146
  50. package/dist/plan/types.d.ts.map +0 -1
  51. package/dist/tools/plan.d.ts +0 -6
  52. package/dist/tools/plan.d.ts.map +0 -1
  53. package/skills/plan/SKILL.md +0 -13
  54. package/skills/plan/references/workflows/annotate.md +0 -6
  55. package/skills/plan/references/workflows/finalize.md +0 -7
  56. package/skills/plan/references/workflows/go.md +0 -7
  57. package/skills/plan/references/workflows/help.md +0 -13
  58. package/skills/plan/references/workflows/init.md +0 -6
  59. package/skills/plan/references/workflows/new.md +0 -7
  60. package/skills/plan/references/workflows/update.md +0 -7
  61. package/templates/plan/PLAN.md +0 -38
package/dist/index.js CHANGED
@@ -95,30 +95,13 @@ function normalizeModelReference(value) {
95
95
  return value.toLowerCase().replace(/^~/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
96
96
  }
97
97
  // src/environment.ts
98
- import { readFile as readFile3 } from "node:fs/promises";
99
- import { basename, join as join5 } from "node:path";
100
-
101
- // src/assets.ts
102
- import { existsSync } from "node:fs";
103
- import { dirname, join as join2 } from "node:path";
104
- import { fileURLToPath } from "node:url";
105
- function resolveBundledAssetDir(name, moduleUrl = import.meta.url) {
106
- const moduleDir = dirname(fileURLToPath(moduleUrl));
107
- const candidates = [join2(moduleDir, name), join2(moduleDir, "..", name), join2(moduleDir, "..", "..", name)];
108
- return candidates.find((path) => existsSync(path)) ?? candidates[1];
109
- }
110
- function resolveBundledAgentsDir(moduleUrl = import.meta.url) {
111
- return resolveBundledAssetDir("agents", moduleUrl);
112
- }
113
- function resolveBundledTemplatesDir(moduleUrl = import.meta.url) {
114
- return resolveBundledAssetDir("templates", moduleUrl);
115
- }
98
+ import { basename } from "node:path";
116
99
 
117
100
  // src/extensions/processx.ts
118
101
  import { spawn } from "node:child_process";
119
102
  import { constants } from "node:fs";
120
103
  import { access } from "node:fs/promises";
121
- import { delimiter, join as join3 } from "node:path";
104
+ import { delimiter, join as join2 } from "node:path";
122
105
  async function findExecutable(name) {
123
106
  if (name.includes("/")) {
124
107
  try {
@@ -131,7 +114,7 @@ async function findExecutable(name) {
131
114
  for (const directory of (process.env.PATH ?? "").split(delimiter)) {
132
115
  if (!directory)
133
116
  continue;
134
- const candidate = join3(directory, name);
117
+ const candidate = join2(directory, name);
135
118
  try {
136
119
  await access(candidate, constants.X_OK);
137
120
  return candidate;
@@ -191,18 +174,17 @@ function appendBounded(current, next) {
191
174
  // src/extensions/zedx.ts
192
175
  import { mkdir, readFile as readFile2, writeFile } from "node:fs/promises";
193
176
  import { homedir as homedir2 } from "node:os";
194
- import { dirname as dirname2, join as join4 } from "node:path";
177
+ import { dirname, join as join3 } from "node:path";
195
178
  var ZED_LOCAL_REVIEW_TASK_NAME = "diffpi: tuicr local review";
196
179
  var ZED_REVIEW_TASK_NAME = ZED_LOCAL_REVIEW_TASK_NAME;
197
180
  var ZED_PR_REVIEW_TASK_NAME = "diffpi: tuicr PR review";
198
- var ZED_PLAN_ANNOTATE_TASK_NAME = "diffpi: annotate plan";
199
181
  var LEGACY_ZED_REVIEW_TASK_NAME = "diffpi: tuicr review";
200
182
  var REVIEW_KEYBINDING = "cmd-alt-r";
201
183
  function zedTasksPath(homeDir = homedir2()) {
202
- return join4(homeDir, ".config", "zed", "tasks.json");
184
+ return join3(homeDir, ".config", "zed", "tasks.json");
203
185
  }
204
186
  function zedKeymapPath(homeDir = homedir2()) {
205
- return join4(homeDir, ".config", "zed", "keymap.json");
187
+ return join3(homeDir, ".config", "zed", "keymap.json");
206
188
  }
207
189
  async function ensureZedReviewTask(homeDir = homedir2(), _command = ["tuicr", "-w", "-r", "main..HEAD"]) {
208
190
  const path = zedTasksPath(homeDir);
@@ -222,33 +204,6 @@ async function ensureZedReviewTask(homeDir = homedir2(), _command = ["tuicr", "-
222
204
  await writeJson(path, next);
223
205
  return { path, changed, existed: currentText !== undefined };
224
206
  }
225
- async function ensureZedPlanTask(packageVersion, homeDir = homedir2()) {
226
- if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(packageVersion)) {
227
- throw new Error(`Invalid @difflab/pi package version: ${packageVersion}.`);
228
- }
229
- const path = zedTasksPath(homeDir);
230
- const currentText = await readOptional(path);
231
- const tasks = parseJsonArray(currentText, path);
232
- const task = {
233
- label: ZED_PLAN_ANNOTATE_TASK_NAME,
234
- command: "npx",
235
- args: ["--yes", `@difflab/pi@${packageVersion}`, "plan", "annotate", "--cwd", "$ZED_WORKTREE_ROOT"],
236
- cwd: "$ZED_WORKTREE_ROOT",
237
- use_new_terminal: true,
238
- reveal: "always",
239
- reveal_target: "center"
240
- };
241
- const next = [...tasks];
242
- const index = next.findIndex((candidate) => candidate.label === task.label);
243
- if (index >= 0)
244
- next[index] = { ...next[index], ...task };
245
- else
246
- next.push(task);
247
- const changed = JSON.stringify(tasks) !== JSON.stringify(next);
248
- if (changed)
249
- await writeJson(path, next);
250
- return { path, changed, existed: currentText !== undefined };
251
- }
252
207
  async function ensureZedReviewKeybinding(homeDir = homedir2()) {
253
208
  const path = zedKeymapPath(homeDir);
254
209
  const currentText = await readOptional(path);
@@ -268,9 +223,6 @@ async function ensureZedReviewKeybinding(homeDir = homedir2()) {
268
223
  return { path, changed, existed: currentText !== undefined };
269
224
  }
270
225
  function zedReviewTaskName(command) {
271
- if (command.some((value, index) => value === "plan" && command[index + 1] === "annotate")) {
272
- return ZED_PLAN_ANNOTATE_TASK_NAME;
273
- }
274
226
  return command[0] === "tuicr" && command[1] === "pr" ? ZED_PR_REVIEW_TASK_NAME : ZED_LOCAL_REVIEW_TASK_NAME;
275
227
  }
276
228
  var LOCAL_REVIEW_SCRIPT = `set -eu
@@ -352,7 +304,7 @@ function parseJsonArray(content, path) {
352
304
  return value;
353
305
  }
354
306
  async function writeJson(path, value) {
355
- await mkdir(dirname2(path), { recursive: true });
307
+ await mkdir(dirname(path), { recursive: true });
356
308
  await writeFile(path, `${JSON.stringify(value, null, 2)}
357
309
  `, "utf8");
358
310
  }
@@ -421,10 +373,7 @@ async function openInNewTab(command, opts) {
421
373
  if (detectIde(env) === "zed") {
422
374
  try {
423
375
  const taskName = zedReviewTaskName(command);
424
- if (taskName === ZED_PLAN_ANNOTATE_TASK_NAME)
425
- await ensureZedPlanTask(await packageVersion(), opts.homeDir);
426
- else
427
- await ensureZedReviewTask(opts.homeDir, command);
376
+ await ensureZedReviewTask(opts.homeDir, command);
428
377
  return {
429
378
  launched: false,
430
379
  configured: true,
@@ -440,12 +389,6 @@ async function openInNewTab(command, opts) {
440
389
  function screenWindowArgs(command, cwd, name) {
441
390
  return ["-X", "screen", "-t", name, "sh", "-lc", 'cd -- "$1" && shift && exec "$@"', "sh", cwd, ...command];
442
391
  }
443
- async function packageVersion() {
444
- const value = JSON.parse(await readFile3(join5(resolveBundledAgentsDir(), "..", "package.json"), "utf8"));
445
- if (typeof value.version !== "string")
446
- throw new Error("Cannot resolve the installed @difflab/pi version.");
447
- return value.version;
448
- }
449
392
  async function openMuxTab(mux, command, cwd, name, printable) {
450
393
  if (mux === "zellij" && await findExecutable("zellij")) {
451
394
  const result = await run("zellij", ["action", "new-tab", "--cwd", cwd, "--name", name, "--", ...command]);
@@ -469,7 +412,7 @@ async function openMuxTab(mux, command, cwd, name, printable) {
469
412
  }
470
413
  // src/extensions/gitx.ts
471
414
  import { realpath } from "node:fs/promises";
472
- import { basename as basename2, isAbsolute, join as join6, resolve } from "node:path";
415
+ import { basename as basename2, isAbsolute, join as join4, resolve } from "node:path";
473
416
  async function gitToplevel(cwd) {
474
417
  const result = await run("git", ["-C", cwd, "rev-parse", "--show-toplevel"]);
475
418
  const top = result.stdout.trim();
@@ -481,7 +424,7 @@ async function inspectGitRepository(cwd) {
481
424
  const remote = remoteResult.code === 0 ? remoteResult.stdout.trim() : "";
482
425
  const commonResult = await run("git", ["-C", root, "rev-parse", "--git-common-dir"]);
483
426
  const common = commonResult.stdout.trim();
484
- const commonPath = commonResult.code === 0 && common ? resolve(isAbsolute(common) ? common : join6(root, common)) : root;
427
+ const commonPath = commonResult.code === 0 && common ? resolve(isAbsolute(common) ? common : join4(root, common)) : root;
485
428
  const commonDir = await canonicalPath(commonPath);
486
429
  return {
487
430
  root,
@@ -753,9 +696,9 @@ function createForgeBackend(vcs) {
753
696
  var createVcsBackend = createForgeBackend;
754
697
  var createForge = createForgeBackend;
755
698
  // src/extensions/misex.ts
756
- import { mkdir as mkdir2, readFile as readFile4, writeFile as writeFile2 } from "node:fs/promises";
699
+ import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
757
700
  import { homedir as homedir3 } from "node:os";
758
- import { basename as basename3, dirname as dirname3, join as join7 } from "node:path";
701
+ import { basename as basename3, dirname as dirname2, join as join5 } from "node:path";
759
702
  var MISE_HOOK_START = "# >>> @difflab/pi mise >>>";
760
703
  var MISE_HOOK_END = "# <<< @difflab/pi mise <<<";
761
704
  var mise = {
@@ -770,7 +713,7 @@ var mise = {
770
713
  const platform = options.platform ?? process.platform;
771
714
  if (platform === "win32")
772
715
  throw new Error("Automatic mise installation supports macOS and Linux only.");
773
- const installedPath = join7(homeDir, ".local", "bin", "mise");
716
+ const installedPath = join5(homeDir, ".local", "bin", "mise");
774
717
  if (options.dryRun)
775
718
  return installedPath;
776
719
  await runChecked("sh", ["-c", "curl -fsSL https://mise.run | sh"]);
@@ -790,7 +733,7 @@ var mise = {
790
733
  const separator = current.length === 0 || current.endsWith(`
791
734
  `) ? "" : `
792
735
  `;
793
- await mkdir2(dirname3(hook.path), { recursive: true });
736
+ await mkdir2(dirname2(hook.path), { recursive: true });
794
737
  await writeFile2(hook.path, `${current}${separator}${hook.content}`, "utf8");
795
738
  return { path: hook.path, changed: true, planned: false };
796
739
  },
@@ -817,7 +760,7 @@ function getShellHook(shell, executable, homeDir) {
817
760
  switch (shell.toLowerCase()) {
818
761
  case "zsh":
819
762
  return {
820
- path: join7(homeDir, ".zshrc"),
763
+ path: join5(homeDir, ".zshrc"),
821
764
  content: `${MISE_HOOK_START}
822
765
  eval "$(${command} activate zsh)"
823
766
  ${MISE_HOOK_END}
@@ -825,7 +768,7 @@ ${MISE_HOOK_END}
825
768
  };
826
769
  case "fish":
827
770
  return {
828
- path: join7(homeDir, ".config", "fish", "config.fish"),
771
+ path: join5(homeDir, ".config", "fish", "config.fish"),
829
772
  content: `${MISE_HOOK_START}
830
773
  ${command} activate fish | source
831
774
  ${MISE_HOOK_END}
@@ -834,7 +777,7 @@ ${MISE_HOOK_END}
834
777
  case "nu":
835
778
  case "nushell":
836
779
  return {
837
- path: join7(homeDir, ".config", "nushell", "config.nu"),
780
+ path: join5(homeDir, ".config", "nushell", "config.nu"),
838
781
  content: `${MISE_HOOK_START}
839
782
  let mise_bin = ${command}
840
783
  let mise_path = $nu.default-config-dir | path join mise.nu
@@ -845,7 +788,7 @@ ${MISE_HOOK_END}
845
788
  };
846
789
  case "xonsh":
847
790
  return {
848
- path: join7(homeDir, ".xonshrc"),
791
+ path: join5(homeDir, ".xonshrc"),
849
792
  content: `${MISE_HOOK_START}
850
793
  execx($(${command} activate xonsh))
851
794
  ${MISE_HOOK_END}
@@ -853,7 +796,7 @@ ${MISE_HOOK_END}
853
796
  };
854
797
  case "elvish":
855
798
  return {
856
- path: join7(homeDir, ".config", "elvish", "rc.elv"),
799
+ path: join5(homeDir, ".config", "elvish", "rc.elv"),
857
800
  content: `${MISE_HOOK_START}
858
801
  var mise: = (ns [&])
859
802
  eval (${command} activate elvish | slurp) &ns=$mise: &on-end={|ns| set mise: = $ns }
@@ -864,7 +807,7 @@ ${MISE_HOOK_END}
864
807
  case "pwsh":
865
808
  case "powershell":
866
809
  return {
867
- path: join7(homeDir, ".config", "powershell", "Microsoft.PowerShell_profile.ps1"),
810
+ path: join5(homeDir, ".config", "powershell", "Microsoft.PowerShell_profile.ps1"),
868
811
  content: `${MISE_HOOK_START}
869
812
  (& ${command} activate pwsh) | Out-String | Invoke-Expression
870
813
  ${MISE_HOOK_END}
@@ -873,7 +816,7 @@ ${MISE_HOOK_END}
873
816
  case "bash":
874
817
  default:
875
818
  return {
876
- path: join7(homeDir, ".bashrc"),
819
+ path: join5(homeDir, ".bashrc"),
877
820
  content: `${MISE_HOOK_START}
878
821
  eval "$(${command} activate bash)"
879
822
  ${MISE_HOOK_END}
@@ -912,7 +855,7 @@ function isVersionAtLeast(version, minimumVersion) {
912
855
  }
913
856
  async function getOptionalFile(path) {
914
857
  try {
915
- return await readFile4(path, "utf8");
858
+ return await readFile3(path, "utf8");
916
859
  } catch (error) {
917
860
  if (error instanceof Error && "code" in error && error.code === "ENOENT")
918
861
  return "";
@@ -1532,10 +1475,10 @@ function hasGitlabDraftNotes(input) {
1532
1475
  }
1533
1476
 
1534
1477
  // src/review/local-review-backend.ts
1535
- import { readFile as readFile6, writeFile as writeFile3 } from "node:fs/promises";
1478
+ import { readFile as readFile5, writeFile as writeFile3 } from "node:fs/promises";
1536
1479
 
1537
1480
  // src/extensions/tuicrx.ts
1538
- import { readFile as readFile5, realpath as realpath2 } from "node:fs/promises";
1481
+ import { readFile as readFile4, realpath as realpath2 } from "node:fs/promises";
1539
1482
  import { resolve as resolve2 } from "node:path";
1540
1483
  async function tuicrAvailable() {
1541
1484
  return Boolean(await findExecutable("tuicr"));
@@ -1626,7 +1569,7 @@ async function findMatchingSession(sessions, cwd, branch) {
1626
1569
  return;
1627
1570
  }
1628
1571
  async function readSession(path) {
1629
- const content = await readFile5(path, "utf8");
1572
+ const content = await readFile4(path, "utf8");
1630
1573
  try {
1631
1574
  return JSON.parse(content);
1632
1575
  } catch {
@@ -1705,7 +1648,7 @@ function LocalReviewBackend(options) {
1705
1648
  },
1706
1649
  async listThreads() {
1707
1650
  try {
1708
- return parseThreadArtifact(await readFile6(options.artifactPath, "utf8"));
1651
+ return parseThreadArtifact(await readFile5(options.artifactPath, "utf8"));
1709
1652
  } catch (error) {
1710
1653
  if (error.code === "ENOENT")
1711
1654
  return [];
@@ -1713,7 +1656,7 @@ function LocalReviewBackend(options) {
1713
1656
  }
1714
1657
  },
1715
1658
  async reply(input) {
1716
- const content = await readFile6(options.artifactPath, "utf8");
1659
+ const content = await readFile5(options.artifactPath, "utf8");
1717
1660
  await writeFile3(options.artifactPath, upsertThreadReply(content, input.threadId, input.body, input.question, input.resolve), "utf8");
1718
1661
  },
1719
1662
  async publish() {
@@ -1735,29 +1678,29 @@ function createLocalReviewBackend(options) {
1735
1678
  }
1736
1679
  // src/review/review-state.ts
1737
1680
  import { createHash as createHash2 } from "node:crypto";
1738
- import { readFile as readFile7, rename, writeFile as writeFile4 } from "node:fs/promises";
1739
- import { join as join9 } from "node:path";
1681
+ import { readFile as readFile6, rename, writeFile as writeFile4 } from "node:fs/promises";
1682
+ import { join as join7 } from "node:path";
1740
1683
  import { z as z4 } from "zod";
1741
1684
 
1742
1685
  // src/store.ts
1743
1686
  import { createHash } from "node:crypto";
1744
1687
  import { lstat, mkdir as mkdir3, readlink, realpath as realpath3, symlink, unlink } from "node:fs/promises";
1745
1688
  import { homedir as homedir4 } from "node:os";
1746
- import { dirname as dirname4, isAbsolute as isAbsolute2, join as join8, resolve as resolve3 } from "node:path";
1689
+ import { dirname as dirname3, isAbsolute as isAbsolute2, join as join6, resolve as resolve3 } from "node:path";
1747
1690
  var STORE_LINK = ".diffpi";
1748
- var LEGACY_STORE_LINK = join8(".pi", "diffpi");
1691
+ var LEGACY_STORE_LINK = join6(".pi", "diffpi");
1749
1692
  async function computeProjectSlug(cwd) {
1750
1693
  return projectSlug(await inspectGitRepository(cwd));
1751
1694
  }
1752
1695
  function storeGlobalRoot(homeDir = homedir4()) {
1753
- return join8(homeDir, ".difflab", "diffpi", "projects");
1696
+ return join6(homeDir, ".difflab", "diffpi", "projects");
1754
1697
  }
1755
1698
  async function ensureStore(cwd, homeDir = homedir4()) {
1756
1699
  const repository = await inspectGitRepository(cwd);
1757
1700
  const root = repository.root;
1758
1701
  const slug = projectSlug(repository);
1759
- const dest = join8(storeGlobalRoot(homeDir), slug);
1760
- const link = join8(root, STORE_LINK);
1702
+ const dest = join6(storeGlobalRoot(homeDir), slug);
1703
+ const link = join6(root, STORE_LINK);
1761
1704
  await mkdir3(dest, { recursive: true });
1762
1705
  try {
1763
1706
  await assertStoreLink(link, dest);
@@ -1766,28 +1709,22 @@ async function ensureStore(cwd, homeDir = homedir4()) {
1766
1709
  throw error;
1767
1710
  await symlink(dest, link);
1768
1711
  }
1769
- await removeLegacyStoreLink(join8(root, LEGACY_STORE_LINK), dest);
1712
+ await removeLegacyStoreLink(join6(root, LEGACY_STORE_LINK), dest);
1770
1713
  return { slug, root, dest, link, linked: true };
1771
1714
  }
1772
1715
  async function storeDir(cwd, homeDir = homedir4()) {
1773
1716
  const store = await ensureStore(cwd, homeDir);
1774
1717
  return store.dest;
1775
1718
  }
1776
- async function plansDir(cwd, homeDir = homedir4()) {
1777
- const store = await ensureStore(cwd, homeDir);
1778
- const dir = join8(store.link, "plan");
1779
- await mkdir3(dir, { recursive: true });
1780
- return dir;
1781
- }
1782
1719
  async function reviewsDir(cwd, homeDir = homedir4()) {
1783
1720
  const store = await ensureStore(cwd, homeDir);
1784
- const dir = join8(store.link, "review");
1721
+ const dir = join6(store.link, "review");
1785
1722
  await mkdir3(dir, { recursive: true });
1786
1723
  return dir;
1787
1724
  }
1788
1725
  async function sessionsDir(cwd, homeDir = homedir4()) {
1789
1726
  const store = await ensureStore(cwd, homeDir);
1790
- const dir = join8(store.link, "sessions");
1727
+ const dir = join6(store.link, "sessions");
1791
1728
  await mkdir3(dir, { recursive: true });
1792
1729
  return dir;
1793
1730
  }
@@ -1814,7 +1751,7 @@ async function removeLegacyStoreLink(path, dest) {
1814
1751
  }
1815
1752
  async function symlinkTarget(path) {
1816
1753
  const target = await readlink(path);
1817
- return canonicalPath3(isAbsolute2(target) ? target : resolve3(dirname4(path), target));
1754
+ return canonicalPath3(isAbsolute2(target) ? target : resolve3(dirname3(path), target));
1818
1755
  }
1819
1756
  async function canonicalPath3(path) {
1820
1757
  try {
@@ -1851,9 +1788,9 @@ function unpublishedReviewComments(comments, knownFingerprints) {
1851
1788
  }
1852
1789
  async function loadReviewPublicationState(cwd, vcs, number, homeDir) {
1853
1790
  const target = `${vcs.provider}:${vcs.owner}/${vcs.repo}#${number}`;
1854
- const path = join9(await sessionsDir(cwd, homeDir), `review-publish-${digest(target).slice(0, 16)}.json`);
1791
+ const path = join7(await sessionsDir(cwd, homeDir), `review-publish-${digest(target).slice(0, 16)}.json`);
1855
1792
  try {
1856
- const parsed = reviewPublicationStateSchema.parse(JSON.parse(await readFile7(path, "utf8")));
1793
+ const parsed = reviewPublicationStateSchema.parse(JSON.parse(await readFile6(path, "utf8")));
1857
1794
  return { path, state: { ...parsed, target } };
1858
1795
  } catch (error) {
1859
1796
  if (error.code === "ENOENT") {
@@ -1875,12 +1812,12 @@ function digest(value) {
1875
1812
  return createHash2("sha256").update(value).digest("hex");
1876
1813
  }
1877
1814
  // src/mcp.ts
1878
- import { mkdir as mkdir4, readFile as readFile8, writeFile as writeFile5 } from "node:fs/promises";
1815
+ import { mkdir as mkdir4, readFile as readFile7, writeFile as writeFile5 } from "node:fs/promises";
1879
1816
  import { homedir as homedir5 } from "node:os";
1880
- import { dirname as dirname5, join as join10 } from "node:path";
1817
+ import { dirname as dirname4, join as join8 } from "node:path";
1881
1818
  var mcp = {
1882
1819
  globalConfigPath(homeDir = homedir5()) {
1883
- return join10(homeDir, ".config", "mcp", "mcp.json");
1820
+ return join8(homeDir, ".config", "mcp", "mcp.json");
1884
1821
  },
1885
1822
  async serversEnsure(servers, options = {}) {
1886
1823
  const path = options.path ?? mcp.globalConfigPath();
@@ -1893,7 +1830,7 @@ var mcp = {
1893
1830
  const next = { ...current, mcpServers: nextServers };
1894
1831
  const changed = JSON.stringify(current) !== JSON.stringify(next);
1895
1832
  if (changed && !options.dryRun) {
1896
- await mkdir4(dirname5(path), { recursive: true });
1833
+ await mkdir4(dirname4(path), { recursive: true });
1897
1834
  await writeFile5(path, `${JSON.stringify(next, null, 2)}
1898
1835
  `, "utf8");
1899
1836
  }
@@ -1923,7 +1860,7 @@ function getParsedConfig(content, path) {
1923
1860
  }
1924
1861
  async function getOptionalFile2(path) {
1925
1862
  try {
1926
- return await readFile8(path, "utf8");
1863
+ return await readFile7(path, "utf8");
1927
1864
  } catch (error) {
1928
1865
  if (error instanceof Error && "code" in error && error.code === "ENOENT")
1929
1866
  return;
@@ -1938,27 +1875,45 @@ import {
1938
1875
  getAgentDir,
1939
1876
  parseFrontmatter as parseFrontmatter2
1940
1877
  } from "@earendil-works/pi-coding-agent";
1941
- import { readFile as readFile9 } from "node:fs/promises";
1878
+ import { readFile as readFile8 } from "node:fs/promises";
1942
1879
  import { homedir as homedir6 } from "node:os";
1943
- import { basename as basename4, extname, join as join11 } from "node:path";
1880
+ import { basename as basename4, extname, join as join10 } from "node:path";
1881
+
1882
+ // src/assets.ts
1883
+ import { existsSync } from "node:fs";
1884
+ import { dirname as dirname5, join as join9 } from "node:path";
1885
+ import { fileURLToPath } from "node:url";
1886
+ function resolveBundledAssetDir(name, moduleUrl = import.meta.url) {
1887
+ const moduleDir = dirname5(fileURLToPath(moduleUrl));
1888
+ const candidates = [join9(moduleDir, name), join9(moduleDir, "..", name), join9(moduleDir, "..", "..", name)];
1889
+ return candidates.find((path) => existsSync(path)) ?? candidates[1];
1890
+ }
1891
+ function resolveBundledAgentsDir(moduleUrl = import.meta.url) {
1892
+ return resolveBundledAssetDir("agents", moduleUrl);
1893
+ }
1894
+ function resolveBundledTemplatesDir(moduleUrl = import.meta.url) {
1895
+ return resolveBundledAssetDir("templates", moduleUrl);
1896
+ }
1897
+
1898
+ // src/modes.ts
1944
1899
  async function discoverAgentModes(options) {
1945
1900
  const agentDir = options.agentDir ?? getAgentDir();
1946
1901
  const homeDir = options.homeDir ?? homedir6();
1947
1902
  const modes = new Map;
1948
1903
  const diagnostics = [];
1949
1904
  await loadAgentModes(options.bundledAgentsDir ?? BUNDLED_AGENTS_DIR, "diffpi agent", modes, diagnostics);
1950
- await loadAgentModes(join11(agentDir, "agents"), "user agent", modes, diagnostics);
1905
+ await loadAgentModes(join10(agentDir, "agents"), "user agent", modes, diagnostics);
1951
1906
  if (options.includeSkills) {
1952
- await loadSkillModes(join11(homeDir, ".agents", "skills"), "user skill", modes, diagnostics);
1953
- await loadSkillModes(join11(agentDir, "skills"), "pi user skill", modes, diagnostics);
1907
+ await loadSkillModes(join10(homeDir, ".agents", "skills"), "user skill", modes, diagnostics);
1908
+ await loadSkillModes(join10(agentDir, "skills"), "pi user skill", modes, diagnostics);
1954
1909
  }
1955
1910
  if (options.projectTrusted === true) {
1956
1911
  if (options.includeSkills) {
1957
- await loadSkillModes(join11(options.cwd, ".agents", "skills"), "project skill", modes, diagnostics);
1958
- await loadSkillModes(join11(options.cwd, ".pi", "skills"), "pi project skill", modes, diagnostics);
1912
+ await loadSkillModes(join10(options.cwd, ".agents", "skills"), "project skill", modes, diagnostics);
1913
+ await loadSkillModes(join10(options.cwd, ".pi", "skills"), "pi project skill", modes, diagnostics);
1959
1914
  }
1960
- await loadAgentModes(join11(options.cwd, ".agents", "agents"), "project agent", modes, diagnostics);
1961
- await loadAgentModes(join11(options.cwd, ".pi", "agents"), "pi project agent", modes, diagnostics);
1915
+ await loadAgentModes(join10(options.cwd, ".agents", "agents"), "project agent", modes, diagnostics);
1916
+ await loadAgentModes(join10(options.cwd, ".pi", "agents"), "pi project agent", modes, diagnostics);
1962
1917
  }
1963
1918
  const userConfig = await loadDiffpiConfig({ homeDir });
1964
1919
  const configuredModes = [...modes.values()].map((mode) => ({
@@ -2129,15 +2084,15 @@ function captureRuntime(pi, ctx) {
2129
2084
  async function loadSkillModes(skillsDir, source, modes, diagnostics) {
2130
2085
  const entries = await readDirectoryIfExists(skillsDir);
2131
2086
  for (const entry of entries.filter((item) => item.isDirectory()).sort((a, b) => a.name.localeCompare(b.name))) {
2132
- await loadAgentModes(join11(skillsDir, entry.name, "agents"), `${source} ${entry.name}`, modes, diagnostics, entry.name);
2087
+ await loadAgentModes(join10(skillsDir, entry.name, "agents"), `${source} ${entry.name}`, modes, diagnostics, entry.name);
2133
2088
  }
2134
2089
  }
2135
2090
  async function loadAgentModes(directory, source, modes, diagnostics, skillName) {
2136
2091
  const entries = await readDirectoryIfExists(directory);
2137
2092
  for (const entry of entries.filter((item) => item.isFile() && item.name.endsWith(".md")).sort((a, b) => a.name.localeCompare(b.name))) {
2138
- const path = join11(directory, entry.name);
2093
+ const path = join10(directory, entry.name);
2139
2094
  try {
2140
- const content = await readFile9(path, "utf8");
2095
+ const content = await readFile8(path, "utf8");
2141
2096
  const { frontmatter, body } = parseFrontmatter2(content.startsWith("\uFEFF") ? content.slice(1) : content);
2142
2097
  if (frontmatter.enabled === false || frontmatter.inline === false)
2143
2098
  continue;
@@ -2195,7 +2150,7 @@ function isModeBaseline(value) {
2195
2150
  // src/pi.ts
2196
2151
  import { mkdir as mkdir5, writeFile as writeFile6 } from "node:fs/promises";
2197
2152
  import { homedir as homedir7 } from "node:os";
2198
- import { dirname as dirname6, join as join12 } from "node:path";
2153
+ import { dirname as dirname6, join as join11 } from "node:path";
2199
2154
  var pi = {
2200
2155
  executableCheck: findPiExecutable,
2201
2156
  packageList: listPiPackages,
@@ -2208,7 +2163,7 @@ var pi = {
2208
2163
  configEnsure: ensurePiConfig
2209
2164
  };
2210
2165
  async function ensurePiAgent(filename, content, agentDir = resolvePiAgentDir(), dryRun = false) {
2211
- const path = join12(agentDir, "agents", filename);
2166
+ const path = join11(agentDir, "agents", filename);
2212
2167
  const currentText = await readTextIfExists(path);
2213
2168
  const changed = currentText !== content;
2214
2169
  if (changed && !dryRun) {
@@ -2217,10 +2172,10 @@ async function ensurePiAgent(filename, content, agentDir = resolvePiAgentDir(),
2217
2172
  }
2218
2173
  return { path, changed, existed: currentText !== undefined, planned: changed && dryRun };
2219
2174
  }
2220
- async function checkGlobalPiSkill(name, agentDir = resolvePiAgentDir(), sharedSkillsDir = join12(homedir7(), ".agents", "skills")) {
2221
- const roots = [join12(agentDir, "skills"), sharedSkillsDir];
2175
+ async function checkGlobalPiSkill(name, agentDir = resolvePiAgentDir(), sharedSkillsDir = join11(homedir7(), ".agents", "skills")) {
2176
+ const roots = [join11(agentDir, "skills"), sharedSkillsDir];
2222
2177
  for (const root of roots) {
2223
- if (await readTextIfExists(join12(root, name, "SKILL.md")) !== undefined)
2178
+ if (await readTextIfExists(join11(root, name, "SKILL.md")) !== undefined)
2224
2179
  return true;
2225
2180
  }
2226
2181
  return false;
@@ -2270,7 +2225,7 @@ async function installPiPackage(executable, source) {
2270
2225
  await runChecked(executable, ["install", source]);
2271
2226
  }
2272
2227
  function resolvePiAgentDir(homeDir = homedir7()) {
2273
- return process.env.PI_CODING_AGENT_DIR ?? (process.env.XDG_CONFIG_HOME ? join12(process.env.XDG_CONFIG_HOME, "pi") : join12(homeDir, ".pi", "agent"));
2228
+ return process.env.PI_CODING_AGENT_DIR ?? (process.env.XDG_CONFIG_HOME ? join11(process.env.XDG_CONFIG_HOME, "pi") : join11(homeDir, ".pi", "agent"));
2274
2229
  }
2275
2230
  function parseJsonObject(content, path) {
2276
2231
  if (!content?.trim())
@@ -2282,975 +2237,11 @@ function parseJsonObject(content, path) {
2282
2237
  } catch {}
2283
2238
  throw new Error(`Expected valid JSON object in ${path}.`);
2284
2239
  }
2285
- // src/plan/annotations.ts
2286
- import { spawn as spawn2 } from "node:child_process";
2287
- import { readFile as readFile12, rename as rename2, writeFile as writeFile8 } from "node:fs/promises";
2288
- import { basename as basename5, dirname as dirname7, join as join14 } from "node:path";
2289
-
2290
- // src/plan/log.ts
2291
- import { randomUUID } from "node:crypto";
2292
- import { appendFile, readFile as readFile10 } from "node:fs/promises";
2293
- async function appendPlanLog(logPath, event) {
2294
- const entry = {
2295
- ...event,
2296
- version: 1,
2297
- eventId: event.eventId ?? randomUUID(),
2298
- timestamp: event.timestamp ?? new Date().toISOString()
2299
- };
2300
- await appendFile(logPath, `${JSON.stringify(entry)}
2301
- `, { encoding: "utf8", mode: 384 });
2302
- return entry;
2303
- }
2304
- async function readPlanLog(logPath) {
2305
- let source;
2306
- try {
2307
- source = await readFile10(logPath, "utf8");
2308
- } catch (error) {
2309
- if (error.code === "ENOENT")
2310
- return [];
2311
- throw error;
2312
- }
2313
- return source.split(`
2314
- `).filter(Boolean).map((line, index) => {
2315
- try {
2316
- return JSON.parse(line);
2317
- } catch {
2318
- throw new Error(`Malformed plan log entry at line ${index + 1}.`);
2319
- }
2320
- });
2321
- }
2322
-
2323
- // src/plan/lock.ts
2324
- import { randomUUID as randomUUID2 } from "node:crypto";
2325
- import { mkdir as mkdir6, readFile as readFile11, rm, writeFile as writeFile7 } from "node:fs/promises";
2326
- import { hostname } from "node:os";
2327
- import { join as join13 } from "node:path";
2328
- async function withPlanLock(planDir, operation, options = {}) {
2329
- const lockDir = join13(planDir, ".lock");
2330
- const waitMs = options.waitMs ?? 5000;
2331
- const pollMs = options.pollMs ?? 25;
2332
- const owner = {
2333
- token: randomUUID2(),
2334
- pid: process.pid,
2335
- hostname: hostname(),
2336
- operation: options.operation ?? "plan mutation",
2337
- acquiredAt: new Date().toISOString()
2338
- };
2339
- const started = Date.now();
2340
- while (true) {
2341
- try {
2342
- await mkdir6(lockDir);
2343
- await writeFile7(join13(lockDir, "owner.json"), `${JSON.stringify(owner)}
2344
- `, { mode: 384 });
2345
- break;
2346
- } catch (error) {
2347
- if (error.code !== "EEXIST")
2348
- throw error;
2349
- if (Date.now() - started >= waitMs) {
2350
- let existing = "unknown owner";
2351
- try {
2352
- existing = (await readFile11(join13(lockDir, "owner.json"), "utf8")).trim();
2353
- } catch {}
2354
- throw new Error(`Timed out waiting for plan lock ${lockDir}; owner: ${existing}. Remove it only after confirming the owner is stale.`);
2355
- }
2356
- await new Promise((resolve) => setTimeout(resolve, pollMs));
2357
- }
2358
- }
2359
- try {
2360
- return await operation();
2361
- } finally {
2362
- let current;
2363
- try {
2364
- current = JSON.parse(await readFile11(join13(lockDir, "owner.json"), "utf8"));
2365
- } catch {}
2366
- if (current?.token === owner.token)
2367
- await rm(lockDir, { recursive: true, force: true });
2368
- }
2369
- }
2370
-
2371
- // src/plan/annotations.ts
2372
- async function annotatePlan(record, runtime = {}) {
2373
- const execute = runtime.execute ?? executeProcess;
2374
- const result = await execute("tuicr", ["--file", record.planPath], dirname7(record.planPath), true);
2375
- const sessionSlug = [...result.stderr.matchAll(/^tuicr-session:\s*(\S+)\s*$/gm)].at(-1)?.[1];
2376
- if (result.code !== 0)
2377
- throw new Error(result.stderr.trim() || `tuicr exited with status ${result.code}.`);
2378
- if (!sessionSlug)
2379
- throw new Error("tuicr did not report a tuicr-session marker.");
2380
- const state = await withPlanLock(record.dir, async () => {
2381
- const previous = await readAnnotationState(record).catch(() => {
2382
- return;
2383
- });
2384
- const next = {
2385
- schemaVersion: 1,
2386
- sessionSlug,
2387
- updatedAt: new Date().toISOString(),
2388
- appliedCommentIds: previous?.appliedCommentIds ?? []
2389
- };
2390
- await atomicJson(annotationStatePath(record), next);
2391
- return next;
2392
- }, { operation: "save annotation session" });
2393
- return { sessionSlug, code: result.code, state };
2394
- }
2395
- async function readPlanAnnotations(record, options = {}) {
2396
- const state = await readAnnotationState(record).catch((error) => {
2397
- if (error.code === "ENOENT")
2398
- return;
2399
- throw error;
2400
- });
2401
- if (!state)
2402
- return { comments: [], pending: [] };
2403
- const execute = options.runtime?.execute ?? executeProcess;
2404
- const response = await execute("tuicr", ["review", "comments", "--session", state.sessionSlug], record.dir, false);
2405
- if (response.code !== 0) {
2406
- if (/not found|no session|deleted/i.test(response.stderr))
2407
- return { state, comments: [], pending: [] };
2408
- throw new Error(response.stderr.trim() || "Cannot read tuicr comments.");
2409
- }
2410
- let raw;
2411
- try {
2412
- raw = JSON.parse(response.stdout);
2413
- } catch {
2414
- throw new Error("tuicr returned malformed comment JSON.");
2415
- }
2416
- if (!Array.isArray(raw))
2417
- throw new Error("tuicr comment output must be a JSON array.");
2418
- const lines = record.source.split(`
2419
- `);
2420
- const applied = new Set(state.appliedCommentIds);
2421
- const comments = raw.map((value, index) => normalizeComment(value, index, lines, applied));
2422
- const pending = comments.filter((comment) => !comment.applied);
2423
- return { state, comments: options.includeApplied ? comments : pending, pending };
2424
- }
2425
- async function acknowledgePlanAnnotations(record, commentIds, summary) {
2426
- if (!summary.trim())
2427
- throw new Error("Annotation acknowledgement summary is required.");
2428
- return withPlanLock(record.dir, async () => {
2429
- const state = await readAnnotationState(record);
2430
- const next = {
2431
- ...state,
2432
- updatedAt: new Date().toISOString(),
2433
- appliedCommentIds: [...new Set([...state.appliedCommentIds, ...commentIds])]
2434
- };
2435
- await atomicJson(annotationStatePath(record), next);
2436
- await appendPlanLog(record.logPath, {
2437
- planRevision: record.document.revision,
2438
- kind: "annotation",
2439
- actor: "planner",
2440
- message: summary,
2441
- data: { commentIds: [...commentIds], sessionSlug: state.sessionSlug }
2442
- });
2443
- return next;
2444
- }, { operation: "acknowledge annotations" });
2445
- }
2446
- function annotationStatePath(record) {
2447
- return join14(record.dir, "annotations.json");
2448
- }
2449
- async function readAnnotationState(record) {
2450
- const value = JSON.parse(await readFile12(annotationStatePath(record), "utf8"));
2451
- if (value.schemaVersion !== 1 || typeof value.sessionSlug !== "string" || !Array.isArray(value.appliedCommentIds)) {
2452
- throw new Error(`Malformed annotation state: ${annotationStatePath(record)}.`);
2453
- }
2454
- return value;
2455
- }
2456
- function normalizeComment(value, index, lines, applied) {
2457
- if (!value || typeof value !== "object")
2458
- throw new Error(`Malformed tuicr comment at index ${index}.`);
2459
- const raw = value;
2460
- if (typeof raw.id !== "string" || typeof raw.content !== "string")
2461
- throw new Error(`Malformed tuicr comment at index ${index}.`);
2462
- const line = integer(raw.start_line) ?? integer(raw.line);
2463
- const endLine = integer(raw.end_line) ?? line;
2464
- const targetPath = typeof raw.path === "string" ? raw.path : undefined;
2465
- const appliesToPlan = !targetPath || basename5(targetPath) === basename5("PLAN.md");
2466
- const validAnchor = appliesToPlan && line !== undefined && line > 0 && line <= lines.length;
2467
- return {
2468
- id: raw.id,
2469
- body: raw.content,
2470
- file: targetPath,
2471
- line,
2472
- endLine,
2473
- context: validAnchor ? lines.slice(line - 1, Math.min(endLine ?? line, lines.length)).join(`
2474
- `) : undefined,
2475
- stale: line !== undefined && !validAnchor,
2476
- applied: applied.has(raw.id)
2477
- };
2478
- }
2479
- async function executeProcess(command, args, cwd, interactive) {
2480
- if (!interactive) {
2481
- const result = await run(command, args, { cwd, capture: "unbounded" });
2482
- return { code: result.code, stdout: result.stdout, stderr: result.stderr };
2483
- }
2484
- return new Promise((resolve, reject) => {
2485
- const child = spawn2(command, args, { cwd, stdio: ["inherit", "inherit", "pipe"] });
2486
- let stderr = "";
2487
- child.stderr.on("data", (chunk) => {
2488
- const text = chunk.toString();
2489
- stderr += text;
2490
- process.stderr.write(text);
2491
- });
2492
- child.on("error", reject);
2493
- child.on("close", (code) => resolve({ code: code ?? 1, stdout: "", stderr }));
2494
- });
2495
- }
2496
- async function atomicJson(path, value) {
2497
- const temp = join14(dirname7(path), `.${basename5(path)}.${crypto.randomUUID()}.tmp`);
2498
- await writeFile8(temp, `${JSON.stringify(value, null, 2)}
2499
- `, { mode: 384, flag: "wx" });
2500
- await rename2(temp, path);
2501
- }
2502
- function integer(value) {
2503
- return typeof value === "number" && Number.isInteger(value) ? value : undefined;
2504
- }
2505
- // src/plan/escalation.ts
2506
- import { z as z5 } from "zod";
2507
- var escalationSchema = z5.object({
2508
- planId: z5.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
2509
- executionId: z5.string().min(1),
2510
- phaseId: z5.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
2511
- taskId: z5.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/).optional(),
2512
- blocker: z5.string().min(1),
2513
- attempts: z5.array(z5.string()),
2514
- evidence: z5.array(z5.string()),
2515
- needsUserDecision: z5.boolean()
2516
- }).strict();
2517
- var OPEN = "<diffpi-planner-escalation>";
2518
- var CLOSE = "</diffpi-planner-escalation>";
2519
- function renderPlannerEscalation(value) {
2520
- return `${OPEN}${JSON.stringify(escalationSchema.parse(value))}${CLOSE}`;
2521
- }
2522
- function parsePlannerEscalation(output) {
2523
- const start = output.indexOf(OPEN);
2524
- const end = output.indexOf(CLOSE, start + OPEN.length);
2525
- if (start < 0 || end < 0 || output.indexOf(OPEN, start + OPEN.length) >= 0)
2526
- throw new Error("Missing or ambiguous planner escalation payload.");
2527
- const payload = output.slice(start + OPEN.length, end);
2528
- try {
2529
- return escalationSchema.parse(JSON.parse(payload));
2530
- } catch (error) {
2531
- throw new Error(`Malformed planner escalation payload: ${error instanceof Error ? error.message : String(error)}`);
2532
- }
2533
- }
2534
- // src/plan/execution.ts
2535
- function createExecutionPacket(plan, coordinator) {
2536
- if (!plan.execution?.active)
2537
- throw new Error(`Plan ${plan.id} has no active execution.`);
2538
- return {
2539
- version: 1,
2540
- planId: plan.id,
2541
- executionId: plan.execution.id,
2542
- policy: plan.execution.policy,
2543
- cwd: plan.execution.cwd,
2544
- branch: plan.execution.branch,
2545
- coordinator
2546
- };
2547
- }
2548
- function renderExecutionPrompt(packet) {
2549
- return `Execute the durable Diffpi plan using this packet: ${JSON.stringify(packet)}. Call plan_context first. Coordinate eligible work, persist every transition and progress event, run phase gates, and honor the commit policy. Do not edit PLAN.md directly. Delegated workers never commit or restructure the plan.`;
2550
- }
2551
- function eligiblePlanTasks(plan) {
2552
- const completedPhases = new Set(plan.phases.filter((phase) => phase.status === "completed" || phase.status === "skipped").map((phase) => phase.id));
2553
- const completedTasks = new Set(plan.phases.flatMap((phase) => phase.tasks).filter((task) => task.status === "completed" || task.status === "skipped").map((task) => task.id));
2554
- for (const phase of plan.phases) {
2555
- if (phase.status === "completed" || phase.status === "skipped")
2556
- continue;
2557
- if (!phase.dependencies.every((dependency) => completedPhases.has(dependency)))
2558
- continue;
2559
- return phase.tasks.filter((task) => task.status === "pending" && task.dependencies.every((dependency) => completedTasks.has(dependency))).map((task) => ({ phase, task }));
2560
- }
2561
- return [];
2562
- }
2563
- function tasksMayRunInParallel(left, right) {
2564
- if (!left.fileScopes.length || !right.fileScopes.length)
2565
- return false;
2566
- if (left.dependencies.includes(right.id) || right.dependencies.includes(left.id))
2567
- return false;
2568
- return left.fileScopes.every((scope) => right.fileScopes.every((other) => !scopesOverlap(scope, other)));
2569
- }
2570
- function phaseCommitCommand(policy) {
2571
- return policy === "commit-per-phase" ? "/git commit --yes --no-push" : undefined;
2572
- }
2573
- function scopesOverlap(left, right) {
2574
- const normalize = (value) => value.replace(/^\.\//, "").replace(/\*.*$/, "").replace(/\/$/, "");
2575
- const a = normalize(left);
2576
- const b = normalize(right);
2577
- return !a || !b || a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`);
2578
- }
2579
- // src/plan/transitions.ts
2580
- var STABLE_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
2581
- var PLAN_TRANSITIONS = {
2582
- draft: ["ready"],
2583
- ready: ["draft", "in_progress"],
2584
- in_progress: ["blocked", "completed"],
2585
- blocked: ["draft", "ready", "in_progress"],
2586
- completed: []
2587
- };
2588
- var WORK_TRANSITIONS = {
2589
- pending: ["in_progress", "skipped"],
2590
- in_progress: ["blocked", "completed"],
2591
- blocked: ["pending", "in_progress", "skipped"],
2592
- completed: [],
2593
- skipped: []
2594
- };
2595
- function isStableId(value) {
2596
- return STABLE_ID.test(value) && value.length <= 80;
2597
- }
2598
- function assertStableId(value, label = "identifier") {
2599
- if (!isStableId(value))
2600
- throw new Error(`${label} must be a lowercase stable slug, not a path: ${value}`);
2601
- }
2602
- function assertPlanTransition(from, to) {
2603
- if (from === to)
2604
- return;
2605
- if (!PLAN_TRANSITIONS[from].includes(to))
2606
- throw new Error(`Invalid plan status transition: ${from} -> ${to}.`);
2607
- }
2608
- function assertPhaseTransition(from, to) {
2609
- if (from === to)
2610
- return;
2611
- if (!WORK_TRANSITIONS[from].includes(to))
2612
- throw new Error(`Invalid phase status transition: ${from} -> ${to}.`);
2613
- }
2614
- function assertTaskTransition(task, to, executionId, actor) {
2615
- if (task.status !== to && !WORK_TRANSITIONS[task.status].includes(to)) {
2616
- throw new Error(`Invalid task status transition: ${task.status} -> ${to}.`);
2617
- }
2618
- if (task.status === "in_progress" && task.executionId && executionId !== task.executionId) {
2619
- throw new Error(`Task ${task.id} is owned by execution ${task.executionId}.`);
2620
- }
2621
- if (task.status === "in_progress" && task.owner && actor && actor !== task.owner) {
2622
- throw new Error(`Task ${task.id} is owned by ${task.owner}.`);
2623
- }
2624
- }
2625
- function assertUniqueIds(ids, label) {
2626
- const seen = new Set;
2627
- for (const id of ids) {
2628
- assertStableId(id, label);
2629
- if (seen.has(id))
2630
- throw new Error(`Duplicate ${label}: ${id}.`);
2631
- seen.add(id);
2632
- }
2633
- }
2634
-
2635
- // src/plan/markdown.ts
2636
- var PLAN_MARKER = /<!-- diffpi-plan: (\{[^\n]+\}) -->/;
2637
- var PHASE_MARKER = /<!-- diffpi-phase: (\{[^\n]+\}) -->/g;
2638
- var TASK_MARKER = /<!-- diffpi-task: (\{[^\n]+\}) -->/g;
2639
- var SUPPORTED_SCHEMA = 1;
2640
- function countDesignWords(plan) {
2641
- return [plan.design.bigIdeas, plan.design.keyApiUpdates, plan.design.consequences].join(" ").trim().split(/\s+/).filter(Boolean).length;
2642
- }
2643
- function validatePlanDocument(plan, options = {}) {
2644
- const issues = [];
2645
- const add = (code, severity, message, path) => issues.push({ code, severity, message, path });
2646
- try {
2647
- assertStableId(plan.id, "plan id");
2648
- assertUniqueIds(plan.phases.map((phase) => phase.id), "phase id");
2649
- assertUniqueIds(plan.phases.flatMap((phase) => phase.tasks.map((task) => task.id)), "task id");
2650
- } catch (error) {
2651
- add("invalid-id", "error", error.message);
2652
- }
2653
- if (plan.schemaVersion !== SUPPORTED_SCHEMA)
2654
- add("schema", "error", `Unsupported plan schema: ${plan.schemaVersion}.`);
2655
- const phaseIds = new Set(plan.phases.map((phase) => phase.id));
2656
- const taskIds = new Set(plan.phases.flatMap((phase) => phase.tasks.map((task) => task.id)));
2657
- for (const phase of plan.phases) {
2658
- for (const dependency of phase.dependencies) {
2659
- if (!phaseIds.has(dependency))
2660
- add("dangling-dependency", "error", `Phase ${phase.id} depends on missing ${dependency}.`);
2661
- }
2662
- for (const task of phase.tasks) {
2663
- for (const dependency of task.dependencies) {
2664
- if (!taskIds.has(dependency))
2665
- add("dangling-dependency", "error", `Task ${task.id} depends on missing ${dependency}.`);
2666
- }
2667
- if (options.strict && task.acceptanceCriteria.length === 0)
2668
- add("acceptance", "error", `Task ${task.id} has no acceptance criteria.`, task.id);
2669
- }
2670
- if (options.strict && phase.tasks.length === 0)
2671
- add("empty-phase", "error", `Phase ${phase.id} has no tasks.`, phase.id);
2672
- }
2673
- for (const cycle of dependencyCycles(plan))
2674
- add("dependency-cycle", "error", `Dependency cycle: ${cycle.join(" -> ")}.`);
2675
- const designWords = countDesignWords(plan);
2676
- if (designWords > 300)
2677
- add("design-length", "warning", `Design is ${designWords} words; prefer 300 or fewer.`);
2678
- if (options.strict && designWords > 800)
2679
- add("design-too-long", "error", `Design is ${designWords} words; finalization allows at most 800.`);
2680
- if (options.strict && plan.phases.length === 0)
2681
- add("no-phases", "error", "Finalization requires at least one phase.");
2682
- if (options.strict && (options.pendingAnnotations ?? 0) > 0)
2683
- add("pending-annotations", "error", `${options.pendingAnnotations} annotations remain pending.`);
2684
- return issues;
2685
- }
2686
- function renderPlanDocument(plan, previousSource) {
2687
- if (previousSource && canPatch(previousSource, plan))
2688
- return patchMarkers(previousSource, plan);
2689
- const marker = {
2690
- schemaVersion: plan.schemaVersion,
2691
- id: plan.id,
2692
- revision: plan.revision,
2693
- branch: plan.branch,
2694
- status: plan.status,
2695
- execution: plan.execution,
2696
- createdAt: plan.createdAt,
2697
- updatedAt: plan.updatedAt
2698
- };
2699
- const requirements = plan.requirements.length ? plan.requirements.map((item) => `- ${item}`).join(`
2700
- `) : "<!-- Add requirements. -->";
2701
- const phases = plan.phases.map(renderPhase).join(`
2702
-
2703
- `);
2704
- const references = plan.references.length ? plan.references.map((reference) => `- <!-- diffpi-reference: ${json(reference)} --> ${reference.value}`).join(`
2705
- `) : "<!-- Add references. -->";
2706
- return `<!-- diffpi-plan: ${json(marker)} -->
2707
- # ${plan.title}
2708
-
2709
- - **Plan ID:** ${plan.id}
2710
- - **Branch:** ${plan.branch}
2711
- - **Status:** ${plan.status}
2712
- - **Revision:** ${plan.revision}
2713
-
2714
- ## Intent
2715
-
2716
- ${plan.intent || "<!-- Describe the intended outcome. -->"}
2717
-
2718
- ## Requirements
2719
-
2720
- ${requirements}
2721
-
2722
- ## Design
2723
-
2724
- ### Big Ideas
2725
-
2726
- ${plan.design.bigIdeas || "<!-- Describe the main approach. -->"}
2727
-
2728
- ### Key API Addition/Updates
2729
-
2730
- ${plan.design.keyApiUpdates || "<!-- Describe public API changes. -->"}
2731
-
2732
- ### Consequences
2733
-
2734
- ${plan.design.consequences || "<!-- Describe trade-offs and limitations. -->"}
2735
-
2736
- ## Implementation
2737
-
2738
- ${phases || "<!-- Add phases with plan_add_phase. -->"}
2739
-
2740
- ## References
2741
-
2742
- ${references}
2743
- `;
2744
- }
2745
- function parsePlanDocument(source, ref = "PLAN.md") {
2746
- const planMatch = source.match(PLAN_MARKER);
2747
- if (!planMatch)
2748
- throw new Error(`${ref}: missing or malformed diffpi-plan marker.`);
2749
- if ((source.match(new RegExp(PLAN_MARKER.source, "g")) ?? []).length !== 1)
2750
- throw new Error(`${ref}: duplicate diffpi-plan marker.`);
2751
- const marker = parseMarker(planMatch[1], `${ref} plan`);
2752
- if (marker.schemaVersion !== SUPPORTED_SCHEMA)
2753
- throw new Error(`${ref}: unsupported plan schema ${String(marker.schemaVersion)}.`);
2754
- assertStableId(marker.id, "plan id");
2755
- const title = source.match(/^# (.+)$/m)?.[1]?.trim();
2756
- if (!title)
2757
- throw new Error(`${ref}: missing plan title.`);
2758
- const phases = parsePhases(section(source, "Implementation"), ref);
2759
- const document = {
2760
- ...marker,
2761
- schemaVersion: 1,
2762
- title,
2763
- intent: cleanPlaceholder(section(source, "Intent")),
2764
- requirements: parseBullets(section(source, "Requirements")),
2765
- design: {
2766
- bigIdeas: cleanPlaceholder(subsection(source, "Design", "Big Ideas")),
2767
- keyApiUpdates: cleanPlaceholder(subsection(source, "Design", "Key API Addition/Updates")),
2768
- consequences: cleanPlaceholder(subsection(source, "Design", "Consequences"))
2769
- },
2770
- phases,
2771
- references: parseReferences(section(source, "References"))
2772
- };
2773
- const errors = validatePlanDocument(document).filter((issue) => issue.severity === "error");
2774
- if (errors.length)
2775
- throw new Error(`${ref}: ${errors.map((issue) => issue.message).join(" ")}`);
2776
- return document;
2777
- }
2778
- function renderPhase(phase) {
2779
- const marker = {
2780
- id: phase.id,
2781
- revision: phase.revision,
2782
- status: phase.status,
2783
- gate: phase.gate,
2784
- commit: phase.commit,
2785
- blocker: phase.blocker
2786
- };
2787
- const dependencies = phase.dependencies.length ? phase.dependencies.join(", ") : "none";
2788
- return `<!-- diffpi-phase: ${json(marker)} -->
2789
- ### Phase: ${phase.title}
2790
-
2791
- **Objective:** ${phase.objective}
2792
-
2793
- **Dependencies:** ${dependencies}
2794
-
2795
- ${phase.tasks.map(renderTask).join(`
2796
-
2797
- `)}
2798
-
2799
- <!-- /diffpi-phase -->`;
2800
- }
2801
- function renderTask(task) {
2802
- const marker = {
2803
- id: task.id,
2804
- revision: task.revision,
2805
- status: task.status,
2806
- owner: task.owner,
2807
- executionId: task.executionId,
2808
- blocker: task.blocker
2809
- };
2810
- const checked = task.status === "completed" || task.status === "skipped" ? "x" : " ";
2811
- return `<!-- diffpi-task: ${json(marker)} -->
2812
- - [${checked}] **${task.title}**
2813
- - Steps: ${list(task.steps ?? [])}
2814
- - Dependencies: ${list(task.dependencies)}
2815
- - File scopes: ${list(task.fileScopes)}
2816
- - Acceptance criteria: ${list(task.acceptanceCriteria)}
2817
- <!-- /diffpi-task -->`;
2818
- }
2819
- function parsePhases(input, ref) {
2820
- const starts = [...input.matchAll(PHASE_MARKER)];
2821
- const phases = starts.map((match, index) => {
2822
- const start = match.index;
2823
- const end = input.indexOf("<!-- /diffpi-phase -->", start);
2824
- if (end < 0)
2825
- throw new Error(`${ref}: phase marker has no closing marker.`);
2826
- const next = starts[index + 1]?.index;
2827
- if (next !== undefined && next < end)
2828
- throw new Error(`${ref}: nested or unclosed phase marker.`);
2829
- const body = input.slice(start + match[0].length, end);
2830
- const marker = parseMarker(match[1], `${ref} phase`);
2831
- assertStableId(marker.id, "phase id");
2832
- const title = body.match(/^### Phase: (.+)$/m)?.[1]?.trim();
2833
- const objective = body.match(/^\*\*Objective:\*\*\s*(.*)$/m)?.[1]?.trim();
2834
- if (!title || !objective)
2835
- throw new Error(`${ref}: phase ${marker.id} is missing title or objective.`);
2836
- return {
2837
- ...marker,
2838
- title,
2839
- objective,
2840
- dependencies: parseCsv(body.match(/^\*\*Dependencies:\*\*\s*(.*)$/m)?.[1]),
2841
- tasks: parseTasks(body, ref)
2842
- };
2843
- });
2844
- assertUniqueIds(phases.map((phase) => phase.id), "phase id");
2845
- return phases;
2846
- }
2847
- function parseTasks(input, ref) {
2848
- const starts = [...input.matchAll(TASK_MARKER)];
2849
- const tasks = starts.map((match, index) => {
2850
- const start = match.index;
2851
- const end = input.indexOf("<!-- /diffpi-task -->", start);
2852
- if (end < 0)
2853
- throw new Error(`${ref}: task marker has no closing marker.`);
2854
- const next = starts[index + 1]?.index;
2855
- if (next !== undefined && next < end)
2856
- throw new Error(`${ref}: nested or unclosed task marker.`);
2857
- const body = input.slice(start + match[0].length, end);
2858
- const marker = parseMarker(match[1], `${ref} task`);
2859
- assertStableId(marker.id, "task id");
2860
- const title = body.match(/^- \[[ xX]\] \*\*(.+)\*\*$/m)?.[1]?.trim();
2861
- if (!title)
2862
- throw new Error(`${ref}: task ${marker.id} is missing its checkbox title.`);
2863
- return {
2864
- ...marker,
2865
- title,
2866
- steps: parseListValue(body, "Steps"),
2867
- dependencies: parseListValue(body, "Dependencies"),
2868
- fileScopes: parseListValue(body, "File scopes"),
2869
- acceptanceCriteria: parseListValue(body, "Acceptance criteria")
2870
- };
2871
- });
2872
- assertUniqueIds(tasks.map((task) => task.id), "task id");
2873
- return tasks;
2874
- }
2875
- function parseReferences(input) {
2876
- return [...input.matchAll(/^- <!-- diffpi-reference: (\{[^\n]+\}) -->\s*(.*)$/gm)].map((match) => {
2877
- const marker = parseMarker(match[1], "reference");
2878
- assertStableId(marker.id, "reference id");
2879
- return { id: marker.id, value: match[2].trim() || marker.value };
2880
- });
2881
- }
2882
- function canPatch(source, next) {
2883
- try {
2884
- const old = parsePlanDocument(source);
2885
- return JSON.stringify(contentShape(old)) === JSON.stringify(contentShape(next));
2886
- } catch {
2887
- return false;
2888
- }
2889
- }
2890
- function patchMarkers(source, plan) {
2891
- const planMarker = {
2892
- schemaVersion: plan.schemaVersion,
2893
- id: plan.id,
2894
- revision: plan.revision,
2895
- branch: plan.branch,
2896
- status: plan.status,
2897
- execution: plan.execution,
2898
- createdAt: plan.createdAt,
2899
- updatedAt: plan.updatedAt
2900
- };
2901
- let output = source.replace(PLAN_MARKER, `<!-- diffpi-plan: ${json(planMarker)} -->`);
2902
- output = output.replace(/^- \*\*Status:\*\* .*$/m, `- **Status:** ${plan.status}`);
2903
- output = output.replace(/^- \*\*Revision:\*\* .*$/m, `- **Revision:** ${plan.revision}`);
2904
- for (const phase of plan.phases) {
2905
- const marker = {
2906
- id: phase.id,
2907
- revision: phase.revision,
2908
- status: phase.status,
2909
- gate: phase.gate,
2910
- commit: phase.commit,
2911
- blocker: phase.blocker
2912
- };
2913
- output = replaceMarkerById(output, "phase", phase.id, marker);
2914
- for (const task of phase.tasks) {
2915
- const taskMarker = {
2916
- id: task.id,
2917
- revision: task.revision,
2918
- status: task.status,
2919
- owner: task.owner,
2920
- executionId: task.executionId,
2921
- blocker: task.blocker
2922
- };
2923
- output = replaceMarkerById(output, "task", task.id, taskMarker);
2924
- const checked = task.status === "completed" || task.status === "skipped" ? "x" : " ";
2925
- const escaped = escapeRegExp(task.title);
2926
- output = output.replace(new RegExp(`^- \\[[ xX]\\] \\*\\*${escaped}\\*\\*$`, "m"), `- [${checked}] **${task.title}**`);
2927
- }
2928
- }
2929
- return output;
2930
- }
2931
- function replaceMarkerById(source, kind, id, marker) {
2932
- const pattern = new RegExp(`<!-- diffpi-${kind}: \\{[^\\n]*"id":"${escapeRegExp(id)}"[^\\n]*\\} -->`);
2933
- if (!pattern.test(source))
2934
- throw new Error(`Cannot update missing ${kind} marker ${id}.`);
2935
- return source.replace(pattern, `<!-- diffpi-${kind}: ${json(marker)} -->`);
2936
- }
2937
- function contentShape(plan) {
2938
- return {
2939
- title: plan.title,
2940
- branch: plan.branch,
2941
- intent: plan.intent,
2942
- requirements: plan.requirements,
2943
- design: plan.design,
2944
- references: plan.references,
2945
- phases: plan.phases.map(({ id, title, objective, dependencies, tasks }) => ({
2946
- id,
2947
- title,
2948
- objective,
2949
- dependencies,
2950
- tasks: tasks.map(({ id: taskId, title: taskTitle, steps, dependencies: taskDependencies, fileScopes, acceptanceCriteria }) => ({
2951
- id: taskId,
2952
- title: taskTitle,
2953
- steps,
2954
- dependencies: taskDependencies,
2955
- fileScopes,
2956
- acceptanceCriteria
2957
- }))
2958
- }))
2959
- };
2960
- }
2961
- function dependencyCycles(plan) {
2962
- const graph = new Map;
2963
- for (const phase of plan.phases)
2964
- graph.set(phase.id, phase.dependencies);
2965
- for (const task of plan.phases.flatMap((phase) => phase.tasks))
2966
- graph.set(task.id, task.dependencies);
2967
- const cycles = [];
2968
- const visiting = new Set;
2969
- const visited = new Set;
2970
- const walk = (id, path) => {
2971
- if (visiting.has(id)) {
2972
- cycles.push([...path.slice(path.indexOf(id)), id]);
2973
- return;
2974
- }
2975
- if (visited.has(id))
2976
- return;
2977
- visiting.add(id);
2978
- for (const dependency of graph.get(id) ?? [])
2979
- walk(dependency, [...path, id]);
2980
- visiting.delete(id);
2981
- visited.add(id);
2982
- };
2983
- for (const id of graph.keys())
2984
- walk(id, []);
2985
- return cycles;
2986
- }
2987
- function section(source, heading) {
2988
- const match = source.match(new RegExp(`^## ${escapeRegExp(heading)}\\s*$`, "m"));
2989
- if (!match?.index) {
2990
- if (match?.index === 0)
2991
- return "";
2992
- throw new Error(`Missing required heading: ${heading}.`);
2993
- }
2994
- const start = match.index + match[0].length;
2995
- const rest = source.slice(start);
2996
- const end = rest.search(/^## /m);
2997
- return (end < 0 ? rest : rest.slice(0, end)).trim();
2998
- }
2999
- function subsection(source, parent, heading) {
3000
- const body = section(source, parent);
3001
- const match = body.match(new RegExp(`^### ${escapeRegExp(heading)}\\s*$`, "m"));
3002
- if (match?.index === undefined)
3003
- throw new Error(`Missing required heading: ${parent}/${heading}.`);
3004
- const rest = body.slice(match.index + match[0].length);
3005
- const end = rest.search(/^### /m);
3006
- return (end < 0 ? rest : rest.slice(0, end)).trim();
3007
- }
3008
- function parseBullets(input) {
3009
- return [...input.matchAll(/^- (?!<!--)(.+)$/gm)].map((match) => match[1].trim());
3010
- }
3011
- function parseListValue(body, label) {
3012
- const value = body.match(new RegExp(`^ - ${escapeRegExp(label)}:\\s*(.*)$`, "m"))?.[1];
3013
- return parseCsv(value);
3014
- }
3015
- function parseCsv(value) {
3016
- if (!value || value.trim().toLowerCase() === "none")
3017
- return [];
3018
- return value.split(",").map((item) => item.trim()).filter(Boolean);
3019
- }
3020
- function cleanPlaceholder(value) {
3021
- return value.replace(/<!--[^]*?-->/g, "").trim();
3022
- }
3023
- function parseMarker(value, label) {
3024
- try {
3025
- return JSON.parse(value);
3026
- } catch {
3027
- throw new Error(`Malformed ${label} marker JSON.`);
3028
- }
3029
- }
3030
- function list(values) {
3031
- return values.length ? values.join(", ") : "none";
3032
- }
3033
- function json(value) {
3034
- return JSON.stringify(value, (_key, entry) => entry === undefined ? undefined : entry);
3035
- }
3036
- function escapeRegExp(value) {
3037
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3038
- }
3039
- // src/plan/store.ts
3040
- import { open, mkdir as mkdir7, readdir as readdir2, readFile as readFile14, realpath as realpath4, rename as rename3, rm as rm2 } from "node:fs/promises";
3041
- import { basename as basename6, dirname as dirname8, join as join16, resolve as resolve4, sep } from "node:path";
3042
-
3043
- // src/templates.ts
3044
- import { readFile as readFile13 } from "node:fs/promises";
3045
- import { homedir as homedir8 } from "node:os";
3046
- import { join as join15, normalize } from "node:path";
3047
- async function loadTemplate(name, options = {}) {
3048
- const relative = templateRelativePath(name);
3049
- const userPath = join15(options.homeDir ?? homedir8(), ".difflab", "diffpi", "templates", relative);
3050
- const bundledPath = join15(options.bundledDir ?? resolveBundledTemplatesDir(), relative);
3051
- const user = await readOptionalFile(userPath);
3052
- if (user !== undefined)
3053
- return { name, path: userPath, source: "user", content: user };
3054
- const bundled = await readOptionalFile(bundledPath);
3055
- if (bundled !== undefined)
3056
- return { name, path: bundledPath, source: "bundled", content: bundled };
3057
- throw new Error(`Template "${name}" was not found at ${userPath} or ${bundledPath}.`);
3058
- }
3059
- function renderTemplate(content, variables) {
3060
- return content.replace(/\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g, (match, key) => variables[key] ?? match);
3061
- }
3062
- function templateRelativePath(name) {
3063
- const normalized = normalize(name.replaceAll("\\", "/")).replace(/^\.\//, "");
3064
- if (!normalized || normalized.startsWith("../") || normalized.includes("/../") || normalized.startsWith("/")) {
3065
- throw new Error(`Invalid template name: ${name}`);
3066
- }
3067
- return normalized.endsWith(".md") ? normalized : `${normalized}.md`;
3068
- }
3069
- async function readOptionalFile(path) {
3070
- try {
3071
- return await readFile13(path, "utf8");
3072
- } catch (error) {
3073
- if (error.code === "ENOENT")
3074
- return;
3075
- throw error;
3076
- }
3077
- }
3078
-
3079
- // src/plan/store.ts
3080
- function planRecordName(shortSlug, date = new Date) {
3081
- const slug = normalizeSlug(shortSlug);
3082
- return `${date.toISOString().slice(2, 10).replaceAll("-", "")}-${slug}`;
3083
- }
3084
- async function resolvePlan(cwd, query, filters = {}, options = {}) {
3085
- const root = await plansDir(cwd, options.homeDir);
3086
- const entries = await readdir2(root, { withFileTypes: true });
3087
- const normalizedQuery = query ? normalizeQuery(query) : undefined;
3088
- const records = [];
3089
- for (const entry of entries) {
3090
- if (!entry.isDirectory() || entry.name === ".tmp" || entry.name === ".lock")
3091
- continue;
3092
- if (normalizedQuery && entry.name !== normalizedQuery && stripDate(entry.name) !== normalizedQuery)
3093
- continue;
3094
- const dir = join16(root, entry.name);
3095
- await assertContained(root, dir);
3096
- try {
3097
- const record = await readRecord(dir);
3098
- if (filters.branch && record.document.branch !== filters.branch)
3099
- continue;
3100
- if (filters.statuses && !filters.statuses.includes(record.document.status))
3101
- continue;
3102
- records.push(record);
3103
- } catch (error) {
3104
- if (error.code !== "ENOENT")
3105
- throw error;
3106
- }
3107
- }
3108
- records.sort((left, right) => left.id.localeCompare(right.id));
3109
- return { candidates: records, record: records.length === 1 ? records[0] : undefined, ambiguous: records.length > 1 };
3110
- }
3111
- function createPlanStore(options = {}) {
3112
- const now = options.now ?? (() => new Date);
3113
- return {
3114
- context(cwd, query, filters) {
3115
- return resolvePlan(cwd, query, filters, options);
3116
- },
3117
- async init(input) {
3118
- const root = await plansDir(input.cwd, options.homeDir);
3119
- const id = planRecordName(input.shortSlug, now());
3120
- const dir = join16(root, id);
3121
- await assertContained(root, dirname8(dir));
3122
- try {
3123
- await mkdir7(dir);
3124
- } catch (error) {
3125
- if (error.code === "EEXIST")
3126
- throw new Error(`Plan ${id} already exists.`);
3127
- throw error;
3128
- }
3129
- try {
3130
- const timestamp = now().toISOString();
3131
- const template = await loadTemplate("plan/PLAN", {
3132
- homeDir: options.homeDir,
3133
- bundledDir: options.bundledTemplatesDir
3134
- });
3135
- const source = renderTemplate(template.content, {
3136
- id,
3137
- branch: input.branch,
3138
- title: input.title?.trim() || titleFromSlug(input.shortSlug),
3139
- intent: input.intent?.trim() || "<!-- Describe the intended outcome. -->",
3140
- created_at: timestamp,
3141
- updated_at: timestamp
3142
- });
3143
- const document = parsePlanDocument(source, join16(dir, "PLAN.md"));
3144
- await atomicWrite(join16(dir, "PLAN.md"), source);
3145
- await atomicWrite(join16(dir, "logs.txt"), "");
3146
- await appendPlanLog(join16(dir, "logs.txt"), {
3147
- planRevision: document.revision,
3148
- kind: "created",
3149
- actor: "diffpi",
3150
- message: `Created plan ${id}.`
3151
- });
3152
- return { id, dir, planPath: join16(dir, "PLAN.md"), logPath: join16(dir, "logs.txt"), document, source };
3153
- } catch (error) {
3154
- await rm2(dir, { recursive: true, force: true });
3155
- throw error;
3156
- }
3157
- },
3158
- async read(cwd, query, filters) {
3159
- const result = await resolvePlan(cwd, query, filters, options);
3160
- if (!result.record) {
3161
- if (result.ambiguous)
3162
- throw new Error(`Plan query "${query}" is ambiguous: ${result.candidates.map((item) => item.id).join(", ")}.`);
3163
- throw new Error(`Plan "${query}" was not found.`);
3164
- }
3165
- return result.record;
3166
- },
3167
- async mutate(cwd, query, operation, update) {
3168
- const initial = await this.read(cwd, query);
3169
- return withPlanLock(initial.dir, async () => {
3170
- const current = await readRecord(initial.dir);
3171
- const next = await update(structuredClone(current.document));
3172
- if (next.id !== current.id)
3173
- throw new Error("A plan mutation cannot change the plan ID.");
3174
- const document = {
3175
- ...next,
3176
- revision: current.document.revision + 1,
3177
- updatedAt: now().toISOString()
3178
- };
3179
- const source = renderPlanDocument(document, current.source);
3180
- parsePlanDocument(source, current.planPath);
3181
- await atomicWrite(current.planPath, source);
3182
- return { ...current, document, source };
3183
- }, { operation });
3184
- },
3185
- async log(cwd, query, event) {
3186
- const record = await this.read(cwd, query);
3187
- return withPlanLock(record.dir, () => appendPlanLog(record.logPath, event), { operation: "append log" });
3188
- }
3189
- };
3190
- }
3191
- async function readRecord(dir) {
3192
- const planPath = join16(dir, "PLAN.md");
3193
- const logPath = join16(dir, "logs.txt");
3194
- const source = await readFile14(planPath, "utf8");
3195
- const document = parsePlanDocument(source, planPath);
3196
- return { id: basename6(dir), dir, planPath, logPath, document, source };
3197
- }
3198
- async function atomicWrite(path, content) {
3199
- const temp = join16(dirname8(path), `.${basename6(path)}.${process.pid}.${crypto.randomUUID()}.tmp`);
3200
- const file = await open(temp, "wx", 384);
3201
- try {
3202
- await file.writeFile(content, "utf8");
3203
- await file.sync();
3204
- } finally {
3205
- await file.close();
3206
- }
3207
- await rename3(temp, path);
3208
- try {
3209
- const directory = await open(dirname8(path), "r");
3210
- try {
3211
- await directory.sync();
3212
- } finally {
3213
- await directory.close();
3214
- }
3215
- } catch {}
3216
- }
3217
- async function assertContained(root, candidate) {
3218
- const canonicalRoot = await realpath4(root);
3219
- let canonicalCandidate;
3220
- try {
3221
- canonicalCandidate = await realpath4(candidate);
3222
- } catch {
3223
- canonicalCandidate = resolve4(candidate);
3224
- }
3225
- if (canonicalCandidate !== canonicalRoot && !canonicalCandidate.startsWith(`${canonicalRoot}${sep}`)) {
3226
- throw new Error(`Plan path escapes the shared store: ${candidate}.`);
3227
- }
3228
- }
3229
- function normalizeSlug(value) {
3230
- if (!value || value.includes("\x00") || value.includes("/") || value.includes("\\") || value.includes("..")) {
3231
- throw new Error(`Invalid plan slug: ${value}.`);
3232
- }
3233
- const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
3234
- assertStableId(slug, "plan slug");
3235
- return slug;
3236
- }
3237
- function normalizeQuery(value) {
3238
- if (!value || value.includes("\x00") || value.includes("/") || value.includes("\\") || value.includes("..")) {
3239
- throw new Error(`Invalid plan query: ${value}.`);
3240
- }
3241
- return value.toLowerCase();
3242
- }
3243
- function stripDate(value) {
3244
- return value.replace(/^\d{6}-/, "");
3245
- }
3246
- function titleFromSlug(value) {
3247
- return normalizeSlug(value).replace(/^(?:[a-z]+-\d+-)/, "").split("-").map((part) => part[0].toUpperCase() + part.slice(1)).join(" ");
3248
- }
3249
2240
  // src/setup.ts
3250
2241
  import { parseFrontmatter as parseFrontmatter3 } from "@earendil-works/pi-coding-agent";
3251
- import { readdir as readdir3, readFile as readFile15 } from "node:fs/promises";
3252
- import { homedir as homedir9 } from "node:os";
3253
- import { basename as basename7, join as join17 } from "node:path";
2242
+ import { readdir as readdir2, readFile as readFile9 } from "node:fs/promises";
2243
+ import { homedir as homedir8 } from "node:os";
2244
+ import { basename as basename5, join as join12 } from "node:path";
3254
2245
  var MISE_DEPENDENCIES = [
3255
2246
  { name: "node", tool: "node", spec: "node@22", minimumVersion: "22.19.0" },
3256
2247
  { name: "zellij", tool: "zellij", spec: "zellij@latest", minimumVersion: undefined },
@@ -3288,8 +2279,8 @@ var FORGE_DEPENDENCIES = {
3288
2279
  gitlab: { name: "glab", tool: "glab", spec: "glab@latest", minimumVersion: undefined }
3289
2280
  };
3290
2281
  async function ensureMise(options = {}) {
3291
- const homeDir = options.homeDir ?? homedir9();
3292
- const current = await mise.executableCheck() ?? await mise.executableCheck(join17(homeDir, ".local", "bin", "mise"));
2282
+ const homeDir = options.homeDir ?? homedir8();
2283
+ const current = await mise.executableCheck() ?? await mise.executableCheck(join12(homeDir, ".local", "bin", "mise"));
3293
2284
  if (current)
3294
2285
  return { executable: current, action: createSetupAction("mise", "ready", current) };
3295
2286
  reportProgress(options, "Installing mise");
@@ -3338,9 +2329,9 @@ async function ensureMiseDeps(miseExecutable, options = {}) {
3338
2329
  async function ensurePiPlugins(options = {}) {
3339
2330
  const actions = await ensurePiPackages(PI_PACKAGES, options);
3340
2331
  const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
3341
- const webSearch = await pi.configEnsure(join17(agentDir, "web-search.json"), (config) => ({ ...config, workflow: "auto-summary" }), options.dryRun);
2332
+ const webSearch = await pi.configEnsure(join12(agentDir, "web-search.json"), (config) => ({ ...config, workflow: "auto-summary" }), options.dryRun);
3342
2333
  actions.push(getConfigSetupAction("web search settings", webSearch));
3343
- const lsp = await pi.configEnsure(join17(agentDir, "pi-lsp.json"), (config) => ({
2334
+ const lsp = await pi.configEnsure(join12(agentDir, "pi-lsp.json"), (config) => ({
3344
2335
  ...config,
3345
2336
  progressive: { ...getRecord(config.progressive), enabled: true, inject: "none" }
3346
2337
  }), options.dryRun);
@@ -3351,11 +2342,11 @@ async function ensurePiAgents(options = {}) {
3351
2342
  const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
3352
2343
  const bundledAgentsDir = options.bundledAgentsDir ?? BUNDLED_AGENTS_DIR2;
3353
2344
  const userConfig = await loadDiffpiConfig({ homeDir: options.homeDir });
3354
- const entries = (await readdir3(bundledAgentsDir, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.startsWith("diffpi-") && entry.name.endsWith(".md")).sort((left, right) => left.name.localeCompare(right.name));
2345
+ const entries = (await readdir2(bundledAgentsDir, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.startsWith("diffpi-") && entry.name.endsWith(".md")).sort((left, right) => left.name.localeCompare(right.name));
3355
2346
  const actions = [];
3356
2347
  for (const entry of entries) {
3357
- const id = basename7(entry.name, ".md").replace(/^diffpi-/, "");
3358
- const source = await readFile15(join17(bundledAgentsDir, entry.name), "utf8");
2348
+ const id = basename5(entry.name, ".md").replace(/^diffpi-/, "");
2349
+ const source = await readFile9(join12(bundledAgentsDir, entry.name), "utf8");
3359
2350
  const content = materializeAgentModels(source, id, userConfig.config, options.availableModels);
3360
2351
  const result = await pi.agentEnsure(entry.name, content, agentDir, options.dryRun);
3361
2352
  actions.push(getConfigSetupAction(`pi agent ${id}`, result));
@@ -3364,7 +2355,7 @@ async function ensurePiAgents(options = {}) {
3364
2355
  }
3365
2356
  async function ensurePiSkills(miseExecutable, options = {}) {
3366
2357
  const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
3367
- const sharedSkillsDir = join17(options.homeDir ?? homedir9(), ".agents", "skills");
2358
+ const sharedSkillsDir = join12(options.homeDir ?? homedir8(), ".agents", "skills");
3368
2359
  const actions = [];
3369
2360
  for (const source of PI_SKILL_SOURCES) {
3370
2361
  const missing = [];
@@ -3445,8 +2436,7 @@ function setupRequiresRestart(actions) {
3445
2436
  async function ensureZedIntegration(options = {}) {
3446
2437
  if (options.dryRun) {
3447
2438
  const actions = [
3448
- createSetupAction("Zed review tasks", "planned", "global static runtime-resolver tasks in tasks.json"),
3449
- createSetupAction("Zed plan task", "planned", "pinned package CLI task in tasks.json")
2439
+ createSetupAction("Zed review tasks", "planned", "global static runtime-resolver tasks in tasks.json")
3450
2440
  ];
3451
2441
  if (options.bindZedKey)
3452
2442
  actions.push(createSetupAction("Zed review keybinding", "planned", "keymap.json"));
@@ -3456,8 +2446,6 @@ async function ensureZedIntegration(options = {}) {
3456
2446
  try {
3457
2447
  const tasks = await ensureZedReviewTask(options.homeDir);
3458
2448
  actions.push(createSetupAction("Zed review tasks", tasks.changed ? "installed" : "ready", tasks.path));
3459
- const planTask = await ensureZedPlanTask(await packageVersion2(), options.homeDir);
3460
- actions.push(createSetupAction("Zed plan task", planTask.changed ? "installed" : "ready", planTask.path));
3461
2449
  } catch (error) {
3462
2450
  actions.push(createSetupAction("Zed review tasks", "skipped", error instanceof Error ? error.message : String(error)));
3463
2451
  }
@@ -3559,45 +2547,61 @@ function createSetupAction(name, status, detail) {
3559
2547
  function reportProgress(options, message) {
3560
2548
  options.onProgress?.(message);
3561
2549
  }
3562
- async function packageVersion2() {
3563
- const source = await readFile15(join17(resolveBundledAgentsDir(), "..", "package.json"), "utf8");
3564
- const value = JSON.parse(source);
3565
- if (typeof value.version !== "string")
3566
- throw new Error("Cannot resolve the installed @difflab/pi version.");
3567
- return value.version;
3568
- }
3569
2550
  function getRecord(value) {
3570
2551
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
3571
2552
  }
2553
+ // src/templates.ts
2554
+ import { readFile as readFile10 } from "node:fs/promises";
2555
+ import { homedir as homedir9 } from "node:os";
2556
+ import { join as join13, normalize } from "node:path";
2557
+ async function loadTemplate(name, options = {}) {
2558
+ const relative = templateRelativePath(name);
2559
+ const userPath = join13(options.homeDir ?? homedir9(), ".difflab", "diffpi", "templates", relative);
2560
+ const bundledPath = join13(options.bundledDir ?? resolveBundledTemplatesDir(), relative);
2561
+ const user = await readOptionalFile(userPath);
2562
+ if (user !== undefined)
2563
+ return { name, path: userPath, source: "user", content: user };
2564
+ const bundled = await readOptionalFile(bundledPath);
2565
+ if (bundled !== undefined)
2566
+ return { name, path: bundledPath, source: "bundled", content: bundled };
2567
+ throw new Error(`Template "${name}" was not found at ${userPath} or ${bundledPath}.`);
2568
+ }
2569
+ function renderTemplate(content, variables) {
2570
+ return content.replace(/\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g, (match, key) => variables[key] ?? match);
2571
+ }
2572
+ function templateRelativePath(name) {
2573
+ const normalized = normalize(name.replaceAll("\\", "/")).replace(/^\.\//, "");
2574
+ if (!normalized || normalized.startsWith("../") || normalized.includes("/../") || normalized.startsWith("/")) {
2575
+ throw new Error(`Invalid template name: ${name}`);
2576
+ }
2577
+ return normalized.endsWith(".md") ? normalized : `${normalized}.md`;
2578
+ }
2579
+ async function readOptionalFile(path) {
2580
+ try {
2581
+ return await readFile10(path, "utf8");
2582
+ } catch (error) {
2583
+ if (error.code === "ENOENT")
2584
+ return;
2585
+ throw error;
2586
+ }
2587
+ }
3572
2588
  export {
3573
2589
  CONVENTIONAL_COMMIT,
3574
2590
  GitHubVcsBackend,
3575
2591
  GitLabVcsBackend,
3576
2592
  ZED_LOCAL_REVIEW_TASK_NAME,
3577
- ZED_PLAN_ANNOTATE_TASK_NAME,
3578
2593
  ZED_PR_REVIEW_TASK_NAME,
3579
2594
  ZED_REVIEW_TASK_NAME,
3580
- acknowledgePlanAnnotations,
3581
2595
  addComment,
3582
- annotatePlan,
3583
- annotationStatePath,
3584
- appendPlanLog,
3585
2596
  assertGitHubMergeReady,
3586
- assertPhaseTransition,
3587
- assertPlanTransition,
3588
2597
  assertReviewEventSupported,
3589
- assertStableId,
3590
- assertTaskTransition,
3591
2598
  checkConventionalSubject,
3592
2599
  ciGate,
3593
2600
  computeProjectSlug,
3594
- countDesignWords,
3595
- createExecutionPacket,
3596
2601
  createForge,
3597
2602
  createForgeBackend,
3598
2603
  createLocalReviewBackend,
3599
2604
  createModeController,
3600
- createPlanStore,
3601
2605
  createRemoteReviewBackend,
3602
2606
  createVcsBackend,
3603
2607
  dedupeFindings,
@@ -3607,7 +2611,6 @@ export {
3607
2611
  detectVcs,
3608
2612
  diffpiConfigPaths,
3609
2613
  discoverAgentModes,
3610
- eligiblePlanTasks,
3611
2614
  ensureMcpAdapters,
3612
2615
  ensureMise,
3613
2616
  ensureMiseDeps,
@@ -3616,7 +2619,6 @@ export {
3616
2619
  ensurePiPlugins,
3617
2620
  ensurePiSkills,
3618
2621
  ensureStore,
3619
- ensureZedPlanTask,
3620
2622
  ensureZedReviewKeybinding,
3621
2623
  ensureZedReviewTask,
3622
2624
  findPreferredModel,
@@ -3635,26 +2637,15 @@ export {
3635
2637
  mise,
3636
2638
  openInNewTab,
3637
2639
  parseGitlabDiffRefs,
3638
- parsePlanDocument,
3639
- parsePlannerEscalation,
3640
2640
  parseRemote,
3641
2641
  parseThreadArtifact,
3642
- phaseCommitCommand,
3643
2642
  pi,
3644
- planRecordName,
3645
- plansDir,
3646
- readPlanAnnotations,
3647
- readPlanLog,
3648
2643
  readSession,
3649
- renderExecutionPrompt,
3650
- renderPlanDocument,
3651
- renderPlannerEscalation,
3652
2644
  renderReviewDoc,
3653
2645
  renderTemplate,
3654
2646
  renderThreadArtifact,
3655
2647
  resolveAgentMode,
3656
2648
  resolveAgentModelPreferences,
3657
- resolvePlan,
3658
2649
  resolvePrSession,
3659
2650
  resolveReviewSession,
3660
2651
  resolveSession,
@@ -3671,15 +2662,12 @@ export {
3671
2662
  severitySchema,
3672
2663
  storeDir,
3673
2664
  storeGlobalRoot,
3674
- tasksMayRunInParallel,
3675
2665
  templateRelativePath,
3676
2666
  toFindings,
3677
2667
  toReviewComments,
3678
2668
  tuicrAvailable,
3679
2669
  unpublishedReviewComments,
3680
2670
  upsertThreadReply,
3681
- validatePlanDocument,
3682
- withPlanLock,
3683
2671
  withRemoteProvenance,
3684
2672
  yymmdd,
3685
2673
  zedKeymapPath,