@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
@@ -124,18 +124,15 @@ function sanitize(value) {
124
124
  return value.replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim();
125
125
  }
126
126
 
127
- // src/tools/plan.ts
128
- import { join as join12 } from "node:path";
129
-
130
- // src/store.ts
131
- import { createHash } from "node:crypto";
132
- import { lstat, mkdir, readlink, realpath as realpath2, symlink, unlink } from "node:fs/promises";
133
- import { homedir } from "node:os";
134
- import { dirname, isAbsolute as isAbsolute2, join as join3, resolve as resolve2 } from "node:path";
127
+ // src/tools/review.ts
128
+ import { existsSync as existsSync2 } from "node:fs";
129
+ import { mkdir as mkdir4, readdir, readFile as readFile7, rename as rename2, stat, unlink as unlink2, writeFile as writeFile5 } from "node:fs/promises";
130
+ import { join as join9 } from "node:path";
131
+ import { defineTool as defineTool3 } from "@earendil-works/pi-coding-agent";
132
+ import { z as z6 } from "zod";
135
133
 
136
- // src/extensions/gitx.ts
137
- import { realpath } from "node:fs/promises";
138
- import { basename, isAbsolute, join as join2, resolve } from "node:path";
134
+ // src/environment.ts
135
+ import { basename } from "node:path";
139
136
 
140
137
  // src/extensions/processx.ts
141
138
  import { spawn } from "node:child_process";
@@ -211,201 +208,21 @@ function appendBounded(current, next) {
211
208
  return combined.length <= MAX_CAPTURED_OUTPUT_LENGTH ? combined : combined.slice(-MAX_CAPTURED_OUTPUT_LENGTH);
212
209
  }
213
210
 
214
- // src/extensions/gitx.ts
215
- async function gitToplevel(cwd) {
216
- const result = await run("git", ["-C", cwd, "rev-parse", "--show-toplevel"]);
217
- const top = result.stdout.trim();
218
- return result.code === 0 && top ? top : resolve(cwd);
219
- }
220
- async function inspectGitRepository(cwd) {
221
- const root = await gitToplevel(cwd);
222
- const remoteResult = await run("git", ["-C", root, "remote", "get-url", "origin"]);
223
- const remote = remoteResult.code === 0 ? remoteResult.stdout.trim() : "";
224
- const commonResult = await run("git", ["-C", root, "rev-parse", "--git-common-dir"]);
225
- const common = commonResult.stdout.trim();
226
- const commonPath = commonResult.code === 0 && common ? resolve(isAbsolute(common) ? common : join2(root, common)) : root;
227
- const commonDir = await canonicalPath(commonPath);
228
- return {
229
- root,
230
- commonDir,
231
- ...remote ? { remote } : {},
232
- name: remote ? repositoryName(remote) : basename(resolve(commonDir, "..")) || basename(root),
233
- identity: remote ? `remote:${normalizeRemote(remote)}` : `git-common-dir:${commonDir}`
234
- };
235
- }
236
- function normalizeRemote(remote) {
237
- return remote.trim().replace(/\.git\/?$/i, "").replace(/\/+$/, "").toLowerCase();
238
- }
239
- function repositoryName(remote) {
240
- const normalized = remote.trim().replace(/\.git\/?$/i, "").replace(/\/+$/, "");
241
- return normalized.split(/[/\\:]/).filter(Boolean).at(-1) ?? "";
242
- }
243
- async function canonicalPath(path) {
244
- try {
245
- return await realpath(path);
246
- } catch {
247
- return resolve(path);
248
- }
249
- }
250
-
251
- // src/store.ts
252
- var STORE_LINK = ".diffpi";
253
- var LEGACY_STORE_LINK = join3(".pi", "diffpi");
254
- function storeGlobalRoot(homeDir = homedir()) {
255
- return join3(homeDir, ".difflab", "diffpi", "projects");
256
- }
257
- async function ensureStore(cwd, homeDir = homedir()) {
258
- const repository = await inspectGitRepository(cwd);
259
- const root = repository.root;
260
- const slug = projectSlug(repository);
261
- const dest = join3(storeGlobalRoot(homeDir), slug);
262
- const link = join3(root, STORE_LINK);
263
- await mkdir(dest, { recursive: true });
264
- try {
265
- await assertStoreLink(link, dest);
266
- } catch (error) {
267
- if (error.code !== "ENOENT")
268
- throw error;
269
- await symlink(dest, link);
270
- }
271
- await removeLegacyStoreLink(join3(root, LEGACY_STORE_LINK), dest);
272
- return { slug, root, dest, link, linked: true };
273
- }
274
- async function plansDir(cwd, homeDir = homedir()) {
275
- const store = await ensureStore(cwd, homeDir);
276
- const dir = join3(store.link, "plan");
277
- await mkdir(dir, { recursive: true });
278
- return dir;
279
- }
280
- async function reviewsDir(cwd, homeDir = homedir()) {
281
- const store = await ensureStore(cwd, homeDir);
282
- const dir = join3(store.link, "review");
283
- await mkdir(dir, { recursive: true });
284
- return dir;
285
- }
286
- async function completedReviewsDir(cwd, homeDir = homedir()) {
287
- const store = await ensureStore(cwd, homeDir);
288
- const dir = join3(store.link, "reviews");
289
- await mkdir(dir, { recursive: true });
290
- return dir;
291
- }
292
- async function sessionsDir(cwd, homeDir = homedir()) {
293
- const store = await ensureStore(cwd, homeDir);
294
- const dir = join3(store.link, "sessions");
295
- await mkdir(dir, { recursive: true });
296
- return dir;
297
- }
298
- async function assertStoreLink(path, dest) {
299
- const entry = await lstat(path);
300
- if (!entry.isSymbolicLink())
301
- throw new Error(`${path} exists and is not a symlink.`);
302
- const target = await symlinkTarget(path);
303
- if (target !== await canonicalPath2(dest))
304
- throw new Error(`${path} points to ${target}, not ${dest}.`);
305
- }
306
- async function removeLegacyStoreLink(path, dest) {
307
- try {
308
- const entry = await lstat(path);
309
- if (!entry.isSymbolicLink())
310
- return;
311
- const target = await symlinkTarget(path);
312
- if (target === await canonicalPath2(dest))
313
- await unlink(path);
314
- } catch (error) {
315
- if (error.code !== "ENOENT")
316
- throw error;
317
- }
318
- }
319
- async function symlinkTarget(path) {
320
- const target = await readlink(path);
321
- return canonicalPath2(isAbsolute2(target) ? target : resolve2(dirname(path), target));
322
- }
323
- async function canonicalPath2(path) {
324
- try {
325
- return await realpath2(path);
326
- } catch {
327
- return resolve2(path);
328
- }
329
- }
330
- function projectSlug(repository) {
331
- const readable = repository.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "unnamed";
332
- const digest = createHash("sha256").update(repository.identity).digest("hex").slice(0, 12);
333
- return `${readable}-${digest}`;
334
- }
335
-
336
- // src/commands/background.ts
337
- async function launchBackgroundPi(pi, options) {
338
- const child = shellCommand([
339
- "pi",
340
- "--mode",
341
- "json",
342
- "--print",
343
- "--no-session",
344
- "--offline",
345
- "--approve",
346
- "--model",
347
- options.model,
348
- "--thinking",
349
- options.thinking,
350
- "--append-system-prompt",
351
- options.agentPath,
352
- "--",
353
- options.prompt
354
- ]);
355
- const command = options.packetPath ? `sh -c ${shellQuote(`trap 'rm -f -- "$1"' EXIT HUP INT TERM; cd -- "$2" || exit; ${child}`)} sh ${shellQuote(options.packetPath)} ${shellQuote(options.cwd)}` : `cd ${shellQuote(options.cwd)} && ${child}`;
356
- await pi.sendUserMessage(`/bg --agent --name ${shellQuote(options.name)} -- ${command}`, {
357
- deliverAs: "followUp",
358
- expandPromptTemplates: true
359
- });
360
- return { name: options.name, command, queued: true };
361
- }
362
- function shellQuote(value) {
363
- return `'${value.replaceAll("'", `'"'"'`)}'`;
364
- }
365
- function shellCommand(args) {
366
- return args.map(shellQuote).join(" ");
367
- }
368
-
369
- // src/tools/plan.ts
370
- import { defineTool as defineTool3 } from "@earendil-works/pi-coding-agent";
371
- import { z as z4 } from "zod";
372
-
373
- // src/assets.ts
374
- import { existsSync } from "node:fs";
375
- import { dirname as dirname2, join as join4 } from "node:path";
376
- import { fileURLToPath } from "node:url";
377
- function resolveBundledAssetDir(name, moduleUrl = import.meta.url) {
378
- const moduleDir = dirname2(fileURLToPath(moduleUrl));
379
- const candidates = [join4(moduleDir, name), join4(moduleDir, "..", name), join4(moduleDir, "..", "..", name)];
380
- return candidates.find((path) => existsSync(path)) ?? candidates[1];
381
- }
382
- function resolveBundledAgentsDir(moduleUrl = import.meta.url) {
383
- return resolveBundledAssetDir("agents", moduleUrl);
384
- }
385
- function resolveBundledTemplatesDir(moduleUrl = import.meta.url) {
386
- return resolveBundledAssetDir("templates", moduleUrl);
387
- }
388
-
389
- // src/environment.ts
390
- import { readFile as readFile2 } from "node:fs/promises";
391
- import { basename as basename2, join as join6 } from "node:path";
392
-
393
211
  // src/extensions/zedx.ts
394
- import { mkdir as mkdir2, readFile, writeFile } from "node:fs/promises";
395
- import { homedir as homedir2 } from "node:os";
396
- import { dirname as dirname3, join as join5 } from "node:path";
212
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
213
+ import { homedir } from "node:os";
214
+ import { dirname, join as join2 } from "node:path";
397
215
  var ZED_LOCAL_REVIEW_TASK_NAME = "diffpi: tuicr local review";
398
216
  var ZED_PR_REVIEW_TASK_NAME = "diffpi: tuicr PR review";
399
- var ZED_PLAN_ANNOTATE_TASK_NAME = "diffpi: annotate plan";
400
217
  var LEGACY_ZED_REVIEW_TASK_NAME = "diffpi: tuicr review";
401
218
  var REVIEW_KEYBINDING = "cmd-alt-r";
402
- function zedTasksPath(homeDir = homedir2()) {
403
- return join5(homeDir, ".config", "zed", "tasks.json");
219
+ function zedTasksPath(homeDir = homedir()) {
220
+ return join2(homeDir, ".config", "zed", "tasks.json");
404
221
  }
405
- function zedKeymapPath(homeDir = homedir2()) {
406
- return join5(homeDir, ".config", "zed", "keymap.json");
222
+ function zedKeymapPath(homeDir = homedir()) {
223
+ return join2(homeDir, ".config", "zed", "keymap.json");
407
224
  }
408
- async function ensureZedReviewTask(homeDir = homedir2(), _command = ["tuicr", "-w", "-r", "main..HEAD"]) {
225
+ async function ensureZedReviewTask(homeDir = homedir(), _command = ["tuicr", "-w", "-r", "main..HEAD"]) {
409
226
  const path = zedTasksPath(homeDir);
410
227
  const currentText = await readOptional(path);
411
228
  const tasks = parseJsonArray(currentText, path);
@@ -423,34 +240,7 @@ async function ensureZedReviewTask(homeDir = homedir2(), _command = ["tuicr", "-
423
240
  await writeJson(path, next);
424
241
  return { path, changed, existed: currentText !== undefined };
425
242
  }
426
- async function ensureZedPlanTask(packageVersion, homeDir = homedir2()) {
427
- if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(packageVersion)) {
428
- throw new Error(`Invalid @difflab/pi package version: ${packageVersion}.`);
429
- }
430
- const path = zedTasksPath(homeDir);
431
- const currentText = await readOptional(path);
432
- const tasks = parseJsonArray(currentText, path);
433
- const task = {
434
- label: ZED_PLAN_ANNOTATE_TASK_NAME,
435
- command: "npx",
436
- args: ["--yes", `@difflab/pi@${packageVersion}`, "plan", "annotate", "--cwd", "$ZED_WORKTREE_ROOT"],
437
- cwd: "$ZED_WORKTREE_ROOT",
438
- use_new_terminal: true,
439
- reveal: "always",
440
- reveal_target: "center"
441
- };
442
- const next = [...tasks];
443
- const index = next.findIndex((candidate) => candidate.label === task.label);
444
- if (index >= 0)
445
- next[index] = { ...next[index], ...task };
446
- else
447
- next.push(task);
448
- const changed = JSON.stringify(tasks) !== JSON.stringify(next);
449
- if (changed)
450
- await writeJson(path, next);
451
- return { path, changed, existed: currentText !== undefined };
452
- }
453
- async function ensureZedReviewKeybinding(homeDir = homedir2()) {
243
+ async function ensureZedReviewKeybinding(homeDir = homedir()) {
454
244
  const path = zedKeymapPath(homeDir);
455
245
  const currentText = await readOptional(path);
456
246
  const entries = parseJsonArray(currentText, path);
@@ -469,9 +259,6 @@ async function ensureZedReviewKeybinding(homeDir = homedir2()) {
469
259
  return { path, changed, existed: currentText !== undefined };
470
260
  }
471
261
  function zedReviewTaskName(command) {
472
- if (command.some((value, index) => value === "plan" && command[index + 1] === "annotate")) {
473
- return ZED_PLAN_ANNOTATE_TASK_NAME;
474
- }
475
262
  return command[0] === "tuicr" && command[1] === "pr" ? ZED_PR_REVIEW_TASK_NAME : ZED_LOCAL_REVIEW_TASK_NAME;
476
263
  }
477
264
  var LOCAL_REVIEW_SCRIPT = `set -eu
@@ -553,7 +340,7 @@ function parseJsonArray(content, path) {
553
340
  return value;
554
341
  }
555
342
  async function writeJson(path, value) {
556
- await mkdir2(dirname3(path), { recursive: true });
343
+ await mkdir(dirname(path), { recursive: true });
557
344
  await writeFile(path, `${JSON.stringify(value, null, 2)}
558
345
  `, "utf8");
559
346
  }
@@ -583,7 +370,7 @@ function detectMux(env = process.env) {
583
370
  return "none";
584
371
  }
585
372
  function detectShell(env = process.env) {
586
- return env.SHELL ? basename2(env.SHELL) : "unknown";
373
+ return env.SHELL ? basename(env.SHELL) : "unknown";
587
374
  }
588
375
  async function detectVcs(cwd) {
589
376
  const root = (await run("git", ["-C", cwd, "rev-parse", "--show-toplevel"])).stdout.trim() || cwd;
@@ -622,10 +409,7 @@ async function openInNewTab(command, opts) {
622
409
  if (detectIde(env) === "zed") {
623
410
  try {
624
411
  const taskName = zedReviewTaskName(command);
625
- if (taskName === ZED_PLAN_ANNOTATE_TASK_NAME)
626
- await ensureZedPlanTask(await packageVersion(), opts.homeDir);
627
- else
628
- await ensureZedReviewTask(opts.homeDir, command);
412
+ await ensureZedReviewTask(opts.homeDir, command);
629
413
  return {
630
414
  launched: false,
631
415
  configured: true,
@@ -641,12 +425,6 @@ async function openInNewTab(command, opts) {
641
425
  function screenWindowArgs(command, cwd, name) {
642
426
  return ["-X", "screen", "-t", name, "sh", "-lc", 'cd -- "$1" && shift && exec "$@"', "sh", cwd, ...command];
643
427
  }
644
- async function packageVersion() {
645
- const value = JSON.parse(await readFile2(join6(resolveBundledAgentsDir(), "..", "package.json"), "utf8"));
646
- if (typeof value.version !== "string")
647
- throw new Error("Cannot resolve the installed @difflab/pi version.");
648
- return value.version;
649
- }
650
428
  async function openMuxTab(mux, command, cwd, name, printable) {
651
429
  if (mux === "zellij" && await findExecutable("zellij")) {
652
430
  const result = await run("zellij", ["action", "new-tab", "--cwd", cwd, "--name", name, "--", ...command]);
@@ -669,1932 +447,24 @@ async function openMuxTab(mux, command, cwd, name, printable) {
669
447
  return;
670
448
  }
671
449
 
672
- // src/extensions/misex.ts
673
- import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
674
- import { homedir as homedir3 } from "node:os";
675
- import { basename as basename3, dirname as dirname4, join as join7 } from "node:path";
676
- var MISE_HOOK_START = "# >>> @difflab/pi mise >>>";
677
- var MISE_HOOK_END = "# <<< @difflab/pi mise <<<";
678
- var mise = {
679
- async executableCheck(name = "mise") {
680
- return findExecutable(name);
681
- },
682
- async run(args, options = {}) {
683
- return run("mise", args, options);
684
- },
685
- async install(options = {}) {
686
- const homeDir = options.homeDir ?? homedir3();
687
- const platform = options.platform ?? process.platform;
688
- if (platform === "win32")
689
- throw new Error("Automatic mise installation supports macOS and Linux only.");
690
- const installedPath = join7(homeDir, ".local", "bin", "mise");
691
- if (options.dryRun)
692
- return installedPath;
693
- await runChecked("sh", ["-c", "curl -fsSL https://mise.run | sh"]);
694
- const executable = await findExecutable(installedPath) ?? await findExecutable("mise");
695
- if (!executable)
696
- throw new Error(`mise installation completed, but ${installedPath} was not found.`);
697
- return executable;
698
- },
699
- async hookEnsure(executable, options = {}) {
700
- const homeDir = options.homeDir ?? homedir3();
701
- const hook = getShellHook(basename3(options.shell ?? process.env.SHELL ?? ""), executable, homeDir);
702
- const current = await getOptionalFile(hook.path);
703
- if (current.includes(MISE_HOOK_START))
704
- return { path: hook.path, changed: false, planned: false };
705
- if (options.dryRun)
706
- return { path: hook.path, changed: true, planned: true };
707
- const separator = current.length === 0 || current.endsWith(`
708
- `) ? "" : `
709
- `;
710
- await mkdir3(dirname4(hook.path), { recursive: true });
711
- await writeFile2(hook.path, `${current}${separator}${hook.content}`, "utf8");
712
- return { path: hook.path, changed: true, planned: false };
713
- },
714
- async toolCheckGlobal(executable, tool, minimumVersion) {
715
- const result = await run(executable, ["ls", "--global", "--installed", tool, "--json"]);
716
- return result.code === 0 && isToolInstalled(result.stdout, minimumVersion);
717
- },
718
- async toolInstallGlobal(executable, specification) {
719
- await runChecked(executable, ["use", "--global", specification]);
720
- },
721
- async toolCheckLocal(executable, tool, cwd = process.cwd()) {
722
- const result = await run(executable, ["ls", "--local", "--installed", tool, "--json"], { cwd });
723
- return result.code === 0 && isToolInstalled(result.stdout);
724
- },
725
- async toolInstallLocal(executable, specification, cwd = process.cwd()) {
726
- await runChecked(executable, ["use", "--path", cwd, specification], { cwd });
727
- },
728
- async toolUpdateAllGlobal(executable, homeDir = homedir3()) {
729
- await runChecked(executable, ["upgrade"], { cwd: homeDir });
730
- }
731
- };
732
- function getShellHook(shell, executable, homeDir) {
733
- const command = getShellQuoted(executable);
734
- switch (shell.toLowerCase()) {
735
- case "zsh":
736
- return {
737
- path: join7(homeDir, ".zshrc"),
738
- content: `${MISE_HOOK_START}
739
- eval "$(${command} activate zsh)"
740
- ${MISE_HOOK_END}
741
- `
742
- };
743
- case "fish":
744
- return {
745
- path: join7(homeDir, ".config", "fish", "config.fish"),
746
- content: `${MISE_HOOK_START}
747
- ${command} activate fish | source
748
- ${MISE_HOOK_END}
749
- `
750
- };
751
- case "nu":
752
- case "nushell":
753
- return {
754
- path: join7(homeDir, ".config", "nushell", "config.nu"),
755
- content: `${MISE_HOOK_START}
756
- let mise_bin = ${command}
757
- let mise_path = $nu.default-config-dir | path join mise.nu
758
- ^$mise_bin activate nu | save $mise_path --force
759
- use ($nu.default-config-dir | path join mise.nu)
760
- ${MISE_HOOK_END}
761
- `
762
- };
763
- case "xonsh":
764
- return {
765
- path: join7(homeDir, ".xonshrc"),
766
- content: `${MISE_HOOK_START}
767
- execx($(${command} activate xonsh))
768
- ${MISE_HOOK_END}
769
- `
770
- };
771
- case "elvish":
772
- return {
773
- path: join7(homeDir, ".config", "elvish", "rc.elv"),
774
- content: `${MISE_HOOK_START}
775
- var mise: = (ns [&])
776
- eval (${command} activate elvish | slurp) &ns=$mise: &on-end={|ns| set mise: = $ns }
777
- mise:activate
778
- ${MISE_HOOK_END}
779
- `
780
- };
781
- case "pwsh":
782
- case "powershell":
783
- return {
784
- path: join7(homeDir, ".config", "powershell", "Microsoft.PowerShell_profile.ps1"),
785
- content: `${MISE_HOOK_START}
786
- (& ${command} activate pwsh) | Out-String | Invoke-Expression
787
- ${MISE_HOOK_END}
788
- `
789
- };
790
- case "bash":
791
- default:
792
- return {
793
- path: join7(homeDir, ".bashrc"),
794
- content: `${MISE_HOOK_START}
795
- eval "$(${command} activate bash)"
796
- ${MISE_HOOK_END}
797
- `
798
- };
799
- }
800
- }
801
- function isToolInstalled(output, minimumVersion) {
802
- try {
803
- const value = JSON.parse(output);
804
- if (!Array.isArray(value))
805
- return false;
806
- return value.some((entry) => {
807
- if (!entry || typeof entry !== "object" || !("installed" in entry) || entry.installed !== true)
808
- return false;
809
- if (!minimumVersion)
810
- return true;
811
- if (!("version" in entry) || typeof entry.version !== "string")
812
- return false;
813
- return isVersionAtLeast(entry.version, minimumVersion);
814
- });
815
- } catch {
816
- return false;
817
- }
818
- }
819
- function isVersionAtLeast(version, minimumVersion) {
820
- const current = version.match(/^v?(\d+)\.(\d+)\.(\d+)/)?.slice(1).map(Number);
821
- const minimum = minimumVersion.match(/^v?(\d+)\.(\d+)\.(\d+)/)?.slice(1).map(Number);
822
- if (!current || !minimum)
823
- return false;
824
- for (let index = 0;index < minimum.length; index += 1) {
825
- if (current[index] !== minimum[index])
826
- return current[index] > minimum[index];
827
- }
828
- return true;
829
- }
830
- async function getOptionalFile(path) {
831
- try {
832
- return await readFile3(path, "utf8");
833
- } catch (error) {
834
- if (error instanceof Error && "code" in error && error.code === "ENOENT")
835
- return "";
836
- throw error;
837
- }
838
- }
839
- function getShellQuoted(value) {
840
- return `'${value.replaceAll("'", "'\\''")}'`;
841
- }
842
-
843
- // src/gates.ts
844
- var CONVENTIONAL_COMMIT = /^(feat|fix|perf|refactor|docs|chore|test|build|ci|style|revert)(\([^)]+\))?!?: .+/;
845
- var MISE_GATES = ["format:check", "lint", "test"];
846
- function checkConventionalSubject(subject) {
847
- const trimmed = subject.trim();
848
- const ok = CONVENTIONAL_COMMIT.test(trimmed);
849
- return {
850
- name: "conventional-subject",
851
- status: ok ? "pass" : "warn",
852
- detail: ok ? trimmed : `not a conventional-commit subject: "${trimmed}"`
853
- };
854
- }
855
- async function runMiseGates(cwd) {
856
- const tasks = await discoverMiseTasks(cwd);
857
- const results = [];
858
- for (const gate of MISE_GATES) {
859
- const targets = tasks.get(gate) ?? [];
860
- if (targets.length === 0) {
861
- results.push({ name: gate, status: "skip", detail: "no mise recipe" });
862
- continue;
863
- }
864
- const invocations = targets.flatMap((target, index) => index === 0 ? [target] : [":::", target]);
865
- const result = await mise.run(["run", ...invocations], { cwd });
866
- results.push({
867
- name: gate,
868
- status: result.code === 0 ? "pass" : "fail",
869
- detail: result.code === 0 ? "clean" : (result.stderr.trim() || result.stdout.trim()).slice(-400)
870
- });
871
- }
872
- return results;
873
- }
874
- function ciGate(checksOutput) {
875
- const text = checksOutput.toLowerCase();
876
- if (!text.trim())
877
- return { name: "ci", status: "skip", detail: "no CI output" };
878
- if (/\bfail|error\b/.test(text))
879
- return { name: "ci", status: "warn", detail: "CI failing" };
880
- if (/\bpending|in progress|queued\b/.test(text))
881
- return { name: "ci", status: "warn", detail: "CI pending" };
882
- return { name: "ci", status: "pass", detail: "CI green" };
883
- }
884
- function parseMiseTasks(input) {
885
- let tasks;
886
- try {
887
- tasks = JSON.parse(input);
888
- } catch {
889
- return new Map;
890
- }
891
- if (!Array.isArray(tasks))
892
- return new Map;
893
- const found = new Map;
894
- for (const gate of MISE_GATES) {
895
- const targets = tasks.flatMap((task) => {
896
- if (typeof task.name !== "string")
897
- return [];
898
- return task.name === gate || task.name.endsWith(`:${gate}`) || task.aliases?.includes(gate) ? [task.name] : [];
899
- });
900
- if (targets.length > 0)
901
- found.set(gate, [...new Set(targets)]);
902
- }
903
- return found;
904
- }
905
- async function discoverMiseTasks(cwd) {
906
- const result = await mise.run(["tasks", "--json", "--all"], { cwd });
907
- if (result.code !== 0)
908
- return new Map;
909
- return parseMiseTasks(result.stdout);
910
- }
911
-
912
- // src/plan/annotations.ts
913
- import { spawn as spawn2 } from "node:child_process";
914
- import { readFile as readFile6, rename, writeFile as writeFile4 } from "node:fs/promises";
915
- import { basename as basename4, dirname as dirname5, join as join9 } from "node:path";
916
-
917
- // src/plan/log.ts
918
- import { randomUUID } from "node:crypto";
919
- import { appendFile, readFile as readFile4 } from "node:fs/promises";
920
- async function appendPlanLog(logPath, event) {
921
- const entry = {
922
- ...event,
923
- version: 1,
924
- eventId: event.eventId ?? randomUUID(),
925
- timestamp: event.timestamp ?? new Date().toISOString()
926
- };
927
- await appendFile(logPath, `${JSON.stringify(entry)}
928
- `, { encoding: "utf8", mode: 384 });
929
- return entry;
930
- }
931
-
932
- // src/plan/lock.ts
933
- import { randomUUID as randomUUID2 } from "node:crypto";
934
- import { mkdir as mkdir4, readFile as readFile5, rm, writeFile as writeFile3 } from "node:fs/promises";
935
- import { hostname } from "node:os";
936
- import { join as join8 } from "node:path";
937
- async function withPlanLock(planDir, operation, options = {}) {
938
- const lockDir = join8(planDir, ".lock");
939
- const waitMs = options.waitMs ?? 5000;
940
- const pollMs = options.pollMs ?? 25;
941
- const owner = {
942
- token: randomUUID2(),
943
- pid: process.pid,
944
- hostname: hostname(),
945
- operation: options.operation ?? "plan mutation",
946
- acquiredAt: new Date().toISOString()
947
- };
948
- const started = Date.now();
949
- while (true) {
950
- try {
951
- await mkdir4(lockDir);
952
- await writeFile3(join8(lockDir, "owner.json"), `${JSON.stringify(owner)}
953
- `, { mode: 384 });
954
- break;
955
- } catch (error) {
956
- if (error.code !== "EEXIST")
957
- throw error;
958
- if (Date.now() - started >= waitMs) {
959
- let existing = "unknown owner";
960
- try {
961
- existing = (await readFile5(join8(lockDir, "owner.json"), "utf8")).trim();
962
- } catch {}
963
- throw new Error(`Timed out waiting for plan lock ${lockDir}; owner: ${existing}. Remove it only after confirming the owner is stale.`);
964
- }
965
- await new Promise((resolve) => setTimeout(resolve, pollMs));
966
- }
967
- }
968
- try {
969
- return await operation();
970
- } finally {
971
- let current;
972
- try {
973
- current = JSON.parse(await readFile5(join8(lockDir, "owner.json"), "utf8"));
974
- } catch {}
975
- if (current?.token === owner.token)
976
- await rm(lockDir, { recursive: true, force: true });
977
- }
978
- }
979
-
980
- // src/plan/annotations.ts
981
- async function readPlanAnnotations(record, options = {}) {
982
- const state = await readAnnotationState(record).catch((error) => {
983
- if (error.code === "ENOENT")
984
- return;
985
- throw error;
986
- });
987
- if (!state)
988
- return { comments: [], pending: [] };
989
- const execute = options.runtime?.execute ?? executeProcess;
990
- const response = await execute("tuicr", ["review", "comments", "--session", state.sessionSlug], record.dir, false);
991
- if (response.code !== 0) {
992
- if (/not found|no session|deleted/i.test(response.stderr))
993
- return { state, comments: [], pending: [] };
994
- throw new Error(response.stderr.trim() || "Cannot read tuicr comments.");
995
- }
996
- let raw;
997
- try {
998
- raw = JSON.parse(response.stdout);
999
- } catch {
1000
- throw new Error("tuicr returned malformed comment JSON.");
1001
- }
1002
- if (!Array.isArray(raw))
1003
- throw new Error("tuicr comment output must be a JSON array.");
1004
- const lines = record.source.split(`
1005
- `);
1006
- const applied = new Set(state.appliedCommentIds);
1007
- const comments = raw.map((value, index) => normalizeComment(value, index, lines, applied));
1008
- const pending = comments.filter((comment) => !comment.applied);
1009
- return { state, comments: options.includeApplied ? comments : pending, pending };
1010
- }
1011
- async function acknowledgePlanAnnotations(record, commentIds, summary) {
1012
- if (!summary.trim())
1013
- throw new Error("Annotation acknowledgement summary is required.");
1014
- return withPlanLock(record.dir, async () => {
1015
- const state = await readAnnotationState(record);
1016
- const next = {
1017
- ...state,
1018
- updatedAt: new Date().toISOString(),
1019
- appliedCommentIds: [...new Set([...state.appliedCommentIds, ...commentIds])]
1020
- };
1021
- await atomicJson(annotationStatePath(record), next);
1022
- await appendPlanLog(record.logPath, {
1023
- planRevision: record.document.revision,
1024
- kind: "annotation",
1025
- actor: "planner",
1026
- message: summary,
1027
- data: { commentIds: [...commentIds], sessionSlug: state.sessionSlug }
1028
- });
1029
- return next;
1030
- }, { operation: "acknowledge annotations" });
1031
- }
1032
- function annotationStatePath(record) {
1033
- return join9(record.dir, "annotations.json");
1034
- }
1035
- async function readAnnotationState(record) {
1036
- const value = JSON.parse(await readFile6(annotationStatePath(record), "utf8"));
1037
- if (value.schemaVersion !== 1 || typeof value.sessionSlug !== "string" || !Array.isArray(value.appliedCommentIds)) {
1038
- throw new Error(`Malformed annotation state: ${annotationStatePath(record)}.`);
1039
- }
1040
- return value;
1041
- }
1042
- function normalizeComment(value, index, lines, applied) {
1043
- if (!value || typeof value !== "object")
1044
- throw new Error(`Malformed tuicr comment at index ${index}.`);
1045
- const raw = value;
1046
- if (typeof raw.id !== "string" || typeof raw.content !== "string")
1047
- throw new Error(`Malformed tuicr comment at index ${index}.`);
1048
- const line = integer(raw.start_line) ?? integer(raw.line);
1049
- const endLine = integer(raw.end_line) ?? line;
1050
- const targetPath = typeof raw.path === "string" ? raw.path : undefined;
1051
- const appliesToPlan = !targetPath || basename4(targetPath) === basename4("PLAN.md");
1052
- const validAnchor = appliesToPlan && line !== undefined && line > 0 && line <= lines.length;
1053
- return {
1054
- id: raw.id,
1055
- body: raw.content,
1056
- file: targetPath,
1057
- line,
1058
- endLine,
1059
- context: validAnchor ? lines.slice(line - 1, Math.min(endLine ?? line, lines.length)).join(`
1060
- `) : undefined,
1061
- stale: line !== undefined && !validAnchor,
1062
- applied: applied.has(raw.id)
1063
- };
1064
- }
1065
- async function executeProcess(command, args, cwd, interactive) {
1066
- if (!interactive) {
1067
- const result = await run(command, args, { cwd, capture: "unbounded" });
1068
- return { code: result.code, stdout: result.stdout, stderr: result.stderr };
1069
- }
1070
- return new Promise((resolve, reject) => {
1071
- const child = spawn2(command, args, { cwd, stdio: ["inherit", "inherit", "pipe"] });
1072
- let stderr = "";
1073
- child.stderr.on("data", (chunk) => {
1074
- const text = chunk.toString();
1075
- stderr += text;
1076
- process.stderr.write(text);
1077
- });
1078
- child.on("error", reject);
1079
- child.on("close", (code) => resolve({ code: code ?? 1, stdout: "", stderr }));
1080
- });
1081
- }
1082
- async function atomicJson(path, value) {
1083
- const temp = join9(dirname5(path), `.${basename4(path)}.${crypto.randomUUID()}.tmp`);
1084
- await writeFile4(temp, `${JSON.stringify(value, null, 2)}
1085
- `, { mode: 384, flag: "wx" });
1086
- await rename(temp, path);
1087
- }
1088
- function integer(value) {
1089
- return typeof value === "number" && Number.isInteger(value) ? value : undefined;
1090
- }
1091
- // src/plan/escalation.ts
1092
- import { z as z3 } from "zod";
1093
- var escalationSchema = z3.object({
1094
- planId: z3.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
1095
- executionId: z3.string().min(1),
1096
- phaseId: z3.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
1097
- taskId: z3.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/).optional(),
1098
- blocker: z3.string().min(1),
1099
- attempts: z3.array(z3.string()),
1100
- evidence: z3.array(z3.string()),
1101
- needsUserDecision: z3.boolean()
1102
- }).strict();
1103
- var OPEN = "<diffpi-planner-escalation>";
1104
- var CLOSE = "</diffpi-planner-escalation>";
1105
- function renderPlannerEscalation(value) {
1106
- return `${OPEN}${JSON.stringify(escalationSchema.parse(value))}${CLOSE}`;
1107
- }
1108
- // src/plan/execution.ts
1109
- function createExecutionPacket(plan, coordinator) {
1110
- if (!plan.execution?.active)
1111
- throw new Error(`Plan ${plan.id} has no active execution.`);
1112
- return {
1113
- version: 1,
1114
- planId: plan.id,
1115
- executionId: plan.execution.id,
1116
- policy: plan.execution.policy,
1117
- cwd: plan.execution.cwd,
1118
- branch: plan.execution.branch,
1119
- coordinator
1120
- };
1121
- }
1122
- function renderExecutionPrompt(packet) {
1123
- 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.`;
1124
- }
1125
- // src/plan/transitions.ts
1126
- var STABLE_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
1127
- var PLAN_TRANSITIONS = {
1128
- draft: ["ready"],
1129
- ready: ["draft", "in_progress"],
1130
- in_progress: ["blocked", "completed"],
1131
- blocked: ["draft", "ready", "in_progress"],
1132
- completed: []
1133
- };
1134
- var WORK_TRANSITIONS = {
1135
- pending: ["in_progress", "skipped"],
1136
- in_progress: ["blocked", "completed"],
1137
- blocked: ["pending", "in_progress", "skipped"],
1138
- completed: [],
1139
- skipped: []
1140
- };
1141
- function isStableId(value) {
1142
- return STABLE_ID.test(value) && value.length <= 80;
1143
- }
1144
- function assertStableId(value, label = "identifier") {
1145
- if (!isStableId(value))
1146
- throw new Error(`${label} must be a lowercase stable slug, not a path: ${value}`);
1147
- }
1148
- function assertPlanTransition(from, to) {
1149
- if (from === to)
1150
- return;
1151
- if (!PLAN_TRANSITIONS[from].includes(to))
1152
- throw new Error(`Invalid plan status transition: ${from} -> ${to}.`);
1153
- }
1154
- function assertPhaseTransition(from, to) {
1155
- if (from === to)
1156
- return;
1157
- if (!WORK_TRANSITIONS[from].includes(to))
1158
- throw new Error(`Invalid phase status transition: ${from} -> ${to}.`);
1159
- }
1160
- function assertTaskTransition(task, to, executionId, actor) {
1161
- if (task.status !== to && !WORK_TRANSITIONS[task.status].includes(to)) {
1162
- throw new Error(`Invalid task status transition: ${task.status} -> ${to}.`);
1163
- }
1164
- if (task.status === "in_progress" && task.executionId && executionId !== task.executionId) {
1165
- throw new Error(`Task ${task.id} is owned by execution ${task.executionId}.`);
1166
- }
1167
- if (task.status === "in_progress" && task.owner && actor && actor !== task.owner) {
1168
- throw new Error(`Task ${task.id} is owned by ${task.owner}.`);
1169
- }
1170
- }
1171
- function assertUniqueIds(ids, label) {
1172
- const seen = new Set;
1173
- for (const id of ids) {
1174
- assertStableId(id, label);
1175
- if (seen.has(id))
1176
- throw new Error(`Duplicate ${label}: ${id}.`);
1177
- seen.add(id);
1178
- }
1179
- }
1180
-
1181
- // src/plan/markdown.ts
1182
- var PLAN_MARKER = /<!-- diffpi-plan: (\{[^\n]+\}) -->/;
1183
- var PHASE_MARKER = /<!-- diffpi-phase: (\{[^\n]+\}) -->/g;
1184
- var TASK_MARKER = /<!-- diffpi-task: (\{[^\n]+\}) -->/g;
1185
- var SUPPORTED_SCHEMA = 1;
1186
- function countDesignWords(plan) {
1187
- return [plan.design.bigIdeas, plan.design.keyApiUpdates, plan.design.consequences].join(" ").trim().split(/\s+/).filter(Boolean).length;
1188
- }
1189
- function validatePlanDocument(plan, options = {}) {
1190
- const issues = [];
1191
- const add = (code, severity, message, path) => issues.push({ code, severity, message, path });
1192
- try {
1193
- assertStableId(plan.id, "plan id");
1194
- assertUniqueIds(plan.phases.map((phase) => phase.id), "phase id");
1195
- assertUniqueIds(plan.phases.flatMap((phase) => phase.tasks.map((task) => task.id)), "task id");
1196
- } catch (error) {
1197
- add("invalid-id", "error", error.message);
1198
- }
1199
- if (plan.schemaVersion !== SUPPORTED_SCHEMA)
1200
- add("schema", "error", `Unsupported plan schema: ${plan.schemaVersion}.`);
1201
- const phaseIds = new Set(plan.phases.map((phase) => phase.id));
1202
- const taskIds = new Set(plan.phases.flatMap((phase) => phase.tasks.map((task) => task.id)));
1203
- for (const phase of plan.phases) {
1204
- for (const dependency of phase.dependencies) {
1205
- if (!phaseIds.has(dependency))
1206
- add("dangling-dependency", "error", `Phase ${phase.id} depends on missing ${dependency}.`);
1207
- }
1208
- for (const task of phase.tasks) {
1209
- for (const dependency of task.dependencies) {
1210
- if (!taskIds.has(dependency))
1211
- add("dangling-dependency", "error", `Task ${task.id} depends on missing ${dependency}.`);
1212
- }
1213
- if (options.strict && task.acceptanceCriteria.length === 0)
1214
- add("acceptance", "error", `Task ${task.id} has no acceptance criteria.`, task.id);
1215
- }
1216
- if (options.strict && phase.tasks.length === 0)
1217
- add("empty-phase", "error", `Phase ${phase.id} has no tasks.`, phase.id);
1218
- }
1219
- for (const cycle of dependencyCycles(plan))
1220
- add("dependency-cycle", "error", `Dependency cycle: ${cycle.join(" -> ")}.`);
1221
- const designWords = countDesignWords(plan);
1222
- if (designWords > 300)
1223
- add("design-length", "warning", `Design is ${designWords} words; prefer 300 or fewer.`);
1224
- if (options.strict && designWords > 800)
1225
- add("design-too-long", "error", `Design is ${designWords} words; finalization allows at most 800.`);
1226
- if (options.strict && plan.phases.length === 0)
1227
- add("no-phases", "error", "Finalization requires at least one phase.");
1228
- if (options.strict && (options.pendingAnnotations ?? 0) > 0)
1229
- add("pending-annotations", "error", `${options.pendingAnnotations} annotations remain pending.`);
1230
- return issues;
1231
- }
1232
- function renderPlanDocument(plan, previousSource) {
1233
- if (previousSource && canPatch(previousSource, plan))
1234
- return patchMarkers(previousSource, plan);
1235
- const marker = {
1236
- schemaVersion: plan.schemaVersion,
1237
- id: plan.id,
1238
- revision: plan.revision,
1239
- branch: plan.branch,
1240
- status: plan.status,
1241
- execution: plan.execution,
1242
- createdAt: plan.createdAt,
1243
- updatedAt: plan.updatedAt
1244
- };
1245
- const requirements = plan.requirements.length ? plan.requirements.map((item) => `- ${item}`).join(`
1246
- `) : "<!-- Add requirements. -->";
1247
- const phases = plan.phases.map(renderPhase).join(`
1248
-
1249
- `);
1250
- const references = plan.references.length ? plan.references.map((reference) => `- <!-- diffpi-reference: ${json(reference)} --> ${reference.value}`).join(`
1251
- `) : "<!-- Add references. -->";
1252
- return `<!-- diffpi-plan: ${json(marker)} -->
1253
- # ${plan.title}
1254
-
1255
- - **Plan ID:** ${plan.id}
1256
- - **Branch:** ${plan.branch}
1257
- - **Status:** ${plan.status}
1258
- - **Revision:** ${plan.revision}
1259
-
1260
- ## Intent
1261
-
1262
- ${plan.intent || "<!-- Describe the intended outcome. -->"}
1263
-
1264
- ## Requirements
1265
-
1266
- ${requirements}
1267
-
1268
- ## Design
1269
-
1270
- ### Big Ideas
1271
-
1272
- ${plan.design.bigIdeas || "<!-- Describe the main approach. -->"}
1273
-
1274
- ### Key API Addition/Updates
1275
-
1276
- ${plan.design.keyApiUpdates || "<!-- Describe public API changes. -->"}
1277
-
1278
- ### Consequences
1279
-
1280
- ${plan.design.consequences || "<!-- Describe trade-offs and limitations. -->"}
1281
-
1282
- ## Implementation
1283
-
1284
- ${phases || "<!-- Add phases with plan_add_phase. -->"}
1285
-
1286
- ## References
1287
-
1288
- ${references}
1289
- `;
1290
- }
1291
- function parsePlanDocument(source, ref = "PLAN.md") {
1292
- const planMatch = source.match(PLAN_MARKER);
1293
- if (!planMatch)
1294
- throw new Error(`${ref}: missing or malformed diffpi-plan marker.`);
1295
- if ((source.match(new RegExp(PLAN_MARKER.source, "g")) ?? []).length !== 1)
1296
- throw new Error(`${ref}: duplicate diffpi-plan marker.`);
1297
- const marker = parseMarker(planMatch[1], `${ref} plan`);
1298
- if (marker.schemaVersion !== SUPPORTED_SCHEMA)
1299
- throw new Error(`${ref}: unsupported plan schema ${String(marker.schemaVersion)}.`);
1300
- assertStableId(marker.id, "plan id");
1301
- const title = source.match(/^# (.+)$/m)?.[1]?.trim();
1302
- if (!title)
1303
- throw new Error(`${ref}: missing plan title.`);
1304
- const phases = parsePhases(section(source, "Implementation"), ref);
1305
- const document = {
1306
- ...marker,
1307
- schemaVersion: 1,
1308
- title,
1309
- intent: cleanPlaceholder(section(source, "Intent")),
1310
- requirements: parseBullets(section(source, "Requirements")),
1311
- design: {
1312
- bigIdeas: cleanPlaceholder(subsection(source, "Design", "Big Ideas")),
1313
- keyApiUpdates: cleanPlaceholder(subsection(source, "Design", "Key API Addition/Updates")),
1314
- consequences: cleanPlaceholder(subsection(source, "Design", "Consequences"))
1315
- },
1316
- phases,
1317
- references: parseReferences(section(source, "References"))
1318
- };
1319
- const errors = validatePlanDocument(document).filter((issue) => issue.severity === "error");
1320
- if (errors.length)
1321
- throw new Error(`${ref}: ${errors.map((issue) => issue.message).join(" ")}`);
1322
- return document;
1323
- }
1324
- function renderPhase(phase) {
1325
- const marker = {
1326
- id: phase.id,
1327
- revision: phase.revision,
1328
- status: phase.status,
1329
- gate: phase.gate,
1330
- commit: phase.commit,
1331
- blocker: phase.blocker
1332
- };
1333
- const dependencies = phase.dependencies.length ? phase.dependencies.join(", ") : "none";
1334
- return `<!-- diffpi-phase: ${json(marker)} -->
1335
- ### Phase: ${phase.title}
1336
-
1337
- **Objective:** ${phase.objective}
1338
-
1339
- **Dependencies:** ${dependencies}
1340
-
1341
- ${phase.tasks.map(renderTask).join(`
1342
-
1343
- `)}
1344
-
1345
- <!-- /diffpi-phase -->`;
1346
- }
1347
- function renderTask(task) {
1348
- const marker = {
1349
- id: task.id,
1350
- revision: task.revision,
1351
- status: task.status,
1352
- owner: task.owner,
1353
- executionId: task.executionId,
1354
- blocker: task.blocker
1355
- };
1356
- const checked = task.status === "completed" || task.status === "skipped" ? "x" : " ";
1357
- return `<!-- diffpi-task: ${json(marker)} -->
1358
- - [${checked}] **${task.title}**
1359
- - Steps: ${list(task.steps ?? [])}
1360
- - Dependencies: ${list(task.dependencies)}
1361
- - File scopes: ${list(task.fileScopes)}
1362
- - Acceptance criteria: ${list(task.acceptanceCriteria)}
1363
- <!-- /diffpi-task -->`;
1364
- }
1365
- function parsePhases(input, ref) {
1366
- const starts = [...input.matchAll(PHASE_MARKER)];
1367
- const phases = starts.map((match, index) => {
1368
- const start = match.index;
1369
- const end = input.indexOf("<!-- /diffpi-phase -->", start);
1370
- if (end < 0)
1371
- throw new Error(`${ref}: phase marker has no closing marker.`);
1372
- const next = starts[index + 1]?.index;
1373
- if (next !== undefined && next < end)
1374
- throw new Error(`${ref}: nested or unclosed phase marker.`);
1375
- const body = input.slice(start + match[0].length, end);
1376
- const marker = parseMarker(match[1], `${ref} phase`);
1377
- assertStableId(marker.id, "phase id");
1378
- const title = body.match(/^### Phase: (.+)$/m)?.[1]?.trim();
1379
- const objective = body.match(/^\*\*Objective:\*\*\s*(.*)$/m)?.[1]?.trim();
1380
- if (!title || !objective)
1381
- throw new Error(`${ref}: phase ${marker.id} is missing title or objective.`);
1382
- return {
1383
- ...marker,
1384
- title,
1385
- objective,
1386
- dependencies: parseCsv(body.match(/^\*\*Dependencies:\*\*\s*(.*)$/m)?.[1]),
1387
- tasks: parseTasks(body, ref)
1388
- };
1389
- });
1390
- assertUniqueIds(phases.map((phase) => phase.id), "phase id");
1391
- return phases;
1392
- }
1393
- function parseTasks(input, ref) {
1394
- const starts = [...input.matchAll(TASK_MARKER)];
1395
- const tasks = starts.map((match, index) => {
1396
- const start = match.index;
1397
- const end = input.indexOf("<!-- /diffpi-task -->", start);
1398
- if (end < 0)
1399
- throw new Error(`${ref}: task marker has no closing marker.`);
1400
- const next = starts[index + 1]?.index;
1401
- if (next !== undefined && next < end)
1402
- throw new Error(`${ref}: nested or unclosed task marker.`);
1403
- const body = input.slice(start + match[0].length, end);
1404
- const marker = parseMarker(match[1], `${ref} task`);
1405
- assertStableId(marker.id, "task id");
1406
- const title = body.match(/^- \[[ xX]\] \*\*(.+)\*\*$/m)?.[1]?.trim();
1407
- if (!title)
1408
- throw new Error(`${ref}: task ${marker.id} is missing its checkbox title.`);
1409
- return {
1410
- ...marker,
1411
- title,
1412
- steps: parseListValue(body, "Steps"),
1413
- dependencies: parseListValue(body, "Dependencies"),
1414
- fileScopes: parseListValue(body, "File scopes"),
1415
- acceptanceCriteria: parseListValue(body, "Acceptance criteria")
1416
- };
1417
- });
1418
- assertUniqueIds(tasks.map((task) => task.id), "task id");
1419
- return tasks;
1420
- }
1421
- function parseReferences(input) {
1422
- return [...input.matchAll(/^- <!-- diffpi-reference: (\{[^\n]+\}) -->\s*(.*)$/gm)].map((match) => {
1423
- const marker = parseMarker(match[1], "reference");
1424
- assertStableId(marker.id, "reference id");
1425
- return { id: marker.id, value: match[2].trim() || marker.value };
1426
- });
1427
- }
1428
- function canPatch(source, next) {
1429
- try {
1430
- const old = parsePlanDocument(source);
1431
- return JSON.stringify(contentShape(old)) === JSON.stringify(contentShape(next));
1432
- } catch {
1433
- return false;
1434
- }
1435
- }
1436
- function patchMarkers(source, plan) {
1437
- const planMarker = {
1438
- schemaVersion: plan.schemaVersion,
1439
- id: plan.id,
1440
- revision: plan.revision,
1441
- branch: plan.branch,
1442
- status: plan.status,
1443
- execution: plan.execution,
1444
- createdAt: plan.createdAt,
1445
- updatedAt: plan.updatedAt
1446
- };
1447
- let output = source.replace(PLAN_MARKER, `<!-- diffpi-plan: ${json(planMarker)} -->`);
1448
- output = output.replace(/^- \*\*Status:\*\* .*$/m, `- **Status:** ${plan.status}`);
1449
- output = output.replace(/^- \*\*Revision:\*\* .*$/m, `- **Revision:** ${plan.revision}`);
1450
- for (const phase of plan.phases) {
1451
- const marker = {
1452
- id: phase.id,
1453
- revision: phase.revision,
1454
- status: phase.status,
1455
- gate: phase.gate,
1456
- commit: phase.commit,
1457
- blocker: phase.blocker
1458
- };
1459
- output = replaceMarkerById(output, "phase", phase.id, marker);
1460
- for (const task of phase.tasks) {
1461
- const taskMarker = {
1462
- id: task.id,
1463
- revision: task.revision,
1464
- status: task.status,
1465
- owner: task.owner,
1466
- executionId: task.executionId,
1467
- blocker: task.blocker
1468
- };
1469
- output = replaceMarkerById(output, "task", task.id, taskMarker);
1470
- const checked = task.status === "completed" || task.status === "skipped" ? "x" : " ";
1471
- const escaped = escapeRegExp(task.title);
1472
- output = output.replace(new RegExp(`^- \\[[ xX]\\] \\*\\*${escaped}\\*\\*$`, "m"), `- [${checked}] **${task.title}**`);
1473
- }
1474
- }
1475
- return output;
1476
- }
1477
- function replaceMarkerById(source, kind, id, marker) {
1478
- const pattern = new RegExp(`<!-- diffpi-${kind}: \\{[^\\n]*"id":"${escapeRegExp(id)}"[^\\n]*\\} -->`);
1479
- if (!pattern.test(source))
1480
- throw new Error(`Cannot update missing ${kind} marker ${id}.`);
1481
- return source.replace(pattern, `<!-- diffpi-${kind}: ${json(marker)} -->`);
1482
- }
1483
- function contentShape(plan) {
1484
- return {
1485
- title: plan.title,
1486
- branch: plan.branch,
1487
- intent: plan.intent,
1488
- requirements: plan.requirements,
1489
- design: plan.design,
1490
- references: plan.references,
1491
- phases: plan.phases.map(({ id, title, objective, dependencies, tasks }) => ({
1492
- id,
1493
- title,
1494
- objective,
1495
- dependencies,
1496
- tasks: tasks.map(({ id: taskId, title: taskTitle, steps, dependencies: taskDependencies, fileScopes, acceptanceCriteria }) => ({
1497
- id: taskId,
1498
- title: taskTitle,
1499
- steps,
1500
- dependencies: taskDependencies,
1501
- fileScopes,
1502
- acceptanceCriteria
1503
- }))
1504
- }))
1505
- };
1506
- }
1507
- function dependencyCycles(plan) {
1508
- const graph = new Map;
1509
- for (const phase of plan.phases)
1510
- graph.set(phase.id, phase.dependencies);
1511
- for (const task of plan.phases.flatMap((phase) => phase.tasks))
1512
- graph.set(task.id, task.dependencies);
1513
- const cycles = [];
1514
- const visiting = new Set;
1515
- const visited = new Set;
1516
- const walk = (id, path) => {
1517
- if (visiting.has(id)) {
1518
- cycles.push([...path.slice(path.indexOf(id)), id]);
1519
- return;
1520
- }
1521
- if (visited.has(id))
1522
- return;
1523
- visiting.add(id);
1524
- for (const dependency of graph.get(id) ?? [])
1525
- walk(dependency, [...path, id]);
1526
- visiting.delete(id);
1527
- visited.add(id);
1528
- };
1529
- for (const id of graph.keys())
1530
- walk(id, []);
1531
- return cycles;
1532
- }
1533
- function section(source, heading) {
1534
- const match = source.match(new RegExp(`^## ${escapeRegExp(heading)}\\s*$`, "m"));
1535
- if (!match?.index) {
1536
- if (match?.index === 0)
1537
- return "";
1538
- throw new Error(`Missing required heading: ${heading}.`);
1539
- }
1540
- const start = match.index + match[0].length;
1541
- const rest = source.slice(start);
1542
- const end = rest.search(/^## /m);
1543
- return (end < 0 ? rest : rest.slice(0, end)).trim();
1544
- }
1545
- function subsection(source, parent, heading) {
1546
- const body = section(source, parent);
1547
- const match = body.match(new RegExp(`^### ${escapeRegExp(heading)}\\s*$`, "m"));
1548
- if (match?.index === undefined)
1549
- throw new Error(`Missing required heading: ${parent}/${heading}.`);
1550
- const rest = body.slice(match.index + match[0].length);
1551
- const end = rest.search(/^### /m);
1552
- return (end < 0 ? rest : rest.slice(0, end)).trim();
1553
- }
1554
- function parseBullets(input) {
1555
- return [...input.matchAll(/^- (?!<!--)(.+)$/gm)].map((match) => match[1].trim());
1556
- }
1557
- function parseListValue(body, label) {
1558
- const value = body.match(new RegExp(`^ - ${escapeRegExp(label)}:\\s*(.*)$`, "m"))?.[1];
1559
- return parseCsv(value);
1560
- }
1561
- function parseCsv(value) {
1562
- if (!value || value.trim().toLowerCase() === "none")
1563
- return [];
1564
- return value.split(",").map((item) => item.trim()).filter(Boolean);
1565
- }
1566
- function cleanPlaceholder(value) {
1567
- return value.replace(/<!--[^]*?-->/g, "").trim();
1568
- }
1569
- function parseMarker(value, label) {
1570
- try {
1571
- return JSON.parse(value);
1572
- } catch {
1573
- throw new Error(`Malformed ${label} marker JSON.`);
1574
- }
1575
- }
1576
- function list(values) {
1577
- return values.length ? values.join(", ") : "none";
1578
- }
1579
- function json(value) {
1580
- return JSON.stringify(value, (_key, entry) => entry === undefined ? undefined : entry);
1581
- }
1582
- function escapeRegExp(value) {
1583
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1584
- }
1585
- // src/plan/store.ts
1586
- import { open, mkdir as mkdir5, readdir, readFile as readFile8, realpath as realpath3, rename as rename2, rm as rm2 } from "node:fs/promises";
1587
- import { basename as basename5, dirname as dirname6, join as join11, resolve as resolve3, sep } from "node:path";
1588
-
1589
- // src/templates.ts
1590
- import { readFile as readFile7 } from "node:fs/promises";
1591
- import { homedir as homedir4 } from "node:os";
1592
- import { join as join10, normalize } from "node:path";
1593
- async function loadTemplate(name, options = {}) {
1594
- const relative = templateRelativePath(name);
1595
- const userPath = join10(options.homeDir ?? homedir4(), ".difflab", "diffpi", "templates", relative);
1596
- const bundledPath = join10(options.bundledDir ?? resolveBundledTemplatesDir(), relative);
1597
- const user = await readOptionalFile(userPath);
1598
- if (user !== undefined)
1599
- return { name, path: userPath, source: "user", content: user };
1600
- const bundled = await readOptionalFile(bundledPath);
1601
- if (bundled !== undefined)
1602
- return { name, path: bundledPath, source: "bundled", content: bundled };
1603
- throw new Error(`Template "${name}" was not found at ${userPath} or ${bundledPath}.`);
1604
- }
1605
- function renderTemplate(content, variables) {
1606
- return content.replace(/\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g, (match, key) => variables[key] ?? match);
1607
- }
1608
- function templateRelativePath(name) {
1609
- const normalized = normalize(name.replaceAll("\\", "/")).replace(/^\.\//, "");
1610
- if (!normalized || normalized.startsWith("../") || normalized.includes("/../") || normalized.startsWith("/")) {
1611
- throw new Error(`Invalid template name: ${name}`);
1612
- }
1613
- return normalized.endsWith(".md") ? normalized : `${normalized}.md`;
1614
- }
1615
- async function readOptionalFile(path) {
1616
- try {
1617
- return await readFile7(path, "utf8");
1618
- } catch (error) {
1619
- if (error.code === "ENOENT")
1620
- return;
1621
- throw error;
1622
- }
1623
- }
1624
-
1625
- // src/plan/store.ts
1626
- function planRecordName(shortSlug, date = new Date) {
1627
- const slug = normalizeSlug(shortSlug);
1628
- return `${date.toISOString().slice(2, 10).replaceAll("-", "")}-${slug}`;
1629
- }
1630
- async function resolvePlan(cwd, query, filters = {}, options = {}) {
1631
- const root = await plansDir(cwd, options.homeDir);
1632
- const entries = await readdir(root, { withFileTypes: true });
1633
- const normalizedQuery = query ? normalizeQuery(query) : undefined;
1634
- const records = [];
1635
- for (const entry of entries) {
1636
- if (!entry.isDirectory() || entry.name === ".tmp" || entry.name === ".lock")
1637
- continue;
1638
- if (normalizedQuery && entry.name !== normalizedQuery && stripDate(entry.name) !== normalizedQuery)
1639
- continue;
1640
- const dir = join11(root, entry.name);
1641
- await assertContained(root, dir);
1642
- try {
1643
- const record = await readRecord(dir);
1644
- if (filters.branch && record.document.branch !== filters.branch)
1645
- continue;
1646
- if (filters.statuses && !filters.statuses.includes(record.document.status))
1647
- continue;
1648
- records.push(record);
1649
- } catch (error) {
1650
- if (error.code !== "ENOENT")
1651
- throw error;
1652
- }
1653
- }
1654
- records.sort((left, right) => left.id.localeCompare(right.id));
1655
- return { candidates: records, record: records.length === 1 ? records[0] : undefined, ambiguous: records.length > 1 };
1656
- }
1657
- function createPlanStore(options = {}) {
1658
- const now = options.now ?? (() => new Date);
1659
- return {
1660
- context(cwd, query, filters) {
1661
- return resolvePlan(cwd, query, filters, options);
1662
- },
1663
- async init(input) {
1664
- const root = await plansDir(input.cwd, options.homeDir);
1665
- const id = planRecordName(input.shortSlug, now());
1666
- const dir = join11(root, id);
1667
- await assertContained(root, dirname6(dir));
1668
- try {
1669
- await mkdir5(dir);
1670
- } catch (error) {
1671
- if (error.code === "EEXIST")
1672
- throw new Error(`Plan ${id} already exists.`);
1673
- throw error;
1674
- }
1675
- try {
1676
- const timestamp = now().toISOString();
1677
- const template = await loadTemplate("plan/PLAN", {
1678
- homeDir: options.homeDir,
1679
- bundledDir: options.bundledTemplatesDir
1680
- });
1681
- const source = renderTemplate(template.content, {
1682
- id,
1683
- branch: input.branch,
1684
- title: input.title?.trim() || titleFromSlug(input.shortSlug),
1685
- intent: input.intent?.trim() || "<!-- Describe the intended outcome. -->",
1686
- created_at: timestamp,
1687
- updated_at: timestamp
1688
- });
1689
- const document = parsePlanDocument(source, join11(dir, "PLAN.md"));
1690
- await atomicWrite(join11(dir, "PLAN.md"), source);
1691
- await atomicWrite(join11(dir, "logs.txt"), "");
1692
- await appendPlanLog(join11(dir, "logs.txt"), {
1693
- planRevision: document.revision,
1694
- kind: "created",
1695
- actor: "diffpi",
1696
- message: `Created plan ${id}.`
1697
- });
1698
- return { id, dir, planPath: join11(dir, "PLAN.md"), logPath: join11(dir, "logs.txt"), document, source };
1699
- } catch (error) {
1700
- await rm2(dir, { recursive: true, force: true });
1701
- throw error;
1702
- }
1703
- },
1704
- async read(cwd, query, filters) {
1705
- const result = await resolvePlan(cwd, query, filters, options);
1706
- if (!result.record) {
1707
- if (result.ambiguous)
1708
- throw new Error(`Plan query "${query}" is ambiguous: ${result.candidates.map((item) => item.id).join(", ")}.`);
1709
- throw new Error(`Plan "${query}" was not found.`);
1710
- }
1711
- return result.record;
1712
- },
1713
- async mutate(cwd, query, operation, update) {
1714
- const initial = await this.read(cwd, query);
1715
- return withPlanLock(initial.dir, async () => {
1716
- const current = await readRecord(initial.dir);
1717
- const next = await update(structuredClone(current.document));
1718
- if (next.id !== current.id)
1719
- throw new Error("A plan mutation cannot change the plan ID.");
1720
- const document = {
1721
- ...next,
1722
- revision: current.document.revision + 1,
1723
- updatedAt: now().toISOString()
1724
- };
1725
- const source = renderPlanDocument(document, current.source);
1726
- parsePlanDocument(source, current.planPath);
1727
- await atomicWrite(current.planPath, source);
1728
- return { ...current, document, source };
1729
- }, { operation });
1730
- },
1731
- async log(cwd, query, event) {
1732
- const record = await this.read(cwd, query);
1733
- return withPlanLock(record.dir, () => appendPlanLog(record.logPath, event), { operation: "append log" });
1734
- }
1735
- };
1736
- }
1737
- async function readRecord(dir) {
1738
- const planPath = join11(dir, "PLAN.md");
1739
- const logPath = join11(dir, "logs.txt");
1740
- const source = await readFile8(planPath, "utf8");
1741
- const document = parsePlanDocument(source, planPath);
1742
- return { id: basename5(dir), dir, planPath, logPath, document, source };
1743
- }
1744
- async function atomicWrite(path, content) {
1745
- const temp = join11(dirname6(path), `.${basename5(path)}.${process.pid}.${crypto.randomUUID()}.tmp`);
1746
- const file = await open(temp, "wx", 384);
1747
- try {
1748
- await file.writeFile(content, "utf8");
1749
- await file.sync();
1750
- } finally {
1751
- await file.close();
1752
- }
1753
- await rename2(temp, path);
1754
- try {
1755
- const directory = await open(dirname6(path), "r");
1756
- try {
1757
- await directory.sync();
1758
- } finally {
1759
- await directory.close();
1760
- }
1761
- } catch {}
1762
- }
1763
- async function assertContained(root, candidate) {
1764
- const canonicalRoot = await realpath3(root);
1765
- let canonicalCandidate;
1766
- try {
1767
- canonicalCandidate = await realpath3(candidate);
1768
- } catch {
1769
- canonicalCandidate = resolve3(candidate);
1770
- }
1771
- if (canonicalCandidate !== canonicalRoot && !canonicalCandidate.startsWith(`${canonicalRoot}${sep}`)) {
1772
- throw new Error(`Plan path escapes the shared store: ${candidate}.`);
1773
- }
1774
- }
1775
- function normalizeSlug(value) {
1776
- if (!value || value.includes("\x00") || value.includes("/") || value.includes("\\") || value.includes("..")) {
1777
- throw new Error(`Invalid plan slug: ${value}.`);
1778
- }
1779
- const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1780
- assertStableId(slug, "plan slug");
1781
- return slug;
1782
- }
1783
- function normalizeQuery(value) {
1784
- if (!value || value.includes("\x00") || value.includes("/") || value.includes("\\") || value.includes("..")) {
1785
- throw new Error(`Invalid plan query: ${value}.`);
1786
- }
1787
- return value.toLowerCase();
1788
- }
1789
- function stripDate(value) {
1790
- return value.replace(/^\d{6}-/, "");
1791
- }
1792
- function titleFromSlug(value) {
1793
- return normalizeSlug(value).replace(/^(?:[a-z]+-\d+-)/, "").split("-").map((part) => part[0].toUpperCase() + part.slice(1)).join(" ");
1794
- }
1795
- // src/tools/plan.ts
1796
- var id = z4.string().min(1).max(80).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/);
1797
- var text = z4.string().trim().min(1);
1798
- var revision = z4.number().int().nonnegative();
1799
- var cwd = z4.string().optional();
1800
- var taskDraft = z4.object({
1801
- id,
1802
- title: text,
1803
- steps: z4.array(text).optional(),
1804
- dependencies: z4.array(id).optional(),
1805
- fileScopes: z4.array(text).optional(),
1806
- acceptanceCriteria: z4.array(text).optional()
1807
- }).strict();
1808
- var phaseDraft = z4.object({
1809
- id,
1810
- title: text,
1811
- objective: text,
1812
- dependencies: z4.array(id).optional(),
1813
- tasks: z4.array(taskDraft).optional()
1814
- }).strict();
1815
- var reference = z4.object({ id, value: text }).strict();
1816
- var design = z4.object({ bigIdeas: z4.string(), keyApiUpdates: z4.string(), consequences: z4.string() }).strict();
1817
- function createPlanTools(pi, modes, store = createPlanStore()) {
1818
- return [
1819
- defineTool3({
1820
- name: "plan_context",
1821
- label: "plan context",
1822
- description: "Resolve one plan and summarize all matching plans without guessing among ambiguous matches.",
1823
- parameters: parameters(z4.object({
1824
- cwd,
1825
- plan: z4.string().optional(),
1826
- branch: z4.string().optional(),
1827
- statuses: z4.array(z4.enum(["draft", "ready", "in_progress", "blocked", "completed"])).optional()
1828
- }).strict()),
1829
- executionMode: "parallel",
1830
- async execute(_id, input) {
1831
- const params = z4.object({
1832
- cwd,
1833
- plan: z4.string().optional(),
1834
- branch: z4.string().optional(),
1835
- statuses: z4.array(z4.enum(["draft", "ready", "in_progress", "blocked", "completed"])).optional()
1836
- }).strict().parse(input);
1837
- const resolution = await store.context(params.cwd ?? process.cwd(), params.plan, {
1838
- branch: params.branch,
1839
- statuses: params.statuses
1840
- });
1841
- return result(resolution.candidates.length ? resolution.candidates.map((record) => `${record.id}: ${record.document.status} r${record.document.revision} (${record.document.branch})`).join(`
1842
- `) : "No matching plans.", { selected: resolution.record, candidates: resolution.candidates, ambiguous: resolution.ambiguous });
1843
- }
1844
- }),
1845
- defineTool3({
1846
- name: "plan_init",
1847
- label: "plan init",
1848
- description: "Create a phase-less editable plan draft.",
1849
- parameters: parameters(z4.object({
1850
- cwd,
1851
- shortSlug: id,
1852
- branch: z4.string().min(1).optional(),
1853
- title: text.optional(),
1854
- intent: text.optional(),
1855
- open: z4.boolean().optional()
1856
- }).strict()),
1857
- executionMode: "sequential",
1858
- async execute(_id, input) {
1859
- const params = z4.object({
1860
- cwd,
1861
- shortSlug: id,
1862
- branch: z4.string().min(1).optional(),
1863
- title: text.optional(),
1864
- intent: text.optional(),
1865
- open: z4.boolean().optional()
1866
- }).strict().parse(input);
1867
- const workingDirectory = params.cwd ?? process.cwd();
1868
- const branch = params.branch ?? await currentBranch(workingDirectory);
1869
- const record = await store.init({ ...params, cwd: workingDirectory, branch });
1870
- return result(`Created ${record.id} at ${record.planPath}.`, { record, openRequested: params.open === true });
1871
- }
1872
- }),
1873
- defineTool3({
1874
- name: "plan_update_overview",
1875
- label: "plan update overview",
1876
- description: "Replace selected plan overview fields using an expected plan revision.",
1877
- parameters: parameters(z4.object({
1878
- cwd,
1879
- plan: text,
1880
- expectedPlanRevision: revision,
1881
- intent: text.optional(),
1882
- requirements: z4.array(text).optional(),
1883
- design: design.optional(),
1884
- references: z4.array(reference).optional()
1885
- }).strict().refine((value) => value.intent || value.requirements || value.design || value.references, "At least one overview field is required.")),
1886
- executionMode: "sequential",
1887
- async execute(_id, input) {
1888
- const schema = z4.object({
1889
- cwd,
1890
- plan: text,
1891
- expectedPlanRevision: revision,
1892
- intent: text.optional(),
1893
- requirements: z4.array(text).optional(),
1894
- design: design.optional(),
1895
- references: z4.array(reference).optional()
1896
- }).strict().refine((value) => value.intent || value.requirements || value.design || value.references, "At least one overview field is required.");
1897
- const params = schema.parse(input);
1898
- const record = await store.mutate(params.cwd ?? process.cwd(), params.plan, "update overview", (plan) => {
1899
- assertAuthoringRevision(plan, params.expectedPlanRevision);
1900
- assertAuthorable(plan);
1901
- return resetDraft({
1902
- ...plan,
1903
- intent: params.intent ?? plan.intent,
1904
- requirements: params.requirements ?? plan.requirements,
1905
- design: params.design ?? plan.design,
1906
- references: params.references ?? plan.references
1907
- });
1908
- });
1909
- return result(`Updated ${record.id} to revision ${record.document.revision}.`, { record });
1910
- }
1911
- }),
1912
- defineTool3({
1913
- name: "plan_add_phase",
1914
- label: "plan add phase",
1915
- description: "Add an ordered phase with stable IDs.",
1916
- parameters: parameters(z4.object({ cwd, plan: text, expectedPlanRevision: revision, afterPhaseId: id.optional(), phase: phaseDraft }).strict()),
1917
- executionMode: "sequential",
1918
- async execute(_id, input) {
1919
- const params = z4.object({ cwd, plan: text, expectedPlanRevision: revision, afterPhaseId: id.optional(), phase: phaseDraft }).strict().parse(input);
1920
- const record = await store.mutate(params.cwd ?? process.cwd(), params.plan, "add phase", (plan) => {
1921
- assertAuthoringRevision(plan, params.expectedPlanRevision);
1922
- assertAuthorable(plan);
1923
- if (allIds(plan).has(params.phase.id))
1924
- throw new Error(`Duplicate stable ID: ${params.phase.id}.`);
1925
- const phase = newPhase(params.phase);
1926
- const phases = [...plan.phases];
1927
- if (params.afterPhaseId) {
1928
- const index = phases.findIndex((item) => item.id === params.afterPhaseId);
1929
- if (index < 0)
1930
- throw new Error(`Unknown phase: ${params.afterPhaseId}.`);
1931
- phases.splice(index + 1, 0, phase);
1932
- } else
1933
- phases.push(phase);
1934
- assertNewIds(plan, phase);
1935
- return resetDraft({ ...plan, phases });
1936
- });
1937
- return result(`Added phase ${params.phase.id} to ${record.id}.`, { record });
1938
- }
1939
- }),
1940
- defineTool3({
1941
- name: "plan_remove_phase",
1942
- label: "plan remove phase",
1943
- description: "Remove an unfinished phase that has no dependents.",
1944
- parameters: parameters(z4.object({ cwd, plan: text, expectedPlanRevision: revision, phaseId: id, reason: text }).strict()),
1945
- executionMode: "sequential",
1946
- async execute(_id, input) {
1947
- const params = z4.object({ cwd, plan: text, expectedPlanRevision: revision, phaseId: id, reason: text }).strict().parse(input);
1948
- const record = await store.mutate(params.cwd ?? process.cwd(), params.plan, "remove phase", (plan) => {
1949
- assertAuthoringRevision(plan, params.expectedPlanRevision);
1950
- assertAuthorable(plan);
1951
- const phase = getPhase(plan, params.phaseId);
1952
- if (phase.status === "completed" || phase.tasks.some((task) => task.status === "completed"))
1953
- throw new Error(`Completed phase ${phase.id} cannot be removed.`);
1954
- if (plan.phases.some((item) => item.dependencies.includes(phase.id)))
1955
- throw new Error(`Phase ${phase.id} has dependents and cannot be removed.`);
1956
- return resetDraft({ ...plan, phases: plan.phases.filter((item) => item.id !== phase.id) });
1957
- });
1958
- await store.log(params.cwd ?? process.cwd(), record.id, {
1959
- planRevision: record.document.revision,
1960
- kind: "updated",
1961
- actor: "planner",
1962
- message: `Removed phase ${params.phaseId}: ${params.reason}`,
1963
- phaseId: params.phaseId
1964
- });
1965
- return result(`Removed phase ${params.phaseId}.`, { record });
1966
- }
1967
- }),
1968
- defineTool3({
1969
- name: "plan_update_phase",
1970
- label: "plan update phase",
1971
- description: "Update an unfinished phase and its ordered tasks.",
1972
- parameters: parameters(z4.object({
1973
- cwd,
1974
- plan: text,
1975
- expectedPlanRevision: revision,
1976
- phaseId: id,
1977
- patch: z4.object({
1978
- title: text.optional(),
1979
- objective: text.optional(),
1980
- dependencies: z4.array(id).optional(),
1981
- tasks: z4.array(taskDraft).optional()
1982
- }).strict()
1983
- }).strict()),
1984
- executionMode: "sequential",
1985
- async execute(_id, input) {
1986
- const params = z4.object({
1987
- cwd,
1988
- plan: text,
1989
- expectedPlanRevision: revision,
1990
- phaseId: id,
1991
- patch: z4.object({
1992
- title: text.optional(),
1993
- objective: text.optional(),
1994
- dependencies: z4.array(id).optional(),
1995
- tasks: z4.array(taskDraft).optional()
1996
- }).strict()
1997
- }).strict().parse(input);
1998
- const record = await store.mutate(params.cwd ?? process.cwd(), params.plan, "update phase", (plan) => {
1999
- assertAuthoringRevision(plan, params.expectedPlanRevision);
2000
- assertAuthorable(plan, params.phaseId);
2001
- const existing = getPhase(plan, params.phaseId);
2002
- if (existing.status === "completed")
2003
- throw new Error(`Completed phase ${existing.id} cannot be changed.`);
2004
- const tasks = params.patch.tasks ? reconcileTasks(existing.tasks, params.patch.tasks) : existing.tasks;
2005
- const updated = {
2006
- ...existing,
2007
- title: params.patch.title ?? existing.title,
2008
- objective: params.patch.objective ?? existing.objective,
2009
- dependencies: params.patch.dependencies ?? existing.dependencies,
2010
- tasks,
2011
- revision: existing.revision + 1,
2012
- status: existing.status === "blocked" ? "in_progress" : existing.status,
2013
- blocker: existing.status === "blocked" ? undefined : existing.blocker
2014
- };
2015
- assertNewIds({ ...plan, phases: plan.phases.filter((phase) => phase.id !== existing.id) }, updated);
2016
- return resetDraft({
2017
- ...plan,
2018
- phases: plan.phases.map((phase) => phase.id === existing.id ? updated : phase)
2019
- });
2020
- });
2021
- return result(`Updated phase ${params.phaseId}.`, { record });
2022
- }
2023
- }),
2024
- defineTool3({
2025
- name: "plan_validate",
2026
- label: "plan validate",
2027
- description: "Validate plan markers, dependencies, completeness, annotations, and Design length.",
2028
- parameters: parameters(z4.object({ cwd, plan: text, strict: z4.boolean().optional() }).strict()),
2029
- executionMode: "parallel",
2030
- async execute(_id, input) {
2031
- const params = z4.object({ cwd, plan: text, strict: z4.boolean().optional() }).strict().parse(input);
2032
- const record = await store.read(params.cwd ?? process.cwd(), params.plan);
2033
- const annotations = params.strict ? await readPlanAnnotations(record) : { pending: [] };
2034
- const issues = validatePlanDocument(record.document, {
2035
- strict: params.strict,
2036
- pendingAnnotations: annotations.pending.length
2037
- });
2038
- const words = countDesignWords(record.document);
2039
- return result(issues.length ? issues.map((issue) => `- ${issue.severity}: ${issue.message}`).join(`
2040
- `) : "Plan is valid.", { record, issues, designWordCount: words, ready: !issues.some((issue) => issue.severity === "error") });
2041
- }
2042
- }),
2043
- defineTool3({
2044
- name: "plan_log_progress",
2045
- label: "plan log progress",
2046
- description: "Append a progress event without changing plan revision.",
2047
- parameters: parameters(z4.object({
2048
- cwd,
2049
- plan: text,
2050
- actor: text,
2051
- message: text,
2052
- executionId: id.optional(),
2053
- phaseId: id.optional(),
2054
- taskId: id.optional(),
2055
- evidence: z4.array(text).optional(),
2056
- data: z4.record(z4.string(), z4.unknown()).optional()
2057
- }).strict()),
2058
- executionMode: "sequential",
2059
- async execute(_id, input) {
2060
- const params = z4.object({
2061
- cwd,
2062
- plan: text,
2063
- actor: text,
2064
- message: text,
2065
- executionId: id.optional(),
2066
- phaseId: id.optional(),
2067
- taskId: id.optional(),
2068
- evidence: z4.array(text).optional(),
2069
- data: z4.record(z4.string(), z4.unknown()).optional()
2070
- }).strict().parse(input);
2071
- const record = await store.read(params.cwd ?? process.cwd(), params.plan);
2072
- const entry = await store.log(params.cwd ?? process.cwd(), params.plan, {
2073
- planRevision: record.document.revision,
2074
- kind: "progress",
2075
- actor: params.actor,
2076
- message: params.message,
2077
- executionId: params.executionId,
2078
- phaseId: params.phaseId,
2079
- taskId: params.taskId,
2080
- evidence: params.evidence,
2081
- data: params.data
2082
- });
2083
- return result(`Logged ${entry.eventId}.`, { entry });
2084
- }
2085
- }),
2086
- createStatusTool(pi, modes, store),
2087
- defineTool3({
2088
- name: "plan_run_gates",
2089
- label: "plan run gates",
2090
- description: "Run format check, lint, and tests without holding the plan lock, then persist non-stale results.",
2091
- parameters: parameters(z4.object({ cwd, plan: text, phaseId: id, expectedPhaseRevision: revision, executionId: id, actor: text }).strict()),
2092
- executionMode: "sequential",
2093
- async execute(_id, input) {
2094
- const params = z4.object({ cwd, plan: text, phaseId: id, expectedPhaseRevision: revision, executionId: id, actor: text }).strict().parse(input);
2095
- const workingDirectory = params.cwd ?? process.cwd();
2096
- const before = await store.read(workingDirectory, params.plan);
2097
- const phase = getPhase(before.document, params.phaseId);
2098
- if (phase.revision !== params.expectedPhaseRevision)
2099
- throw new Error(`Stale phase revision for ${phase.id}.`);
2100
- if (!before.document.execution?.active || before.document.execution.id !== params.executionId)
2101
- throw new Error(`Execution ${params.executionId} does not own this plan.`);
2102
- if (phase.tasks.some((task) => task.status !== "completed" && task.status !== "skipped"))
2103
- throw new Error(`Phase ${phase.id} still has incomplete tasks.`);
2104
- const results = await runMiseGates(workingDirectory);
2105
- const passed = results.every((gate) => gate.status === "pass" || gate.status === "skip");
2106
- const record = await store.mutate(workingDirectory, params.plan, "persist gates", (plan) => {
2107
- const current = getPhase(plan, params.phaseId);
2108
- if (current.revision !== params.expectedPhaseRevision)
2109
- throw new Error(`Gate results for ${current.id} are stale.`);
2110
- if (current.tasks.some((task) => task.status !== "completed" && task.status !== "skipped"))
2111
- throw new Error(`Gate results for ${current.id} are stale because task state changed.`);
2112
- const updated = {
2113
- ...current,
2114
- revision: current.revision + 1,
2115
- gate: {
2116
- phaseRevision: params.expectedPhaseRevision,
2117
- status: passed ? "passed" : "failed",
2118
- results,
2119
- completedAt: new Date().toISOString()
2120
- }
2121
- };
2122
- return {
2123
- ...heartbeat(plan),
2124
- phases: plan.phases.map((item) => item.id === updated.id ? updated : item)
2125
- };
2126
- });
2127
- await store.log(workingDirectory, record.id, {
2128
- planRevision: record.document.revision,
2129
- kind: "gate",
2130
- actor: params.actor,
2131
- message: `Phase ${phase.id} gates ${passed ? "passed" : "failed"}.`,
2132
- executionId: params.executionId,
2133
- phaseId: phase.id,
2134
- data: { results }
2135
- });
2136
- return result(results.map((gate) => `${gate.name}: ${gate.status} — ${gate.detail}`).join(`
2137
- `), {
2138
- results,
2139
- passed,
2140
- record
2141
- });
2142
- }
2143
- }),
2144
- defineTool3({
2145
- name: "plan_annotate",
2146
- label: "plan annotate",
2147
- description: "Launch the selected PLAN.md in tuicr standalone file annotation mode.",
2148
- parameters: parameters(z4.object({ cwd, plan: z4.string().optional() }).strict()),
2149
- executionMode: "sequential",
2150
- async execute(_id, input) {
2151
- const params = z4.object({ cwd, plan: z4.string().optional() }).strict().parse(input);
2152
- const workingDirectory = params.cwd ?? process.cwd();
2153
- const resolution = await store.context(workingDirectory, params.plan);
2154
- if (!resolution.record)
2155
- throw new Error(resolution.ambiguous ? `Plan is ambiguous: ${resolution.candidates.map((item) => item.id).join(", ")}.` : "No matching plan.");
2156
- const cliPath = join12(resolveBundledAgentsDir(), "..", "dist", "cli.js");
2157
- const command = [
2158
- process.execPath,
2159
- cliPath,
2160
- "plan",
2161
- "annotate",
2162
- resolution.record.id,
2163
- "--cwd",
2164
- workingDirectory
2165
- ];
2166
- const launched = await openInNewTab(command, { cwd: workingDirectory, name: "plan-annotate" });
2167
- const fallbackCommand = `npx --yes @difflab/pi plan annotate ${resolution.record.id} --cwd ${JSON.stringify(workingDirectory)}`;
2168
- return result(launched.launched ? `Opened ${resolution.record.id} for annotation.` : launched.instruction ?? `Run: ${fallbackCommand}`, { record: resolution.record, launched, fallbackCommand, followUp: `/plan update ${resolution.record.id}` });
2169
- }
2170
- }),
2171
- defineTool3({
2172
- name: "plan_annotations",
2173
- label: "plan annotations",
2174
- description: "Read normalized pending comments from the selected plan annotation session.",
2175
- parameters: parameters(z4.object({ cwd, plan: z4.string().optional(), includeApplied: z4.boolean().optional() }).strict()),
2176
- executionMode: "parallel",
2177
- async execute(_id, input) {
2178
- const params = z4.object({ cwd, plan: z4.string().optional(), includeApplied: z4.boolean().optional() }).strict().parse(input);
2179
- const workingDirectory = params.cwd ?? process.cwd();
2180
- const resolution = await store.context(workingDirectory, params.plan);
2181
- if (!resolution.record)
2182
- throw new Error(resolution.ambiguous ? `Plan is ambiguous: ${resolution.candidates.map((item) => item.id).join(", ")}.` : "No matching plan.");
2183
- const annotations = await readPlanAnnotations(resolution.record, { includeApplied: params.includeApplied });
2184
- return result(annotations.comments.length ? annotations.comments.map((comment) => `${comment.id}: ${comment.body}`).join(`
2185
- `) : "No pending annotations.", { record: resolution.record, ...annotations });
2186
- }
2187
- }),
2188
- defineTool3({
2189
- name: "plan_ack_annotations",
2190
- label: "plan acknowledge annotations",
2191
- description: "Acknowledge only annotation comments successfully applied to the plan.",
2192
- parameters: parameters(z4.object({ cwd, plan: text, commentIds: z4.array(text).min(1), summary: text }).strict()),
2193
- executionMode: "sequential",
2194
- async execute(_id, input) {
2195
- const params = z4.object({ cwd, plan: text, commentIds: z4.array(text).min(1), summary: text }).strict().parse(input);
2196
- const record = await store.read(params.cwd ?? process.cwd(), params.plan);
2197
- const state = await acknowledgePlanAnnotations(record, params.commentIds, params.summary);
2198
- return result(`Acknowledged ${params.commentIds.length} annotations for ${record.id}.`, {
2199
- record,
2200
- state,
2201
- acknowledged: params.commentIds
2202
- });
2203
- }
2204
- }),
2205
- defineTool3({
2206
- name: "plan_start_execution",
2207
- label: "plan start execution",
2208
- description: "Start one inline or background plan execution.",
2209
- parameters: parameters(z4.object({
2210
- cwd,
2211
- plan: text,
2212
- mode: z4.enum(["inline", "background"]),
2213
- policy: z4.enum(["commit-per-phase", "no-commit"]),
2214
- actor: text
2215
- }).strict()),
2216
- executionMode: "sequential",
2217
- async execute(_id, input, _signal, _onUpdate, ctx) {
2218
- const params = z4.object({
2219
- cwd,
2220
- plan: text,
2221
- mode: z4.enum(["inline", "background"]),
2222
- policy: z4.enum(["commit-per-phase", "no-commit"]),
2223
- actor: text
2224
- }).strict().parse(input);
2225
- return startExecution(pi, modes, store, params, ctx);
2226
- }
2227
- })
2228
- ];
2229
- }
2230
- function createStatusTool(pi, modes, store) {
2231
- const schema = z4.object({
2232
- cwd,
2233
- plan: text,
2234
- target: z4.object({ type: z4.enum(["plan", "phase", "task"]), id: id.optional() }).strict().refine((value) => value.type === "plan" || value.id, "Phase and task targets require an ID."),
2235
- expectedStatus: z4.enum(["draft", "ready", "in_progress", "blocked", "completed", "pending", "skipped"]),
2236
- status: z4.enum(["draft", "ready", "in_progress", "blocked", "completed", "pending", "skipped"]),
2237
- actor: text,
2238
- message: text,
2239
- executionId: id.optional(),
2240
- evidence: z4.array(text).optional(),
2241
- blockedReason: text.optional(),
2242
- attempts: z4.array(text).optional(),
2243
- needsUserDecision: z4.boolean().optional(),
2244
- commit: z4.object({ sha: z4.string().regex(/^[a-f0-9]{7,64}$/i), subject: text, completedAt: z4.string().datetime() }).strict().optional()
2245
- }).strict();
2246
- return defineTool3({
2247
- name: "plan_update_status",
2248
- label: "plan update status",
2249
- description: "Apply an ownership-checked plan, phase, or task status transition and append an audit event.",
2250
- parameters: parameters(schema),
2251
- executionMode: "sequential",
2252
- async execute(_id, input, _signal, _onUpdate, ctx) {
2253
- const params = schema.parse(input);
2254
- const workingDirectory = params.cwd ?? process.cwd();
2255
- let escalation;
2256
- if (params.commit) {
2257
- if (params.target.type !== "phase" || params.status !== "completed")
2258
- throw new Error("Commit metadata is accepted only when completing a phase.");
2259
- const verified = await verifyCommit(workingDirectory, params.commit.sha);
2260
- if (verified.sha !== params.commit.sha && !verified.sha.startsWith(params.commit.sha))
2261
- throw new Error(`Commit ${params.commit.sha} is not the current HEAD ${verified.sha}.`);
2262
- if (verified.subject !== params.commit.subject)
2263
- throw new Error(`Commit subject does not match HEAD: ${verified.subject}.`);
2264
- }
2265
- const record = await store.mutate(workingDirectory, params.plan, "update status", (plan) => {
2266
- if (params.target.type === "plan") {
2267
- if (plan.status !== params.expectedStatus)
2268
- throw new Error(`Expected plan status ${params.expectedStatus}, found ${plan.status}.`);
2269
- assertPlanTransition(plan.status, params.status);
2270
- if (params.status === "ready") {
2271
- const errors = validatePlanDocument(plan, { strict: true }).filter((issue) => issue.severity === "error");
2272
- if (errors.length)
2273
- throw new Error(`Plan cannot become ready: ${errors.map((issue) => issue.message).join(" ")}`);
2274
- }
2275
- if ((params.status === "blocked" || params.status === "completed") && plan.status === "in_progress") {
2276
- assertExecutionOwner(plan, params.executionId);
2277
- }
2278
- if (params.status === "completed" && plan.phases.some((phase) => phase.status !== "completed" && phase.status !== "skipped"))
2279
- throw new Error("Every phase must be complete or skipped before the plan completes.");
2280
- return {
2281
- ...heartbeat(plan),
2282
- status: params.status,
2283
- execution: params.status === "completed" || params.status === "blocked" ? plan.execution && { ...plan.execution, active: false, heartbeatAt: new Date().toISOString() } : plan.execution
2284
- };
2285
- }
2286
- if (!params.target.id)
2287
- throw new Error("Target ID is required.");
2288
- const phase = params.target.type === "phase" ? getPhase(plan, params.target.id) : findTaskPhase(plan, params.target.id);
2289
- if (params.target.type === "phase") {
2290
- if (phase.status !== params.expectedStatus)
2291
- throw new Error(`Expected phase status ${params.expectedStatus}, found ${phase.status}.`);
2292
- assertExecutionOwner(plan, params.executionId);
2293
- assertPhaseTransition(phase.status, params.status);
2294
- if (params.status === "completed") {
2295
- if (phase.tasks.some((task) => task.status !== "completed" && task.status !== "skipped"))
2296
- throw new Error("All phase tasks must be complete or skipped.");
2297
- if (phase.gate.status !== "passed")
2298
- throw new Error("Phase gates must pass before completion.");
2299
- if (plan.execution?.policy === "commit-per-phase" && !params.commit)
2300
- throw new Error("Commit-per-phase execution requires commit metadata before phase completion.");
2301
- if (plan.execution?.policy === "no-commit" && params.commit)
2302
- throw new Error("No-commit execution cannot record a phase commit.");
2303
- }
2304
- const updated = {
2305
- ...phase,
2306
- status: params.status,
2307
- revision: phase.revision + 1,
2308
- commit: params.commit ?? phase.commit,
2309
- blocker: params.blockedReason ? blocker(params) : phase.blocker
2310
- };
2311
- if (params.status === "blocked" && params.blockedReason)
2312
- escalation = escalationFor(plan, updated.id, undefined, params);
2313
- return {
2314
- ...heartbeat(plan),
2315
- phases: plan.phases.map((item) => item.id === updated.id ? updated : item)
2316
- };
2317
- }
2318
- const task = getTask(phase, params.target.id);
2319
- if (task.status !== params.expectedStatus)
2320
- throw new Error(`Expected task status ${params.expectedStatus}, found ${task.status}.`);
2321
- assertExecutionOwner(plan, params.executionId);
2322
- assertTaskTransition(task, params.status, params.executionId, params.actor);
2323
- const updatedTask = {
2324
- ...task,
2325
- status: params.status,
2326
- revision: task.revision + 1,
2327
- owner: params.status === "in_progress" ? params.actor : task.owner,
2328
- executionId: params.status === "in_progress" ? params.executionId : task.executionId,
2329
- blocker: params.blockedReason ? blocker(params) : task.blocker
2330
- };
2331
- if (params.status === "blocked" && params.blockedReason)
2332
- escalation = escalationFor(plan, phase.id, task.id, params);
2333
- const updatedPhase = {
2334
- ...phase,
2335
- revision: phase.revision + 1,
2336
- tasks: phase.tasks.map((item) => item.id === updatedTask.id ? updatedTask : item)
2337
- };
2338
- return {
2339
- ...heartbeat(plan),
2340
- phases: plan.phases.map((item) => item.id === updatedPhase.id ? updatedPhase : item)
2341
- };
2342
- });
2343
- await store.log(workingDirectory, record.id, {
2344
- planRevision: record.document.revision,
2345
- kind: params.status === "blocked" ? "blocker" : "status",
2346
- actor: params.actor,
2347
- message: params.message,
2348
- executionId: params.executionId,
2349
- phaseId: params.target.type === "phase" ? params.target.id : escalation?.phaseId,
2350
- taskId: params.target.type === "task" ? params.target.id : undefined,
2351
- evidence: params.evidence
2352
- });
2353
- if (escalation && record.document.execution?.mode !== "background") {
2354
- const selected = await modes.set("planner", ctx);
2355
- if (selected.ok)
2356
- pi.sendUserMessage(renderPlannerEscalation(escalation), {
2357
- deliverAs: "followUp"
2358
- });
2359
- }
2360
- return result(`Updated ${params.target.type} to ${params.status}.`, { record, escalation });
2361
- }
2362
- });
2363
- }
2364
- async function startExecution(pi, modes, store, params, ctx) {
2365
- const workingDirectory = params.cwd ?? process.cwd();
2366
- const before = await store.read(workingDirectory, params.plan);
2367
- if (before.document.status !== "ready" && before.document.status !== "blocked")
2368
- throw new Error(`Plan ${before.id} must be ready or blocked before execution.`);
2369
- if (before.document.execution?.active)
2370
- throw new Error(`Plan ${before.id} already has an active execution.`);
2371
- const branch = await currentBranch(workingDirectory);
2372
- if (branch !== before.document.branch)
2373
- throw new Error(`Plan branch is ${before.document.branch}; current branch is ${branch}.`);
2374
- if (params.policy === "commit-per-phase") {
2375
- const dirty = await runChecked("git", ["-C", workingDirectory, "status", "--porcelain"]);
2376
- if (dirty.stdout.trim())
2377
- throw new Error(`Commit-per-phase requires a clean worktree:
2378
- ${dirty.stdout.trim()}`);
2379
- }
2380
- const head = (await runChecked("git", ["-C", workingDirectory, "rev-parse", "HEAD"])).stdout.trim();
2381
- const executionId = crypto.randomUUID();
2382
- const timestamp = new Date().toISOString();
2383
- const record = await store.mutate(workingDirectory, before.id, "start execution", (plan) => ({
2384
- ...plan,
2385
- status: "in_progress",
2386
- execution: {
2387
- id: executionId,
2388
- mode: params.mode,
2389
- policy: params.policy,
2390
- cwd: workingDirectory,
2391
- branch: plan.branch,
2392
- baseHead: head,
2393
- actor: params.actor,
2394
- startedAt: timestamp,
2395
- heartbeatAt: timestamp,
2396
- active: true
2397
- }
2398
- }));
2399
- await store.log(workingDirectory, record.id, {
2400
- planRevision: record.document.revision,
2401
- kind: "execution",
2402
- actor: params.actor,
2403
- message: `Started ${params.mode} execution ${executionId}.`,
2404
- executionId
2405
- });
2406
- const coordinator = params.mode === "inline" ? "worker" : "orchestrator";
2407
- const prompt = renderExecutionPrompt(createExecutionPacket(record.document, coordinator));
2408
- if (params.mode === "inline") {
2409
- const selected = await modes.set("worker", ctx);
2410
- if (!selected.ok)
2411
- throw new Error(selected.message);
2412
- if (!pi.sendMessage)
2413
- throw new Error("Inline execution dispatch is unavailable.");
2414
- pi.sendMessage({ customType: "diffpi-plan-execution", display: false, content: prompt }, { triggerTurn: true, deliverAs: "followUp" });
2415
- } else {
2416
- await launchBackgroundPi(pi, {
2417
- name: `Plan go ${record.id}`,
2418
- agentPath: join12(resolveBundledAgentsDir(), "diffpi-orchestrator.md"),
2419
- model: "openai-codex/gpt-5.6-luna",
2420
- thinking: "medium",
2421
- cwd: workingDirectory,
2422
- prompt
2423
- });
2424
- }
2425
- return result(`Started ${params.mode} execution ${executionId}.`, {
2426
- record,
2427
- executionId,
2428
- dispatched: true,
2429
- queued: true,
2430
- foregroundModeChanged: params.mode === "inline"
2431
- });
2432
- }
2433
- function newPhase(input) {
2434
- return {
2435
- id: input.id,
2436
- revision: 0,
2437
- title: input.title,
2438
- objective: input.objective,
2439
- dependencies: input.dependencies ?? [],
2440
- tasks: (input.tasks ?? []).map(newTask),
2441
- status: "pending",
2442
- gate: { phaseRevision: 0, status: "pending", results: [] }
2443
- };
2444
- }
2445
- function newTask(input) {
2446
- return {
2447
- id: input.id,
2448
- revision: 0,
2449
- title: input.title,
2450
- steps: input.steps,
2451
- dependencies: input.dependencies ?? [],
2452
- fileScopes: input.fileScopes ?? [],
2453
- acceptanceCriteria: input.acceptanceCriteria ?? [],
2454
- status: "pending"
2455
- };
2456
- }
2457
- function reconcileTasks(existing, drafts) {
2458
- const byId = new Map(existing.map((task) => [task.id, task]));
2459
- const requested = new Set(drafts.map((task) => task.id));
2460
- const removedCompleted = existing.find((task) => task.status === "completed" && !requested.has(task.id));
2461
- if (removedCompleted)
2462
- throw new Error(`Completed task ${removedCompleted.id} cannot be removed.`);
2463
- return drafts.map((draft) => {
2464
- const current = byId.get(draft.id);
2465
- if (current?.status === "completed")
2466
- return current;
2467
- return current ? {
2468
- ...newTask(draft),
2469
- revision: current.revision + 1,
2470
- status: current.status === "blocked" ? "pending" : current.status,
2471
- owner: current.status === "blocked" ? undefined : current.owner,
2472
- executionId: current.status === "blocked" ? undefined : current.executionId,
2473
- blocker: current.status === "blocked" ? undefined : current.blocker
2474
- } : newTask(draft);
2475
- });
2476
- }
2477
- function assertAuthoringRevision(plan, expected) {
2478
- if (plan.revision !== expected)
2479
- throw new Error(`Stale plan revision: expected ${expected}, found ${plan.revision}.`);
2480
- }
2481
- function assertAuthorable(plan, blockedPhaseId) {
2482
- if (plan.status === "completed")
2483
- throw new Error("Completed plans cannot be structurally changed.");
2484
- if (!plan.execution?.active)
2485
- return;
2486
- if (!blockedPhaseId)
2487
- throw new Error("An active execution owns this plan.");
2488
- const phase = getPhase(plan, blockedPhaseId);
2489
- if (phase.status !== "blocked" && !phase.tasks.some((task) => task.status === "blocked"))
2490
- throw new Error("Planner may amend only blocked work during an active execution.");
2491
- }
2492
- function resetDraft(plan) {
2493
- return plan.status === "ready" || plan.status === "blocked" ? { ...plan, status: "draft" } : plan;
2494
- }
2495
- function allIds(plan) {
2496
- return new Set([plan.id, ...plan.phases.flatMap((phase) => [phase.id, ...phase.tasks.map((task) => task.id)])]);
2497
- }
2498
- function assertNewIds(plan, phase) {
2499
- const known = allIds(plan);
2500
- for (const candidate of [phase.id, ...phase.tasks.map((task) => task.id)]) {
2501
- assertStableId(candidate);
2502
- if (known.has(candidate))
2503
- throw new Error(`Duplicate stable ID: ${candidate}.`);
2504
- known.add(candidate);
2505
- }
2506
- }
2507
- function getPhase(plan, phaseId) {
2508
- const phase = plan.phases.find((item) => item.id === phaseId);
2509
- if (!phase)
2510
- throw new Error(`Unknown phase: ${phaseId}.`);
2511
- return phase;
2512
- }
2513
- function findTaskPhase(plan, taskId) {
2514
- const phase = plan.phases.find((item) => item.tasks.some((task) => task.id === taskId));
2515
- if (!phase)
2516
- throw new Error(`Unknown task: ${taskId}.`);
2517
- return phase;
2518
- }
2519
- function getTask(phase, taskId) {
2520
- const task = phase.tasks.find((item) => item.id === taskId);
2521
- if (!task)
2522
- throw new Error(`Unknown task: ${taskId}.`);
2523
- return task;
2524
- }
2525
- function assertExecutionOwner(plan, executionId) {
2526
- if (!plan.execution?.active || !executionId || plan.execution.id !== executionId)
2527
- throw new Error(`Execution ${executionId ?? "(missing)"} does not own this plan.`);
2528
- }
2529
- function blocker(params) {
2530
- return {
2531
- reason: params.blockedReason,
2532
- attempts: params.attempts,
2533
- evidence: params.evidence,
2534
- needsUserDecision: params.needsUserDecision
2535
- };
2536
- }
2537
- function escalationFor(plan, phaseId, taskId, params) {
2538
- return {
2539
- planId: plan.id,
2540
- executionId: params.executionId,
2541
- phaseId,
2542
- taskId,
2543
- blocker: params.blockedReason,
2544
- attempts: params.attempts ?? [],
2545
- evidence: params.evidence ?? [],
2546
- needsUserDecision: params.needsUserDecision ?? false
2547
- };
2548
- }
2549
- function heartbeat(plan) {
2550
- return plan.execution?.active ? { ...plan, execution: { ...plan.execution, heartbeatAt: new Date().toISOString() } } : plan;
2551
- }
2552
- async function verifyCommit(cwd, requestedSha) {
2553
- await runChecked("git", ["-C", cwd, "cat-file", "-e", `${requestedSha}^{commit}`]);
2554
- const [sha, subject] = await Promise.all([
2555
- runChecked("git", ["-C", cwd, "rev-parse", "HEAD"]),
2556
- runChecked("git", ["-C", cwd, "log", "-1", "--format=%s"])
2557
- ]);
2558
- return { sha: sha.stdout.trim(), subject: subject.stdout.trim() };
2559
- }
2560
- async function currentBranch(cwd) {
2561
- const result = await run("git", ["-C", cwd, "branch", "--show-current"]);
2562
- if (result.code !== 0 || !result.stdout.trim())
2563
- throw new Error(`Cannot determine current branch in ${cwd}.`);
2564
- return result.stdout.trim();
2565
- }
2566
- function parameters(schema) {
2567
- return z4.toJSONSchema(schema, { io: "input" });
2568
- }
2569
- function result(text, details = {}) {
2570
- return { content: [{ type: "text", text }], details };
2571
- }
2572
-
2573
- // src/tools/review.ts
2574
- import { existsSync as existsSync2 } from "node:fs";
2575
- import { mkdir as mkdir6, readdir as readdir2, readFile as readFile12, rename as rename4, stat, unlink as unlink2, writeFile as writeFile7 } from "node:fs/promises";
2576
- import { join as join14 } from "node:path";
2577
- import { defineTool as defineTool4 } from "@earendil-works/pi-coding-agent";
2578
- import { z as z8 } from "zod";
2579
-
2580
- // src/extensions/ghx.ts
2581
- function gh(args, options) {
2582
- return run("gh", args, options);
2583
- }
2584
- function ghChecked(args, options) {
2585
- return runChecked("gh", args, options);
2586
- }
2587
-
2588
- // src/vcs/helpers.ts
2589
- function isConfirmedMissingChange(provider, output) {
2590
- const message = output.toLowerCase();
2591
- return provider === "github" ? message.includes("no pull requests found for branch") || message.includes("could not find pull request") || message.includes("could not resolve to a pullrequest") : provider === "gitlab" ? message.includes("no open merge request") || /failed to get open merge request/.test(message) && /404(?: not found)?/.test(message) : false;
2592
- }
2593
- function parseJson(value, name) {
2594
- try {
2595
- return JSON.parse(value);
2596
- } catch {
2597
- throw new Error(`Cannot parse the ${name} response as JSON.`);
450
+ // src/extensions/ghx.ts
451
+ function gh(args, options) {
452
+ return run("gh", args, options);
453
+ }
454
+ function ghChecked(args, options) {
455
+ return runChecked("gh", args, options);
456
+ }
457
+
458
+ // src/vcs/helpers.ts
459
+ function isConfirmedMissingChange(provider, output) {
460
+ const message = output.toLowerCase();
461
+ return provider === "github" ? message.includes("no pull requests found for branch") || message.includes("could not find pull request") || message.includes("could not resolve to a pullrequest") : provider === "gitlab" ? message.includes("no open merge request") || /failed to get open merge request/.test(message) && /404(?: not found)?/.test(message) : false;
462
+ }
463
+ function parseJson(value, name) {
464
+ try {
465
+ return JSON.parse(value);
466
+ } catch {
467
+ throw new Error(`Cannot parse the ${name} response as JSON.`);
2598
468
  }
2599
469
  }
2600
470
  function commandFailure(command, args, result) {
@@ -2814,26 +684,266 @@ function GitLabVcsBackend(vcs) {
2814
684
  };
2815
685
  }
2816
686
 
2817
- // src/vcs/forge-backend.ts
2818
- function createForgeBackend(vcs) {
2819
- if (vcs.provider === "github")
2820
- return GitHubVcsBackend(vcs);
2821
- if (vcs.provider === "gitlab")
2822
- return GitLabVcsBackend(vcs);
2823
- throw new Error("No supported forge detected from the git remote. Use --local for an offline review.");
687
+ // src/vcs/forge-backend.ts
688
+ function createForgeBackend(vcs) {
689
+ if (vcs.provider === "github")
690
+ return GitHubVcsBackend(vcs);
691
+ if (vcs.provider === "gitlab")
692
+ return GitLabVcsBackend(vcs);
693
+ throw new Error("No supported forge detected from the git remote. Use --local for an offline review.");
694
+ }
695
+ var createVcsBackend = createForgeBackend;
696
+ // src/extensions/misex.ts
697
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
698
+ import { homedir as homedir2 } from "node:os";
699
+ import { basename as basename2, dirname as dirname2, join as join3 } from "node:path";
700
+ var MISE_HOOK_START = "# >>> @difflab/pi mise >>>";
701
+ var MISE_HOOK_END = "# <<< @difflab/pi mise <<<";
702
+ var mise = {
703
+ async executableCheck(name = "mise") {
704
+ return findExecutable(name);
705
+ },
706
+ async run(args, options = {}) {
707
+ return run("mise", args, options);
708
+ },
709
+ async install(options = {}) {
710
+ const homeDir = options.homeDir ?? homedir2();
711
+ const platform = options.platform ?? process.platform;
712
+ if (platform === "win32")
713
+ throw new Error("Automatic mise installation supports macOS and Linux only.");
714
+ const installedPath = join3(homeDir, ".local", "bin", "mise");
715
+ if (options.dryRun)
716
+ return installedPath;
717
+ await runChecked("sh", ["-c", "curl -fsSL https://mise.run | sh"]);
718
+ const executable = await findExecutable(installedPath) ?? await findExecutable("mise");
719
+ if (!executable)
720
+ throw new Error(`mise installation completed, but ${installedPath} was not found.`);
721
+ return executable;
722
+ },
723
+ async hookEnsure(executable, options = {}) {
724
+ const homeDir = options.homeDir ?? homedir2();
725
+ const hook = getShellHook(basename2(options.shell ?? process.env.SHELL ?? ""), executable, homeDir);
726
+ const current = await getOptionalFile(hook.path);
727
+ if (current.includes(MISE_HOOK_START))
728
+ return { path: hook.path, changed: false, planned: false };
729
+ if (options.dryRun)
730
+ return { path: hook.path, changed: true, planned: true };
731
+ const separator = current.length === 0 || current.endsWith(`
732
+ `) ? "" : `
733
+ `;
734
+ await mkdir2(dirname2(hook.path), { recursive: true });
735
+ await writeFile2(hook.path, `${current}${separator}${hook.content}`, "utf8");
736
+ return { path: hook.path, changed: true, planned: false };
737
+ },
738
+ async toolCheckGlobal(executable, tool, minimumVersion) {
739
+ const result = await run(executable, ["ls", "--global", "--installed", tool, "--json"]);
740
+ return result.code === 0 && isToolInstalled(result.stdout, minimumVersion);
741
+ },
742
+ async toolInstallGlobal(executable, specification) {
743
+ await runChecked(executable, ["use", "--global", specification]);
744
+ },
745
+ async toolCheckLocal(executable, tool, cwd = process.cwd()) {
746
+ const result = await run(executable, ["ls", "--local", "--installed", tool, "--json"], { cwd });
747
+ return result.code === 0 && isToolInstalled(result.stdout);
748
+ },
749
+ async toolInstallLocal(executable, specification, cwd = process.cwd()) {
750
+ await runChecked(executable, ["use", "--path", cwd, specification], { cwd });
751
+ },
752
+ async toolUpdateAllGlobal(executable, homeDir = homedir2()) {
753
+ await runChecked(executable, ["upgrade"], { cwd: homeDir });
754
+ }
755
+ };
756
+ function getShellHook(shell, executable, homeDir) {
757
+ const command = getShellQuoted(executable);
758
+ switch (shell.toLowerCase()) {
759
+ case "zsh":
760
+ return {
761
+ path: join3(homeDir, ".zshrc"),
762
+ content: `${MISE_HOOK_START}
763
+ eval "$(${command} activate zsh)"
764
+ ${MISE_HOOK_END}
765
+ `
766
+ };
767
+ case "fish":
768
+ return {
769
+ path: join3(homeDir, ".config", "fish", "config.fish"),
770
+ content: `${MISE_HOOK_START}
771
+ ${command} activate fish | source
772
+ ${MISE_HOOK_END}
773
+ `
774
+ };
775
+ case "nu":
776
+ case "nushell":
777
+ return {
778
+ path: join3(homeDir, ".config", "nushell", "config.nu"),
779
+ content: `${MISE_HOOK_START}
780
+ let mise_bin = ${command}
781
+ let mise_path = $nu.default-config-dir | path join mise.nu
782
+ ^$mise_bin activate nu | save $mise_path --force
783
+ use ($nu.default-config-dir | path join mise.nu)
784
+ ${MISE_HOOK_END}
785
+ `
786
+ };
787
+ case "xonsh":
788
+ return {
789
+ path: join3(homeDir, ".xonshrc"),
790
+ content: `${MISE_HOOK_START}
791
+ execx($(${command} activate xonsh))
792
+ ${MISE_HOOK_END}
793
+ `
794
+ };
795
+ case "elvish":
796
+ return {
797
+ path: join3(homeDir, ".config", "elvish", "rc.elv"),
798
+ content: `${MISE_HOOK_START}
799
+ var mise: = (ns [&])
800
+ eval (${command} activate elvish | slurp) &ns=$mise: &on-end={|ns| set mise: = $ns }
801
+ mise:activate
802
+ ${MISE_HOOK_END}
803
+ `
804
+ };
805
+ case "pwsh":
806
+ case "powershell":
807
+ return {
808
+ path: join3(homeDir, ".config", "powershell", "Microsoft.PowerShell_profile.ps1"),
809
+ content: `${MISE_HOOK_START}
810
+ (& ${command} activate pwsh) | Out-String | Invoke-Expression
811
+ ${MISE_HOOK_END}
812
+ `
813
+ };
814
+ case "bash":
815
+ default:
816
+ return {
817
+ path: join3(homeDir, ".bashrc"),
818
+ content: `${MISE_HOOK_START}
819
+ eval "$(${command} activate bash)"
820
+ ${MISE_HOOK_END}
821
+ `
822
+ };
823
+ }
824
+ }
825
+ function isToolInstalled(output, minimumVersion) {
826
+ try {
827
+ const value = JSON.parse(output);
828
+ if (!Array.isArray(value))
829
+ return false;
830
+ return value.some((entry) => {
831
+ if (!entry || typeof entry !== "object" || !("installed" in entry) || entry.installed !== true)
832
+ return false;
833
+ if (!minimumVersion)
834
+ return true;
835
+ if (!("version" in entry) || typeof entry.version !== "string")
836
+ return false;
837
+ return isVersionAtLeast(entry.version, minimumVersion);
838
+ });
839
+ } catch {
840
+ return false;
841
+ }
842
+ }
843
+ function isVersionAtLeast(version, minimumVersion) {
844
+ const current = version.match(/^v?(\d+)\.(\d+)\.(\d+)/)?.slice(1).map(Number);
845
+ const minimum = minimumVersion.match(/^v?(\d+)\.(\d+)\.(\d+)/)?.slice(1).map(Number);
846
+ if (!current || !minimum)
847
+ return false;
848
+ for (let index = 0;index < minimum.length; index += 1) {
849
+ if (current[index] !== minimum[index])
850
+ return current[index] > minimum[index];
851
+ }
852
+ return true;
853
+ }
854
+ async function getOptionalFile(path) {
855
+ try {
856
+ return await readFile2(path, "utf8");
857
+ } catch (error) {
858
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
859
+ return "";
860
+ throw error;
861
+ }
862
+ }
863
+ function getShellQuoted(value) {
864
+ return `'${value.replaceAll("'", "'\\''")}'`;
865
+ }
866
+
867
+ // src/gates.ts
868
+ var CONVENTIONAL_COMMIT = /^(feat|fix|perf|refactor|docs|chore|test|build|ci|style|revert)(\([^)]+\))?!?: .+/;
869
+ var MISE_GATES = ["format:check", "lint", "test"];
870
+ function checkConventionalSubject(subject) {
871
+ const trimmed = subject.trim();
872
+ const ok = CONVENTIONAL_COMMIT.test(trimmed);
873
+ return {
874
+ name: "conventional-subject",
875
+ status: ok ? "pass" : "warn",
876
+ detail: ok ? trimmed : `not a conventional-commit subject: "${trimmed}"`
877
+ };
2824
878
  }
2825
- var createVcsBackend = createForgeBackend;
879
+ async function runMiseGates(cwd) {
880
+ const tasks = await discoverMiseTasks(cwd);
881
+ const results = [];
882
+ for (const gate of MISE_GATES) {
883
+ const targets = tasks.get(gate) ?? [];
884
+ if (targets.length === 0) {
885
+ results.push({ name: gate, status: "skip", detail: "no mise recipe" });
886
+ continue;
887
+ }
888
+ const invocations = targets.flatMap((target, index) => index === 0 ? [target] : [":::", target]);
889
+ const result = await mise.run(["run", ...invocations], { cwd });
890
+ results.push({
891
+ name: gate,
892
+ status: result.code === 0 ? "pass" : "fail",
893
+ detail: result.code === 0 ? "clean" : (result.stderr.trim() || result.stdout.trim()).slice(-400)
894
+ });
895
+ }
896
+ return results;
897
+ }
898
+ function ciGate(checksOutput) {
899
+ const text = checksOutput.toLowerCase();
900
+ if (!text.trim())
901
+ return { name: "ci", status: "skip", detail: "no CI output" };
902
+ if (/\bfail|error\b/.test(text))
903
+ return { name: "ci", status: "warn", detail: "CI failing" };
904
+ if (/\bpending|in progress|queued\b/.test(text))
905
+ return { name: "ci", status: "warn", detail: "CI pending" };
906
+ return { name: "ci", status: "pass", detail: "CI green" };
907
+ }
908
+ function parseMiseTasks(input) {
909
+ let tasks;
910
+ try {
911
+ tasks = JSON.parse(input);
912
+ } catch {
913
+ return new Map;
914
+ }
915
+ if (!Array.isArray(tasks))
916
+ return new Map;
917
+ const found = new Map;
918
+ for (const gate of MISE_GATES) {
919
+ const targets = tasks.flatMap((task) => {
920
+ if (typeof task.name !== "string")
921
+ return [];
922
+ return task.name === gate || task.name.endsWith(`:${gate}`) || task.aliases?.includes(gate) ? [task.name] : [];
923
+ });
924
+ if (targets.length > 0)
925
+ found.set(gate, [...new Set(targets)]);
926
+ }
927
+ return found;
928
+ }
929
+ async function discoverMiseTasks(cwd) {
930
+ const result = await mise.run(["tasks", "--json", "--all"], { cwd });
931
+ if (result.code !== 0)
932
+ return new Map;
933
+ return parseMiseTasks(result.stdout);
934
+ }
935
+
2826
936
  // src/review/types.ts
2827
- import { z as z5 } from "zod";
2828
- var severitySchema = z5.enum(["BLOCKING", "CONSIDER", "NOTE"]);
2829
- var findingSchema = z5.object({
2830
- file: z5.string().min(1),
2831
- line: z5.number().int().nonnegative(),
937
+ import { z as z3 } from "zod";
938
+ var severitySchema = z3.enum(["BLOCKING", "CONSIDER", "NOTE"]);
939
+ var findingSchema = z3.object({
940
+ file: z3.string().min(1),
941
+ line: z3.number().int().nonnegative(),
2832
942
  severity: severitySchema,
2833
- body: z5.string().min(1),
2834
- reference: z5.string().optional().default("")
943
+ body: z3.string().min(1),
944
+ reference: z3.string().optional().default("")
2835
945
  });
2836
- var findingsSchema = z5.array(findingSchema);
946
+ var findingsSchema = z3.array(findingSchema);
2837
947
  function reviewSlug(input) {
2838
948
  return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50);
2839
949
  }
@@ -2897,17 +1007,17 @@ function renderCommentBody(finding) {
2897
1007
  return `${prefix}${finding.body}${reference}`;
2898
1008
  }
2899
1009
  // src/review/review-markdown.ts
2900
- import { z as z6 } from "zod";
2901
- var reviewThreadRecordSchema = z6.object({
2902
- id: z6.string().min(1),
2903
- file: z6.string().optional(),
2904
- line: z6.number().int().positive().optional(),
2905
- body: z6.string(),
2906
- author: z6.string().optional(),
2907
- resolved: z6.boolean(),
2908
- addressed: z6.boolean().optional(),
2909
- question: z6.boolean(),
2910
- replies: z6.array(z6.string()).optional()
1010
+ import { z as z4 } from "zod";
1011
+ var reviewThreadRecordSchema = z4.object({
1012
+ id: z4.string().min(1),
1013
+ file: z4.string().optional(),
1014
+ line: z4.number().int().positive().optional(),
1015
+ body: z4.string(),
1016
+ author: z4.string().optional(),
1017
+ resolved: z4.boolean(),
1018
+ addressed: z4.boolean().optional(),
1019
+ question: z4.boolean(),
1020
+ replies: z4.array(z4.string()).optional()
2911
1021
  });
2912
1022
  function renderReviewDoc(input) {
2913
1023
  const anchored = dedupeFindings(input.findings).filter((finding) => finding.line > 0);
@@ -2994,7 +1104,7 @@ function parseThreadArtifact(content) {
2994
1104
  let threads;
2995
1105
  try {
2996
1106
  const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
2997
- threads = z6.array(reviewThreadRecordSchema).parse(decoded);
1107
+ threads = z4.array(reviewThreadRecordSchema).parse(decoded);
2998
1108
  } catch {
2999
1109
  throw new Error("Cannot parse the Diffpi thread artifact payload.");
3000
1110
  }
@@ -3367,11 +1477,52 @@ function hasGitlabDraftNotes(input) {
3367
1477
  }
3368
1478
 
3369
1479
  // src/review/local-review-backend.ts
3370
- import { readFile as readFile10, writeFile as writeFile5 } from "node:fs/promises";
1480
+ import { readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
1481
+
1482
+ // src/extensions/tuicrx.ts
1483
+ import { readFile as readFile3, realpath as realpath2 } from "node:fs/promises";
1484
+ import { resolve as resolve2 } from "node:path";
1485
+
1486
+ // src/extensions/gitx.ts
1487
+ import { realpath } from "node:fs/promises";
1488
+ import { basename as basename3, isAbsolute, join as join4, resolve } from "node:path";
1489
+ async function gitToplevel(cwd) {
1490
+ const result = await run("git", ["-C", cwd, "rev-parse", "--show-toplevel"]);
1491
+ const top = result.stdout.trim();
1492
+ return result.code === 0 && top ? top : resolve(cwd);
1493
+ }
1494
+ async function inspectGitRepository(cwd) {
1495
+ const root = await gitToplevel(cwd);
1496
+ const remoteResult = await run("git", ["-C", root, "remote", "get-url", "origin"]);
1497
+ const remote = remoteResult.code === 0 ? remoteResult.stdout.trim() : "";
1498
+ const commonResult = await run("git", ["-C", root, "rev-parse", "--git-common-dir"]);
1499
+ const common = commonResult.stdout.trim();
1500
+ const commonPath = commonResult.code === 0 && common ? resolve(isAbsolute(common) ? common : join4(root, common)) : root;
1501
+ const commonDir = await canonicalPath(commonPath);
1502
+ return {
1503
+ root,
1504
+ commonDir,
1505
+ ...remote ? { remote } : {},
1506
+ name: remote ? repositoryName(remote) : basename3(resolve(commonDir, "..")) || basename3(root),
1507
+ identity: remote ? `remote:${normalizeRemote(remote)}` : `git-common-dir:${commonDir}`
1508
+ };
1509
+ }
1510
+ function normalizeRemote(remote) {
1511
+ return remote.trim().replace(/\.git\/?$/i, "").replace(/\/+$/, "").toLowerCase();
1512
+ }
1513
+ function repositoryName(remote) {
1514
+ const normalized = remote.trim().replace(/\.git\/?$/i, "").replace(/\/+$/, "");
1515
+ return normalized.split(/[/\\:]/).filter(Boolean).at(-1) ?? "";
1516
+ }
1517
+ async function canonicalPath(path) {
1518
+ try {
1519
+ return await realpath(path);
1520
+ } catch {
1521
+ return resolve(path);
1522
+ }
1523
+ }
3371
1524
 
3372
1525
  // src/extensions/tuicrx.ts
3373
- import { readFile as readFile9, realpath as realpath4 } from "node:fs/promises";
3374
- import { resolve as resolve4 } from "node:path";
3375
1526
  async function tuicrAvailable() {
3376
1527
  return Boolean(await findExecutable("tuicr"));
3377
1528
  }
@@ -3446,7 +1597,7 @@ async function resolvePrSession(cwd, owner, repo, number) {
3446
1597
  });
3447
1598
  }
3448
1599
  async function findMatchingSession(sessions, cwd, branch) {
3449
- const repository = await canonicalPath3(await gitToplevel(cwd));
1600
+ const repository = await canonicalPath2(await gitToplevel(cwd));
3450
1601
  for (const session of sessions) {
3451
1602
  if (session.kind !== "local")
3452
1603
  continue;
@@ -3454,14 +1605,14 @@ async function findMatchingSession(sessions, cwd, branch) {
3454
1605
  const data = await readSession(session.path);
3455
1606
  if (data.branch_name !== branch || !data.repo_path)
3456
1607
  continue;
3457
- if (await canonicalPath3(data.repo_path) === repository)
1608
+ if (await canonicalPath2(data.repo_path) === repository)
3458
1609
  return session;
3459
1610
  } catch {}
3460
1611
  }
3461
1612
  return;
3462
1613
  }
3463
1614
  async function readSession(path) {
3464
- const content = await readFile9(path, "utf8");
1615
+ const content = await readFile3(path, "utf8");
3465
1616
  try {
3466
1617
  return JSON.parse(content);
3467
1618
  } catch {
@@ -3541,11 +1692,11 @@ async function requireTuicr() {
3541
1692
  function commentAuthor(comment) {
3542
1693
  return comment.username ?? comment.author;
3543
1694
  }
3544
- async function canonicalPath3(path) {
1695
+ async function canonicalPath2(path) {
3545
1696
  try {
3546
- return await realpath4(path);
1697
+ return await realpath2(path);
3547
1698
  } catch {
3548
- return resolve4(path);
1699
+ return resolve2(path);
3549
1700
  }
3550
1701
  }
3551
1702
 
@@ -3570,7 +1721,7 @@ function LocalReviewBackend(options) {
3570
1721
  },
3571
1722
  async listThreads() {
3572
1723
  try {
3573
- return parseThreadArtifact(await readFile10(options.artifactPath, "utf8"));
1724
+ return parseThreadArtifact(await readFile4(options.artifactPath, "utf8"));
3574
1725
  } catch (error) {
3575
1726
  if (error.code === "ENOENT")
3576
1727
  return [];
@@ -3578,8 +1729,8 @@ function LocalReviewBackend(options) {
3578
1729
  }
3579
1730
  },
3580
1731
  async reply(input) {
3581
- const content = await readFile10(options.artifactPath, "utf8");
3582
- await writeFile5(options.artifactPath, upsertThreadReply(content, input.threadId, input.body, input.question, input.resolve), "utf8");
1732
+ const content = await readFile4(options.artifactPath, "utf8");
1733
+ await writeFile3(options.artifactPath, upsertThreadReply(content, input.threadId, input.body, input.question, input.resolve), "utf8");
3583
1734
  },
3584
1735
  async publish() {
3585
1736
  throw new Error("Promote a local draft through a remote review backend before publishing it.");
@@ -3600,15 +1751,100 @@ function createLocalReviewBackend(options) {
3600
1751
  }
3601
1752
  // src/review/review-state.ts
3602
1753
  import { createHash as createHash2 } from "node:crypto";
3603
- import { readFile as readFile11, rename as rename3, writeFile as writeFile6 } from "node:fs/promises";
3604
- import { join as join13 } from "node:path";
3605
- import { z as z7 } from "zod";
3606
- var reviewPublicationStateSchema = z7.object({
3607
- target: z7.string().optional(),
3608
- bodies: z7.array(z7.string()).default([]),
3609
- comments: z7.array(z7.string()).default([]),
3610
- replies: z7.array(z7.string()).default([]),
3611
- overlayPath: z7.string().optional()
1754
+ import { readFile as readFile5, rename, writeFile as writeFile4 } from "node:fs/promises";
1755
+ import { join as join6 } from "node:path";
1756
+ import { z as z5 } from "zod";
1757
+
1758
+ // src/store.ts
1759
+ import { createHash } from "node:crypto";
1760
+ import { lstat, mkdir as mkdir3, readlink, realpath as realpath3, symlink, unlink } from "node:fs/promises";
1761
+ import { homedir as homedir3 } from "node:os";
1762
+ import { dirname as dirname3, isAbsolute as isAbsolute2, join as join5, resolve as resolve3 } from "node:path";
1763
+ var STORE_LINK = ".diffpi";
1764
+ var LEGACY_STORE_LINK = join5(".pi", "diffpi");
1765
+ function storeGlobalRoot(homeDir = homedir3()) {
1766
+ return join5(homeDir, ".difflab", "diffpi", "projects");
1767
+ }
1768
+ async function ensureStore(cwd, homeDir = homedir3()) {
1769
+ const repository = await inspectGitRepository(cwd);
1770
+ const root = repository.root;
1771
+ const slug = projectSlug(repository);
1772
+ const dest = join5(storeGlobalRoot(homeDir), slug);
1773
+ const link = join5(root, STORE_LINK);
1774
+ await mkdir3(dest, { recursive: true });
1775
+ try {
1776
+ await assertStoreLink(link, dest);
1777
+ } catch (error) {
1778
+ if (error.code !== "ENOENT")
1779
+ throw error;
1780
+ await symlink(dest, link);
1781
+ }
1782
+ await removeLegacyStoreLink(join5(root, LEGACY_STORE_LINK), dest);
1783
+ return { slug, root, dest, link, linked: true };
1784
+ }
1785
+ async function reviewsDir(cwd, homeDir = homedir3()) {
1786
+ const store = await ensureStore(cwd, homeDir);
1787
+ const dir = join5(store.link, "review");
1788
+ await mkdir3(dir, { recursive: true });
1789
+ return dir;
1790
+ }
1791
+ async function completedReviewsDir(cwd, homeDir = homedir3()) {
1792
+ const store = await ensureStore(cwd, homeDir);
1793
+ const dir = join5(store.link, "reviews");
1794
+ await mkdir3(dir, { recursive: true });
1795
+ return dir;
1796
+ }
1797
+ async function sessionsDir(cwd, homeDir = homedir3()) {
1798
+ const store = await ensureStore(cwd, homeDir);
1799
+ const dir = join5(store.link, "sessions");
1800
+ await mkdir3(dir, { recursive: true });
1801
+ return dir;
1802
+ }
1803
+ async function assertStoreLink(path, dest) {
1804
+ const entry = await lstat(path);
1805
+ if (!entry.isSymbolicLink())
1806
+ throw new Error(`${path} exists and is not a symlink.`);
1807
+ const target = await symlinkTarget(path);
1808
+ if (target !== await canonicalPath3(dest))
1809
+ throw new Error(`${path} points to ${target}, not ${dest}.`);
1810
+ }
1811
+ async function removeLegacyStoreLink(path, dest) {
1812
+ try {
1813
+ const entry = await lstat(path);
1814
+ if (!entry.isSymbolicLink())
1815
+ return;
1816
+ const target = await symlinkTarget(path);
1817
+ if (target === await canonicalPath3(dest))
1818
+ await unlink(path);
1819
+ } catch (error) {
1820
+ if (error.code !== "ENOENT")
1821
+ throw error;
1822
+ }
1823
+ }
1824
+ async function symlinkTarget(path) {
1825
+ const target = await readlink(path);
1826
+ return canonicalPath3(isAbsolute2(target) ? target : resolve3(dirname3(path), target));
1827
+ }
1828
+ async function canonicalPath3(path) {
1829
+ try {
1830
+ return await realpath3(path);
1831
+ } catch {
1832
+ return resolve3(path);
1833
+ }
1834
+ }
1835
+ function projectSlug(repository) {
1836
+ const readable = repository.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "unnamed";
1837
+ const digest = createHash("sha256").update(repository.identity).digest("hex").slice(0, 12);
1838
+ return `${readable}-${digest}`;
1839
+ }
1840
+
1841
+ // src/review/review-state.ts
1842
+ var reviewPublicationStateSchema = z5.object({
1843
+ target: z5.string().optional(),
1844
+ bodies: z5.array(z5.string()).default([]),
1845
+ comments: z5.array(z5.string()).default([]),
1846
+ replies: z5.array(z5.string()).default([]),
1847
+ overlayPath: z5.string().optional()
3612
1848
  });
3613
1849
  function reviewBodyFingerprint(body) {
3614
1850
  return digest(body);
@@ -3624,15 +1860,15 @@ function unpublishedReviewComments(comments, knownFingerprints) {
3624
1860
  }
3625
1861
  async function loadReviewPublicationState(cwd, vcs, number, homeDir) {
3626
1862
  const target = `${vcs.provider}:${vcs.owner}/${vcs.repo}#${number}`;
3627
- const path = join13(await sessionsDir(cwd, homeDir), `review-publish-${digest(target).slice(0, 16)}.json`);
1863
+ const path = join6(await sessionsDir(cwd, homeDir), `review-publish-${digest(target).slice(0, 16)}.json`);
3628
1864
  try {
3629
- const parsed = reviewPublicationStateSchema.parse(JSON.parse(await readFile11(path, "utf8")));
1865
+ const parsed = reviewPublicationStateSchema.parse(JSON.parse(await readFile5(path, "utf8")));
3630
1866
  return { path, state: { ...parsed, target } };
3631
1867
  } catch (error) {
3632
1868
  if (error.code === "ENOENT") {
3633
1869
  return { path, state: { target, bodies: [], comments: [], replies: [] } };
3634
1870
  }
3635
- if (error instanceof SyntaxError || error instanceof z7.ZodError) {
1871
+ if (error instanceof SyntaxError || error instanceof z5.ZodError) {
3636
1872
  throw new Error(`Cannot parse review publication state: ${path}`);
3637
1873
  }
3638
1874
  throw error;
@@ -3640,49 +1876,103 @@ async function loadReviewPublicationState(cwd, vcs, number, homeDir) {
3640
1876
  }
3641
1877
  async function saveReviewPublicationState(path, state) {
3642
1878
  const temp = `${path}.${process.pid}.tmp`;
3643
- await writeFile6(temp, `${JSON.stringify(state, null, 2)}
1879
+ await writeFile4(temp, `${JSON.stringify(state, null, 2)}
3644
1880
  `, "utf8");
3645
- await rename3(temp, path);
1881
+ await rename(temp, path);
3646
1882
  }
3647
1883
  function digest(value) {
3648
1884
  return createHash2("sha256").update(value).digest("hex");
3649
1885
  }
1886
+ // src/templates.ts
1887
+ import { readFile as readFile6 } from "node:fs/promises";
1888
+ import { homedir as homedir4 } from "node:os";
1889
+ import { join as join8, normalize } from "node:path";
1890
+
1891
+ // src/assets.ts
1892
+ import { existsSync } from "node:fs";
1893
+ import { dirname as dirname4, join as join7 } from "node:path";
1894
+ import { fileURLToPath } from "node:url";
1895
+ function resolveBundledAssetDir(name, moduleUrl = import.meta.url) {
1896
+ const moduleDir = dirname4(fileURLToPath(moduleUrl));
1897
+ const candidates = [join7(moduleDir, name), join7(moduleDir, "..", name), join7(moduleDir, "..", "..", name)];
1898
+ return candidates.find((path) => existsSync(path)) ?? candidates[1];
1899
+ }
1900
+ function resolveBundledAgentsDir(moduleUrl = import.meta.url) {
1901
+ return resolveBundledAssetDir("agents", moduleUrl);
1902
+ }
1903
+ function resolveBundledTemplatesDir(moduleUrl = import.meta.url) {
1904
+ return resolveBundledAssetDir("templates", moduleUrl);
1905
+ }
1906
+
1907
+ // src/templates.ts
1908
+ async function loadTemplate(name, options = {}) {
1909
+ const relative = templateRelativePath(name);
1910
+ const userPath = join8(options.homeDir ?? homedir4(), ".difflab", "diffpi", "templates", relative);
1911
+ const bundledPath = join8(options.bundledDir ?? resolveBundledTemplatesDir(), relative);
1912
+ const user = await readOptionalFile(userPath);
1913
+ if (user !== undefined)
1914
+ return { name, path: userPath, source: "user", content: user };
1915
+ const bundled = await readOptionalFile(bundledPath);
1916
+ if (bundled !== undefined)
1917
+ return { name, path: bundledPath, source: "bundled", content: bundled };
1918
+ throw new Error(`Template "${name}" was not found at ${userPath} or ${bundledPath}.`);
1919
+ }
1920
+ function renderTemplate(content, variables) {
1921
+ return content.replace(/\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g, (match, key) => variables[key] ?? match);
1922
+ }
1923
+ function templateRelativePath(name) {
1924
+ const normalized = normalize(name.replaceAll("\\", "/")).replace(/^\.\//, "");
1925
+ if (!normalized || normalized.startsWith("../") || normalized.includes("/../") || normalized.startsWith("/")) {
1926
+ throw new Error(`Invalid template name: ${name}`);
1927
+ }
1928
+ return normalized.endsWith(".md") ? normalized : `${normalized}.md`;
1929
+ }
1930
+ async function readOptionalFile(path) {
1931
+ try {
1932
+ return await readFile6(path, "utf8");
1933
+ } catch (error) {
1934
+ if (error.code === "ENOENT")
1935
+ return;
1936
+ throw error;
1937
+ }
1938
+ }
1939
+
3650
1940
  // src/tools/review.ts
3651
- var contextSchema = z8.object({
3652
- cwd: z8.string().optional(),
3653
- target: z8.string().optional(),
3654
- workingTree: z8.boolean().optional()
1941
+ var contextSchema = z6.object({
1942
+ cwd: z6.string().optional(),
1943
+ target: z6.string().optional(),
1944
+ workingTree: z6.boolean().optional()
3655
1945
  });
3656
- var localSchema = contextSchema.extend({ local: z8.boolean().optional() });
1946
+ var localSchema = contextSchema.extend({ local: z6.boolean().optional() });
3657
1947
  var openSchema = localSchema.extend({
3658
- title: z8.string().optional(),
3659
- intent: z8.string().optional(),
3660
- issueUrl: z8.string().url().optional(),
3661
- base: z8.string().optional()
1948
+ title: z6.string().optional(),
1949
+ intent: z6.string().optional(),
1950
+ issueUrl: z6.string().url().optional(),
1951
+ base: z6.string().optional()
3662
1952
  });
3663
1953
  var submitSchema = localSchema.extend({
3664
1954
  findings: findingsSchema,
3665
- overallIssues: z8.array(z8.string()).optional(),
3666
- notVerified: z8.array(z8.string()).optional(),
3667
- title: z8.string().optional()
1955
+ overallIssues: z6.array(z6.string()).optional(),
1956
+ notVerified: z6.array(z6.string()).optional(),
1957
+ title: z6.string().optional()
3668
1958
  });
3669
1959
  var addCommentSchema = localSchema.extend({
3670
- body: z8.string().min(1),
3671
- file: z8.string().min(1),
3672
- line: z8.number().int().positive(),
3673
- side: z8.enum(["LEFT", "RIGHT"]).optional()
1960
+ body: z6.string().min(1),
1961
+ file: z6.string().min(1),
1962
+ line: z6.number().int().positive(),
1963
+ side: z6.enum(["LEFT", "RIGHT"]).optional()
3674
1964
  });
3675
1965
  var respondSchema = localSchema.extend({
3676
- threadId: z8.string().min(1),
3677
- body: z8.string().min(1),
3678
- question: z8.boolean().optional(),
3679
- resolve: z8.boolean().optional()
1966
+ threadId: z6.string().min(1),
1967
+ body: z6.string().min(1),
1968
+ question: z6.boolean().optional(),
1969
+ resolve: z6.boolean().optional()
3680
1970
  });
3681
1971
  var publishSchema = localSchema.extend({
3682
- status: z8.enum(["COMMENT", "APPROVE", "REQUEST_CHANGES", "CLOSE"]).optional()
1972
+ status: z6.enum(["COMMENT", "APPROVE", "REQUEST_CHANGES", "CLOSE"]).optional()
3683
1973
  });
3684
1974
  var completeSchema = localSchema.extend({
3685
- action: z8.enum(["approve", "reject", "abandon"]).optional()
1975
+ action: z6.enum(["approve", "reject", "abandon"]).optional()
3686
1976
  });
3687
1977
  function hasReviewDraft(comments, body) {
3688
1978
  return comments.length > 0 || body.trim().length > 0;
@@ -3706,13 +1996,13 @@ async function workingTreeDiff(cwd) {
3706
1996
  }
3707
1997
  function createReviewTools() {
3708
1998
  return [
3709
- defineTool4({
1999
+ defineTool3({
3710
2000
  name: "review_context",
3711
2001
  label: "review context",
3712
2002
  description: "Orient to the target, backend, forge, environment, shared store, PR/MR, and tuicr session.",
3713
2003
  promptSnippet: "Call review_context first",
3714
2004
  promptGuidelines: ["Call this before every review workflow."],
3715
- parameters: parameters2(localSchema),
2005
+ parameters: parameters(localSchema),
3716
2006
  executionMode: "parallel",
3717
2007
  async execute(_id, input) {
3718
2008
  const params = localSchema.parse(input);
@@ -3722,7 +2012,7 @@ function createReviewTools() {
3722
2012
  const session = await resolveTuicrSession(review, Boolean(params.local || params.workingTree));
3723
2013
  const baseRef = review.pr?.baseRef ?? (review.forge ? await review.forge.defaultBranch() : "local");
3724
2014
  const backend = params.local || params.workingTree ? "tuicr" : review.forge?.provider ?? "unsupported";
3725
- return result2([
2015
+ return result([
3726
2016
  `Backend: ${backend}`,
3727
2017
  `Forge: ${review.vcs.provider}${review.vcs.provider === "none" ? "" : ` (${review.vcs.owner}/${review.vcs.repo})`}`,
3728
2018
  `Branch: ${review.vcs.branch} → ${baseRef}`,
@@ -3734,13 +2024,13 @@ function createReviewTools() {
3734
2024
  `), { ...review, env, store, session, baseRef, backend });
3735
2025
  }
3736
2026
  }),
3737
- defineTool4({
2027
+ defineTool3({
3738
2028
  name: "review_new",
3739
2029
  label: "review new",
3740
2030
  description: "Create a new local tuicr review or a remote draft PR/MR and open it in tuicr.",
3741
2031
  promptSnippet: "Call review_new to start",
3742
2032
  promptGuidelines: ["Use local to select tuicr as the review backend."],
3743
- parameters: parameters2(openSchema),
2033
+ parameters: parameters(openSchema),
3744
2034
  executionMode: "sequential",
3745
2035
  async execute(_id, input) {
3746
2036
  const params = openSchema.parse(input);
@@ -3749,9 +2039,9 @@ function createReviewTools() {
3749
2039
  if (params.local || params.workingTree)
3750
2040
  return launchLocalReview(review, true, params.base);
3751
2041
  if (!review.forge)
3752
- return result2(unsupportedForgeMessage());
2042
+ return result(unsupportedForgeMessage());
3753
2043
  if (review.pr)
3754
- return result2(`A remote review already exists for this branch: ${review.pr.url}. Use review_edit to open it.`);
2044
+ return result(`A remote review already exists for this branch: ${review.pr.url}. Use review_edit to open it.`);
3755
2045
  await assertRemoteBranchReady(review.cwd);
3756
2046
  const base = params.base ?? await review.forge.defaultBranch();
3757
2047
  const template = await loadTemplate("review/draft-pr");
@@ -3769,7 +2059,7 @@ function createReviewTools() {
3769
2059
  });
3770
2060
  const launched = await launch(review.cwd, pr.number);
3771
2061
  const openMessage = launched.launched ? `Opened PR/MR #${pr.number} in tuicr (${launched.via}).` : launched.instruction ?? `Run: ${launched.command}`;
3772
- return result2(`Draft PR/MR created from ${template.source} template: ${pr.url}
2062
+ return result(`Draft PR/MR created from ${template.source} template: ${pr.url}
3773
2063
  ${openMessage}`, {
3774
2064
  pr,
3775
2065
  template,
@@ -3777,13 +2067,13 @@ ${openMessage}`, {
3777
2067
  });
3778
2068
  }
3779
2069
  }),
3780
- defineTool4({
2070
+ defineTool3({
3781
2071
  name: "review_edit",
3782
2072
  label: "review edit",
3783
2073
  description: "Open an existing local tuicr session or remote PR/MR in tuicr without generating findings.",
3784
2074
  promptSnippet: "Call review_edit to continue an existing review",
3785
2075
  promptGuidelines: ["This tool never creates review findings or a PR/MR."],
3786
- parameters: parameters2(localSchema),
2076
+ parameters: parameters(localSchema),
3787
2077
  executionMode: "sequential",
3788
2078
  async execute(_id, input) {
3789
2079
  const params = localSchema.parse(input);
@@ -3791,46 +2081,46 @@ ${openMessage}`, {
3791
2081
  await ensureStore(review.cwd);
3792
2082
  if (params.local || params.workingTree) {
3793
2083
  if (!await resolveTuicrSession(review, true))
3794
- return result2("No local tuicr review exists. Use review_new with local=true to create one.");
2084
+ return result("No local tuicr review exists. Use review_new with local=true to create one.");
3795
2085
  return launchLocalReview(review, true);
3796
2086
  }
3797
2087
  if (!review.forge)
3798
- return result2(unsupportedForgeMessage());
2088
+ return result(unsupportedForgeMessage());
3799
2089
  if (!review.pr)
3800
- return result2("No remote PR/MR exists. Use review_new to create a draft review first.");
2090
+ return result("No remote PR/MR exists. Use review_new to create a draft review first.");
3801
2091
  return launchLocalReview(review, false);
3802
2092
  }
3803
2093
  }),
3804
- defineTool4({
2094
+ defineTool3({
3805
2095
  name: "review_diff",
3806
2096
  label: "review diff",
3807
2097
  description: "Fetch the target PR/MR diff or auto-detected local working-tree diff.",
3808
2098
  promptSnippet: "Call review_diff for the code under review",
3809
2099
  promptGuidelines: ["Ground findings in this diff."],
3810
- parameters: parameters2(localSchema),
2100
+ parameters: parameters(localSchema),
3811
2101
  executionMode: "parallel",
3812
2102
  async execute(_id, input) {
3813
2103
  const params = localSchema.parse(input);
3814
2104
  const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
3815
2105
  if (params.local || params.workingTree) {
3816
2106
  const diff = await workingTreeDiff(review.cwd);
3817
- return result2(diff || "No working-tree changes.", { diff, target: "local" });
2107
+ return result(diff || "No working-tree changes.", { diff, target: "local" });
3818
2108
  }
3819
2109
  if (!review.forge)
3820
- return result2(unsupportedForgeMessage());
2110
+ return result(unsupportedForgeMessage());
3821
2111
  if (!review.pr)
3822
- return result2("No PR/MR matches this remote review target.");
2112
+ return result("No PR/MR matches this remote review target.");
3823
2113
  const diff = await review.forge.prDiff(review.pr.number);
3824
- return result2(diff || "Empty diff.", { diff, pr: review.pr });
2114
+ return result(diff || "Empty diff.", { diff, pr: review.pr });
3825
2115
  }
3826
2116
  }),
3827
- defineTool4({
2117
+ defineTool3({
3828
2118
  name: "review_gates",
3829
2119
  label: "review gates",
3830
2120
  description: "Run format, lint, test, conventional-subject, and available CI checks.",
3831
2121
  promptSnippet: "Call review_gates before submitting findings",
3832
2122
  promptGuidelines: ["Report skipped gates as skipped."],
3833
- parameters: parameters2(localSchema),
2123
+ parameters: parameters(localSchema),
3834
2124
  executionMode: "parallel",
3835
2125
  async execute(_id, input) {
3836
2126
  const params = localSchema.parse(input);
@@ -3842,19 +2132,19 @@ ${openMessage}`, {
3842
2132
  gates.push(checkConventionalSubject(subject));
3843
2133
  if (!params.local && review.pr && review.forge)
3844
2134
  gates.push(ciGate(await review.forge.prChecks(review.pr.number)));
3845
- return result2(gates.map((gate) => `- ${gate.name}: ${gate.status} — ${gate.detail}`).join(`
2135
+ return result(gates.map((gate) => `- ${gate.name}: ${gate.status} — ${gate.detail}`).join(`
3846
2136
  `), {
3847
2137
  results: gates
3848
2138
  });
3849
2139
  }
3850
2140
  }),
3851
- defineTool4({
2141
+ defineTool3({
3852
2142
  name: "review_submit",
3853
2143
  label: "review submit",
3854
2144
  description: "Write the review artifact and stage comments in the selected local or remote backend.",
3855
2145
  promptSnippet: "Call review_submit with the findings JSON",
3856
2146
  promptGuidelines: ["Remote comments remain pending until review_publish."],
3857
- parameters: parameters2(submitSchema),
2147
+ parameters: parameters(submitSchema),
3858
2148
  executionMode: "sequential",
3859
2149
  async execute(_id, input, _signal, _onUpdate, ctx) {
3860
2150
  const params = submitSchema.parse(input);
@@ -3862,12 +2152,12 @@ ${openMessage}`, {
3862
2152
  const model = modelRoute(ctx);
3863
2153
  const useLocalBackend = Boolean(params.local || params.workingTree);
3864
2154
  if (!useLocalBackend && !review.forge)
3865
- return result2(unsupportedForgeMessage());
2155
+ return result(unsupportedForgeMessage());
3866
2156
  const findings = dedupeFindings(params.findings);
3867
2157
  const gates = await runMiseGates(review.cwd);
3868
2158
  const baseRef = review.pr?.baseRef ?? (review.forge ? await review.forge.defaultBranch() : "local");
3869
2159
  const artifact = await newReviewArtifactPath(review.cwd, params.local || params.workingTree || !review.pr ? "uncommitted" : await reviewTargetId(review));
3870
- await writeFile7(artifact, renderReviewDoc({
2160
+ await writeFile5(artifact, renderReviewDoc({
3871
2161
  title: params.title ?? review.pr?.title ?? review.vcs.branch,
3872
2162
  number: useLocalBackend ? undefined : review.pr?.number,
3873
2163
  url: useLocalBackend ? undefined : review.pr?.url,
@@ -3885,7 +2175,7 @@ ${openMessage}`, {
3885
2175
  if (useLocalBackend) {
3886
2176
  const session = await resolveTuicrSession(review, Boolean(params.local || params.workingTree));
3887
2177
  if (!session) {
3888
- return result2(`Review written: ${artifact}. Open tuicr, then call review_submit again to seed comments.`, {
2178
+ return result(`Review written: ${artifact}. Open tuicr, then call review_submit again to seed comments.`, {
3889
2179
  artifact,
3890
2180
  count: findings.length
3891
2181
  });
@@ -3896,28 +2186,28 @@ ${openMessage}`, {
3896
2186
  author: localReviewAuthor(model)
3897
2187
  });
3898
2188
  await backend.stage({ comments, body });
3899
- return result2(`Local review staged in tuicr: ${artifact}`, { artifact, count: findings.length, session });
2189
+ return result(`Local review staged in tuicr: ${artifact}`, { artifact, count: findings.length, session });
3900
2190
  }
3901
2191
  if (!review.pr)
3902
- return result2(`Review written: ${artifact}. No PR/MR matches this remote target.`, { artifact });
2192
+ return result(`Review written: ${artifact}. No PR/MR matches this remote target.`, { artifact });
3903
2193
  const hasDraft = hasReviewDraft(comments, body);
3904
2194
  if (hasDraft) {
3905
2195
  await createRemoteReviewBackend(review.vcs, review.pr.number).stage({ comments, body });
3906
2196
  }
3907
- return result2(`${hasDraft ? "Pending review staged" : "Clean review recorded"} on #${review.pr.number}. Artifact: ${artifact}`, {
2197
+ return result(`${hasDraft ? "Pending review staged" : "Clean review recorded"} on #${review.pr.number}. Artifact: ${artifact}`, {
3908
2198
  artifact,
3909
2199
  pr: review.pr,
3910
2200
  count: findings.length
3911
2201
  });
3912
2202
  }
3913
2203
  }),
3914
- defineTool4({
2204
+ defineTool3({
3915
2205
  name: "review_add_comment",
3916
2206
  label: "review add comment",
3917
2207
  description: "Add one provenance-marked comment through tuicr or the remote pending-review backend.",
3918
2208
  promptSnippet: "Call review_add_comment for incremental comments",
3919
2209
  promptGuidelines: ["Pass local=true when tuicr owns the draft."],
3920
- parameters: parameters2(addCommentSchema),
2210
+ parameters: parameters(addCommentSchema),
3921
2211
  executionMode: "sequential",
3922
2212
  async execute(_id, input, _signal, _onUpdate, ctx) {
3923
2213
  const params = addCommentSchema.parse(input);
@@ -3925,7 +2215,7 @@ ${openMessage}`, {
3925
2215
  const model = modelRoute(ctx);
3926
2216
  const useLocalBackend = Boolean(params.local || params.workingTree);
3927
2217
  if (!useLocalBackend && !review.forge)
3928
- return result2(unsupportedForgeMessage());
2218
+ return result(unsupportedForgeMessage());
3929
2219
  const comment = {
3930
2220
  file: params.file,
3931
2221
  line: params.line,
@@ -3935,28 +2225,28 @@ ${openMessage}`, {
3935
2225
  if (useLocalBackend) {
3936
2226
  const session = await resolveTuicrSession(review, Boolean(params.local || params.workingTree));
3937
2227
  if (!session)
3938
- return result2("No matching tuicr session. Open review_new or review_launch_ui first.");
2228
+ return result("No matching tuicr session. Open review_new or review_launch_ui first.");
3939
2229
  const backend = createLocalReviewBackend({
3940
2230
  session: session.path,
3941
2231
  artifactPath: "",
3942
2232
  author: localReviewAuthor(model)
3943
2233
  });
3944
2234
  await backend.stage({ comments: [comment], body: "" });
3945
- return result2(`Comment added to tuicr session ${session.slug}.`, { session, comment });
2235
+ return result(`Comment added to tuicr session ${session.slug}.`, { session, comment });
3946
2236
  }
3947
2237
  if (!review.pr)
3948
- return result2("No PR/MR matches this remote review target.");
2238
+ return result("No PR/MR matches this remote review target.");
3949
2239
  await createRemoteReviewBackend(review.vcs, review.pr.number).stage({ comments: [comment], body: "" });
3950
- return result2(`Draft comment added to #${review.pr.number}.`, { pr: review.pr, comment });
2240
+ return result(`Draft comment added to #${review.pr.number}.`, { pr: review.pr, comment });
3951
2241
  }
3952
2242
  }),
3953
- defineTool4({
2243
+ defineTool3({
3954
2244
  name: "review_comments",
3955
2245
  label: "review comments",
3956
2246
  description: "Pull review threads or local tuicr comments and write a target-named artifact.",
3957
2247
  promptSnippet: "Call review_comments before addressing findings",
3958
2248
  promptGuidelines: ["Pass local=true to prepare the local reply overlay."],
3959
- parameters: parameters2(localSchema),
2249
+ parameters: parameters(localSchema),
3960
2250
  executionMode: "sequential",
3961
2251
  async execute(_id, input) {
3962
2252
  const params = localSchema.parse(input);
@@ -3966,28 +2256,28 @@ ${openMessage}`, {
3966
2256
  if (params.local || params.workingTree) {
3967
2257
  const session = await resolveTuicrSession(review, true);
3968
2258
  if (!session)
3969
- return result2("No matching tuicr session found.");
2259
+ return result("No matching tuicr session found.");
3970
2260
  artifact = await localThreadArtifactPath(review.cwd, session.slug);
3971
2261
  threads = await syncLocalThreadArtifact(artifact, review.pr?.title ?? review.vcs.branch, session.slug, toLocalReviewThreads(await readSession(session.path)));
3972
2262
  } else if (review.pr && review.forge) {
3973
2263
  threads = await createRemoteReviewBackend(review.vcs, review.pr.number).listThreads();
3974
2264
  const target = await reviewTargetId(review);
3975
2265
  artifact = await newReviewArtifactPath(review.cwd, target);
3976
- await writeFile7(artifact, renderThreadArtifact(review.pr.title, target, threads, { number: review.pr.number, url: review.pr.url }), "utf8");
2266
+ await writeFile5(artifact, renderThreadArtifact(review.pr.title, target, threads, { number: review.pr.number, url: review.pr.url }), "utf8");
3977
2267
  } else {
3978
- return result2(review.forge ? "No PR/MR matches this remote review target." : unsupportedForgeMessage());
2268
+ return result(review.forge ? "No PR/MR matches this remote review target." : unsupportedForgeMessage());
3979
2269
  }
3980
- return result2(threads.map((thread) => `${thread.id} ${thread.file ?? "review"}:${thread.line ?? "-"} — ${thread.body}`).join(`
2270
+ return result(threads.map((thread) => `${thread.id} ${thread.file ?? "review"}:${thread.line ?? "-"} — ${thread.body}`).join(`
3981
2271
  `) || "No comments.", { artifact, threads });
3982
2272
  }
3983
2273
  }),
3984
- defineTool4({
2274
+ defineTool3({
3985
2275
  name: "review_respond",
3986
2276
  label: "review respond",
3987
2277
  description: "Post local question replies to tuicr and record their publish overlay, or reply to remote threads.",
3988
2278
  promptSnippet: "Call review_respond after addressing a comment",
3989
2279
  promptGuidelines: ["Question replies remain unresolved."],
3990
- parameters: parameters2(respondSchema),
2280
+ parameters: parameters(respondSchema),
3991
2281
  executionMode: "sequential",
3992
2282
  async execute(_id, input, _signal, _onUpdate, ctx) {
3993
2283
  const params = respondSchema.parse(input);
@@ -3996,10 +2286,10 @@ ${openMessage}`, {
3996
2286
  if (params.local) {
3997
2287
  const tuicrSession = await resolveTuicrSession(review, true);
3998
2288
  if (!tuicrSession)
3999
- return result2("No matching tuicr session. Open review_new or review_launch_ui first.");
2289
+ return result("No matching tuicr session. Open review_new or review_launch_ui first.");
4000
2290
  const artifact = await localThreadArtifactPath(review.cwd, tuicrSession.slug);
4001
2291
  if (!existsSync2(artifact))
4002
- return result2("No local review artifact. Run review_comments with local=true first.");
2292
+ return result("No local review artifact. Run review_comments with local=true first.");
4003
2293
  const threads = await readThreadArtifact(artifact);
4004
2294
  const known = threads.find((thread) => thread.id === params.threadId);
4005
2295
  const question = known?.question === true || params.question === true || !known && params.question === undefined;
@@ -4021,10 +2311,10 @@ ${params.body}`, {
4021
2311
  resolve: false,
4022
2312
  question
4023
2313
  });
4024
- return result2(`Local ${question ? "answer" : "response"} posted to tuicr and recorded in ${artifact}${awaitUserDeletion ? "; delete the source comment in tuicr to resolve it" : ""}.`, { artifact, question, awaitUserDeletion, session: tuicrSession });
2314
+ return result(`Local ${question ? "answer" : "response"} posted to tuicr and recorded in ${artifact}${awaitUserDeletion ? "; delete the source comment in tuicr to resolve it" : ""}.`, { artifact, question, awaitUserDeletion, session: tuicrSession });
4025
2315
  }
4026
2316
  if (!review.pr || !review.forge)
4027
- return result2("No remote PR/MR for this reply.");
2317
+ return result("No remote PR/MR for this reply.");
4028
2318
  const remote = createRemoteReviewBackend(review.vcs, review.pr.number);
4029
2319
  const remoteThreads = await remote.listThreads();
4030
2320
  const known = remoteThreads.find((thread) => thread.id === params.threadId);
@@ -4036,35 +2326,35 @@ ${params.body}`, {
4036
2326
  resolve,
4037
2327
  question
4038
2328
  });
4039
- return result2(`Replied to ${params.threadId}${resolve ? " and resolved it" : " and left it open"}.`, {
2329
+ return result(`Replied to ${params.threadId}${resolve ? " and resolved it" : " and left it open"}.`, {
4040
2330
  pr: review.pr,
4041
2331
  resolve
4042
2332
  });
4043
2333
  }
4044
2334
  }),
4045
- defineTool4({
2335
+ defineTool3({
4046
2336
  name: "review_publish",
4047
2337
  label: "review publish",
4048
2338
  description: "Publish pending remote draft PR/MR review comments and status, or promote a local tuicr draft before publishing.",
4049
2339
  promptSnippet: "Call review_publish to make remote review work public",
4050
2340
  promptGuidelines: ["Statuses are COMMENT, APPROVE, REQUEST_CHANGES, or CLOSE."],
4051
- parameters: parameters2(publishSchema),
2341
+ parameters: parameters(publishSchema),
4052
2342
  executionMode: "sequential",
4053
2343
  async execute(_id, input, _signal, _onUpdate, ctx) {
4054
2344
  const params = publishSchema.parse(input);
4055
2345
  const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
4056
2346
  if (!review.pr || !review.forge)
4057
- return result2("No remote PR/MR to publish.");
2347
+ return result("No remote PR/MR to publish.");
4058
2348
  return publishResolvedReview(review, params, modelRoute(ctx));
4059
2349
  }
4060
2350
  }),
4061
- defineTool4({
2351
+ defineTool3({
4062
2352
  name: "review_complete",
4063
2353
  label: "review complete",
4064
2354
  description: "Approve, reject, or abandon a remote review, or archive a local review artifact.",
4065
2355
  promptSnippet: "Call review_complete to finish a review without merging it.",
4066
2356
  promptGuidelines: ["A local completion archives the Diffpi artifact and deletes the matching tuicr session."],
4067
- parameters: parameters2(completeSchema),
2357
+ parameters: parameters(completeSchema),
4068
2358
  executionMode: "sequential",
4069
2359
  async execute(_id, input, _signal, _onUpdate, ctx) {
4070
2360
  const params = completeSchema.parse(input);
@@ -4072,63 +2362,63 @@ ${params.body}`, {
4072
2362
  if (params.local) {
4073
2363
  const session = await resolveTuicrSession(review, true);
4074
2364
  if (!session)
4075
- return result2("No matching local tuicr session to close.");
2365
+ return result("No matching local tuicr session to close.");
4076
2366
  const artifact = await localThreadArtifactPath(review.cwd, session.slug);
4077
2367
  if (!existsSync2(artifact))
4078
- return result2("No local review artifact. Run review_comments with local=true first.");
2368
+ return result("No local review artifact. Run review_comments with local=true first.");
4079
2369
  const archived = await uniqueRecordPath(await completedReviewsDir(review.cwd), reviewSlug(session.slug));
4080
- await rename4(artifact, archived);
2370
+ await rename2(artifact, archived);
4081
2371
  await unlink2(session.path);
4082
- return result2(`Archived ${archived} and deleted the tuicr session ${session.slug}.`, {
2372
+ return result(`Archived ${archived} and deleted the tuicr session ${session.slug}.`, {
4083
2373
  artifact: archived,
4084
2374
  session
4085
2375
  });
4086
2376
  }
4087
2377
  if (!review.pr || !review.forge)
4088
- return result2("No remote PR/MR to complete.");
2378
+ return result("No remote PR/MR to complete.");
4089
2379
  if (!params.action)
4090
- return result2("Choose a complete action: approve, reject, or abandon.");
2380
+ return result("Choose a complete action: approve, reject, or abandon.");
4091
2381
  const status = { approve: "APPROVE", reject: "REQUEST_CHANGES", abandon: "CLOSE" }[params.action];
4092
2382
  return publishResolvedReview(review, { ...params, status }, modelRoute(ctx));
4093
2383
  }
4094
2384
  }),
4095
- defineTool4({
2385
+ defineTool3({
4096
2386
  name: "review_merge",
4097
2387
  label: "review merge",
4098
2388
  description: "Squash-merge an approved GitHub PR after checking its conventional subject.",
4099
2389
  promptSnippet: "Call review_merge only after review_publish APPROVE",
4100
2390
  promptGuidelines: ["This is intentionally GitHub-only until GitLab merge support is added."],
4101
- parameters: parameters2(contextSchema.extend({ subject: z8.string().optional() })),
2391
+ parameters: parameters(contextSchema.extend({ subject: z6.string().optional() })),
4102
2392
  executionMode: "sequential",
4103
2393
  async execute(_id, input) {
4104
- const params = contextSchema.extend({ subject: z8.string().optional() }).parse(input);
2394
+ const params = contextSchema.extend({ subject: z6.string().optional() }).parse(input);
4105
2395
  const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
4106
2396
  if (review.vcs.provider !== "github" || !review.forge)
4107
- return result2("review_merge currently supports GitHub only.");
2397
+ return result("review_merge currently supports GitHub only.");
4108
2398
  if (!review.pr)
4109
- return result2("No open PR/MR for this branch.");
2399
+ return result("No open PR/MR for this branch.");
4110
2400
  const subject = params.subject ?? review.pr.title;
4111
2401
  const guard = checkConventionalSubject(subject);
4112
2402
  if (guard.status !== "pass")
4113
- return result2(`Merge blocked: ${guard.detail}`, { pr: review.pr, guard });
2403
+ return result(`Merge blocked: ${guard.detail}`, { pr: review.pr, guard });
4114
2404
  await review.forge.mergePr(review.pr.number, subject);
4115
- return result2(`Merged #${review.pr.number} with subject: ${subject}.`, { pr: review.pr, guard });
2405
+ return result(`Merged #${review.pr.number} with subject: ${subject}.`, { pr: review.pr, guard });
4116
2406
  }
4117
2407
  }),
4118
- defineTool4({
2408
+ defineTool3({
4119
2409
  name: "review_launch_ui",
4120
2410
  label: "review launch UI",
4121
2411
  description: "Open a tuicr UI in a mux tab, configure Zed, or return the command to run.",
4122
2412
  promptSnippet: "Call review_launch_ui for the interactive tuicr TUI",
4123
2413
  promptGuidelines: ["Show the returned command when launch cannot open a tab."],
4124
- parameters: parameters2(localSchema),
2414
+ parameters: parameters(localSchema),
4125
2415
  executionMode: "sequential",
4126
2416
  async execute(_id, input) {
4127
2417
  const params = localSchema.parse(input);
4128
2418
  const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
4129
2419
  const workingTree = Boolean(params.local || params.workingTree);
4130
2420
  if (!workingTree && !review.forge)
4131
- return result2(unsupportedForgeMessage());
2421
+ return result(unsupportedForgeMessage());
4132
2422
  return launchLocalReview(review, workingTree);
4133
2423
  }
4134
2424
  })
@@ -4137,7 +2427,7 @@ ${params.body}`, {
4137
2427
  async function syncLocalThreadArtifact(path, title, sessionSlug, threads) {
4138
2428
  let previous = [];
4139
2429
  try {
4140
- previous = parseThreadArtifact(await readFile12(path, "utf8"));
2430
+ previous = parseThreadArtifact(await readFile7(path, "utf8"));
4141
2431
  } catch (error) {
4142
2432
  if (error.code !== "ENOENT")
4143
2433
  throw error;
@@ -4155,16 +2445,16 @@ async function syncLocalThreadArtifact(path, title, sessionSlug, threads) {
4155
2445
  });
4156
2446
  const removed = previous.filter((thread) => !currentIds.has(thread.id)).map((thread) => ({ ...thread, resolved: true }));
4157
2447
  const merged = [...current, ...removed];
4158
- await writeFile7(path, renderThreadArtifact(title, sessionSlug, merged), "utf8");
2448
+ await writeFile5(path, renderThreadArtifact(title, sessionSlug, merged), "utf8");
4159
2449
  return merged;
4160
2450
  }
4161
- function parameters2(schema) {
4162
- return z8.toJSONSchema(schema, { io: "input" });
2451
+ function parameters(schema) {
2452
+ return z6.toJSONSchema(schema, { io: "input" });
4163
2453
  }
4164
2454
  function resolveWorkingDirectory(params) {
4165
2455
  return params.cwd ?? process.cwd();
4166
2456
  }
4167
- function result2(text, details = {}) {
2457
+ function result(text, details = {}) {
4168
2458
  return { content: [{ type: "text", text }], details };
4169
2459
  }
4170
2460
  function unsupportedForgeMessage() {
@@ -4199,7 +2489,7 @@ async function publishResolvedReview(review, params, model) {
4199
2489
  const promotedBodies = promotion?.promotedBodies ?? 0;
4200
2490
  const promotedComments = promotion?.promotedComments ?? 0;
4201
2491
  const promotedReplies = promotion?.promotedReplies ?? 0;
4202
- return result2(`Published #${review.pr.number} (${status}); promoted ${promotedBodies} review bodies, ${promotedComments} comments, and ${promotedReplies} replies.`, { pr: finalPr ?? review.pr, status, promotedBodies, promotedComments, promotedReplies });
2492
+ return result(`Published #${review.pr.number} (${status}); promoted ${promotedBodies} review bodies, ${promotedComments} comments, and ${promotedReplies} replies.`, { pr: finalPr ?? review.pr, status, promotedBodies, promotedComments, promotedReplies });
4203
2493
  }
4204
2494
  async function promoteLocalReview(review, remote, model, workingTree) {
4205
2495
  const publication = await loadReviewPublicationState(review.cwd, review.vcs, review.pr.number);
@@ -4288,7 +2578,7 @@ async function launchLocalReview(review, workingTree, requestedBase) {
4288
2578
  const base = workingTree ? requestedBase ?? review.pr?.baseRef ?? (review.forge ? await review.forge.defaultBranch() : undefined) : undefined;
4289
2579
  const launched = await launch(review.cwd, workingTree ? undefined : review.pr?.number, base);
4290
2580
  const commandTarget = workingTree || !review.pr ? "full branch" : `PR/MR #${review.pr.number}`;
4291
- return result2(launched.launched ? `Opened ${commandTarget} in tuicr (${launched.via}).` : launched.instruction ?? `Run: ${launched.command}`, { launched, pr: review.pr, target: commandTarget });
2581
+ return result(launched.launched ? `Opened ${commandTarget} in tuicr (${launched.via}).` : launched.instruction ?? `Run: ${launched.command}`, { launched, pr: review.pr, target: commandTarget });
4292
2582
  }
4293
2583
  async function assertRemoteBranchReady(cwd) {
4294
2584
  const dirty = await runChecked("git", ["-C", cwd, "status", "--porcelain"]);
@@ -4320,16 +2610,16 @@ async function headSha(cwd) {
4320
2610
  }
4321
2611
  async function newReviewArtifactPath(cwd, target) {
4322
2612
  const dir = await reviewsDir(cwd);
4323
- await mkdir6(dir, { recursive: true });
2613
+ await mkdir4(dir, { recursive: true });
4324
2614
  return uniqueRecordPath(dir, reviewRecordName(target));
4325
2615
  }
4326
2616
  async function localThreadArtifactPath(cwd, sessionSlug) {
4327
2617
  const dir = await reviewsDir(cwd);
4328
- return join14(dir, `${reviewSlug(sessionSlug) || "local-review"}.md`);
2618
+ return join9(dir, `${reviewSlug(sessionSlug) || "local-review"}.md`);
4329
2619
  }
4330
2620
  async function readThreadArtifact(path) {
4331
2621
  try {
4332
- return parseThreadArtifact(await readFile12(path, "utf8"));
2622
+ return parseThreadArtifact(await readFile7(path, "utf8"));
4333
2623
  } catch (error) {
4334
2624
  if (error.code === "ENOENT") {
4335
2625
  throw new Error(`Expected local reply overlay is missing: ${path}`);
@@ -4353,9 +2643,9 @@ async function findLatestThreadArtifact(cwd, review, target) {
4353
2643
  return [];
4354
2644
  return [
4355
2645
  (async () => {
4356
- const path = join14(dir, name);
2646
+ const path = join9(dir, name);
4357
2647
  const info = await stat(path);
4358
- return { path, name, content: await readFile12(path, "utf8"), modified: info.mtimeMs };
2648
+ return { path, name, content: await readFile7(path, "utf8"), modified: info.mtimeMs };
4359
2649
  })()
4360
2650
  ];
4361
2651
  }));
@@ -4369,7 +2659,7 @@ async function findLatestThreadArtifact(cwd, review, target) {
4369
2659
  }
4370
2660
  async function listReviewArtifactNames(dir) {
4371
2661
  try {
4372
- return await readdir2(dir);
2662
+ return await readdir(dir);
4373
2663
  } catch (error) {
4374
2664
  throw new Error(`Cannot read review artifacts in ${dir}.`, { cause: error });
4375
2665
  }
@@ -4378,10 +2668,10 @@ function deriveTitle(branch) {
4378
2668
  return branch.replace(/^(feature|feat|fix|bug|chore)\//, "").replace(/^eng-\d+-/i, "").replace(/[-_]+/g, " ").replace(/^\w/, (char) => char.toUpperCase());
4379
2669
  }
4380
2670
  function uniqueRecordPath(dir, base) {
4381
- let path = join14(dir, `${base}.md`);
2671
+ let path = join9(dir, `${base}.md`);
4382
2672
  let count = 2;
4383
2673
  while (existsSync2(path))
4384
- path = join14(dir, `${base}-${count++}.md`);
2674
+ path = join9(dir, `${base}-${count++}.md`);
4385
2675
  return path;
4386
2676
  }
4387
2677
  function artifactNameMatches(name, suffix) {
@@ -4397,26 +2687,26 @@ function artifactNameMatches(name, suffix) {
4397
2687
  }
4398
2688
 
4399
2689
  // src/tools/setup.ts
4400
- import { defineTool as defineTool5 } from "@earendil-works/pi-coding-agent";
4401
- import { z as z10 } from "zod";
2690
+ import { defineTool as defineTool4 } from "@earendil-works/pi-coding-agent";
2691
+ import { z as z8 } from "zod";
4402
2692
 
4403
2693
  // src/setup.ts
4404
2694
  import { parseFrontmatter as parseFrontmatter2 } from "@earendil-works/pi-coding-agent";
4405
- import { readdir as readdir4, readFile as readFile15 } from "node:fs/promises";
2695
+ import { readdir as readdir3, readFile as readFile10 } from "node:fs/promises";
4406
2696
  import { homedir as homedir8 } from "node:os";
4407
- import { basename as basename6, join as join18 } from "node:path";
2697
+ import { basename as basename4, join as join13 } from "node:path";
4408
2698
 
4409
2699
  // src/config.ts
4410
2700
  import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
4411
- import { z as z9 } from "zod";
2701
+ import { z as z7 } from "zod";
4412
2702
  import { homedir as homedir5 } from "node:os";
4413
- import { join as join15 } from "node:path";
2703
+ import { join as join10 } from "node:path";
4414
2704
 
4415
2705
  // src/fsx.ts
4416
- import { readdir as readdir3, readFile as readFile13 } from "node:fs/promises";
2706
+ import { readdir as readdir2, readFile as readFile8 } from "node:fs/promises";
4417
2707
  async function readTextIfExists(path) {
4418
2708
  try {
4419
- return await readFile13(path, "utf8");
2709
+ return await readFile8(path, "utf8");
4420
2710
  } catch (error) {
4421
2711
  if (isMissingPath(error))
4422
2712
  return;
@@ -4428,18 +2718,18 @@ function isMissingPath(error) {
4428
2718
  }
4429
2719
 
4430
2720
  // src/config.ts
4431
- var modelReferenceSchema = z9.string().trim().min(1);
4432
- var agentConfigSchema = z9.object({
4433
- models: z9.array(modelReferenceSchema).optional()
2721
+ var modelReferenceSchema = z7.string().trim().min(1);
2722
+ var agentConfigSchema = z7.object({
2723
+ models: z7.array(modelReferenceSchema).optional()
4434
2724
  }).strict();
4435
- var diffpiConfigSchema = z9.object({
4436
- agents: z9.record(z9.string(), agentConfigSchema).optional()
2725
+ var diffpiConfigSchema = z7.object({
2726
+ agents: z7.record(z7.string(), agentConfigSchema).optional()
4437
2727
  }).strict();
4438
2728
  function diffpiConfigPaths(homeDir = homedir5()) {
4439
- const directory = join15(homeDir, ".difflab", "diffpi");
2729
+ const directory = join10(homeDir, ".difflab", "diffpi");
4440
2730
  return {
4441
- yaml: join15(directory, "config.yaml"),
4442
- json: join15(directory, "config.json")
2731
+ yaml: join10(directory, "config.yaml"),
2732
+ json: join10(directory, "config.json")
4443
2733
  };
4444
2734
  }
4445
2735
  async function loadDiffpiConfig(options = {}) {
@@ -4495,12 +2785,12 @@ function normalizeModelReference(value) {
4495
2785
  }
4496
2786
 
4497
2787
  // src/mcp.ts
4498
- import { mkdir as mkdir7, readFile as readFile14, writeFile as writeFile8 } from "node:fs/promises";
2788
+ import { mkdir as mkdir5, readFile as readFile9, writeFile as writeFile6 } from "node:fs/promises";
4499
2789
  import { homedir as homedir6 } from "node:os";
4500
- import { dirname as dirname7, join as join16 } from "node:path";
2790
+ import { dirname as dirname5, join as join11 } from "node:path";
4501
2791
  var mcp = {
4502
2792
  globalConfigPath(homeDir = homedir6()) {
4503
- return join16(homeDir, ".config", "mcp", "mcp.json");
2793
+ return join11(homeDir, ".config", "mcp", "mcp.json");
4504
2794
  },
4505
2795
  async serversEnsure(servers, options = {}) {
4506
2796
  const path = options.path ?? mcp.globalConfigPath();
@@ -4513,8 +2803,8 @@ var mcp = {
4513
2803
  const next = { ...current, mcpServers: nextServers };
4514
2804
  const changed = JSON.stringify(current) !== JSON.stringify(next);
4515
2805
  if (changed && !options.dryRun) {
4516
- await mkdir7(dirname7(path), { recursive: true });
4517
- await writeFile8(path, `${JSON.stringify(next, null, 2)}
2806
+ await mkdir5(dirname5(path), { recursive: true });
2807
+ await writeFile6(path, `${JSON.stringify(next, null, 2)}
4518
2808
  `, "utf8");
4519
2809
  }
4520
2810
  return { path, changed, existed: currentText !== undefined, planned: changed && options.dryRun === true };
@@ -4543,7 +2833,7 @@ function getParsedConfig(content, path) {
4543
2833
  }
4544
2834
  async function getOptionalFile2(path) {
4545
2835
  try {
4546
- return await readFile14(path, "utf8");
2836
+ return await readFile9(path, "utf8");
4547
2837
  } catch (error) {
4548
2838
  if (error instanceof Error && "code" in error && error.code === "ENOENT")
4549
2839
  return;
@@ -4555,9 +2845,9 @@ function isRecord(value) {
4555
2845
  }
4556
2846
 
4557
2847
  // src/pi.ts
4558
- import { mkdir as mkdir8, writeFile as writeFile9 } from "node:fs/promises";
2848
+ import { mkdir as mkdir6, writeFile as writeFile7 } from "node:fs/promises";
4559
2849
  import { homedir as homedir7 } from "node:os";
4560
- import { dirname as dirname8, join as join17 } from "node:path";
2850
+ import { dirname as dirname6, join as join12 } from "node:path";
4561
2851
  var pi = {
4562
2852
  executableCheck: findPiExecutable,
4563
2853
  packageList: listPiPackages,
@@ -4570,19 +2860,19 @@ var pi = {
4570
2860
  configEnsure: ensurePiConfig
4571
2861
  };
4572
2862
  async function ensurePiAgent(filename, content, agentDir = resolvePiAgentDir(), dryRun = false) {
4573
- const path = join17(agentDir, "agents", filename);
2863
+ const path = join12(agentDir, "agents", filename);
4574
2864
  const currentText = await readTextIfExists(path);
4575
2865
  const changed = currentText !== content;
4576
2866
  if (changed && !dryRun) {
4577
- await mkdir8(dirname8(path), { recursive: true });
4578
- await writeFile9(path, content, "utf8");
2867
+ await mkdir6(dirname6(path), { recursive: true });
2868
+ await writeFile7(path, content, "utf8");
4579
2869
  }
4580
2870
  return { path, changed, existed: currentText !== undefined, planned: changed && dryRun };
4581
2871
  }
4582
- async function checkGlobalPiSkill(name, agentDir = resolvePiAgentDir(), sharedSkillsDir = join17(homedir7(), ".agents", "skills")) {
4583
- const roots = [join17(agentDir, "skills"), sharedSkillsDir];
2872
+ async function checkGlobalPiSkill(name, agentDir = resolvePiAgentDir(), sharedSkillsDir = join12(homedir7(), ".agents", "skills")) {
2873
+ const roots = [join12(agentDir, "skills"), sharedSkillsDir];
4584
2874
  for (const root of roots) {
4585
- if (await readTextIfExists(join17(root, name, "SKILL.md")) !== undefined)
2875
+ if (await readTextIfExists(join12(root, name, "SKILL.md")) !== undefined)
4586
2876
  return true;
4587
2877
  }
4588
2878
  return false;
@@ -4611,8 +2901,8 @@ async function ensurePiConfig(path, update, dryRun = false) {
4611
2901
  const next = update(current);
4612
2902
  const changed = JSON.stringify(current) !== JSON.stringify(next);
4613
2903
  if (changed && !dryRun) {
4614
- await mkdir8(dirname8(path), { recursive: true });
4615
- await writeFile9(path, `${JSON.stringify(next, null, 2)}
2904
+ await mkdir6(dirname6(path), { recursive: true });
2905
+ await writeFile7(path, `${JSON.stringify(next, null, 2)}
4616
2906
  `, "utf8");
4617
2907
  }
4618
2908
  return { path, changed, existed: currentText !== undefined, planned: changed && dryRun };
@@ -4632,7 +2922,7 @@ async function installPiPackage(executable, source) {
4632
2922
  await runChecked(executable, ["install", source]);
4633
2923
  }
4634
2924
  function resolvePiAgentDir(homeDir = homedir7()) {
4635
- return process.env.PI_CODING_AGENT_DIR ?? (process.env.XDG_CONFIG_HOME ? join17(process.env.XDG_CONFIG_HOME, "pi") : join17(homeDir, ".pi", "agent"));
2925
+ return process.env.PI_CODING_AGENT_DIR ?? (process.env.XDG_CONFIG_HOME ? join12(process.env.XDG_CONFIG_HOME, "pi") : join12(homeDir, ".pi", "agent"));
4636
2926
  }
4637
2927
  function parseJsonObject(content, path) {
4638
2928
  if (!content?.trim())
@@ -4684,7 +2974,7 @@ var FORGE_DEPENDENCIES = {
4684
2974
  };
4685
2975
  async function ensureMise(options = {}) {
4686
2976
  const homeDir = options.homeDir ?? homedir8();
4687
- const current = await mise.executableCheck() ?? await mise.executableCheck(join18(homeDir, ".local", "bin", "mise"));
2977
+ const current = await mise.executableCheck() ?? await mise.executableCheck(join13(homeDir, ".local", "bin", "mise"));
4688
2978
  if (current)
4689
2979
  return { executable: current, action: createSetupAction("mise", "ready", current) };
4690
2980
  reportProgress(options, "Installing mise");
@@ -4733,9 +3023,9 @@ async function ensureMiseDeps(miseExecutable, options = {}) {
4733
3023
  async function ensurePiPlugins(options = {}) {
4734
3024
  const actions = await ensurePiPackages(PI_PACKAGES, options);
4735
3025
  const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
4736
- const webSearch = await pi.configEnsure(join18(agentDir, "web-search.json"), (config) => ({ ...config, workflow: "auto-summary" }), options.dryRun);
3026
+ const webSearch = await pi.configEnsure(join13(agentDir, "web-search.json"), (config) => ({ ...config, workflow: "auto-summary" }), options.dryRun);
4737
3027
  actions.push(getConfigSetupAction("web search settings", webSearch));
4738
- const lsp = await pi.configEnsure(join18(agentDir, "pi-lsp.json"), (config) => ({
3028
+ const lsp = await pi.configEnsure(join13(agentDir, "pi-lsp.json"), (config) => ({
4739
3029
  ...config,
4740
3030
  progressive: { ...getRecord(config.progressive), enabled: true, inject: "none" }
4741
3031
  }), options.dryRun);
@@ -4746,11 +3036,11 @@ async function ensurePiAgents(options = {}) {
4746
3036
  const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
4747
3037
  const bundledAgentsDir = options.bundledAgentsDir ?? BUNDLED_AGENTS_DIR;
4748
3038
  const userConfig = await loadDiffpiConfig({ homeDir: options.homeDir });
4749
- const entries = (await readdir4(bundledAgentsDir, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.startsWith("diffpi-") && entry.name.endsWith(".md")).sort((left, right) => left.name.localeCompare(right.name));
3039
+ 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));
4750
3040
  const actions = [];
4751
3041
  for (const entry of entries) {
4752
- const id = basename6(entry.name, ".md").replace(/^diffpi-/, "");
4753
- const source = await readFile15(join18(bundledAgentsDir, entry.name), "utf8");
3042
+ const id = basename4(entry.name, ".md").replace(/^diffpi-/, "");
3043
+ const source = await readFile10(join13(bundledAgentsDir, entry.name), "utf8");
4754
3044
  const content = materializeAgentModels(source, id, userConfig.config, options.availableModels);
4755
3045
  const result = await pi.agentEnsure(entry.name, content, agentDir, options.dryRun);
4756
3046
  actions.push(getConfigSetupAction(`pi agent ${id}`, result));
@@ -4759,7 +3049,7 @@ async function ensurePiAgents(options = {}) {
4759
3049
  }
4760
3050
  async function ensurePiSkills(miseExecutable, options = {}) {
4761
3051
  const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
4762
- const sharedSkillsDir = join18(options.homeDir ?? homedir8(), ".agents", "skills");
3052
+ const sharedSkillsDir = join13(options.homeDir ?? homedir8(), ".agents", "skills");
4763
3053
  const actions = [];
4764
3054
  for (const source of PI_SKILL_SOURCES) {
4765
3055
  const missing = [];
@@ -4840,8 +3130,7 @@ function setupRequiresRestart(actions) {
4840
3130
  async function ensureZedIntegration(options = {}) {
4841
3131
  if (options.dryRun) {
4842
3132
  const actions = [
4843
- createSetupAction("Zed review tasks", "planned", "global static runtime-resolver tasks in tasks.json"),
4844
- createSetupAction("Zed plan task", "planned", "pinned package CLI task in tasks.json")
3133
+ createSetupAction("Zed review tasks", "planned", "global static runtime-resolver tasks in tasks.json")
4845
3134
  ];
4846
3135
  if (options.bindZedKey)
4847
3136
  actions.push(createSetupAction("Zed review keybinding", "planned", "keymap.json"));
@@ -4851,8 +3140,6 @@ async function ensureZedIntegration(options = {}) {
4851
3140
  try {
4852
3141
  const tasks = await ensureZedReviewTask(options.homeDir);
4853
3142
  actions.push(createSetupAction("Zed review tasks", tasks.changed ? "installed" : "ready", tasks.path));
4854
- const planTask = await ensureZedPlanTask(await packageVersion2(), options.homeDir);
4855
- actions.push(createSetupAction("Zed plan task", planTask.changed ? "installed" : "ready", planTask.path));
4856
3143
  } catch (error) {
4857
3144
  actions.push(createSetupAction("Zed review tasks", "skipped", error instanceof Error ? error.message : String(error)));
4858
3145
  }
@@ -4954,26 +3241,19 @@ function createSetupAction(name, status, detail) {
4954
3241
  function reportProgress(options, message) {
4955
3242
  options.onProgress?.(message);
4956
3243
  }
4957
- async function packageVersion2() {
4958
- const source = await readFile15(join18(resolveBundledAgentsDir(), "..", "package.json"), "utf8");
4959
- const value = JSON.parse(source);
4960
- if (typeof value.version !== "string")
4961
- throw new Error("Cannot resolve the installed @difflab/pi version.");
4962
- return value.version;
4963
- }
4964
3244
  function getRecord(value) {
4965
3245
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
4966
3246
  }
4967
3247
 
4968
3248
  // src/tools/setup.ts
4969
- var setupParametersSchema = z10.object({
4970
- issueTracker: z10.enum(["none", "linear", "jira"]).default("none").describe("Issue tracker MCP server to configure. Use none unless the user explicitly selects Linear or Jira."),
4971
- forges: z10.array(z10.enum(["github", "gitlab"])).default([]).describe("Hosted VCS MCP servers to configure. Installs matching CLIs and registers each MCP server."),
4972
- forge: z10.enum(["none", "github", "gitlab"]).optional().describe("Deprecated single-host alias. Prefer forges for one or more VCS MCP servers."),
4973
- bindZedKey: z10.boolean().default(false).describe("Opt in to a Zed keybinding for the tuicr review task.")
3249
+ var setupParametersSchema = z8.object({
3250
+ issueTracker: z8.enum(["none", "linear", "jira"]).default("none").describe("Issue tracker MCP server to configure. Use none unless the user explicitly selects Linear or Jira."),
3251
+ forges: z8.array(z8.enum(["github", "gitlab"])).default([]).describe("Hosted VCS MCP servers to configure. Installs matching CLIs and registers each MCP server."),
3252
+ forge: z8.enum(["none", "github", "gitlab"]).optional().describe("Deprecated single-host alias. Prefer forges for one or more VCS MCP servers."),
3253
+ bindZedKey: z8.boolean().default(false).describe("Opt in to a Zed keybinding for the tuicr review task.")
4974
3254
  });
4975
- var setupParameters = z10.toJSONSchema(setupParametersSchema, { io: "input" });
4976
- var diffpiSetupTool = defineTool5({
3255
+ var setupParameters = z8.toJSONSchema(setupParametersSchema, { io: "input" });
3256
+ var diffpiSetupTool = defineTool4({
4977
3257
  name: "diffpi_setup",
4978
3258
  label: "diffpi setup",
4979
3259
  description: "Install or repair the @difflab/pi environment. This mutates user-level tool installations and configuration files.",
@@ -5002,7 +3282,7 @@ var diffpiSetupTool = defineTool5({
5002
3282
  return formatResult(result, "Setup complete.");
5003
3283
  }
5004
3284
  });
5005
- var diffpiValidateTool = defineTool5({
3285
+ var diffpiValidateTool = defineTool4({
5006
3286
  name: "diffpi_validate",
5007
3287
  label: "diffpi validate",
5008
3288
  description: "Inspect the @difflab/pi environment without installing software or changing configuration files.",
@@ -5044,20 +3324,20 @@ ${lines.join(`
5044
3324
  }
5045
3325
 
5046
3326
  // src/tools/templates.ts
5047
- import { defineTool as defineTool6 } from "@earendil-works/pi-coding-agent";
5048
- import { z as z11 } from "zod";
5049
- var templateSchema = z11.object({
5050
- name: z11.string().min(1),
5051
- variables: z11.record(z11.string(), z11.string()).optional(),
5052
- homeDir: z11.string().optional()
3327
+ import { defineTool as defineTool5 } from "@earendil-works/pi-coding-agent";
3328
+ import { z as z9 } from "zod";
3329
+ var templateSchema = z9.object({
3330
+ name: z9.string().min(1),
3331
+ variables: z9.record(z9.string(), z9.string()).optional(),
3332
+ homeDir: z9.string().optional()
5053
3333
  });
5054
- var diffpiTemplateTool = defineTool6({
3334
+ var diffpiTemplateTool = defineTool5({
5055
3335
  name: "diffpi_template",
5056
3336
  label: "diffpi template",
5057
3337
  description: "Load a bundled Diffpi template or a user override and render named variables.",
5058
3338
  promptSnippet: "Use diffpi_template for package workflow templates",
5059
3339
  promptGuidelines: ["User overrides live under ~/.difflab/diffpi/templates."],
5060
- parameters: z11.toJSONSchema(templateSchema, { io: "input" }),
3340
+ parameters: z9.toJSONSchema(templateSchema, { io: "input" }),
5061
3341
  executionMode: "parallel",
5062
3342
  async execute(_id, input) {
5063
3343
  const params = templateSchema.parse(input);
@@ -5078,15 +3358,13 @@ function createPiTools(pi, modes) {
5078
3358
  createDiffpiReloadTool(pi),
5079
3359
  diffpiTemplateTool,
5080
3360
  ...createModeTools(modes),
5081
- ...createReviewTools(),
5082
- ...createPlanTools(pi, modes)
3361
+ ...createReviewTools()
5083
3362
  ];
5084
3363
  }
5085
3364
  export {
5086
3365
  createDiffpiReloadTool,
5087
3366
  createModeTools,
5088
3367
  createPiTools,
5089
- createPlanTools,
5090
3368
  createReviewTools,
5091
3369
  diffpiSetupTool,
5092
3370
  diffpiTemplateTool,