@difflab/pi 0.8.0 → 0.9.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 (67) hide show
  1. package/README.md +8 -10
  2. package/agents/diffpi-orchestrator.md +1 -1
  3. package/agents/diffpi-planner.md +2 -2
  4. package/agents/diffpi-reviewer.md +2 -2
  5. package/agents/diffpi-worker.md +6 -6
  6. package/dist/environment.d.ts +0 -1
  7. package/dist/environment.d.ts.map +1 -1
  8. package/dist/extensions/index.js +319 -1495
  9. package/dist/index.d.ts +5 -9
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +140 -759
  12. package/dist/log.d.ts +6 -11
  13. package/dist/log.d.ts.map +1 -1
  14. package/dist/plan/verify.d.ts.map +1 -1
  15. package/dist/review/index.d.ts +2 -3
  16. package/dist/review/index.d.ts.map +1 -1
  17. package/dist/review/review-backend.d.ts +2 -3
  18. package/dist/review/review-backend.d.ts.map +1 -1
  19. package/dist/setup.d.ts +0 -2
  20. package/dist/setup.d.ts.map +1 -1
  21. package/dist/tools/index.js +319 -1495
  22. package/dist/tools/log.d.ts.map +1 -1
  23. package/dist/tools/review.d.ts +1 -7
  24. package/dist/tools/review.d.ts.map +1 -1
  25. package/dist/tools/setup.d.ts.map +1 -1
  26. package/package.json +1 -5
  27. package/skills/diffpi-setup/SKILL.md +3 -3
  28. package/skills/plan/SKILL.md +3 -4
  29. package/skills/plan/references/design-quality.md +1 -1
  30. package/skills/plan/references/workflows/finalize.md +2 -2
  31. package/skills/plan/references/workflows/go.md +7 -5
  32. package/skills/plan/references/workflows/help.md +1 -2
  33. package/skills/plan/references/workflows/init.md +2 -2
  34. package/skills/plan/references/workflows/inline-fallback.md +3 -3
  35. package/skills/plan/references/workflows/new.md +3 -3
  36. package/skills/plan/references/workflows/update.md +6 -4
  37. package/skills/plan/references/workflows/validate.md +5 -5
  38. package/skills/review/SKILL.md +5 -5
  39. package/skills/review/references/review-standards.md +9 -1
  40. package/skills/review/references/workflows/address.md +9 -15
  41. package/skills/review/references/workflows/auto.md +9 -17
  42. package/skills/review/references/workflows/complete.md +2 -2
  43. package/skills/review/references/workflows/edit.md +7 -5
  44. package/skills/review/references/workflows/help.md +12 -20
  45. package/skills/review/references/workflows/new.md +14 -13
  46. package/skills/review/references/workflows/publish.md +2 -2
  47. package/skills/review/references/workflows/status.md +1 -1
  48. package/skills/review/templates/REVIEW.md +39 -0
  49. package/templates/plan/PLAN.md +9 -1
  50. package/templates/review/draft-pr.md +3 -9
  51. package/dist/cli/plan.d.ts +0 -17
  52. package/dist/cli/plan.d.ts.map +0 -1
  53. package/dist/cli.d.ts +0 -4
  54. package/dist/cli.d.ts.map +0 -1
  55. package/dist/cli.js +0 -2258
  56. package/dist/extensions/tuicrx.d.ts +0 -66
  57. package/dist/extensions/tuicrx.d.ts.map +0 -1
  58. package/dist/extensions/zedx.d.ts +0 -18
  59. package/dist/extensions/zedx.d.ts.map +0 -1
  60. package/dist/review/local-review-backend.d.ts +0 -3
  61. package/dist/review/local-review-backend.d.ts.map +0 -1
  62. package/dist/review/local-reviews.d.ts +0 -31
  63. package/dist/review/local-reviews.d.ts.map +0 -1
  64. package/dist/review/review-markdown.d.ts +0 -6
  65. package/dist/review/review-markdown.d.ts.map +0 -1
  66. package/skills/plan/references/workflows/annotate.md +0 -13
  67. package/skills/review/references/workflows/open.md +0 -11
@@ -13373,30 +13373,13 @@ import { defineTool } from "@earendil-works/pi-coding-agent";
13373
13373
  import { z as z2 } from "zod";
13374
13374
 
13375
13375
  // src/environment.ts
13376
- import { readFile as readFile2 } from "node:fs/promises";
13377
- import { basename, join as join6 } from "node:path";
13378
-
13379
- // src/assets.ts
13380
- import { existsSync as existsSync2 } from "node:fs";
13381
- import { dirname as dirname2, join as join3 } from "node:path";
13382
- import { fileURLToPath } from "node:url";
13383
- function resolveBundledAssetDir(name, moduleUrl = import.meta.url) {
13384
- const moduleDir = dirname2(fileURLToPath(moduleUrl));
13385
- const candidates = [join3(moduleDir, name), join3(moduleDir, "..", name), join3(moduleDir, "..", "..", name)];
13386
- return candidates.find((path) => existsSync2(path)) ?? candidates[1];
13387
- }
13388
- function resolveBundledAgentsDir(moduleUrl = import.meta.url) {
13389
- return resolveBundledAssetDir("agents", moduleUrl);
13390
- }
13391
- function resolveBundledTemplatesDir(moduleUrl = import.meta.url) {
13392
- return resolveBundledAssetDir("templates", moduleUrl);
13393
- }
13376
+ import { basename } from "node:path";
13394
13377
 
13395
13378
  // src/extensions/processx.ts
13396
13379
  import { spawn as spawn2 } from "node:child_process";
13397
13380
  import { constants } from "node:fs";
13398
13381
  import { access } from "node:fs/promises";
13399
- import { delimiter, join as join4 } from "node:path";
13382
+ import { delimiter, join as join3 } from "node:path";
13400
13383
  async function findExecutable(name) {
13401
13384
  if (name.includes("/")) {
13402
13385
  try {
@@ -13409,7 +13392,7 @@ async function findExecutable(name) {
13409
13392
  for (const directory of (process.env.PATH ?? "").split(delimiter)) {
13410
13393
  if (!directory)
13411
13394
  continue;
13412
- const candidate = join4(directory, name);
13395
+ const candidate = join3(directory, name);
13413
13396
  try {
13414
13397
  await access(candidate, constants.X_OK);
13415
13398
  return candidate;
@@ -13473,174 +13456,6 @@ function appendBounded(current, next) {
13473
13456
  return combined.length <= MAX_CAPTURED_OUTPUT_LENGTH ? combined : combined.slice(-MAX_CAPTURED_OUTPUT_LENGTH);
13474
13457
  }
13475
13458
 
13476
- // src/extensions/zedx.ts
13477
- import { mkdir, readFile, writeFile } from "node:fs/promises";
13478
- import { homedir as homedir2 } from "node:os";
13479
- import { dirname as dirname3, join as join5 } from "node:path";
13480
- var ZED_LOCAL_REVIEW_TASK_NAME = "diffpi: tuicr local review";
13481
- var ZED_PR_REVIEW_TASK_NAME = "diffpi: tuicr PR review";
13482
- var ZED_PLAN_ANNOTATE_TASK_NAME = "diffpi: annotate plan";
13483
- var LEGACY_ZED_REVIEW_TASK_NAME = "diffpi: tuicr review";
13484
- var REVIEW_KEYBINDING = "cmd-alt-r";
13485
- function zedTasksPath(homeDir = homedir2()) {
13486
- return join5(homeDir, ".config", "zed", "tasks.json");
13487
- }
13488
- function zedKeymapPath(homeDir = homedir2()) {
13489
- return join5(homeDir, ".config", "zed", "keymap.json");
13490
- }
13491
- async function ensureZedReviewTask(homeDir = homedir2(), _command = ["tuicr", "-w", "-r", "main..HEAD"]) {
13492
- const path = zedTasksPath(homeDir);
13493
- const currentText = await readOptional(path);
13494
- const tasks = parseJsonArray(currentText, path);
13495
- const migrated = tasks.filter((task) => task.label !== LEGACY_ZED_REVIEW_TASK_NAME);
13496
- const next = [...migrated];
13497
- for (const task of [localReviewTask(), prReviewTask()]) {
13498
- const index = next.findIndex((existing) => existing.label === task.label);
13499
- if (index >= 0)
13500
- next[index] = { ...next[index], ...task };
13501
- else
13502
- next.push(task);
13503
- }
13504
- const changed = JSON.stringify(tasks) !== JSON.stringify(next);
13505
- if (changed)
13506
- await writeJson(path, next);
13507
- return { path, changed, existed: currentText !== undefined };
13508
- }
13509
- async function ensureZedPlanTask(packageVersion, homeDir = homedir2()) {
13510
- if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(packageVersion)) {
13511
- throw new Error(`Invalid @difflab/pi package version: ${packageVersion}.`);
13512
- }
13513
- const path = zedTasksPath(homeDir);
13514
- const currentText = await readOptional(path);
13515
- const tasks = parseJsonArray(currentText, path);
13516
- const task = {
13517
- label: ZED_PLAN_ANNOTATE_TASK_NAME,
13518
- command: "npx",
13519
- args: ["--yes", `@difflab/pi@${packageVersion}`, "plan", "annotate", "$ZED_FILE", "--cwd", "$ZED_WORKTREE_ROOT"],
13520
- cwd: "$ZED_WORKTREE_ROOT",
13521
- use_new_terminal: true,
13522
- reveal: "always",
13523
- reveal_target: "center"
13524
- };
13525
- const next = [...tasks];
13526
- const index = next.findIndex((candidate) => candidate.label === task.label);
13527
- if (index >= 0)
13528
- next[index] = { ...next[index], ...task };
13529
- else
13530
- next.push(task);
13531
- const changed = JSON.stringify(tasks) !== JSON.stringify(next);
13532
- if (changed)
13533
- await writeJson(path, next);
13534
- return { path, changed, existed: currentText !== undefined };
13535
- }
13536
- async function ensureZedReviewKeybinding(homeDir = homedir2()) {
13537
- const path = zedKeymapPath(homeDir);
13538
- const currentText = await readOptional(path);
13539
- const entries = parseJsonArray(currentText, path);
13540
- const migrated = entries.map(migrateReviewKeymapEntry);
13541
- const alreadyBound = migrated.some((entry) => Object.values(entry.bindings ?? {}).some((action) => Array.isArray(action) && action[0] === "task::Spawn" && bindsReviewTask(action[1])));
13542
- const next = alreadyBound ? migrated : [
13543
- ...migrated,
13544
- {
13545
- context: "Workspace",
13546
- bindings: { [REVIEW_KEYBINDING]: ["task::Spawn", { task_name: ZED_LOCAL_REVIEW_TASK_NAME }] }
13547
- }
13548
- ];
13549
- const changed = JSON.stringify(entries) !== JSON.stringify(next);
13550
- if (changed)
13551
- await writeJson(path, next);
13552
- return { path, changed, existed: currentText !== undefined };
13553
- }
13554
- function zedReviewTaskName(command) {
13555
- if (command.some((value, index) => value === "plan" && command[index + 1] === "annotate")) {
13556
- return ZED_PLAN_ANNOTATE_TASK_NAME;
13557
- }
13558
- return command[0] === "tuicr" && command[1] === "pr" ? ZED_PR_REVIEW_TASK_NAME : ZED_LOCAL_REVIEW_TASK_NAME;
13559
- }
13560
- var LOCAL_REVIEW_SCRIPT = `set -eu
13561
- remote=$(git remote get-url origin 2>/dev/null || true)
13562
- branch=$(git branch --show-current)
13563
- base=
13564
- case "$remote" in
13565
- *github.com*)
13566
- base=$(gh pr list --head "$branch" --state open --limit 100 --json baseRefName,createdAt --jq 'sort_by(.createdAt) | reverse | .[0].baseRefName // empty' 2>/dev/null || true)
13567
- [ -n "$base" ] || base=$(gh repo view --json defaultBranchRef --jq .defaultBranchRef.name 2>/dev/null || true)
13568
- ;;
13569
- *gitlab*)
13570
- mr=$(glab mr list --source-branch "$branch" --order created_at --sort desc --per-page 100 --output json 2>/dev/null || true)
13571
- base=$(printf '%s' "$mr" | node -e 'let s=""; process.stdin.on("data",d=>s+=d).on("end",()=>{try{let a=JSON.parse(s);process.stdout.write(a[0]?.target_branch||"")}catch{}})')
13572
- [ -n "$base" ] || base=$(glab repo view --output json 2>/dev/null | node -e 'let s=""; process.stdin.on("data",d=>s+=d).on("end",()=>{try{process.stdout.write(JSON.parse(s).default_branch||"")}catch{}})' || true)
13573
- ;;
13574
- esac
13575
- [ -n "$base" ] || { echo 'diffpi: could not resolve a GitHub/GitLab review base' >&2; exit 1; }
13576
- exec tuicr -w -r "$base..HEAD"`;
13577
- var PR_REVIEW_SCRIPT = `set -eu
13578
- remote=$(git remote get-url origin 2>/dev/null || true)
13579
- branch=$(git branch --show-current)
13580
- case "$remote" in
13581
- *github.com*) number=$(gh pr list --head "$branch" --state open --limit 100 --json number,createdAt --jq 'sort_by(.createdAt) | reverse | .[0].number // empty' 2>/dev/null || true) ;;
13582
- *gitlab*) number=$(glab mr list --source-branch "$branch" --order created_at --sort desc --per-page 100 --output json 2>/dev/null | node -e 'let s=""; process.stdin.on("data",d=>s+=d).on("end",()=>{try{let a=JSON.parse(s);process.stdout.write(String(a[0]?.iid||a[0]?.number||""))}catch{}})' || true) ;;
13583
- *) number= ;;
13584
- esac
13585
- [ -n "$number" ] || { echo 'diffpi: could not resolve the current GitHub PR or GitLab MR' >&2; exit 1; }
13586
- exec tuicr pr "$number"`;
13587
- function staticReviewTask(label, script) {
13588
- return {
13589
- label,
13590
- command: "sh",
13591
- args: ["-lc", script],
13592
- cwd: "$ZED_WORKTREE_ROOT",
13593
- use_new_terminal: true,
13594
- reveal: "always",
13595
- reveal_target: "center"
13596
- };
13597
- }
13598
- function localReviewTask() {
13599
- return staticReviewTask(ZED_LOCAL_REVIEW_TASK_NAME, LOCAL_REVIEW_SCRIPT);
13600
- }
13601
- function prReviewTask() {
13602
- return staticReviewTask(ZED_PR_REVIEW_TASK_NAME, PR_REVIEW_SCRIPT);
13603
- }
13604
- function bindsReviewTask(payload) {
13605
- return typeof payload === "object" && payload !== null && payload.task_name === ZED_LOCAL_REVIEW_TASK_NAME;
13606
- }
13607
- function migrateReviewKeymapEntry(entry) {
13608
- const bindings = Object.fromEntries(Object.entries(entry.bindings ?? {}).map(([key, action]) => {
13609
- if (Array.isArray(action) && action[0] === "task::Spawn" && typeof action[1] === "object" && action[1] !== null && action[1].task_name === LEGACY_ZED_REVIEW_TASK_NAME) {
13610
- return [key, ["task::Spawn", { ...action[1], task_name: ZED_LOCAL_REVIEW_TASK_NAME }]];
13611
- }
13612
- return [key, action];
13613
- }));
13614
- return entry.bindings ? { ...entry, bindings } : entry;
13615
- }
13616
- async function readOptional(path) {
13617
- try {
13618
- return await readFile(path, "utf8");
13619
- } catch (error) {
13620
- if (error instanceof Error && "code" in error && error.code === "ENOENT")
13621
- return;
13622
- throw error;
13623
- }
13624
- }
13625
- function parseJsonArray(content, path) {
13626
- if (!content?.trim())
13627
- return [];
13628
- let value;
13629
- try {
13630
- value = JSON.parse(content);
13631
- } catch {
13632
- throw new Error(`Cannot safely edit ${path}: not strict JSON (it may contain JSONC comments).`);
13633
- }
13634
- if (!Array.isArray(value))
13635
- throw new Error(`Expected a JSON array in ${path}.`);
13636
- return value;
13637
- }
13638
- async function writeJson(path, value) {
13639
- await mkdir(dirname3(path), { recursive: true });
13640
- await writeFile(path, `${JSON.stringify(value, null, 2)}
13641
- `, "utf8");
13642
- }
13643
-
13644
13459
  // src/environment.ts
13645
13460
  function detectIde(env = process.env) {
13646
13461
  const program = (env.TERM_PROGRAM ?? "").toLowerCase();
@@ -13692,73 +13507,6 @@ function parseRemote(remote) {
13692
13507
  const provider = /github/i.test(host) ? "github" : /gitlab/i.test(host) ? "gitlab" : "none";
13693
13508
  return { provider, host, owner, repo };
13694
13509
  }
13695
- function diffpiLaunchName(cwd, workflow) {
13696
- return `diffpi: ${basename(cwd)} / ${workflow}`;
13697
- }
13698
- async function openInNewTab(command, opts) {
13699
- const env = opts.env ?? process.env;
13700
- const name = opts.name ?? "review";
13701
- const printable = command.join(" ");
13702
- const mux = detectMux(env);
13703
- if (mux !== "none") {
13704
- const opened = await openMuxTab(mux, command, opts.cwd, name, printable);
13705
- if (opened)
13706
- return opened;
13707
- }
13708
- if (detectIde(env) === "zed") {
13709
- try {
13710
- const taskName = zedReviewTaskName(command);
13711
- if (taskName === ZED_PLAN_ANNOTATE_TASK_NAME)
13712
- await ensureZedPlanTask(await packageVersion(), opts.homeDir);
13713
- else
13714
- await ensureZedReviewTask(opts.homeDir, command);
13715
- return {
13716
- launched: false,
13717
- configured: true,
13718
- via: "zed-task",
13719
- command: printable,
13720
- taskName,
13721
- instruction: `Run the Zed task "${taskName}".`
13722
- };
13723
- } catch {}
13724
- }
13725
- return { launched: false, via: "print", command: printable };
13726
- }
13727
- function screenWindowArgs(command, cwd, name) {
13728
- return ["-X", "screen", "-t", name, "sh", "-lc", 'cd -- "$1" && shift && exec "$@"', "sh", cwd, ...command];
13729
- }
13730
- async function packageVersion() {
13731
- let value;
13732
- try {
13733
- value = JSON.parse(await readFile2(join6(resolveBundledAgentsDir(), "..", "package.json"), "utf8"));
13734
- } catch (error) {
13735
- throw new Error("Cannot parse the installed @difflab/pi package metadata.", { cause: error });
13736
- }
13737
- if (typeof value.version !== "string")
13738
- throw new Error("Cannot resolve the installed @difflab/pi version.");
13739
- return value.version;
13740
- }
13741
- async function openMuxTab(mux, command, cwd, name, printable, tmuxCommand) {
13742
- if (mux === "zellij" && await findExecutable("zellij")) {
13743
- const result = await run("zellij", ["action", "new-tab", "--cwd", cwd, "--name", name, "--", ...command]);
13744
- if (result.code === 0)
13745
- return { launched: true, via: "zellij", command: printable };
13746
- const fallback = await run("zellij", ["run", "--cwd", cwd, "--name", name, "--", ...command]);
13747
- if (fallback.code === 0)
13748
- return { launched: true, via: "zellij-run", command: printable };
13749
- }
13750
- if (mux === "tmux" && await findExecutable("tmux")) {
13751
- const result = tmuxCommand ? await run("tmux", ["new-window", "-c", cwd, "-n", name, ...tmuxCommand]) : await run("tmux", ["new-window", "-c", cwd, "-n", name, printable]);
13752
- if (result.code === 0)
13753
- return { launched: true, via: "tmux", command: printable };
13754
- }
13755
- if (mux === "screen" && await findExecutable("screen")) {
13756
- const result = await run("screen", screenWindowArgs(command, cwd, name));
13757
- if (result.code === 0)
13758
- return { launched: true, via: "screen", command: printable };
13759
- }
13760
- return;
13761
- }
13762
13510
 
13763
13511
  // src/extensions/zodx.ts
13764
13512
  import { z } from "zod";
@@ -14127,33 +13875,21 @@ function result(text, status, detail = text) {
14127
13875
 
14128
13876
  // src/tools/log.ts
14129
13877
  import { defineTool as defineTool2 } from "@earendil-works/pi-coding-agent";
14130
- import { join as join9 } from "node:path";
14131
13878
  import { z as z3 } from "zod";
14132
13879
 
14133
13880
  // src/log.ts
14134
- import { randomUUID } from "node:crypto";
14135
- import { appendFile, readFile as readFile3 } from "node:fs/promises";
14136
- async function appendLogEntry(logPath, event) {
14137
- const entry = {
14138
- ...event,
14139
- version: 1,
14140
- eventId: event.eventId ?? randomUUID(),
14141
- timestamp: event.timestamp ?? new Date().toISOString()
14142
- };
14143
- await appendFile(logPath, `${JSON.stringify(entry)}
14144
- `, { encoding: "utf8", mode: 384 });
14145
- return entry;
14146
- }
13881
+ import { appendFile, lstat as lstat2, mkdir as mkdir2, readFile, realpath as realpath3 } from "node:fs/promises";
13882
+ import { dirname as dirname3, isAbsolute as isAbsolute4, join as join6, relative, resolve as resolve4, sep } from "node:path";
14147
13883
 
14148
13884
  // src/store.ts
14149
13885
  import { createHash } from "node:crypto";
14150
- import { lstat, mkdir as mkdir2, readlink, realpath as realpath2, symlink, unlink } from "node:fs/promises";
14151
- import { homedir as homedir3 } from "node:os";
14152
- import { dirname as dirname4, isAbsolute as isAbsolute3, join as join8, resolve as resolve3 } from "node:path";
13886
+ import { lstat, mkdir, readlink, realpath as realpath2, symlink, unlink } from "node:fs/promises";
13887
+ import { homedir as homedir2 } from "node:os";
13888
+ import { dirname as dirname2, isAbsolute as isAbsolute3, join as join5, resolve as resolve3 } from "node:path";
14153
13889
 
14154
13890
  // src/extensions/gitx.ts
14155
13891
  import { realpath } from "node:fs/promises";
14156
- import { basename as basename2, isAbsolute as isAbsolute2, join as join7, resolve as resolve2 } from "node:path";
13892
+ import { basename as basename2, isAbsolute as isAbsolute2, join as join4, resolve as resolve2 } from "node:path";
14157
13893
  async function gitToplevel(cwd) {
14158
13894
  const result = await run("git", ["-C", cwd, "rev-parse", "--show-toplevel"]);
14159
13895
  const top = result.stdout.trim();
@@ -14165,7 +13901,7 @@ async function inspectGitRepository(cwd) {
14165
13901
  const remote = remoteResult.code === 0 ? remoteResult.stdout.trim() : "";
14166
13902
  const commonResult = await run("git", ["-C", root, "rev-parse", "--git-common-dir"]);
14167
13903
  const common = commonResult.stdout.trim();
14168
- const commonPath = commonResult.code === 0 && common ? resolve2(isAbsolute2(common) ? common : join7(root, common)) : root;
13904
+ const commonPath = commonResult.code === 0 && common ? resolve2(isAbsolute2(common) ? common : join4(root, common)) : root;
14169
13905
  const commonDir = await canonicalPath(commonPath);
14170
13906
  return {
14171
13907
  root,
@@ -14192,17 +13928,17 @@ async function canonicalPath(path) {
14192
13928
 
14193
13929
  // src/store.ts
14194
13930
  var STORE_LINK = ".diffpi";
14195
- var LEGACY_STORE_LINK = join8(".pi", "diffpi");
14196
- function storeGlobalRoot(homeDir = homedir3()) {
14197
- return join8(homeDir, ".difflab", "diffpi", "projects");
13931
+ var LEGACY_STORE_LINK = join5(".pi", "diffpi");
13932
+ function storeGlobalRoot(homeDir = homedir2()) {
13933
+ return join5(homeDir, ".difflab", "diffpi", "projects");
14198
13934
  }
14199
- async function ensureStore(cwd, homeDir = homedir3()) {
13935
+ async function ensureStore(cwd, homeDir = homedir2()) {
14200
13936
  const repository = await inspectGitRepository(cwd);
14201
13937
  const root = repository.root;
14202
13938
  const slug = projectSlug(repository);
14203
- const dest = join8(storeGlobalRoot(homeDir), slug);
14204
- const link = join8(root, STORE_LINK);
14205
- await mkdir2(dest, { recursive: true });
13939
+ const dest = join5(storeGlobalRoot(homeDir), slug);
13940
+ const link = join5(root, STORE_LINK);
13941
+ await mkdir(dest, { recursive: true });
14206
13942
  try {
14207
13943
  await assertStoreLink(link, dest);
14208
13944
  } catch (error) {
@@ -14210,32 +13946,12 @@ async function ensureStore(cwd, homeDir = homedir3()) {
14210
13946
  throw error;
14211
13947
  await symlink(dest, link);
14212
13948
  }
14213
- await removeLegacyStoreLink(join8(root, LEGACY_STORE_LINK), dest);
13949
+ await removeLegacyStoreLink(join5(root, LEGACY_STORE_LINK), dest);
14214
13950
  return { slug, root, dest, link, linked: true };
14215
13951
  }
14216
- async function logsDir(cwd, homeDir = homedir3()) {
14217
- const store = await ensureStore(cwd, homeDir);
14218
- const dir = join8(store.link, "logs");
14219
- await mkdir2(dir, { recursive: true });
14220
- return dir;
14221
- }
14222
- async function reviewsDir(cwd, homeDir = homedir3()) {
13952
+ async function storeDir(cwd, homeDir = homedir2()) {
14223
13953
  const store = await ensureStore(cwd, homeDir);
14224
- const dir = join8(store.link, "review");
14225
- await mkdir2(dir, { recursive: true });
14226
- return dir;
14227
- }
14228
- async function completedReviewsDir(cwd, homeDir = homedir3()) {
14229
- const store = await ensureStore(cwd, homeDir);
14230
- const dir = join8(store.link, "reviews");
14231
- await mkdir2(dir, { recursive: true });
14232
- return dir;
14233
- }
14234
- async function sessionsDir(cwd, homeDir = homedir3()) {
14235
- const store = await ensureStore(cwd, homeDir);
14236
- const dir = join8(store.link, "sessions");
14237
- await mkdir2(dir, { recursive: true });
14238
- return dir;
13954
+ return store.dest;
14239
13955
  }
14240
13956
  async function assertStoreLink(path, dest) {
14241
13957
  const entry = await lstat(path);
@@ -14260,7 +13976,7 @@ async function removeLegacyStoreLink(path, dest) {
14260
13976
  }
14261
13977
  async function symlinkTarget(path) {
14262
13978
  const target = await readlink(path);
14263
- return canonicalPath2(isAbsolute3(target) ? target : resolve3(dirname4(path), target));
13979
+ return canonicalPath2(isAbsolute3(target) ? target : resolve3(dirname2(path), target));
14264
13980
  }
14265
13981
  async function canonicalPath2(path) {
14266
13982
  try {
@@ -14275,41 +13991,94 @@ function projectSlug(repository) {
14275
13991
  return `${readable}-${digest}`;
14276
13992
  }
14277
13993
 
13994
+ // src/log.ts
13995
+ async function appendLogEntry(logPath, entry) {
13996
+ await appendFile(logPath, `${JSON.stringify(entry)}
13997
+ `, { encoding: "utf8", mode: 384 });
13998
+ return entry;
13999
+ }
14000
+ async function appendLog(cwd, filename, message, label) {
14001
+ const entry = label === undefined ? { message } : { message, label };
14002
+ return appendLogEntry(await resolveLogPath(cwd, filename), entry);
14003
+ }
14004
+ async function resolveLogPath(cwd, filename) {
14005
+ if (!filename || isAbsolute4(filename))
14006
+ throw new Error("Log filename must be a non-empty relative path.");
14007
+ const root = await realpath3(cwd);
14008
+ const lexicalTarget = resolve4(root, filename);
14009
+ assertContained(root, lexicalTarget);
14010
+ let safeRoot = root;
14011
+ let target = lexicalTarget;
14012
+ if (filename.startsWith(`.diffpi${sep}`)) {
14013
+ const insideStore = filename.slice(`.diffpi${sep}`.length);
14014
+ if (insideStore.split(sep).includes(".."))
14015
+ throw new Error("Log filename must stay within cwd.");
14016
+ safeRoot = await realpath3(await storeDir(root));
14017
+ target = resolve4(safeRoot, insideStore);
14018
+ assertContained(safeRoot, target);
14019
+ if (target === safeRoot)
14020
+ throw new Error("Log filename must name a file within the store.");
14021
+ }
14022
+ await ensureSafeParent(safeRoot, dirname3(target));
14023
+ try {
14024
+ const entry = await lstat2(target);
14025
+ if (entry.isSymbolicLink())
14026
+ throw new Error("Log filename must not point through a symlink.");
14027
+ } catch (error) {
14028
+ if (error.code !== "ENOENT")
14029
+ throw error;
14030
+ }
14031
+ return target;
14032
+ }
14033
+ async function ensureSafeParent(root, parent) {
14034
+ assertContained(root, parent);
14035
+ let current = root;
14036
+ const suffix = relative(root, parent);
14037
+ if (!suffix)
14038
+ return;
14039
+ for (const part of suffix.split(sep)) {
14040
+ current = join6(current, part);
14041
+ try {
14042
+ await mkdir2(current);
14043
+ } catch (error) {
14044
+ if (error.code !== "EEXIST")
14045
+ throw error;
14046
+ }
14047
+ const entry = await lstat2(current);
14048
+ if (!entry.isDirectory() || entry.isSymbolicLink())
14049
+ throw new Error("Log filename must not pass through a symlink or non-directory.");
14050
+ }
14051
+ }
14052
+ function assertContained(root, target) {
14053
+ const targetRelative = relative(root, target);
14054
+ if (targetRelative === ".." || targetRelative.startsWith(`..${sep}`) || isAbsolute4(targetRelative))
14055
+ throw new Error("Log filename must stay within cwd.");
14056
+ }
14057
+
14278
14058
  // src/tools/log.ts
14279
14059
  var schema7 = z3.object({
14280
14060
  cwd: zx.cwd,
14281
- channel: zx.id,
14282
- kind: z3.enum(["progress", "issue", "deviation"]),
14283
- actor: zx.text,
14284
- message: zx.text,
14285
- correlation: z3.record(z3.string(), z3.string()).optional(),
14286
- evidence: z3.array(zx.text).optional(),
14287
- data: z3.record(z3.string(), z3.unknown()).optional()
14061
+ filename: z3.string().trim().min(1),
14062
+ label: z3.string().trim().min(1).optional(),
14063
+ message: zx.text
14288
14064
  }).strict();
14289
14065
  var diffpiLogTool = defineTool2({
14290
14066
  name: "diffpi_log",
14291
14067
  label: "diffpi log",
14292
- description: "Append a reusable progress, issue, or deviation event to a project-scoped Diffpi JSONL channel.",
14293
- promptSnippet: "Use diffpi_log when a skill or workflow needs durable progress, issue, or deviation events.",
14068
+ description: "Append a message to a caller-selected relative project JSONL file.",
14069
+ promptSnippet: "Use diffpi_log to append durable progress messages to the supplied relative JSONL filename.",
14294
14070
  promptGuidelines: [
14295
- "Use stable channel names shared by one workflow, such as plan, flow, or session.",
14296
- "Keep plan status and task checkboxes in live PLAN.md files; diffpi_log is a general activity log, not another task system."
14071
+ "For /plan go, use cwd at the canonical Git root and filename .diffpi/plan/<plan-id>/logs.jsonl, directly beside PLAN.md.",
14072
+ "Use label only when a reusable category helps readers filter messages."
14297
14073
  ],
14298
14074
  parameters: z3.toJSONSchema(schema7, { io: "input" }),
14299
14075
  executionMode: "sequential",
14300
14076
  async execute(_id, input) {
14301
14077
  const params = schema7.parse(input);
14302
- const entry = await appendLogEntry(join9(await logsDir(params.cwd ?? process.cwd()), `${params.channel}.jsonl`), {
14303
- kind: params.kind,
14304
- actor: params.actor,
14305
- message: params.message,
14306
- correlation: params.correlation,
14307
- evidence: params.evidence,
14308
- data: params.data
14309
- });
14078
+ const entry = await appendLog(params.cwd ?? process.cwd(), params.filename, params.message, params.label);
14310
14079
  return {
14311
- content: [{ type: "text", text: `Logged ${entry.eventId} to ${params.channel}.` }],
14312
- details: { entry, channel: params.channel }
14080
+ content: [{ type: "text", text: `Logged message to ${params.filename}.` }],
14081
+ details: { entry, filename: params.filename }
14313
14082
  };
14314
14083
  }
14315
14084
  });
@@ -14320,19 +14089,35 @@ import { z as z5 } from "zod";
14320
14089
 
14321
14090
  // src/doctor.ts
14322
14091
  import { createHash as createHash2 } from "node:crypto";
14323
- import { lstat as lstat2, mkdir as mkdir6, readFile as readFile8, readdir as readdir3, writeFile as writeFile5 } from "node:fs/promises";
14324
- import { basename as basename5, join as join15 } from "node:path";
14092
+ import { lstat as lstat3, mkdir as mkdir6, readFile as readFile6, readdir as readdir3, writeFile as writeFile4 } from "node:fs/promises";
14093
+ import { basename as basename5, join as join13 } from "node:path";
14094
+
14095
+ // src/assets.ts
14096
+ import { existsSync as existsSync2 } from "node:fs";
14097
+ import { dirname as dirname4, join as join7 } from "node:path";
14098
+ import { fileURLToPath } from "node:url";
14099
+ function resolveBundledAssetDir(name, moduleUrl = import.meta.url) {
14100
+ const moduleDir = dirname4(fileURLToPath(moduleUrl));
14101
+ const candidates = [join7(moduleDir, name), join7(moduleDir, "..", name), join7(moduleDir, "..", "..", name)];
14102
+ return candidates.find((path) => existsSync2(path)) ?? candidates[1];
14103
+ }
14104
+ function resolveBundledAgentsDir(moduleUrl = import.meta.url) {
14105
+ return resolveBundledAssetDir("agents", moduleUrl);
14106
+ }
14107
+ function resolveBundledTemplatesDir(moduleUrl = import.meta.url) {
14108
+ return resolveBundledAssetDir("templates", moduleUrl);
14109
+ }
14325
14110
 
14326
14111
  // src/pi.ts
14327
- import { mkdir as mkdir3, writeFile as writeFile2 } from "node:fs/promises";
14328
- import { homedir as homedir4 } from "node:os";
14329
- import { dirname as dirname5, join as join10 } from "node:path";
14112
+ import { mkdir as mkdir3, writeFile } from "node:fs/promises";
14113
+ import { homedir as homedir3 } from "node:os";
14114
+ import { dirname as dirname5, join as join8 } from "node:path";
14330
14115
 
14331
14116
  // src/fsx.ts
14332
- import { readdir, readFile as readFile4 } from "node:fs/promises";
14117
+ import { readdir, readFile as readFile2 } from "node:fs/promises";
14333
14118
  async function readTextIfExists(path) {
14334
14119
  try {
14335
- return await readFile4(path, "utf8");
14120
+ return await readFile2(path, "utf8");
14336
14121
  } catch (error) {
14337
14122
  if (isMissingPath(error))
14338
14123
  return;
@@ -14356,19 +14141,19 @@ var pi = {
14356
14141
  configEnsure: ensurePiConfig
14357
14142
  };
14358
14143
  async function ensurePiAgent(filename, content, agentDir = resolvePiAgentDir(), dryRun = false) {
14359
- const path = join10(agentDir, "agents", filename);
14144
+ const path = join8(agentDir, "agents", filename);
14360
14145
  const currentText = await readTextIfExists(path);
14361
14146
  const changed = currentText !== content;
14362
14147
  if (changed && !dryRun) {
14363
14148
  await mkdir3(dirname5(path), { recursive: true });
14364
- await writeFile2(path, content, "utf8");
14149
+ await writeFile(path, content, "utf8");
14365
14150
  }
14366
14151
  return { path, changed, existed: currentText !== undefined, planned: changed && dryRun };
14367
14152
  }
14368
- async function checkGlobalPiSkill(name, agentDir = resolvePiAgentDir(), sharedSkillsDir = join10(homedir4(), ".agents", "skills")) {
14369
- const roots = [join10(agentDir, "skills"), sharedSkillsDir];
14153
+ async function checkGlobalPiSkill(name, agentDir = resolvePiAgentDir(), sharedSkillsDir = join8(homedir3(), ".agents", "skills")) {
14154
+ const roots = [join8(agentDir, "skills"), sharedSkillsDir];
14370
14155
  for (const root of roots) {
14371
- if (await readTextIfExists(join10(root, name, "SKILL.md")) !== undefined)
14156
+ if (await readTextIfExists(join8(root, name, "SKILL.md")) !== undefined)
14372
14157
  return true;
14373
14158
  }
14374
14159
  return false;
@@ -14398,7 +14183,7 @@ async function ensurePiConfig(path, update, dryRun = false) {
14398
14183
  const changed = JSON.stringify(current) !== JSON.stringify(next);
14399
14184
  if (changed && !dryRun) {
14400
14185
  await mkdir3(dirname5(path), { recursive: true });
14401
- await writeFile2(path, `${JSON.stringify(next, null, 2)}
14186
+ await writeFile(path, `${JSON.stringify(next, null, 2)}
14402
14187
  `, "utf8");
14403
14188
  }
14404
14189
  return { path, changed, existed: currentText !== undefined, planned: changed && dryRun };
@@ -14417,8 +14202,8 @@ function hasPiPackage(listOutput, source) {
14417
14202
  async function installPiPackage(executable, source) {
14418
14203
  await runChecked(executable, ["install", source]);
14419
14204
  }
14420
- function resolvePiAgentDir(homeDir = homedir4()) {
14421
- return process.env.PI_CODING_AGENT_DIR ?? (process.env.XDG_CONFIG_HOME ? join10(process.env.XDG_CONFIG_HOME, "pi") : join10(homeDir, ".pi", "agent"));
14205
+ function resolvePiAgentDir(homeDir = homedir3()) {
14206
+ return process.env.PI_CODING_AGENT_DIR ?? (process.env.XDG_CONFIG_HOME ? join8(process.env.XDG_CONFIG_HOME, "pi") : join8(homeDir, ".pi", "agent"));
14422
14207
  }
14423
14208
  function parseJsonObject(content, path) {
14424
14209
  if (!content?.trim())
@@ -14433,15 +14218,15 @@ function parseJsonObject(content, path) {
14433
14218
 
14434
14219
  // src/setup.ts
14435
14220
  import { parseFrontmatter as parseFrontmatter2 } from "@earendil-works/pi-coding-agent";
14436
- import { readdir as readdir2, readFile as readFile7 } from "node:fs/promises";
14437
- import { homedir as homedir8 } from "node:os";
14438
- import { basename as basename4, join as join14 } from "node:path";
14221
+ import { readdir as readdir2, readFile as readFile5 } from "node:fs/promises";
14222
+ import { homedir as homedir7 } from "node:os";
14223
+ import { basename as basename4, join as join12 } from "node:path";
14439
14224
 
14440
14225
  // src/config.ts
14441
14226
  import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
14442
14227
  import { z as z4 } from "zod";
14443
- import { homedir as homedir5 } from "node:os";
14444
- import { join as join11 } from "node:path";
14228
+ import { homedir as homedir4 } from "node:os";
14229
+ import { join as join9 } from "node:path";
14445
14230
  var modelReferenceSchema = zx.text;
14446
14231
  var agentConfigSchema = z4.object({
14447
14232
  models: z4.array(modelReferenceSchema).optional()
@@ -14449,11 +14234,11 @@ var agentConfigSchema = z4.object({
14449
14234
  var diffpiConfigSchema = z4.object({
14450
14235
  agents: z4.record(z4.string(), agentConfigSchema).optional()
14451
14236
  }).strict();
14452
- function diffpiConfigPaths(homeDir = homedir5()) {
14453
- const directory = join11(homeDir, ".difflab", "diffpi");
14237
+ function diffpiConfigPaths(homeDir = homedir4()) {
14238
+ const directory = join9(homeDir, ".difflab", "diffpi");
14454
14239
  return {
14455
- yaml: join11(directory, "config.yaml"),
14456
- json: join11(directory, "config.json")
14240
+ yaml: join9(directory, "config.yaml"),
14241
+ json: join9(directory, "config.json")
14457
14242
  };
14458
14243
  }
14459
14244
  async function loadDiffpiConfig(options = {}) {
@@ -14509,12 +14294,12 @@ function normalizeModelReference(value) {
14509
14294
  }
14510
14295
 
14511
14296
  // src/mcp.ts
14512
- import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile3 } from "node:fs/promises";
14513
- import { homedir as homedir6 } from "node:os";
14514
- import { dirname as dirname6, join as join12 } from "node:path";
14297
+ import { mkdir as mkdir4, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
14298
+ import { homedir as homedir5 } from "node:os";
14299
+ import { dirname as dirname6, join as join10 } from "node:path";
14515
14300
  var mcp = {
14516
- globalConfigPath(homeDir = homedir6()) {
14517
- return join12(homeDir, ".config", "mcp", "mcp.json");
14301
+ globalConfigPath(homeDir = homedir5()) {
14302
+ return join10(homeDir, ".config", "mcp", "mcp.json");
14518
14303
  },
14519
14304
  async serversEnsure(servers, options = {}) {
14520
14305
  const path = options.path ?? mcp.globalConfigPath();
@@ -14528,7 +14313,7 @@ var mcp = {
14528
14313
  const changed = JSON.stringify(current) !== JSON.stringify(next);
14529
14314
  if (changed && !options.dryRun) {
14530
14315
  await mkdir4(dirname6(path), { recursive: true });
14531
- await writeFile3(path, `${JSON.stringify(next, null, 2)}
14316
+ await writeFile2(path, `${JSON.stringify(next, null, 2)}
14532
14317
  `, "utf8");
14533
14318
  }
14534
14319
  return { path, changed, existed: currentText !== undefined, planned: changed && options.dryRun === true };
@@ -14557,7 +14342,7 @@ function getParsedConfig(content, path) {
14557
14342
  }
14558
14343
  async function getOptionalFile(path) {
14559
14344
  try {
14560
- return await readFile5(path, "utf8");
14345
+ return await readFile3(path, "utf8");
14561
14346
  } catch (error) {
14562
14347
  if (error instanceof Error && "code" in error && error.code === "ENOENT")
14563
14348
  return;
@@ -14569,9 +14354,9 @@ function isRecord(value) {
14569
14354
  }
14570
14355
 
14571
14356
  // src/extensions/misex.ts
14572
- import { mkdir as mkdir5, readFile as readFile6, writeFile as writeFile4 } from "node:fs/promises";
14573
- import { homedir as homedir7 } from "node:os";
14574
- import { basename as basename3, dirname as dirname7, join as join13 } from "node:path";
14357
+ import { mkdir as mkdir5, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
14358
+ import { homedir as homedir6 } from "node:os";
14359
+ import { basename as basename3, dirname as dirname7, join as join11 } from "node:path";
14575
14360
  var MISE_HOOK_START = "# >>> @difflab/pi mise >>>";
14576
14361
  var MISE_HOOK_END = "# <<< @difflab/pi mise <<<";
14577
14362
  var mise = {
@@ -14582,11 +14367,11 @@ var mise = {
14582
14367
  return run("mise", args, options);
14583
14368
  },
14584
14369
  async install(options = {}) {
14585
- const homeDir = options.homeDir ?? homedir7();
14370
+ const homeDir = options.homeDir ?? homedir6();
14586
14371
  const platform = options.platform ?? process.platform;
14587
14372
  if (platform === "win32")
14588
14373
  throw new Error("Automatic mise installation supports macOS and Linux only.");
14589
- const installedPath = join13(homeDir, ".local", "bin", "mise");
14374
+ const installedPath = join11(homeDir, ".local", "bin", "mise");
14590
14375
  if (options.dryRun)
14591
14376
  return installedPath;
14592
14377
  await runChecked("sh", ["-c", "curl -fsSL https://mise.run | sh"]);
@@ -14596,7 +14381,7 @@ var mise = {
14596
14381
  return executable;
14597
14382
  },
14598
14383
  async hookEnsure(executable, options = {}) {
14599
- const homeDir = options.homeDir ?? homedir7();
14384
+ const homeDir = options.homeDir ?? homedir6();
14600
14385
  const hook = getShellHook(basename3(options.shell ?? process.env.SHELL ?? ""), executable, homeDir);
14601
14386
  const current = await getOptionalFile2(hook.path);
14602
14387
  if (current.includes(MISE_HOOK_START))
@@ -14607,7 +14392,7 @@ var mise = {
14607
14392
  `) ? "" : `
14608
14393
  `;
14609
14394
  await mkdir5(dirname7(hook.path), { recursive: true });
14610
- await writeFile4(hook.path, `${current}${separator}${hook.content}`, "utf8");
14395
+ await writeFile3(hook.path, `${current}${separator}${hook.content}`, "utf8");
14611
14396
  return { path: hook.path, changed: true, planned: false };
14612
14397
  },
14613
14398
  async toolCheckGlobal(executable, tool, minimumVersion) {
@@ -14624,7 +14409,7 @@ var mise = {
14624
14409
  async toolInstallLocal(executable, specification, cwd = process.cwd()) {
14625
14410
  await runChecked(executable, ["use", "--path", cwd, specification], { cwd });
14626
14411
  },
14627
- async toolUpdateAllGlobal(executable, homeDir = homedir7()) {
14412
+ async toolUpdateAllGlobal(executable, homeDir = homedir6()) {
14628
14413
  await runChecked(executable, ["upgrade"], { cwd: homeDir });
14629
14414
  }
14630
14415
  };
@@ -14633,7 +14418,7 @@ function getShellHook(shell, executable, homeDir) {
14633
14418
  switch (shell.toLowerCase()) {
14634
14419
  case "zsh":
14635
14420
  return {
14636
- path: join13(homeDir, ".zshrc"),
14421
+ path: join11(homeDir, ".zshrc"),
14637
14422
  content: `${MISE_HOOK_START}
14638
14423
  eval "$(${command} activate zsh)"
14639
14424
  ${MISE_HOOK_END}
@@ -14641,7 +14426,7 @@ ${MISE_HOOK_END}
14641
14426
  };
14642
14427
  case "fish":
14643
14428
  return {
14644
- path: join13(homeDir, ".config", "fish", "config.fish"),
14429
+ path: join11(homeDir, ".config", "fish", "config.fish"),
14645
14430
  content: `${MISE_HOOK_START}
14646
14431
  ${command} activate fish | source
14647
14432
  ${MISE_HOOK_END}
@@ -14650,7 +14435,7 @@ ${MISE_HOOK_END}
14650
14435
  case "nu":
14651
14436
  case "nushell":
14652
14437
  return {
14653
- path: join13(homeDir, ".config", "nushell", "config.nu"),
14438
+ path: join11(homeDir, ".config", "nushell", "config.nu"),
14654
14439
  content: `${MISE_HOOK_START}
14655
14440
  let mise_bin = ${command}
14656
14441
  let mise_path = $nu.default-config-dir | path join mise.nu
@@ -14661,7 +14446,7 @@ ${MISE_HOOK_END}
14661
14446
  };
14662
14447
  case "xonsh":
14663
14448
  return {
14664
- path: join13(homeDir, ".xonshrc"),
14449
+ path: join11(homeDir, ".xonshrc"),
14665
14450
  content: `${MISE_HOOK_START}
14666
14451
  execx($(${command} activate xonsh))
14667
14452
  ${MISE_HOOK_END}
@@ -14669,7 +14454,7 @@ ${MISE_HOOK_END}
14669
14454
  };
14670
14455
  case "elvish":
14671
14456
  return {
14672
- path: join13(homeDir, ".config", "elvish", "rc.elv"),
14457
+ path: join11(homeDir, ".config", "elvish", "rc.elv"),
14673
14458
  content: `${MISE_HOOK_START}
14674
14459
  var mise: = (ns [&])
14675
14460
  eval (${command} activate elvish | slurp) &ns=$mise: &on-end={|ns| set mise: = $ns }
@@ -14680,7 +14465,7 @@ ${MISE_HOOK_END}
14680
14465
  case "pwsh":
14681
14466
  case "powershell":
14682
14467
  return {
14683
- path: join13(homeDir, ".config", "powershell", "Microsoft.PowerShell_profile.ps1"),
14468
+ path: join11(homeDir, ".config", "powershell", "Microsoft.PowerShell_profile.ps1"),
14684
14469
  content: `${MISE_HOOK_START}
14685
14470
  (& ${command} activate pwsh) | Out-String | Invoke-Expression
14686
14471
  ${MISE_HOOK_END}
@@ -14689,7 +14474,7 @@ ${MISE_HOOK_END}
14689
14474
  case "bash":
14690
14475
  default:
14691
14476
  return {
14692
- path: join13(homeDir, ".bashrc"),
14477
+ path: join11(homeDir, ".bashrc"),
14693
14478
  content: `${MISE_HOOK_START}
14694
14479
  eval "$(${command} activate bash)"
14695
14480
  ${MISE_HOOK_END}
@@ -14728,7 +14513,7 @@ function isVersionAtLeast(version, minimumVersion) {
14728
14513
  }
14729
14514
  async function getOptionalFile2(path) {
14730
14515
  try {
14731
- return await readFile6(path, "utf8");
14516
+ return await readFile4(path, "utf8");
14732
14517
  } catch (error) {
14733
14518
  if (error instanceof Error && "code" in error && error.code === "ENOENT")
14734
14519
  return "";
@@ -14744,12 +14529,6 @@ var MISE_DEPENDENCIES = [
14744
14529
  { name: "node", tool: "node", spec: "node@22", minimumVersion: "22.19.0" },
14745
14530
  { name: "zellij", tool: "zellij", spec: "zellij@latest", minimumVersion: undefined },
14746
14531
  { name: "helix", tool: "helix", spec: "helix@latest", minimumVersion: undefined },
14747
- {
14748
- name: "tuicr",
14749
- tool: "github:agavra/tuicr",
14750
- spec: "github:agavra/tuicr@latest",
14751
- minimumVersion: undefined
14752
- },
14753
14532
  {
14754
14533
  name: "context-mode",
14755
14534
  tool: "npm:context-mode",
@@ -14778,8 +14557,8 @@ var FORGE_DEPENDENCIES = {
14778
14557
  gitlab: { name: "glab", tool: "glab", spec: "glab@latest", minimumVersion: undefined }
14779
14558
  };
14780
14559
  async function ensureMise(options = {}) {
14781
- const homeDir = options.homeDir ?? homedir8();
14782
- const current = await mise.executableCheck() ?? await mise.executableCheck(join14(homeDir, ".local", "bin", "mise"));
14560
+ const homeDir = options.homeDir ?? homedir7();
14561
+ const current = await mise.executableCheck() ?? await mise.executableCheck(join12(homeDir, ".local", "bin", "mise"));
14783
14562
  if (current)
14784
14563
  return { executable: current, action: createSetupAction("mise", "ready", current) };
14785
14564
  reportProgress(options, "Installing mise");
@@ -14828,9 +14607,9 @@ async function ensureMiseDeps(miseExecutable, options = {}) {
14828
14607
  async function ensurePiPlugins(options = {}) {
14829
14608
  const actions = await ensurePiPackages(PI_PACKAGES, options);
14830
14609
  const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
14831
- const webSearch = await pi.configEnsure(join14(agentDir, "web-search.json"), (config) => ({ ...config, workflow: "auto-summary" }), options.dryRun);
14610
+ const webSearch = await pi.configEnsure(join12(agentDir, "web-search.json"), (config) => ({ ...config, workflow: "auto-summary" }), options.dryRun);
14832
14611
  actions.push(getConfigSetupAction("web search settings", webSearch));
14833
- const lsp = await pi.configEnsure(join14(agentDir, "pi-lsp.json"), (config) => ({
14612
+ const lsp = await pi.configEnsure(join12(agentDir, "pi-lsp.json"), (config) => ({
14834
14613
  ...config,
14835
14614
  progressive: { ...getRecord(config.progressive), enabled: true, inject: "none" }
14836
14615
  }), options.dryRun);
@@ -14845,7 +14624,7 @@ async function ensurePiAgents(options = {}) {
14845
14624
  const actions = [];
14846
14625
  for (const entry of entries) {
14847
14626
  const id = basename4(entry.name, ".md").replace(/^diffpi-/, "");
14848
- const source = await readFile7(join14(bundledAgentsDir, entry.name), "utf8");
14627
+ const source = await readFile5(join12(bundledAgentsDir, entry.name), "utf8");
14849
14628
  const content = materializeAgentModels(source, id, userConfig.config, options.availableModels);
14850
14629
  const result = await pi.agentEnsure(entry.name, content, agentDir, options.dryRun);
14851
14630
  actions.push(getConfigSetupAction(`pi agent ${id}`, result));
@@ -14854,7 +14633,7 @@ async function ensurePiAgents(options = {}) {
14854
14633
  }
14855
14634
  async function ensurePiSkills(miseExecutable, options = {}) {
14856
14635
  const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
14857
- const sharedSkillsDir = join14(options.homeDir ?? homedir8(), ".agents", "skills");
14636
+ const sharedSkillsDir = join12(options.homeDir ?? homedir7(), ".agents", "skills");
14858
14637
  const actions = [];
14859
14638
  for (const source of PI_SKILL_SOURCES) {
14860
14639
  const missing = [];
@@ -14922,8 +14701,6 @@ async function setupPi(options = {}) {
14922
14701
  actions.push(...await ensurePiAgents(options));
14923
14702
  actions.push(...await ensurePiSkills(miseResult.executable, options));
14924
14703
  actions.push(...await ensureMcpAdapters(miseResult.executable, options));
14925
- if (options.bindZedKey)
14926
- actions.push(...await ensureZedIntegration(options));
14927
14704
  return {
14928
14705
  actions,
14929
14706
  restartPi: setupRequiresRestart(actions)
@@ -14932,35 +14709,6 @@ async function setupPi(options = {}) {
14932
14709
  function setupRequiresRestart(actions) {
14933
14710
  return actions.some((item) => (item.status === "installed" || item.status === "updated") && (item.name.startsWith("pi package ") || item.name.startsWith("pi agent ") || item.name.startsWith("pi skill ") || item.name === "MCP configuration" || item.name === "web search settings" || item.name === "pi-lsp settings"));
14934
14711
  }
14935
- async function ensureZedIntegration(options = {}) {
14936
- if (options.dryRun) {
14937
- const actions = [
14938
- createSetupAction("Zed review tasks", "planned", "global static runtime-resolver tasks in tasks.json"),
14939
- createSetupAction("Zed plan task", "planned", "pinned package CLI task in tasks.json")
14940
- ];
14941
- if (options.bindZedKey)
14942
- actions.push(createSetupAction("Zed review keybinding", "planned", "keymap.json"));
14943
- return actions;
14944
- }
14945
- const actions = [];
14946
- try {
14947
- const tasks = await ensureZedReviewTask(options.homeDir);
14948
- actions.push(createSetupAction("Zed review tasks", tasks.changed ? "installed" : "ready", tasks.path));
14949
- const planTask = await ensureZedPlanTask(await packageVersion2(), options.homeDir);
14950
- actions.push(createSetupAction("Zed plan task", planTask.changed ? "installed" : "ready", planTask.path));
14951
- } catch (error) {
14952
- actions.push(createSetupAction("Zed review tasks", "skipped", error instanceof Error ? error.message : String(error)));
14953
- }
14954
- if (options.bindZedKey) {
14955
- try {
14956
- const key = await ensureZedReviewKeybinding(options.homeDir);
14957
- actions.push(createSetupAction("Zed review keybinding", key.changed ? "installed" : "ready", key.path));
14958
- } catch (error) {
14959
- actions.push(createSetupAction("Zed review keybinding", "skipped", error instanceof Error ? error.message : String(error)));
14960
- }
14961
- }
14962
- return actions;
14963
- }
14964
14712
  async function gitlabMcpHost(projectDir) {
14965
14713
  try {
14966
14714
  const vcs = await detectVcs(projectDir);
@@ -15049,18 +14797,6 @@ function createSetupAction(name, status, detail) {
15049
14797
  function reportProgress(options, message) {
15050
14798
  options.onProgress?.(message);
15051
14799
  }
15052
- async function packageVersion2() {
15053
- const source = await readFile7(join14(resolveBundledAgentsDir(), "..", "package.json"), "utf8");
15054
- let value;
15055
- try {
15056
- value = JSON.parse(source);
15057
- } catch (error) {
15058
- throw new Error("Cannot parse the installed @difflab/pi package metadata.", { cause: error });
15059
- }
15060
- if (typeof value.version !== "string")
15061
- throw new Error("Cannot resolve the installed @difflab/pi version.");
15062
- return value.version;
15063
- }
15064
14800
  function getRecord(value) {
15065
14801
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
15066
14802
  }
@@ -15076,13 +14812,13 @@ var REQUIRED_AGENTS = [
15076
14812
  async function doctorAgents(options = {}) {
15077
14813
  const bundledDir = options.bundledAgentsDir ?? resolveBundledAgentsDir();
15078
14814
  const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
15079
- const targetDir = join15(agentDir, "agents");
14815
+ const targetDir = join13(agentDir, "agents");
15080
14816
  const bundled = (await readdir3(bundledDir, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.startsWith("diffpi-") && entry.name.endsWith(".md")).map((entry) => entry.name);
15081
14817
  for (const name of REQUIRED_AGENTS)
15082
14818
  if (!bundled.includes(name))
15083
14819
  throw new Error(`Installed @difflab/pi is missing bundled agent ${name}`);
15084
14820
  for (const name of bundled) {
15085
- const content = await readFile8(join15(bundledDir, name), "utf8");
14821
+ const content = await readFile6(join13(bundledDir, name), "utf8");
15086
14822
  if (content.includes("diffpi_modes_"))
15087
14823
  throw new Error(`Installed agent ${name} still requires removed mode tools`);
15088
14824
  }
@@ -15096,10 +14832,10 @@ async function doctorAgents(options = {}) {
15096
14832
  if (!bundled.includes(basename5(path)))
15097
14833
  throw new Error(`Unexpected agent destination: ${path}`);
15098
14834
  try {
15099
- const stat = await lstat2(path);
14835
+ const stat = await lstat3(path);
15100
14836
  if (!stat.isFile())
15101
14837
  throw new Error(`Refusing to overwrite non-file agent: ${path}`);
15102
- originals.push({ path, content: await readFile8(path) });
14838
+ originals.push({ path, content: await readFile6(path) });
15103
14839
  } catch (error) {
15104
14840
  if (error.code !== "ENOENT")
15105
14841
  throw error;
@@ -15107,17 +14843,17 @@ async function doctorAgents(options = {}) {
15107
14843
  }
15108
14844
  const backups = [];
15109
14845
  if (originals.length > 0) {
15110
- const backupDir = join15(targetDir, ".diffpi-doctor-backups");
14846
+ const backupDir = join13(targetDir, ".diffpi-doctor-backups");
15111
14847
  await mkdir6(backupDir, { recursive: true, mode: 448 });
15112
14848
  for (const original of originals) {
15113
14849
  const digest = createHash2("sha256").update(original.content).digest("hex").slice(0, 16);
15114
- const backupPath = join15(backupDir, `${basename5(original.path)}.${digest}.bak`);
14850
+ const backupPath = join13(backupDir, `${basename5(original.path)}.${digest}.bak`);
15115
14851
  try {
15116
- await writeFile5(backupPath, original.content, { flag: "wx", mode: 384 });
14852
+ await writeFile4(backupPath, original.content, { flag: "wx", mode: 384 });
15117
14853
  } catch (error) {
15118
14854
  if (error.code !== "EEXIST")
15119
14855
  throw error;
15120
- const existing = await readFile8(backupPath);
14856
+ const existing = await readFile6(backupPath);
15121
14857
  if (!existing.equals(original.content))
15122
14858
  throw new Error(`Backup hash collision: ${backupPath}`);
15123
14859
  }
@@ -15169,14 +14905,14 @@ import { defineTool as defineTool4 } from "@earendil-works/pi-coding-agent";
15169
14905
  import { z as z6 } from "zod";
15170
14906
 
15171
14907
  // src/plan/verify.ts
15172
- import { readFile as readFile9, readdir as readdir4, stat } from "node:fs/promises";
15173
- import { basename as basename6, dirname as dirname8, join as join16, resolve as resolve4 } from "node:path";
14908
+ import { lstat as lstat4, readFile as readFile7, readdir as readdir4, stat } from "node:fs/promises";
14909
+ import { basename as basename6, dirname as dirname8, join as join14, resolve as resolve5 } from "node:path";
15174
14910
  async function verifyLivePlan(requested, cwd = process.cwd()) {
15175
- const path = resolve4(cwd, requested);
14911
+ const path = resolve5(cwd, requested);
15176
14912
  let planPath = path;
15177
14913
  try {
15178
14914
  if ((await stat(path)).isDirectory())
15179
- planPath = join16(path, "PLAN.md");
14915
+ planPath = join14(path, "PLAN.md");
15180
14916
  } catch {}
15181
14917
  const issues = [];
15182
14918
  const briefPaths = [];
@@ -15189,7 +14925,7 @@ async function verifyLivePlan(requested, cwd = process.cwd()) {
15189
14925
  addIssue(issues, planPath, 0, "layout", "Place PLAN.md under .diffpi/plan/<YYMMDD[-ticket]-short-slug>/.");
15190
14926
  let source;
15191
14927
  try {
15192
- source = await readFile9(planPath, "utf8");
14928
+ source = await readFile7(planPath, "utf8");
15193
14929
  } catch {
15194
14930
  addIssue(issues, planPath, 0, "missing-plan", "PLAN.md is not readable.");
15195
14931
  return { planPath, briefPaths, ok: false, issues };
@@ -15203,7 +14939,7 @@ async function verifyLivePlan(requested, cwd = process.cwd()) {
15203
14939
  if (!planLines[line]?.startsWith(`### Phase ${index + 1}: `))
15204
14940
  addIssue(issues, planPath, line + 1, "phase-order", "Number phase headings from 1 without gaps.");
15205
14941
  }
15206
- const implementation = join16(planDir, "implementation");
14942
+ const implementation = join14(planDir, "implementation");
15207
14943
  let rootEntries = [];
15208
14944
  let briefEntries = [];
15209
14945
  try {
@@ -15211,19 +14947,28 @@ async function verifyLivePlan(requested, cwd = process.cwd()) {
15211
14947
  briefEntries = await readdir4(implementation);
15212
14948
  } catch {}
15213
14949
  for (const entry of rootEntries) {
15214
- if (entry !== "PLAN.md" && entry !== "implementation")
15215
- addIssue(issues, join16(planDir, entry), 0, "layout", "Only PLAN.md and implementation/ belong in the plan directory.");
14950
+ const directory = ["implementation", "revisions", "logs"].includes(entry);
14951
+ const logFile = entry === "logs.jsonl";
14952
+ let valid = entry === "PLAN.md";
14953
+ if (directory || logFile) {
14954
+ try {
14955
+ const details = await lstat4(join14(planDir, entry));
14956
+ valid = !details.isSymbolicLink() && (directory ? details.isDirectory() : details.isFile());
14957
+ } catch {}
14958
+ }
14959
+ if (!valid)
14960
+ addIssue(issues, join14(planDir, entry), 0, "layout", "Only PLAN.md, implementation/, revisions/, logs.jsonl, and historical logs/ belong in the plan directory.");
15216
14961
  }
15217
14962
  for (const entry of briefEntries) {
15218
14963
  const ordinal = /^phase-(\d+)\.md$/.exec(entry)?.[1];
15219
14964
  if (!ordinal || !Number(ordinal) || Number(ordinal) > phases.length || entry !== `phase-${Number(ordinal)}.md`)
15220
- addIssue(issues, join16(implementation, entry), 0, "extra-brief", "Only matching numbered phase briefs belong in implementation/.");
14965
+ addIssue(issues, join14(implementation, entry), 0, "extra-brief", "Only matching numbered phase briefs belong in implementation/.");
15221
14966
  }
15222
14967
  for (let index = 0;index < phases.length; index++) {
15223
- const brief = join16(implementation, `phase-${index + 1}.md`);
14968
+ const brief = join14(implementation, `phase-${index + 1}.md`);
15224
14969
  briefPaths.push(brief);
15225
14970
  try {
15226
- const lines = visibleLines(await readFile9(brief, "utf8"));
14971
+ const lines = visibleLines(await readFile7(brief, "utf8"));
15227
14972
  if (!/^# Phase \d+:\s*\S/.test(lines[0] ?? "") || !lines[0]?.startsWith(`# Phase ${index + 1}: `))
15228
14973
  addIssue(issues, brief, 1, "phase-title", `Start with # Phase ${index + 1}: <title>.`);
15229
14974
  checkHeadings(lines, brief, ["Objective", "Files Affected", "Tasks", "Implementation Constraints"], issues);
@@ -15304,11 +15049,8 @@ function createDiffpiReloadTool(pi) {
15304
15049
  }
15305
15050
 
15306
15051
  // src/tools/review.ts
15307
- import { existsSync as existsSync3 } from "node:fs";
15308
- import { mkdir as mkdir7, readdir as readdir5, readFile as readFile14, rename as rename2, stat as stat2, unlink as unlink2, writeFile as writeFile8 } from "node:fs/promises";
15309
- import { join as join19 } from "node:path";
15310
15052
  import { defineTool as defineTool6 } from "@earendil-works/pi-coding-agent";
15311
- import { z as z11 } from "zod";
15053
+ import { z as z10 } from "zod";
15312
15054
 
15313
15055
  // src/gates.ts
15314
15056
  var CONVENTIONAL_COMMIT = /^(feat|fix|perf|refactor|docs|chore|test|build|ci|style|revert)(\([^)]+\))?!?: .+/;
@@ -15380,33 +15122,6 @@ var findingSchema = z8.object({
15380
15122
  reference: z8.string().optional().default("")
15381
15123
  });
15382
15124
  var findingsSchema = z8.array(findingSchema);
15383
- function reviewSlug(input) {
15384
- return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50);
15385
- }
15386
- function yymmdd(date = new Date) {
15387
- const yy = String(date.getFullYear() % 100).padStart(2, "0");
15388
- const mm = String(date.getMonth() + 1).padStart(2, "0");
15389
- const dd = String(date.getDate()).padStart(2, "0");
15390
- return `${yy}${mm}${dd}`;
15391
- }
15392
- function reviewRecordName(target, date = new Date) {
15393
- return `${yymmdd(date)}-${reviewSlug(target) || "uncommitted"}`;
15394
- }
15395
- function localResponseMarker(threadId) {
15396
- return `<!-- diffpi -->`;
15397
- }
15398
- function isLocalResponse(body) {
15399
- return /^<!-- diffpi(?:-local-response:[A-Za-z0-9_-]+)? -->\n/m.test(body);
15400
- }
15401
- function parseReviewThreadAction(body) {
15402
- const match = body.match(/^\[(REOPEN|RESOLVE|DELETE)\]\s*/i);
15403
- if (!match)
15404
- return { body };
15405
- return {
15406
- action: match[1].toLowerCase(),
15407
- body: body.slice(match[0].length)
15408
- };
15409
- }
15410
15125
  function dedupeFindings(findings) {
15411
15126
  const rank = { BLOCKING: 3, CONSIDER: 2, NOTE: 1 };
15412
15127
  const byKey = new Map;
@@ -15441,9 +15156,6 @@ function withRemoteProvenance(body, model) {
15441
15156
 
15442
15157
  Generated review by Diffpi using \`${model}\`.`;
15443
15158
  }
15444
- function localReviewAuthor(model) {
15445
- return `Agent: ${model}`;
15446
- }
15447
15159
  function renderCommentBody(finding) {
15448
15160
  const prefix = finding.severity === "BLOCKING" ? "**BLOCKING** " : "";
15449
15161
  const reference = finding.reference ? `
@@ -15451,142 +15163,6 @@ function renderCommentBody(finding) {
15451
15163
  > **Reference:** ${finding.reference}` : "";
15452
15164
  return `${prefix}${finding.body}${reference}`;
15453
15165
  }
15454
- // src/review/review-markdown.ts
15455
- import { z as z9 } from "zod";
15456
- var reviewThreadRecordSchema = z9.object({
15457
- id: z9.string().min(1),
15458
- file: z9.string().nullable().optional().transform((value) => value ?? undefined),
15459
- line: z9.number().int().positive().nullable().optional().transform((value) => value ?? undefined),
15460
- body: z9.string(),
15461
- author: z9.string().optional(),
15462
- resolved: z9.boolean(),
15463
- addressed: z9.boolean().optional(),
15464
- question: z9.boolean(),
15465
- replies: z9.array(z9.string()).optional()
15466
- });
15467
- function renderReviewDoc(input) {
15468
- const anchored = dedupeFindings(input.findings).filter((finding) => finding.line > 0);
15469
- const lines = [`# Review: ${input.title}`, "", "## Metadata"];
15470
- if (input.number !== undefined)
15471
- lines.push(`- **PR/MR**: #${input.number}${input.url ? ` — ${input.url}` : ""}`);
15472
- if (input.author)
15473
- lines.push(`- **Author**: ${input.author}`);
15474
- if (input.model)
15475
- lines.push(`- **Review agent**: ${input.model}`);
15476
- if (input.headRef && input.baseRef)
15477
- lines.push(`- **Branch**: ${input.headRef} → ${input.baseRef}`);
15478
- if (input.additions !== undefined)
15479
- lines.push(`- **Stats**: +${input.additions} -${input.deletions ?? 0} across ${input.changedFiles ?? 0} files`);
15480
- lines.push(`- **Reviewed**: ${input.timestamp ?? new Date().toISOString()}`, "");
15481
- if (input.overallIssues.length > 0) {
15482
- lines.push("## Overall issues", "");
15483
- for (const issue of input.overallIssues)
15484
- lines.push(`- ${issue}`);
15485
- lines.push("");
15486
- }
15487
- lines.push("## Verification", "");
15488
- for (const gate of input.gates)
15489
- lines.push(`- ${gate.name}: ${gate.status} — ${gate.detail}`);
15490
- lines.push("");
15491
- if (input.notVerified.length > 0) {
15492
- lines.push("## What was NOT verified", "");
15493
- for (const item of input.notVerified)
15494
- lines.push(`- ${item}`);
15495
- lines.push("");
15496
- }
15497
- lines.push("## Inline Comments", "");
15498
- for (const finding of anchored) {
15499
- lines.push(`### ${finding.file}:${finding.line} — ${finding.severity}`, "", finding.body, "");
15500
- if (finding.reference)
15501
- lines.push(`> **Reference:** ${finding.reference}`, "");
15502
- lines.push("---", "");
15503
- }
15504
- return `${lines.join(`
15505
- `).trimEnd()}
15506
- `;
15507
- }
15508
- function renderThreadArtifact(title, target, threads, options = {}) {
15509
- const records = threads.map((thread) => ({
15510
- id: thread.id,
15511
- file: thread.file,
15512
- line: thread.line,
15513
- body: thread.body,
15514
- author: thread.author,
15515
- resolved: thread.resolved,
15516
- addressed: thread.addressed,
15517
- question: thread.question,
15518
- replies: thread.replies
15519
- }));
15520
- const payload = Buffer.from(JSON.stringify(records), "utf8").toString("base64url");
15521
- const lines = [
15522
- `<!-- diffpi-threads:${payload} -->`,
15523
- `# Review threads: ${title}`,
15524
- "",
15525
- "## Metadata",
15526
- "",
15527
- `- Target: ${target}`,
15528
- `- Pulled: ${options.timestamp ?? new Date().toISOString()}`
15529
- ];
15530
- if (options.number !== undefined)
15531
- lines.push(`- PR/MR: #${options.number}${options.url ? ` — ${options.url}` : ""}`);
15532
- lines.push("", "## Replies", "");
15533
- for (const thread of threads) {
15534
- const id = Buffer.from(thread.id, "utf8").toString("base64url");
15535
- lines.push(`### ${thread.file ?? "review"}:${thread.line ?? "n/a"} (${thread.id})`, "", `<!-- diffpi-reply-start:${id} -->`, thread.reply ?? "", `<!-- diffpi-reply-end:${id} -->`, "");
15536
- }
15537
- lines.push("## Source comments", "");
15538
- for (const thread of threads) {
15539
- lines.push(`### ${thread.file ?? "review"}:${thread.line ?? "n/a"} — ${thread.author ?? "unknown"}`, "", `Thread: ${thread.id}`, `Status: ${thread.resolved ? "resolved" : thread.addressed ? "addressed" : "open"}`, "", "**Source comment**", "", thread.body, "", "**Recorded response**", "", thread.reply?.trim() || "_No response recorded._", "", "---", "");
15540
- }
15541
- return `${lines.join(`
15542
- `).trimEnd()}
15543
- `;
15544
- }
15545
- function parseThreadArtifact(content) {
15546
- const payload = content.match(/^<!-- diffpi-threads:([A-Za-z0-9_-]+) -->$/m)?.[1];
15547
- if (!payload)
15548
- throw new Error("This file is not a Diffpi thread artifact.");
15549
- let threads;
15550
- try {
15551
- const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
15552
- threads = z9.array(reviewThreadRecordSchema).parse(decoded);
15553
- } catch {
15554
- throw new Error("Cannot parse the Diffpi thread artifact payload.");
15555
- }
15556
- const replies = content.split(/^## Source comments$/m, 1)[0] ?? "";
15557
- return threads.map((thread) => {
15558
- const id = Buffer.from(thread.id, "utf8").toString("base64url");
15559
- const startMarker = `<!-- diffpi-reply-start:${id} -->
15560
- `;
15561
- const endMarker = `
15562
- <!-- diffpi-reply-end:${id} -->`;
15563
- const start = replies.indexOf(startMarker);
15564
- const end = start < 0 ? -1 : replies.indexOf(endMarker, start + startMarker.length);
15565
- const reply = start >= 0 && end >= 0 ? replies.slice(start + startMarker.length, end).trim() : "";
15566
- return reply ? { ...thread, reply } : thread;
15567
- });
15568
- }
15569
- function upsertThreadReply(content, threadId, body, question, resolved) {
15570
- const threads = parseThreadArtifact(content);
15571
- const thread = threads.find((candidate) => candidate.id === threadId);
15572
- if (!thread)
15573
- throw new Error(`Review thread ${threadId} was not found in the local artifact.`);
15574
- thread.reply = body;
15575
- thread.addressed = true;
15576
- if (question !== undefined)
15577
- thread.question = question;
15578
- if (resolved !== undefined)
15579
- thread.resolved = resolved;
15580
- const title = content.match(/^# Review threads: (.+)$/m)?.[1] ?? "review";
15581
- const target = content.match(/^- Target: (.+)$/m)?.[1] ?? "local";
15582
- const timestamp = content.match(/^- Pulled: (.+)$/m)?.[1];
15583
- const pr = content.match(/^- PR\/MR: #(\d+)(?: — (.+))?$/m);
15584
- return renderThreadArtifact(title, target, threads, {
15585
- timestamp,
15586
- number: pr ? Number.parseInt(pr[1], 10) : undefined,
15587
- url: pr?.[2]
15588
- });
15589
- }
15590
15166
  // src/review/github-review-backend.ts
15591
15167
  function GitHubReviewBackend(vcs, number) {
15592
15168
  const pendingReview = async () => {
@@ -15966,245 +15542,6 @@ function hasGitlabDraftNotes(input) {
15966
15542
  }
15967
15543
  }
15968
15544
 
15969
- // src/review/local-review-backend.ts
15970
- import { readFile as readFile11, writeFile as writeFile6 } from "node:fs/promises";
15971
-
15972
- // src/extensions/tuicrx.ts
15973
- import { readFile as readFile10, realpath as realpath3 } from "node:fs/promises";
15974
- import { resolve as resolve5 } from "node:path";
15975
- async function tuicrAvailable() {
15976
- return Boolean(await findExecutable("tuicr"));
15977
- }
15978
- async function listSessions(repo = ".") {
15979
- await requireTuicr();
15980
- const result = await run("tuicr", ["review", "list", "--repo", repo]);
15981
- if (result.code !== 0 || !result.stdout.trim())
15982
- return [];
15983
- try {
15984
- const raw = JSON.parse(result.stdout);
15985
- return raw.map((entry) => ({
15986
- slug: entry.slug,
15987
- kind: entry.kind,
15988
- path: entry.path,
15989
- updatedAt: entry.updated_at,
15990
- commentCount: entry.comment_count,
15991
- anchor: entry.anchor,
15992
- active: entry.active
15993
- }));
15994
- } catch {
15995
- throw new Error("Cannot parse tuicr review list output.");
15996
- }
15997
- }
15998
- async function addComment(session, body, opts = {}) {
15999
- await requireTuicr();
16000
- const args = ["review", "add", "--session", session, body];
16001
- if (opts.targetFile)
16002
- args.push("--target-file", opts.targetFile);
16003
- if (opts.line !== undefined)
16004
- args.push("--line", String(opts.line));
16005
- if (opts.side)
16006
- args.push("--side", opts.side);
16007
- if (opts.username)
16008
- args.push("--username", opts.username);
16009
- await runChecked("tuicr", args);
16010
- }
16011
- async function launch(cwd, pr, localBase) {
16012
- let command;
16013
- if (pr !== undefined)
16014
- command = ["tuicr", "pr", String(pr)];
16015
- else if (localBase)
16016
- command = ["tuicr", "-w", "-r", `${localBase}..HEAD`];
16017
- else
16018
- throw new Error("Cannot launch a local tuicr review: no base branch could be determined.");
16019
- if (!await tuicrAvailable()) {
16020
- return {
16021
- launched: false,
16022
- via: "print",
16023
- command: command.join(" "),
16024
- reason: "tuicr is not installed or is not available on PATH.",
16025
- instruction: `Install tuicr, then run: ${command.join(" ")}`
16026
- };
16027
- }
16028
- const name = diffpiLaunchName(cwd, pr === undefined ? "local review" : `PR #${pr}`);
16029
- return openInNewTab(command, { cwd, name });
16030
- }
16031
- async function resolveSession(cwd, branch) {
16032
- const sessions = await listSessions(cwd);
16033
- return findMatchingSession(sessions, cwd, branch);
16034
- }
16035
- async function resolveReviewSession(cwd, target) {
16036
- if (!target.workingTree && target.owner && target.repo && target.number !== undefined) {
16037
- return resolvePrSession(cwd, target.owner, target.repo, target.number);
16038
- }
16039
- return resolveSession(cwd, target.branch);
16040
- }
16041
- async function resolvePublishSession(cwd, target) {
16042
- if (target.owner && target.repo && target.number !== undefined)
16043
- return resolvePrSession(cwd, target.owner, target.repo, target.number);
16044
- return resolveSession(cwd, target.branch);
16045
- }
16046
- async function resolvePrSession(cwd, owner, repo, number) {
16047
- const coordinate = `${owner}/${repo}`.toLowerCase();
16048
- const sessions = await listSessions(cwd);
16049
- return sessions.find((session) => {
16050
- const slug = session.slug.toLowerCase();
16051
- return session.kind === "pr" && slug.includes(coordinate) && (slug.endsWith(`/pr/${number}`) || slug.endsWith(`/mr/${number}`));
16052
- });
16053
- }
16054
- async function findMatchingSession(sessions, cwd, branch) {
16055
- const repository = await canonicalPath3(await gitToplevel(cwd));
16056
- for (const session of sessions) {
16057
- if (session.kind !== "local")
16058
- continue;
16059
- try {
16060
- const data = await readSession(session.path);
16061
- if (data.branch_name !== branch || !data.repo_path)
16062
- continue;
16063
- if (await canonicalPath3(data.repo_path) === repository)
16064
- return session;
16065
- } catch {}
16066
- }
16067
- return;
16068
- }
16069
- async function readSession(path) {
16070
- const content = await readFile10(path, "utf8");
16071
- try {
16072
- return JSON.parse(content);
16073
- } catch {
16074
- throw new Error(`Cannot parse tuicr session JSON: ${path}`);
16075
- }
16076
- }
16077
- function toLocalReviewThreads(session) {
16078
- const threads = [];
16079
- const add = (comment, location = {}) => {
16080
- if (isLocalResponse(comment.content))
16081
- return;
16082
- const author = commentAuthor(comment);
16083
- threads.push({
16084
- id: comment.id ?? `local-${threads.length + 1}`,
16085
- ...location,
16086
- body: comment.content,
16087
- ...author ? { author } : {},
16088
- resolved: false,
16089
- question: /\?\s*$/.test(comment.content.trim())
16090
- });
16091
- };
16092
- for (const comment of session.review_comments ?? [])
16093
- add(comment);
16094
- for (const [file, entry] of Object.entries(session.files ?? {})) {
16095
- for (const comment of entry.file_comments ?? [])
16096
- add(comment, { file });
16097
- for (const [lineKey, lineComments] of Object.entries(entry.line_comments ?? {})) {
16098
- const line = Number.parseInt(lineKey, 10);
16099
- if (!Number.isFinite(line))
16100
- continue;
16101
- for (const comment of lineComments)
16102
- add(comment, { file, line });
16103
- }
16104
- }
16105
- return threads;
16106
- }
16107
- function toFindings(session, options = {}) {
16108
- const comments = [];
16109
- const include = (comment) => (!options.agentOnly || commentAuthor(comment)?.startsWith("Agent: ")) && (!options.excludeLocalResponses || !isLocalResponse(comment.content));
16110
- const publishableBody = (comment) => {
16111
- const author = commentAuthor(comment);
16112
- const route = author?.match(/^Agent:\s*(.+)$/)?.[1];
16113
- return route ? withRemoteProvenance(comment.content, route) : comment.content;
16114
- };
16115
- const bodyParts = (session.review_comments ?? []).flatMap((comment) => include(comment) ? [publishableBody(comment)] : []);
16116
- for (const [file, entry] of Object.entries(session.files ?? {})) {
16117
- for (const fileComment of entry.file_comments ?? []) {
16118
- if (!include(fileComment))
16119
- continue;
16120
- const comment = {
16121
- file,
16122
- body: fileComment.content,
16123
- ...fileComment.id ? { sourceCommentId: String(fileComment.id) } : {}
16124
- };
16125
- const author = commentAuthor(fileComment);
16126
- if (author)
16127
- comment.author = author;
16128
- comments.push(comment);
16129
- }
16130
- for (const [lineKey, lineComments] of Object.entries(entry.line_comments ?? {})) {
16131
- const line = Number.parseInt(lineKey, 10);
16132
- if (!Number.isFinite(line))
16133
- continue;
16134
- for (const lineComment of lineComments) {
16135
- if (!include(lineComment))
16136
- continue;
16137
- const comment = {
16138
- file,
16139
- line,
16140
- side: lineComment.side === "old" ? "LEFT" : "RIGHT",
16141
- body: lineComment.content,
16142
- ...lineComment.id ? { sourceCommentId: String(lineComment.id) } : {}
16143
- };
16144
- const author = commentAuthor(lineComment);
16145
- if (author)
16146
- comment.author = author;
16147
- comments.push(comment);
16148
- }
16149
- }
16150
- }
16151
- return { comments, body: bodyParts.join(`
16152
-
16153
- `) };
16154
- }
16155
- async function requireTuicr() {
16156
- if (!await tuicrAvailable())
16157
- throw new Error("tuicr is not installed or is not available on PATH.");
16158
- }
16159
- function commentAuthor(comment) {
16160
- return comment.username ?? comment.author;
16161
- }
16162
- async function canonicalPath3(path) {
16163
- try {
16164
- return await realpath3(path);
16165
- } catch {
16166
- return resolve5(path);
16167
- }
16168
- }
16169
-
16170
- // src/review/local-review-backend.ts
16171
- function LocalReviewBackend(options) {
16172
- return {
16173
- kind: "local",
16174
- async stage(draft) {
16175
- for (const comment of draft.comments) {
16176
- await addComment(options.session, comment.body, {
16177
- targetFile: comment.file,
16178
- line: comment.line,
16179
- side: comment.side === "LEFT" ? "old" : "new",
16180
- username: options.author
16181
- });
16182
- }
16183
- if (draft.body.trim())
16184
- await addComment(options.session, draft.body, { username: options.author });
16185
- },
16186
- async readDraft() {
16187
- return toFindings(await readSession(options.session), { excludeLocalResponses: true });
16188
- },
16189
- async listThreads() {
16190
- try {
16191
- return parseThreadArtifact(await readFile11(options.artifactPath, "utf8"));
16192
- } catch (error) {
16193
- if (error.code === "ENOENT")
16194
- return [];
16195
- throw error;
16196
- }
16197
- },
16198
- async reply(input) {
16199
- const content = await readFile11(options.artifactPath, "utf8");
16200
- await writeFile6(options.artifactPath, upsertThreadReply(content, input.threadId, input.body, input.question, input.resolve), "utf8");
16201
- },
16202
- async publish() {
16203
- throw new Error("Promote a local draft through a remote review backend before publishing it.");
16204
- }
16205
- };
16206
- }
16207
-
16208
15545
  // src/review/review-backend.ts
16209
15546
  function createRemoteReviewBackend(vcs, number) {
16210
15547
  if (vcs.provider === "github")
@@ -16213,67 +15550,24 @@ function createRemoteReviewBackend(vcs, number) {
16213
15550
  return GitLabReviewBackend(vcs, number);
16214
15551
  throw new Error("A remote review backend requires a GitHub or GitLab repository.");
16215
15552
  }
16216
- function createLocalReviewBackend(options) {
16217
- return LocalReviewBackend(options);
16218
- }
16219
15553
  // src/review/review-state.ts
16220
- import { createHash as createHash3 } from "node:crypto";
16221
- import { readFile as readFile12, rename, writeFile as writeFile7 } from "node:fs/promises";
16222
- import { join as join17 } from "node:path";
16223
- import { z as z10 } from "zod";
16224
- var reviewPublicationStateSchema = z10.object({
16225
- target: z10.string().optional(),
16226
- bodies: z10.array(z10.string()).default([]),
16227
- comments: z10.array(z10.string()).default([]),
16228
- stagedComments: z10.array(z10.string()).default([]),
16229
- replies: z10.array(z10.string()).default([]),
16230
- overlayPath: z10.string().optional()
15554
+ import { z as z9 } from "zod";
15555
+ var reviewPublicationStateSchema = z9.object({
15556
+ target: z9.string().optional(),
15557
+ bodies: z9.array(z9.string()).default([]),
15558
+ comments: z9.array(z9.string()).default([]),
15559
+ stagedComments: z9.array(z9.string()).default([]),
15560
+ replies: z9.array(z9.string()).default([]),
15561
+ overlayPath: z9.string().optional()
16231
15562
  });
16232
- function reviewBodyFingerprint(body) {
16233
- return digest(body);
16234
- }
16235
- function reviewCommentFingerprint(comment) {
16236
- return digest([comment.file, String(comment.line), comment.side ?? "RIGHT", comment.body].join("\x00"));
16237
- }
16238
- function reviewReplyFingerprint(threadId, body) {
16239
- return digest(`${threadId}\x00${body}`);
16240
- }
16241
- function unpublishedReviewComments(comments, knownFingerprints) {
16242
- return comments.filter((comment) => !knownFingerprints.has(reviewCommentFingerprint(comment)));
16243
- }
16244
- async function loadReviewPublicationState(cwd, vcs, number, homeDir) {
16245
- const target = `${vcs.provider}:${vcs.owner}/${vcs.repo}#${number}`;
16246
- const path = join17(await sessionsDir(cwd, homeDir), `review-publish-${digest(target).slice(0, 16)}.json`);
16247
- try {
16248
- const parsed = reviewPublicationStateSchema.parse(JSON.parse(await readFile12(path, "utf8")));
16249
- return { path, state: { ...parsed, target } };
16250
- } catch (error) {
16251
- if (error.code === "ENOENT") {
16252
- return { path, state: { target, bodies: [], comments: [], stagedComments: [], replies: [] } };
16253
- }
16254
- if (error instanceof SyntaxError || error instanceof z10.ZodError) {
16255
- throw new Error(`Cannot parse review publication state: ${path}`);
16256
- }
16257
- throw error;
16258
- }
16259
- }
16260
- async function saveReviewPublicationState(path, state) {
16261
- const temp = `${path}.${process.pid}.tmp`;
16262
- await writeFile7(temp, `${JSON.stringify(state, null, 2)}
16263
- `, "utf8");
16264
- await rename(temp, path);
16265
- }
16266
- function digest(value) {
16267
- return createHash3("sha256").update(value).digest("hex");
16268
- }
16269
15563
  // src/templates.ts
16270
- import { readFile as readFile13 } from "node:fs/promises";
16271
- import { homedir as homedir9 } from "node:os";
16272
- import { join as join18, normalize } from "node:path";
15564
+ import { readFile as readFile8 } from "node:fs/promises";
15565
+ import { homedir as homedir8 } from "node:os";
15566
+ import { join as join15, normalize } from "node:path";
16273
15567
  async function loadTemplate(name, options = {}) {
16274
15568
  const relative = templateRelativePath(name);
16275
- const userPath = join18(options.homeDir ?? homedir9(), ".difflab", "diffpi", "templates", relative);
16276
- const bundledPath = join18(options.bundledDir ?? resolveBundledTemplatesDir(), relative);
15569
+ const userPath = join15(options.homeDir ?? homedir8(), ".difflab", "diffpi", "templates", relative);
15570
+ const bundledPath = join15(options.bundledDir ?? resolveBundledTemplatesDir(), relative);
16277
15571
  const user = await readOptionalFile(userPath);
16278
15572
  if (user !== undefined)
16279
15573
  return { name, path: userPath, source: "user", content: user };
@@ -16294,7 +15588,7 @@ function templateRelativePath(name) {
16294
15588
  }
16295
15589
  async function readOptionalFile(path) {
16296
15590
  try {
16297
- return await readFile13(path, "utf8");
15591
+ return await readFile8(path, "utf8");
16298
15592
  } catch (error) {
16299
15593
  if (error.code === "ENOENT")
16300
15594
  return;
@@ -16303,94 +15597,64 @@ async function readOptionalFile(path) {
16303
15597
  }
16304
15598
 
16305
15599
  // src/tools/review.ts
16306
- var contextSchema = z11.object({
16307
- cwd: z11.string().optional(),
16308
- target: z11.string().optional(),
16309
- workingTree: z11.boolean().optional()
15600
+ var contextSchema = z10.object({
15601
+ cwd: z10.string().optional(),
15602
+ target: z10.string().optional(),
15603
+ workingTree: z10.boolean().optional()
16310
15604
  });
16311
- var localSchema = contextSchema.extend({ local: z11.boolean().optional() });
15605
+ var localSchema = contextSchema.extend({ local: z10.boolean().optional() });
16312
15606
  var openSchema = localSchema.extend({
16313
- title: z11.string().optional(),
16314
- intent: z11.string().optional(),
16315
- issueUrl: z11.string().url().optional(),
16316
- base: z11.string().optional()
15607
+ title: z10.string().optional(),
15608
+ intent: z10.string().optional(),
15609
+ issueUrl: z10.string().url().optional(),
15610
+ base: z10.string().optional(),
15611
+ body: z10.string().optional()
16317
15612
  });
16318
15613
  var submitSchema = localSchema.extend({
16319
15614
  findings: findingsSchema,
16320
- overallIssues: z11.array(z11.string()).optional(),
16321
- notVerified: z11.array(z11.string()).optional(),
16322
- title: z11.string().optional()
15615
+ overallIssues: z10.array(z10.string()).optional(),
15616
+ notVerified: z10.array(z10.string()).optional(),
15617
+ title: z10.string().optional()
16323
15618
  });
16324
15619
  var addCommentSchema = localSchema.extend({
16325
- body: z11.string().min(1),
16326
- file: z11.string().min(1),
16327
- line: z11.number().int().positive(),
16328
- side: z11.enum(["LEFT", "RIGHT"]).optional()
15620
+ body: z10.string().min(1),
15621
+ file: z10.string().min(1),
15622
+ line: z10.number().int().positive(),
15623
+ side: z10.enum(["LEFT", "RIGHT"]).optional()
16329
15624
  });
16330
15625
  var respondSchema = localSchema.extend({
16331
- threadId: z11.string().min(1),
16332
- body: z11.string().min(1),
16333
- question: z11.boolean().optional(),
16334
- resolve: z11.boolean().optional()
15626
+ threadId: z10.string().min(1),
15627
+ body: z10.string().min(1),
15628
+ question: z10.boolean().optional(),
15629
+ resolve: z10.boolean().optional()
16335
15630
  });
16336
15631
  var publishSchema = localSchema.extend({
16337
- status: z11.enum(["COMMENT", "APPROVE", "REQUEST_CHANGES", "CLOSE"]).optional()
15632
+ status: z10.enum(["COMMENT", "APPROVE", "REQUEST_CHANGES", "CLOSE"]).optional()
16338
15633
  });
16339
15634
  var completeSchema = localSchema.extend({
16340
- action: z11.enum(["approve", "reject", "abandon"]).optional()
15635
+ action: z10.enum(["approve", "reject", "close"]).optional()
16341
15636
  });
16342
15637
  function hasReviewDraft(comments, body) {
16343
15638
  return comments.length > 0 || body.trim().length > 0;
16344
15639
  }
16345
- function reviewBrowserCommand(url, platform = process.platform) {
16346
- return platform === "darwin" ? { command: "open", args: [url] } : platform === "win32" ? { command: "cmd", args: ["/c", "start", "", url] } : { command: "xdg-open", args: [url] };
16347
- }
16348
- async function openReviewUrl(url, execute = run, platform = process.platform) {
16349
- const launch = reviewBrowserCommand(url, platform);
16350
- try {
16351
- return (await execute(launch.command, launch.args)).code === 0;
16352
- } catch {
16353
- return false;
16354
- }
16355
- }
16356
- function partitionReviewComments(comments, knownFingerprints, model) {
16357
- const normalized = comments.map((comment) => {
16358
- const published = { ...comment, body: withCommentProvenance(comment, model) };
16359
- return { source: comment, published, fingerprint: reviewCommentFingerprint(published) };
16360
- });
16361
- return {
16362
- checkpointed: normalized.filter((comment) => knownFingerprints.has(comment.fingerprint)),
16363
- candidates: normalized.filter((comment) => !knownFingerprints.has(comment.fingerprint))
16364
- };
16365
- }
16366
15640
  function reviewResponseResolution(question, requested) {
16367
15641
  return question ? false : requested ?? false;
16368
15642
  }
16369
- function workingTreeReplyDraft(thread, body) {
16370
- if (!thread.file)
16371
- return { comments: [], body };
16372
- return {
16373
- comments: [
16374
- {
16375
- file: thread.file,
16376
- ...thread.line === undefined ? {} : { line: thread.line, side: "RIGHT" },
16377
- body
16378
- }
16379
- ],
16380
- body: ""
16381
- };
16382
- }
16383
- function resolveReviewThread(comment, threads) {
16384
- const sourceCommentId = comment.sourceCommentId;
16385
- if (sourceCommentId) {
16386
- const identified = threads.find((thread) => thread.id === sourceCommentId || thread.rootCommentId === sourceCommentId || thread.commentIds?.includes(sourceCommentId));
16387
- if (identified)
16388
- return identified;
15643
+ function assertRenderedReviewBody(body) {
15644
+ if (/{{[^{}]+}}/.test(body))
15645
+ throw new Error("Review body contains unresolved template placeholders.");
15646
+ if (/<!--/.test(body))
15647
+ throw new Error("Review body contains HTML comments.");
15648
+ const headings = [...body.matchAll(/^## (.+)$/gm)];
15649
+ for (const section of ["Intent", "Changes", "Validation", "References", "Further Work"]) {
15650
+ const index = headings.findIndex((match) => match[1].trim() === section);
15651
+ const current = headings[index];
15652
+ if (!current || current.index === undefined)
15653
+ throw new Error(`Review body requires a filled ${section} section.`);
15654
+ const content = body.slice(current.index + current[0].length, headings[index + 1]?.index).trim();
15655
+ if (!content)
15656
+ throw new Error(`Review body requires a filled ${section} section.`);
16389
15657
  }
16390
- const matches = threads.filter((thread) => thread.file === comment.file && (thread.line ?? undefined) === (comment.line ?? undefined));
16391
- if (matches.length > 1)
16392
- throw new Error(`Ambiguous remote review threads at ${comment.file}:${comment.line ?? "file"}: ${matches.map((thread) => thread.id).join(", ")}.`);
16393
- return matches[0];
16394
15658
  }
16395
15659
  async function workingTreeDiff(cwd) {
16396
15660
  const tracked = await run("git", ["-C", cwd, "diff", "HEAD"], { capture: "unbounded" });
@@ -16414,7 +15678,7 @@ function createReviewTools() {
16414
15678
  defineTool6({
16415
15679
  name: "review_context",
16416
15680
  label: "review context",
16417
- description: "Orient to the target, backend, forge, environment, shared store, PR/MR, and tuicr session.",
15681
+ description: "Orient to the target, forge, environment, shared store, and PR/MR.",
16418
15682
  promptSnippet: "Call review_context first",
16419
15683
  promptGuidelines: ["Call this before every review workflow."],
16420
15684
  parameters: parameters5(localSchema),
@@ -16424,19 +15688,15 @@ function createReviewTools() {
16424
15688
  const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
16425
15689
  const store = await ensureStore(review.cwd);
16426
15690
  const env = { ide: detectIde(), mux: detectMux(), shell: detectShell() };
16427
- const session = await resolveTuicrSession(review, Boolean(params.local || params.workingTree)).catch(() => {
16428
- return;
16429
- });
16430
15691
  const baseRef = review.pr?.baseRef ?? (review.forge ? await review.forge.defaultBranch() : "local");
16431
- const backend = params.local || params.workingTree ? "tuicr" : review.forge?.provider ?? "unsupported";
15692
+ const backend = review.forge?.provider ?? "unsupported";
16432
15693
  return result2([
16433
15694
  `Backend: ${backend}`,
16434
15695
  `Forge: ${review.vcs.provider}${review.vcs.provider === "none" ? "" : ` (${review.vcs.owner}/${review.vcs.repo})`}`,
16435
15696
  `Branch: ${review.vcs.branch} → ${baseRef}`,
16436
15697
  `Env: ide=${env.ide} mux=${env.mux} shell=${env.shell}`,
16437
15698
  `Store: ${store.link} → ${store.dest}`,
16438
- review.pr ? `PR/MR: #${review.pr.number} ${review.pr.url}` : "PR/MR: none",
16439
- session ? `tuicr: ${session.slug} (${session.commentCount} comments)` : "tuicr: none"
15699
+ review.pr ? `PR/MR: #${review.pr.number} ${review.pr.url}` : "PR/MR: none"
16440
15700
  ].join(`
16441
15701
  `), {
16442
15702
  cwd: review.cwd,
@@ -16444,7 +15704,6 @@ function createReviewTools() {
16444
15704
  pr: review.pr,
16445
15705
  env,
16446
15706
  store,
16447
- session,
16448
15707
  baseRef,
16449
15708
  backend
16450
15709
  });
@@ -16453,9 +15712,9 @@ function createReviewTools() {
16453
15712
  defineTool6({
16454
15713
  name: "review_status",
16455
15714
  label: "review status",
16456
- description: "Report local and remote review lifecycle status without requiring a forge or tuicr.",
15715
+ description: "Report remote review lifecycle status without requiring a forge.",
16457
15716
  promptSnippet: "Call review_status for review lifecycle state",
16458
- promptGuidelines: ["Use this for status even when no forge or tuicr binary is available."],
15717
+ promptGuidelines: ["Use this for status even when no forge is available."],
16459
15718
  parameters: parameters5(localSchema),
16460
15719
  executionMode: "parallel",
16461
15720
  async execute(_id, input) {
@@ -16468,9 +15727,9 @@ function createReviewTools() {
16468
15727
  defineTool6({
16469
15728
  name: "review_new",
16470
15729
  label: "review new",
16471
- description: "Create a new local tuicr review or a remote draft PR/MR and open it in tuicr.",
15730
+ description: "Create a filled remote draft PR/MR.",
16472
15731
  promptSnippet: "Call review_new to start",
16473
- promptGuidelines: ["Use local to select tuicr as the review backend."],
15732
+ promptGuidelines: ["Use the skill workflow for direct-file local reviews."],
16474
15733
  parameters: parameters5(openSchema),
16475
15734
  executionMode: "sequential",
16476
15735
  async execute(_id, input) {
@@ -16478,7 +15737,7 @@ function createReviewTools() {
16478
15737
  const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
16479
15738
  await ensureStore(review.cwd);
16480
15739
  if (params.local || params.workingTree)
16481
- return launchLocalReview(review, true, params.base);
15740
+ return result2("Local reviews are direct Markdown files; follow the review new workflow instead.");
16482
15741
  if (!review.forge)
16483
15742
  return result2(unsupportedForgeMessage());
16484
15743
  if (review.pr)
@@ -16486,57 +15745,25 @@ function createReviewTools() {
16486
15745
  await assertRemoteBranchReady(review.cwd);
16487
15746
  const base = params.base ?? await review.forge.defaultBranch();
16488
15747
  const template = await loadTemplate("review/draft-pr");
16489
- const body = renderTemplate(template.content, {
16490
- intent: params.intent ?? "<!-- Describe why this change is needed. -->",
16491
- issue_url: params.issueUrl ?? "<!-- Add issue tracker URL. -->",
16492
- head: review.vcs.branch,
16493
- base
16494
- });
15748
+ if (!params.body)
15749
+ throw new Error("A fully rendered draft PR body is required.");
15750
+ assertRenderedReviewBody(params.body);
16495
15751
  const pr = await review.forge.createDraftPr({
16496
15752
  title: params.title ?? deriveTitle(review.vcs.branch),
16497
- body,
15753
+ body: params.body,
16498
15754
  base,
16499
15755
  head: review.vcs.branch
16500
15756
  });
16501
- const launched = await launch(review.cwd, pr.number);
16502
- const openMessage = launched.launched ? `Opened PR/MR #${pr.number} in tuicr (${launched.via}).` : launched.instruction ?? `Run: ${launched.command}`;
16503
- return result2(`Draft PR/MR created from ${template.source} template: ${pr.url}
16504
- ${openMessage}`, {
15757
+ return result2(`Draft PR/MR created from ${template.source} template: ${pr.url}`, {
16505
15758
  pr,
16506
- template,
16507
- launched
16508
- });
16509
- }
16510
- }),
16511
- defineTool6({
16512
- name: "review_open",
16513
- label: "review open",
16514
- description: "Open an existing remote PR/MR in the system browser; never creates one.",
16515
- promptSnippet: "Call review_open to open an existing PR/MR",
16516
- promptGuidelines: ["Direct users to review_new when no remote review exists."],
16517
- parameters: parameters5(localSchema),
16518
- executionMode: "sequential",
16519
- async execute(_id, input) {
16520
- const params = localSchema.parse(input);
16521
- const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
16522
- if (params.local || params.workingTree)
16523
- return launchLocalReview(review, true);
16524
- if (!review.forge)
16525
- return result2(`${unsupportedForgeMessage()} Use review_new with local=true for an offline review.`);
16526
- if (!review.pr)
16527
- return result2("No remote PR/MR exists. Use review_new to create a draft review first.");
16528
- const opened = await openReviewUrl(review.pr.url);
16529
- return result2(`${opened ? "Opened" : "Open this URL"} PR/MR #${review.pr.number}: ${review.pr.url}`, {
16530
- pr: review.pr,
16531
- opened,
16532
- url: review.pr.url
15759
+ template
16533
15760
  });
16534
15761
  }
16535
15762
  }),
16536
15763
  defineTool6({
16537
15764
  name: "review_edit",
16538
15765
  label: "review edit",
16539
- description: "Open an existing local tuicr session or remote PR/MR in tuicr without generating findings.",
15766
+ description: "Print the selected local review path or remote PR/MR URL without generating findings.",
16540
15767
  promptSnippet: "Call review_edit to continue an existing review",
16541
15768
  promptGuidelines: ["This tool never creates review findings or a PR/MR."],
16542
15769
  parameters: parameters5(localSchema),
@@ -16545,16 +15772,13 @@ ${openMessage}`, {
16545
15772
  const params = localSchema.parse(input);
16546
15773
  const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
16547
15774
  await ensureStore(review.cwd);
16548
- if (params.local || params.workingTree) {
16549
- if (!await resolveTuicrSession(review, true))
16550
- return result2("No local tuicr review exists. Use review_new with local=true to create one.");
16551
- return launchLocalReview(review, true);
16552
- }
15775
+ if (params.local || params.workingTree)
15776
+ return result2("Local reviews are direct Markdown files; print the selected REVIEW.md path from the workflow.");
16553
15777
  if (!review.forge)
16554
15778
  return result2(unsupportedForgeMessage());
16555
15779
  if (!review.pr)
16556
15780
  return result2("No remote PR/MR exists. Use review_new to create a draft review first.");
16557
- return launchLocalReview(review, false);
15781
+ return result2(`PR/MR #${review.pr.number}: ${review.pr.url}`, { pr: review.pr, url: review.pr.url });
16558
15782
  }
16559
15783
  }),
16560
15784
  defineTool6({
@@ -16622,52 +15846,21 @@ ${openMessage}`, {
16622
15846
  const params = submitSchema.parse(input);
16623
15847
  const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
16624
15848
  const model = modelRoute(ctx);
16625
- const useLocalBackend = Boolean(params.local || params.workingTree);
16626
- if (!useLocalBackend && !review.forge)
15849
+ if (params.local || params.workingTree)
15850
+ return result2("Local reviews are direct Markdown files; follow the review address workflow instead.");
15851
+ if (!review.forge)
16627
15852
  return result2(unsupportedForgeMessage());
16628
15853
  const findings = dedupeFindings(params.findings);
16629
- const gates = await runMiseGates(review.cwd);
16630
- const baseRef = review.pr?.baseRef ?? (review.forge ? await review.forge.defaultBranch() : "local");
16631
- const artifact = await newReviewArtifactPath(review.cwd, params.local || params.workingTree || !review.pr ? "uncommitted" : await reviewTargetId(review));
16632
- await writeFile8(artifact, renderReviewDoc({
16633
- title: params.title ?? review.pr?.title ?? review.vcs.branch,
16634
- number: useLocalBackend ? undefined : review.pr?.number,
16635
- url: useLocalBackend ? undefined : review.pr?.url,
16636
- model,
16637
- headRef: review.vcs.branch,
16638
- baseRef,
16639
- findings,
16640
- overallIssues: params.overallIssues ?? [],
16641
- gates,
16642
- notVerified: params.notVerified ?? []
16643
- }), "utf8");
16644
- const comments = toReviewComments(findings, useLocalBackend ? undefined : model);
15854
+ const comments = toReviewComments(findings, model);
16645
15855
  const body = (params.overallIssues ?? []).join(`
16646
15856
  `);
16647
- if (useLocalBackend) {
16648
- const session = await resolveTuicrSession(review, Boolean(params.local || params.workingTree));
16649
- if (!session) {
16650
- return result2(`Review written: ${artifact}. Open tuicr, then call review_submit again to seed comments.`, {
16651
- artifact,
16652
- count: findings.length
16653
- });
16654
- }
16655
- const backend = createLocalReviewBackend({
16656
- session: session.path,
16657
- artifactPath: artifact,
16658
- author: localReviewAuthor(model)
16659
- });
16660
- await backend.stage({ comments, body });
16661
- return result2(`Local review staged in tuicr: ${artifact}`, { artifact, count: findings.length, session });
16662
- }
16663
15857
  if (!review.pr)
16664
- return result2(`Review written: ${artifact}. No PR/MR matches this remote target.`, { artifact });
15858
+ return result2("No PR/MR matches this remote target.");
16665
15859
  const hasDraft = hasReviewDraft(comments, body);
16666
15860
  if (hasDraft) {
16667
15861
  await createRemoteReviewBackend(review.vcs, review.pr.number).stage({ comments, body });
16668
15862
  }
16669
- return result2(`${hasDraft ? "Pending review staged" : "Clean review recorded"} on #${review.pr.number}. Artifact: ${artifact}`, {
16670
- artifact,
15863
+ return result2(`${hasDraft ? "Pending review staged" : "Clean review recorded"} on #${review.pr.number}.`, {
16671
15864
  pr: review.pr,
16672
15865
  count: findings.length
16673
15866
  });
@@ -16676,36 +15869,25 @@ ${openMessage}`, {
16676
15869
  defineTool6({
16677
15870
  name: "review_add_comment",
16678
15871
  label: "review add comment",
16679
- description: "Add one provenance-marked comment through tuicr or the remote pending-review backend.",
15872
+ description: "Add one provenance-marked comment through the remote pending-review backend.",
16680
15873
  promptSnippet: "Call review_add_comment for incremental comments",
16681
- promptGuidelines: ["Pass local=true when tuicr owns the draft."],
15874
+ promptGuidelines: ["Use the direct-file workflow for local reviews."],
16682
15875
  parameters: parameters5(addCommentSchema),
16683
15876
  executionMode: "sequential",
16684
15877
  async execute(_id, input, _signal, _onUpdate, ctx) {
16685
15878
  const params = addCommentSchema.parse(input);
16686
15879
  const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
16687
15880
  const model = modelRoute(ctx);
16688
- const useLocalBackend = Boolean(params.local || params.workingTree);
16689
- if (!useLocalBackend && !review.forge)
15881
+ if (params.local || params.workingTree)
15882
+ return result2("Local reviews are direct Markdown files; edit the selected REVIEW.md instead.");
15883
+ if (!review.forge)
16690
15884
  return result2(unsupportedForgeMessage());
16691
15885
  const comment = {
16692
15886
  file: params.file,
16693
15887
  line: params.line,
16694
15888
  side: params.side ?? "RIGHT",
16695
- body: useLocalBackend ? params.body : withRemoteProvenance(params.body, model)
15889
+ body: withRemoteProvenance(params.body, model)
16696
15890
  };
16697
- if (useLocalBackend) {
16698
- const session = await resolveTuicrSession(review, Boolean(params.local || params.workingTree));
16699
- if (!session)
16700
- return result2("No matching tuicr session. Open review_new or review_launch_ui first.");
16701
- const backend = createLocalReviewBackend({
16702
- session: session.path,
16703
- artifactPath: "",
16704
- author: localReviewAuthor(model)
16705
- });
16706
- await backend.stage({ comments: [comment], body: "" });
16707
- return result2(`Comment added to tuicr session ${session.slug}.`, { session, comment });
16708
- }
16709
15891
  if (!review.pr)
16710
15892
  return result2("No PR/MR matches this remote review target.");
16711
15893
  await createRemoteReviewBackend(review.vcs, review.pr.number).stage({ comments: [comment], body: "" });
@@ -16715,38 +15897,30 @@ ${openMessage}`, {
16715
15897
  defineTool6({
16716
15898
  name: "review_comments",
16717
15899
  label: "review comments",
16718
- description: "Pull review threads or local tuicr comments and write a target-named artifact.",
15900
+ description: "Pull remote review threads and return them for the direct-file workflow.",
16719
15901
  promptSnippet: "Call review_comments before addressing findings",
16720
- promptGuidelines: ["Pass local=true to prepare the local reply overlay."],
15902
+ promptGuidelines: ["Use the direct-file workflow for local review files."],
16721
15903
  parameters: parameters5(localSchema),
16722
15904
  executionMode: "sequential",
16723
15905
  async execute(_id, input) {
16724
15906
  const params = localSchema.parse(input);
16725
15907
  const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
16726
15908
  let threads;
16727
- let artifact;
16728
- if (params.local || params.workingTree) {
16729
- const session = await resolveTuicrSession(review, true);
16730
- if (!session)
16731
- return result2("No matching tuicr session found.");
16732
- artifact = await localThreadArtifactPath(review.cwd, session.slug);
16733
- threads = await syncLocalThreadArtifact(artifact, review.pr?.title ?? review.vcs.branch, session.slug, toLocalReviewThreads(await readSession(session.path)));
16734
- } else if (review.pr && review.forge) {
15909
+ if (params.local || params.workingTree)
15910
+ return result2("Local reviews are direct Markdown files; read the selected REVIEW.md instead.");
15911
+ if (review.pr && review.forge) {
16735
15912
  threads = await createRemoteReviewBackend(review.vcs, review.pr.number).listThreads();
16736
- const target = await reviewTargetId(review);
16737
- artifact = await newReviewArtifactPath(review.cwd, target);
16738
- await writeFile8(artifact, renderThreadArtifact(review.pr.title, target, threads, { number: review.pr.number, url: review.pr.url }), "utf8");
16739
15913
  } else {
16740
15914
  return result2(review.forge ? "No PR/MR matches this remote review target." : unsupportedForgeMessage());
16741
15915
  }
16742
15916
  return result2(threads.map((thread) => `${thread.id} ${thread.file ?? "review"}:${thread.line ?? "-"} — ${thread.body}`).join(`
16743
- `) || "No comments.", { artifact, threads });
15917
+ `) || "No comments.", { threads });
16744
15918
  }
16745
15919
  }),
16746
15920
  defineTool6({
16747
15921
  name: "review_respond",
16748
15922
  label: "review respond",
16749
- description: "Post local question replies to tuicr and record their publish overlay, or reply to remote threads.",
15923
+ description: "Reply to remote review threads.",
16750
15924
  promptSnippet: "Call review_respond after addressing a comment",
16751
15925
  promptGuidelines: ["Question replies remain unresolved."],
16752
15926
  parameters: parameters5(respondSchema),
@@ -16755,36 +15929,8 @@ ${openMessage}`, {
16755
15929
  const params = respondSchema.parse(input);
16756
15930
  const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
16757
15931
  const model = modelRoute(ctx);
16758
- if (params.local) {
16759
- const tuicrSession = await resolveTuicrSession(review, true);
16760
- if (!tuicrSession)
16761
- return result2("No matching tuicr session. Open review_new or review_launch_ui first.");
16762
- const artifact = await localThreadArtifactPath(review.cwd, tuicrSession.slug);
16763
- if (!existsSync3(artifact))
16764
- return result2("No local review artifact. Run review_comments with local=true first.");
16765
- const threads = await readThreadArtifact(artifact);
16766
- const known = threads.find((thread) => thread.id === params.threadId);
16767
- const question = known?.question === true || params.question === true || !known && params.question === undefined;
16768
- const awaitUserDeletion = !question && params.resolve === true;
16769
- await addComment(tuicrSession.path, `${localResponseMarker(params.threadId)}
16770
- ${params.body}`, {
16771
- targetFile: known?.file,
16772
- line: known?.line,
16773
- side: "new",
16774
- username: localReviewAuthor(model)
16775
- });
16776
- await createLocalReviewBackend({
16777
- session: tuicrSession.path,
16778
- artifactPath: artifact,
16779
- author: localReviewAuthor(model)
16780
- }).reply({
16781
- threadId: params.threadId,
16782
- body: params.body,
16783
- resolve: false,
16784
- question
16785
- });
16786
- 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 });
16787
- }
15932
+ if (params.local || params.workingTree)
15933
+ return result2("Local reviews are direct Markdown files; append the response to REVIEW.md instead.");
16788
15934
  if (!review.pr || !review.forge)
16789
15935
  return result2("No remote PR/MR for this reply.");
16790
15936
  const remote = createRemoteReviewBackend(review.vcs, review.pr.number);
@@ -16807,7 +15953,7 @@ ${params.body}`, {
16807
15953
  defineTool6({
16808
15954
  name: "review_publish",
16809
15955
  label: "review publish",
16810
- description: "Publish pending remote draft PR/MR review comments and status, or promote a local tuicr draft before publishing.",
15956
+ description: "Publish pending remote draft PR/MR review comments and status.",
16811
15957
  promptSnippet: "Call review_publish to make remote review work public",
16812
15958
  promptGuidelines: ["Statuses are COMMENT, APPROVE, REQUEST_CHANGES, or CLOSE."],
16813
15959
  parameters: parameters5(publishSchema),
@@ -16815,6 +15961,8 @@ ${params.body}`, {
16815
15961
  async execute(_id, input, _signal, _onUpdate, ctx) {
16816
15962
  const params = publishSchema.parse(input);
16817
15963
  const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
15964
+ if (params.local || params.workingTree)
15965
+ return result2("Local publish is unsupported; select a remote PR/MR.");
16818
15966
  if (!review.pr || !review.forge)
16819
15967
  return result2("No remote PR/MR to publish.");
16820
15968
  return publishResolvedReview(review, params, modelRoute(ctx));
@@ -16823,34 +15971,25 @@ ${params.body}`, {
16823
15971
  defineTool6({
16824
15972
  name: "review_complete",
16825
15973
  label: "review complete",
16826
- description: "Approve, reject, or abandon a remote review, or archive a local review artifact.",
15974
+ description: "Approve, reject, or close a remote review.",
16827
15975
  promptSnippet: "Call review_complete to finish a review without merging it.",
16828
- promptGuidelines: ["A local completion archives the Diffpi artifact and deletes the matching tuicr session."],
15976
+ promptGuidelines: ["Close closes the remote PR/MR without publishing a review first."],
16829
15977
  parameters: parameters5(completeSchema),
16830
15978
  executionMode: "sequential",
16831
15979
  async execute(_id, input, _signal, _onUpdate, ctx) {
16832
15980
  const params = completeSchema.parse(input);
16833
15981
  const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
16834
- if (params.local) {
16835
- const session = await resolveTuicrSession(review, true);
16836
- if (!session)
16837
- return result2("No matching local tuicr session to close.");
16838
- const artifact = await localThreadArtifactPath(review.cwd, session.slug);
16839
- if (!existsSync3(artifact))
16840
- return result2("No local review artifact. Run review_comments with local=true first.");
16841
- const archived = await uniqueRecordPath(await completedReviewsDir(review.cwd), reviewSlug(session.slug));
16842
- await rename2(artifact, archived);
16843
- await unlink2(session.path);
16844
- return result2(`Archived ${archived} and deleted the tuicr session ${session.slug}.`, {
16845
- artifact: archived,
16846
- session
16847
- });
16848
- }
15982
+ if (params.local || params.workingTree)
15983
+ return result2("Local complete is unsupported; select a remote PR/MR.");
16849
15984
  if (!review.pr || !review.forge)
16850
15985
  return result2("No remote PR/MR to complete.");
16851
15986
  if (!params.action)
16852
- return result2("Choose a complete action: approve, reject, or abandon.");
16853
- const status = { approve: "APPROVE", reject: "REQUEST_CHANGES", abandon: "CLOSE" }[params.action];
15987
+ return result2("Choose a complete action: approve, reject, or close.");
15988
+ if (params.action === "close") {
15989
+ await review.forge.closePr(review.pr.number);
15990
+ return result2(`Closed #${review.pr.number}.`, { pr: review.pr });
15991
+ }
15992
+ const status = params.action === "approve" ? "APPROVE" : "REQUEST_CHANGES";
16854
15993
  return publishResolvedReview(review, { ...params, status }, modelRoute(ctx));
16855
15994
  }
16856
15995
  }),
@@ -16863,10 +16002,10 @@ ${params.body}`, {
16863
16002
  "Do not query GitHub through MCP or duplicate readiness checks before calling review_merge.",
16864
16003
  "This is intentionally GitHub-only until GitLab merge support is added."
16865
16004
  ],
16866
- parameters: parameters5(contextSchema.extend({ subject: z11.string().optional() })),
16005
+ parameters: parameters5(contextSchema.extend({ subject: z10.string().optional() })),
16867
16006
  executionMode: "sequential",
16868
16007
  async execute(_id, input) {
16869
- const params = contextSchema.extend({ subject: z11.string().optional() }).parse(input);
16008
+ const params = contextSchema.extend({ subject: z10.string().optional() }).parse(input);
16870
16009
  const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
16871
16010
  if (review.vcs.provider !== "github" || !review.forge)
16872
16011
  return result2("review_merge currently supports GitHub only.");
@@ -16879,72 +16018,28 @@ ${params.body}`, {
16879
16018
  await review.forge.mergePr(review.pr.number, subject);
16880
16019
  return result2(`Merged #${review.pr.number} with subject: ${subject}.`, { pr: review.pr, guard });
16881
16020
  }
16882
- }),
16883
- defineTool6({
16884
- name: "review_launch_ui",
16885
- label: "review launch UI",
16886
- description: "Open a tuicr UI in a mux tab, configure Zed, or return the command to run.",
16887
- promptSnippet: "Call review_launch_ui for the interactive tuicr TUI",
16888
- promptGuidelines: ["Show the returned command when launch cannot open a tab."],
16889
- parameters: parameters5(localSchema),
16890
- executionMode: "sequential",
16891
- async execute(_id, input) {
16892
- const params = localSchema.parse(input);
16893
- const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
16894
- const workingTree = Boolean(params.local || params.workingTree);
16895
- if (!workingTree && !review.forge)
16896
- return result2(unsupportedForgeMessage());
16897
- return launchLocalReview(review, workingTree);
16898
- }
16899
16021
  })
16900
16022
  ];
16901
16023
  }
16902
- async function syncLocalThreadArtifact(path, title, sessionSlug, threads) {
16903
- let previous = [];
16904
- try {
16905
- previous = parseThreadArtifact(await readFile14(path, "utf8"));
16906
- } catch (error) {
16907
- if (error.code !== "ENOENT")
16908
- throw error;
16909
- }
16910
- const previousById = new Map(previous.map((thread) => [thread.id, thread]));
16911
- const currentIds = new Set(threads.map((thread) => thread.id));
16912
- const current = threads.map((thread) => {
16913
- const existing = previousById.get(thread.id);
16914
- return {
16915
- ...thread,
16916
- resolved: false,
16917
- addressed: existing?.addressed ?? Boolean(existing?.reply),
16918
- reply: existing?.reply
16919
- };
16920
- });
16921
- const removed = previous.filter((thread) => !currentIds.has(thread.id)).map((thread) => ({ ...thread, resolved: true }));
16922
- const merged = [...current, ...removed];
16923
- await writeFile8(path, renderThreadArtifact(title, sessionSlug, merged), "utf8");
16924
- return merged;
16925
- }
16926
16024
  function parameters5(schema) {
16927
- return z11.toJSONSchema(schema, { io: "input" });
16025
+ return z10.toJSONSchema(schema, { io: "input" });
16928
16026
  }
16929
16027
  function resolveWorkingDirectory(params) {
16930
16028
  return params.cwd ?? process.cwd();
16931
16029
  }
16030
+ function deriveTitle(branch) {
16031
+ return branch.replace(/^(feature|feat|fix|bug|chore)\//, "").replace(/^eng-\d+-/i, "").replace(/[-_]+/g, " ").replace(/^\w/, (char) => char.toUpperCase());
16032
+ }
16932
16033
  function result2(text, details = {}) {
16933
16034
  return { content: [{ type: "text", text }], details };
16934
16035
  }
16935
16036
  function unsupportedForgeMessage() {
16936
- return "No supported GitHub or GitLab remote was detected. Use local=true for an offline tuicr review.";
16037
+ return "No supported GitHub or GitLab remote was detected.";
16937
16038
  }
16938
16039
  async function collectReviewStatus(review) {
16939
- const [porcelain, branch, localSession, remoteSession] = await Promise.all([
16040
+ const [porcelain, branch] = await Promise.all([
16940
16041
  runChecked("git", ["-C", review.cwd, "status", "--porcelain"]),
16941
- runChecked("git", ["-C", review.cwd, "branch", "--show-current"]),
16942
- resolveTuicrSession(review, true).catch(() => {
16943
- return;
16944
- }),
16945
- resolveTuicrSession(review, false).catch(() => {
16946
- return;
16947
- })
16042
+ runChecked("git", ["-C", review.cwd, "branch", "--show-current"])
16948
16043
  ]);
16949
16044
  const entries = porcelain.stdout.split("\\n").filter(Boolean);
16950
16045
  const staged = entries.filter((line) => !line.startsWith("??") && line[0] !== " ").length;
@@ -16955,10 +16050,8 @@ async function collectReviewStatus(review) {
16955
16050
  const text = [
16956
16051
  `Branch: ${branch.stdout.trim() || review.vcs.branch}`,
16957
16052
  `Worktree: ${entries.length ? "dirty" : "clean"} (staged=${staged}, unstaged=${unstaged}, untracked=${untracked})`,
16958
- `Local working-tree tuicr review: ${localSession ? `yes (${localSession.slug}, ${localSession.commentCount} comments)` : "no"}`,
16959
16053
  `Remote PR/MR: ${remote}`,
16960
- `Remote URL: ${url}`,
16961
- `Remote tuicr session: ${remoteSession ? `yes (${remoteSession.slug}, ${remoteSession.commentCount} comments)` : "no"}`
16054
+ `Remote URL: ${url}`
16962
16055
  ].join("\\n");
16963
16056
  return {
16964
16057
  text,
@@ -16967,8 +16060,6 @@ async function collectReviewStatus(review) {
16967
16060
  staged,
16968
16061
  unstaged,
16969
16062
  untracked,
16970
- localSession,
16971
- remoteSession,
16972
16063
  pr: review.pr,
16973
16064
  url
16974
16065
  }
@@ -16984,167 +16075,13 @@ async function publishResolvedReview(review, params, model) {
16984
16075
  const event = status === "CLOSE" ? "COMMENT" : status;
16985
16076
  assertReviewEventSupported(review.vcs.provider, event);
16986
16077
  const remote = createRemoteReviewBackend(review.vcs, review.pr.number);
16987
- const promotion = params.local ? await promoteLocalReview(review, remote, model) : undefined;
16988
16078
  if (status !== "CLOSE" && review.pr.isDraft)
16989
16079
  await review.forge.markReady(review.pr.number);
16990
16080
  await remote.publish(event);
16991
- if (promotion) {
16992
- promotion.publication.state.bodies = [
16993
- ...new Set([...promotion.publication.state.bodies, ...promotion.bodyFingerprints])
16994
- ];
16995
- promotion.publication.state.comments = [
16996
- ...new Set([...promotion.publication.state.comments, ...promotion.commentFingerprints])
16997
- ];
16998
- const published = new Set(promotion.commentFingerprints);
16999
- promotion.publication.state.stagedComments = promotion.publication.state.stagedComments.filter((fingerprint) => !published.has(fingerprint));
17000
- await saveReviewPublicationState(promotion.publication.path, promotion.publication.state);
17001
- }
17002
16081
  if (status === "CLOSE")
17003
16082
  await review.forge.closePr(review.pr.number);
17004
16083
  const finalPr = await review.forge.viewPr(String(review.pr.number));
17005
- const promotedBodies = promotion?.promotedBodies ?? 0;
17006
- const promotedComments = promotion?.promotedComments ?? 0;
17007
- const promotedReplies = promotion?.promotedReplies ?? 0;
17008
- return result2(`Published #${review.pr.number} (${status}); promoted ${promotedBodies} review bodies, ${promotedComments} comments, and ${promotedReplies} replies.`, { pr: finalPr ?? review.pr, status, promotedBodies, promotedComments, promotedReplies });
17009
- }
17010
- async function promoteLocalReview(review, remote, model) {
17011
- const publication = await loadReviewPublicationState(review.cwd, review.vcs, review.pr.number);
17012
- const session = await resolvePublishSession(review.cwd, {
17013
- branch: review.vcs.branch,
17014
- owner: review.vcs.provider === "none" ? undefined : review.vcs.owner,
17015
- repo: review.vcs.provider === "none" ? undefined : review.vcs.repo,
17016
- number: review.pr?.number
17017
- });
17018
- if (!session)
17019
- throw new Error("No matching tuicr session to publish.");
17020
- const sessionIsWorkingTree = session.kind === "local";
17021
- const local = createLocalReviewBackend({
17022
- session: session.path,
17023
- artifactPath: "",
17024
- author: localReviewAuthor(model)
17025
- });
17026
- const draft = await local.readDraft();
17027
- const remoteDraft = await remote.readDraft();
17028
- const knownRootComments = new Set([
17029
- ...publication.state.comments,
17030
- ...publication.state.stagedComments,
17031
- ...remoteDraft.comments.map(reviewCommentFingerprint)
17032
- ]);
17033
- const { checkpointed, candidates } = partitionReviewComments(draft.comments, knownRootComments, model);
17034
- const remoteThreads = await remote.listThreads();
17035
- const threadReplies = sessionIsWorkingTree ? [] : candidates.flatMap(({ source: comment }) => {
17036
- const thread = resolveReviewThread(comment, remoteThreads);
17037
- return thread ? [{ comment, thread }] : [];
17038
- });
17039
- const unmatchedActions = sessionIsWorkingTree ? [] : candidates.map(({ source }) => source).filter((comment) => parseReviewThreadAction(comment.body).action && !threadReplies.some((reply) => reply.comment === comment));
17040
- if (unmatchedActions.length > 0) {
17041
- const locations = unmatchedActions.map(({ file, line }) => `${file}:${line ?? "?"}`).join(", ");
17042
- throw new Error(`Cannot apply review thread action: no matching remote thread at ${locations}.`);
17043
- }
17044
- const comments = candidates.filter(({ source }) => !threadReplies.some((reply) => reply.comment === source)).map(({ published }) => published);
17045
- const body = draft.body.trim();
17046
- const bodyFingerprints = body ? [reviewBodyFingerprint(body)] : [];
17047
- const commentFingerprints = [
17048
- ...checkpointed.map(({ fingerprint }) => fingerprint),
17049
- ...comments.map(reviewCommentFingerprint)
17050
- ];
17051
- const knownBodies = new Set(publication.state.bodies);
17052
- if (remoteDraft.body.trim())
17053
- knownBodies.add(reviewBodyFingerprint(remoteDraft.body));
17054
- const unpublishedBody = body && !knownBodies.has(reviewBodyFingerprint(body)) ? body : "";
17055
- const unpublished = unpublishedReviewComments(comments, knownRootComments);
17056
- if (unpublishedBody)
17057
- await remote.stage({ comments: [], body: unpublishedBody });
17058
- let promotedComments = 0;
17059
- for (const comment of unpublished) {
17060
- await remote.stage({ comments: [comment], body: "" });
17061
- publication.state.stagedComments.push(reviewCommentFingerprint(comment));
17062
- await saveReviewPublicationState(publication.path, publication.state);
17063
- promotedComments += 1;
17064
- }
17065
- const promotedReplies = await promoteLocalReplies(review, remote, publication, model, sessionIsWorkingTree) + await promoteLocationReplies(remote, publication, threadReplies, model);
17066
- return {
17067
- publication,
17068
- bodyFingerprints,
17069
- commentFingerprints,
17070
- promotedBodies: unpublishedBody ? 1 : 0,
17071
- promotedComments,
17072
- promotedReplies
17073
- };
17074
- }
17075
- async function promoteLocationReplies(remote, publication, replies, model) {
17076
- let count = 0;
17077
- for (const { comment, thread } of replies) {
17078
- const parsed = parseReviewThreadAction(comment.body);
17079
- const body = parsed.body.trim();
17080
- const action = parsed.action;
17081
- const fingerprint = reviewReplyFingerprint(thread.id, `${action ?? "reply"}\x00${body}`);
17082
- if (publication.state.replies.includes(fingerprint))
17083
- continue;
17084
- if (action === "delete") {
17085
- if (!remote.deleteThread)
17086
- throw new Error(`The review backend cannot delete thread ${thread.id}.`);
17087
- await remote.deleteThread(thread.id);
17088
- publication.state.replies.push(fingerprint);
17089
- await saveReviewPublicationState(publication.path, publication.state);
17090
- count += 1;
17091
- continue;
17092
- }
17093
- const remoteBody = withCommentProvenance({ ...comment, body }, model);
17094
- if (body && !thread.replies?.some((reply) => reply === body || reply === remoteBody)) {
17095
- await remote.reply({
17096
- threadId: thread.id,
17097
- body: remoteBody,
17098
- resolve: false
17099
- });
17100
- }
17101
- if (action && remote.setResolved)
17102
- await remote.setResolved(thread.id, action === "resolve");
17103
- publication.state.replies.push(fingerprint);
17104
- await saveReviewPublicationState(publication.path, publication.state);
17105
- count += 1;
17106
- }
17107
- return count;
17108
- }
17109
- async function promoteLocalReplies(review, remote, publication, model, workingTree) {
17110
- const target = workingTree ? "uncommitted" : await reviewTargetId(review);
17111
- const artifact = workingTree ? await latestThreadArtifact(review.cwd, review, target) : publication.state.overlayPath ?? await latestThreadArtifact(review.cwd, review, target);
17112
- if (!artifact)
17113
- return 0;
17114
- if (!workingTree)
17115
- publication.state.overlayPath = artifact;
17116
- let count = 0;
17117
- for (const thread of await readThreadArtifact(artifact)) {
17118
- if (!thread.reply)
17119
- continue;
17120
- const body = withRemoteProvenance(thread.reply, model);
17121
- const fingerprint = reviewReplyFingerprint(thread.id, body);
17122
- if (publication.state.replies.includes(fingerprint))
17123
- continue;
17124
- if (workingTree) {
17125
- const draft = workingTreeReplyDraft(thread, body);
17126
- const comment = draft.comments[0];
17127
- if (comment) {
17128
- const commentFingerprint = reviewCommentFingerprint(comment);
17129
- if (!publication.state.stagedComments.includes(commentFingerprint)) {
17130
- await remote.stage(draft);
17131
- publication.state.stagedComments.push(commentFingerprint);
17132
- }
17133
- } else {
17134
- const bodyFingerprint = reviewBodyFingerprint(draft.body);
17135
- if (!publication.state.bodies.includes(bodyFingerprint)) {
17136
- await remote.stage(draft);
17137
- publication.state.bodies.push(bodyFingerprint);
17138
- }
17139
- }
17140
- } else {
17141
- await remote.reply({ threadId: thread.id, body, resolve: false, question: thread.question });
17142
- }
17143
- publication.state.replies.push(fingerprint);
17144
- await saveReviewPublicationState(publication.path, publication.state);
17145
- count += 1;
17146
- }
17147
- return count;
16084
+ return result2(`Published #${review.pr.number} (${status}).`, { pr: finalPr ?? review.pr, status });
17148
16085
  }
17149
16086
  async function resolveReviewContext(cwd, target) {
17150
16087
  const vcs = await detectVcs(cwd);
@@ -17156,21 +16093,6 @@ async function resolveReviewContext(cwd, target) {
17156
16093
  function normalizeTarget(target) {
17157
16094
  return target.match(/\/(?:pull|merge_requests)\/(\d+)(?:\/|$)/)?.[1] ?? target;
17158
16095
  }
17159
- function resolveTuicrSession(review, workingTree = false) {
17160
- return resolveReviewSession(review.cwd, {
17161
- branch: review.vcs.branch,
17162
- workingTree,
17163
- owner: review.vcs.provider === "none" ? undefined : review.vcs.owner,
17164
- repo: review.vcs.provider === "none" ? undefined : review.vcs.repo,
17165
- number: review.pr?.number
17166
- });
17167
- }
17168
- async function launchLocalReview(review, workingTree, requestedBase) {
17169
- const base = workingTree ? requestedBase ?? review.pr?.baseRef ?? (review.forge ? await review.forge.defaultBranch() : undefined) : undefined;
17170
- const launched = await launch(review.cwd, workingTree ? undefined : review.pr?.number, base);
17171
- const commandTarget = workingTree || !review.pr ? "full branch" : `PR/MR #${review.pr.number}`;
17172
- return result2(launched.launched ? `Opened ${commandTarget} in tuicr (${launched.via}).` : launched.instruction ?? `Run: ${launched.command}`, { launched, pr: review.pr, target: commandTarget });
17173
- }
17174
16096
  async function assertRemoteBranchReady(cwd) {
17175
16097
  const dirty = await runChecked("git", ["-C", cwd, "status", "--porcelain"]);
17176
16098
  if (dirty.stdout.trim())
@@ -17185,112 +16107,16 @@ async function assertRemoteBranchReady(cwd) {
17185
16107
  if (head.stdout.trim() !== pushed.stdout.trim())
17186
16108
  throw new Error("Push the current branch before opening a remote review.");
17187
16109
  }
17188
- function withCommentProvenance(comment, fallbackModel) {
17189
- const route = comment.author?.match(/^Agent:\s*(.+)$/)?.[1];
17190
- return route ? withRemoteProvenance(comment.body, route || fallbackModel) : comment.body;
17191
- }
17192
- async function reviewTargetId(review) {
17193
- if (!review.pr)
17194
- return "uncommitted";
17195
- if (review.pr.headSha)
17196
- return review.pr.headSha.slice(0, 12);
17197
- if (review.pr.headRef === review.vcs.branch)
17198
- return headSha(review.cwd);
17199
- const remote = await run("git", ["-C", review.cwd, "ls-remote", "origin", `refs/heads/${review.pr.headRef}`]);
17200
- return remote.stdout.trim().split(/\s+/)[0]?.slice(0, 12) || headSha(review.cwd);
17201
- }
17202
- async function headSha(cwd) {
17203
- const result = await runChecked("git", ["-C", cwd, "rev-parse", "--short=12", "HEAD"]);
17204
- return result.stdout.trim();
17205
- }
17206
- async function newReviewArtifactPath(cwd, target) {
17207
- const dir = await reviewsDir(cwd);
17208
- await mkdir7(dir, { recursive: true });
17209
- return uniqueRecordPath(dir, reviewRecordName(target));
17210
- }
17211
- async function localThreadArtifactPath(cwd, sessionSlug) {
17212
- const dir = await reviewsDir(cwd);
17213
- return join19(dir, `${reviewSlug(sessionSlug) || "local-review"}.md`);
17214
- }
17215
- async function readThreadArtifact(path) {
17216
- try {
17217
- return parseThreadArtifact(await readFile14(path, "utf8"));
17218
- } catch (error) {
17219
- if (error.code === "ENOENT") {
17220
- throw new Error(`Expected local reply overlay is missing: ${path}`);
17221
- }
17222
- throw error;
17223
- }
17224
- }
17225
- async function latestThreadArtifact(cwd, review, target) {
17226
- try {
17227
- return await findLatestThreadArtifact(cwd, review, target);
17228
- } catch (error) {
17229
- throw new Error(`Cannot locate the latest review thread artifact for ${target}.`, { cause: error });
17230
- }
17231
- }
17232
- async function findLatestThreadArtifact(cwd, review, target) {
17233
- const dir = await reviewsDir(cwd);
17234
- const suffix = reviewSlug(target) || "uncommitted";
17235
- const names = await listReviewArtifactNames(dir);
17236
- const candidates = await Promise.all(names.flatMap((name) => {
17237
- if (!name.endsWith(".md"))
17238
- return [];
17239
- return [
17240
- (async () => {
17241
- const path = join19(dir, name);
17242
- const info = await stat2(path);
17243
- return { path, name, content: await readFile14(path, "utf8"), modified: info.mtimeMs };
17244
- })()
17245
- ];
17246
- }));
17247
- return candidates.filter(({ name, content }) => {
17248
- if (!content.startsWith("<!-- diffpi-threads:"))
17249
- return false;
17250
- if (review.pr && target !== "local" && target !== "uncommitted")
17251
- return content.includes(`- PR/MR: #${review.pr.number}`);
17252
- return artifactNameMatches(name, suffix);
17253
- }).sort((a, b) => b.modified - a.modified)[0]?.path;
17254
- }
17255
- async function listReviewArtifactNames(dir) {
17256
- try {
17257
- return await readdir5(dir);
17258
- } catch (error) {
17259
- throw new Error(`Cannot read review artifacts in ${dir}.`, { cause: error });
17260
- }
17261
- }
17262
- function deriveTitle(branch) {
17263
- return branch.replace(/^(feature|feat|fix|bug|chore)\//, "").replace(/^eng-\d+-/i, "").replace(/[-_]+/g, " ").replace(/^\w/, (char) => char.toUpperCase());
17264
- }
17265
- function uniqueRecordPath(dir, base) {
17266
- let path = join19(dir, `${base}.md`);
17267
- let count = 2;
17268
- while (existsSync3(path))
17269
- path = join19(dir, `${base}-${count++}.md`);
17270
- return path;
17271
- }
17272
- function artifactNameMatches(name, suffix) {
17273
- if (!name.endsWith(".md"))
17274
- return false;
17275
- const stem = name.slice(0, -3);
17276
- const marker = `-${suffix}`;
17277
- const markerIndex = stem.lastIndexOf(marker);
17278
- if (markerIndex < 0)
17279
- return false;
17280
- const tail = stem.slice(markerIndex + marker.length);
17281
- return tail === "" || /^-\d+$/.test(tail);
17282
- }
17283
16110
 
17284
16111
  // src/tools/setup.ts
17285
16112
  import { defineTool as defineTool7 } from "@earendil-works/pi-coding-agent";
17286
- import { z as z12 } from "zod";
17287
- var setupParametersSchema = z12.object({
17288
- issueTracker: z12.enum(["none", "linear", "jira"]).default("none").describe("Issue tracker MCP server to configure. Use none unless the user explicitly selects Linear or Jira."),
17289
- forges: z12.array(z12.enum(["github", "gitlab"])).default([]).describe("Hosted VCS MCP servers to configure. Installs matching CLIs and registers each MCP server."),
17290
- forge: z12.enum(["none", "github", "gitlab"]).optional().describe("Deprecated single-host alias. Prefer forges for one or more VCS MCP servers."),
17291
- bindZedKey: z12.boolean().default(false).describe("Opt in to a Zed keybinding for the tuicr review task.")
16113
+ import { z as z11 } from "zod";
16114
+ var setupParametersSchema = z11.object({
16115
+ issueTracker: z11.enum(["none", "linear", "jira"]).default("none").describe("Issue tracker MCP server to configure. Use none unless the user explicitly selects Linear or Jira."),
16116
+ forges: z11.array(z11.enum(["github", "gitlab"])).default([]).describe("Hosted VCS MCP servers to configure. Installs matching CLIs and registers each MCP server."),
16117
+ forge: z11.enum(["none", "github", "gitlab"]).optional().describe("Deprecated single-host alias. Prefer forges for one or more VCS MCP servers.")
17292
16118
  });
17293
- var setupParameters = z12.toJSONSchema(setupParametersSchema, { io: "input" });
16119
+ var setupParameters = z11.toJSONSchema(setupParametersSchema, { io: "input" });
17294
16120
  var diffpiSetupTool = defineTool7({
17295
16121
  name: "diffpi_setup",
17296
16122
  label: "diffpi setup",
@@ -17310,7 +16136,6 @@ var diffpiSetupTool = defineTool7({
17310
16136
  issueTracker: params.issueTracker,
17311
16137
  forges: params.forges,
17312
16138
  forge: params.forge,
17313
- bindZedKey: params.bindZedKey,
17314
16139
  installMiseHook: true,
17315
16140
  availableModels: ctx.modelRegistry.getAvailable(),
17316
16141
  onProgress(message) {
@@ -17338,7 +16163,6 @@ var diffpiValidateTool = defineTool7({
17338
16163
  issueTracker: params.issueTracker,
17339
16164
  forges: params.forges,
17340
16165
  forge: params.forge,
17341
- bindZedKey: params.bindZedKey,
17342
16166
  installMiseHook: true,
17343
16167
  dryRun: true,
17344
16168
  availableModels: ctx.modelRegistry.getAvailable()
@@ -17363,11 +16187,11 @@ ${lines.join(`
17363
16187
 
17364
16188
  // src/tools/templates.ts
17365
16189
  import { defineTool as defineTool8 } from "@earendil-works/pi-coding-agent";
17366
- import { z as z13 } from "zod";
17367
- var templateSchema = z13.object({
17368
- name: z13.string().min(1),
17369
- variables: z13.record(z13.string(), z13.string()).optional(),
17370
- homeDir: z13.string().optional()
16190
+ import { z as z12 } from "zod";
16191
+ var templateSchema = z12.object({
16192
+ name: z12.string().min(1),
16193
+ variables: z12.record(z12.string(), z12.string()).optional(),
16194
+ homeDir: z12.string().optional()
17371
16195
  });
17372
16196
  var diffpiTemplateTool = defineTool8({
17373
16197
  name: "diffpi_template",
@@ -17375,7 +16199,7 @@ var diffpiTemplateTool = defineTool8({
17375
16199
  description: "Load a bundled Diffpi template or a user override and render named variables.",
17376
16200
  promptSnippet: "Use diffpi_template for package workflow templates",
17377
16201
  promptGuidelines: ["User overrides live under ~/.difflab/diffpi/templates."],
17378
- parameters: z13.toJSONSchema(templateSchema, { io: "input" }),
16202
+ parameters: z12.toJSONSchema(templateSchema, { io: "input" }),
17379
16203
  executionMode: "parallel",
17380
16204
  async execute(_id, input) {
17381
16205
  const params = templateSchema.parse(input);