@difflab/pi 0.5.0 → 0.6.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 (53) hide show
  1. package/README.md +5 -5
  2. package/agents/diffpi-copilot.md +6 -3
  3. package/agents/diffpi-reviewer.md +3 -3
  4. package/dist/commands/review.d.ts +0 -1
  5. package/dist/commands/review.d.ts.map +1 -1
  6. package/dist/extensions/index.js +344 -810
  7. package/dist/extensions/tuicrx.d.ts +1 -8
  8. package/dist/extensions/tuicrx.d.ts.map +1 -1
  9. package/dist/index.d.ts +7 -8
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +197 -458
  12. package/dist/review/github-review-backend.d.ts.map +1 -1
  13. package/dist/review/gitlab-review-backend.d.ts.map +1 -1
  14. package/dist/review/index.d.ts +6 -6
  15. package/dist/review/index.d.ts.map +1 -1
  16. package/dist/review/local-reviews.d.ts +31 -0
  17. package/dist/review/local-reviews.d.ts.map +1 -0
  18. package/dist/review/review-backend.d.ts +2 -3
  19. package/dist/review/review-backend.d.ts.map +1 -1
  20. package/dist/review/review-markdown.d.ts +1 -4
  21. package/dist/review/review-markdown.d.ts.map +1 -1
  22. package/dist/review/types.d.ts +0 -28
  23. package/dist/review/types.d.ts.map +1 -1
  24. package/dist/store.d.ts +0 -2
  25. package/dist/store.d.ts.map +1 -1
  26. package/dist/tools/index.js +185 -783
  27. package/dist/tools/review.d.ts +0 -30
  28. package/dist/tools/review.d.ts.map +1 -1
  29. package/dist/workflow.d.ts +47 -0
  30. package/dist/workflow.d.ts.map +1 -1
  31. package/package.json +1 -1
  32. package/skills/review/SKILL.md +6 -8
  33. package/workflows/review/address.md +8 -0
  34. package/workflows/review/auto.md +8 -0
  35. package/workflows/review/complete.md +7 -0
  36. package/workflows/review/edit.md +7 -0
  37. package/workflows/review/help.md +14 -0
  38. package/workflows/review/merge.md +6 -0
  39. package/workflows/review/new.md +7 -0
  40. package/workflows/review/publish.md +6 -0
  41. package/dist/review/local-review-backend.d.ts +0 -3
  42. package/dist/review/local-review-backend.d.ts.map +0 -1
  43. package/dist/review/review-state.d.ts +0 -20
  44. package/dist/review/review-state.d.ts.map +0 -1
  45. package/skills/review/references/workflows/address.md +0 -11
  46. package/skills/review/references/workflows/auto.md +0 -18
  47. package/skills/review/references/workflows/complete.md +0 -7
  48. package/skills/review/references/workflows/edit.md +0 -7
  49. package/skills/review/references/workflows/help.md +0 -30
  50. package/skills/review/references/workflows/merge.md +0 -6
  51. package/skills/review/references/workflows/new.md +0 -19
  52. package/skills/review/references/workflows/open.md +0 -3
  53. package/skills/review/references/workflows/publish.md +0 -8
package/dist/index.js CHANGED
@@ -1301,9 +1301,6 @@ function yymmdd(date = new Date) {
1301
1301
  function reviewRecordName(target, date = new Date) {
1302
1302
  return `${yymmdd(date)}-${reviewSlug(target) || "uncommitted"}`;
1303
1303
  }
1304
- function isLocalResponse(body) {
1305
- return /^<!-- diffpi(?:-local-response:[A-Za-z0-9_-]+)? -->\n/m.test(body);
1306
- }
1307
1304
  function dedupeFindings(findings) {
1308
1305
  const rank = { BLOCKING: 3, CONSIDER: 2, NOTE: 1 };
1309
1306
  const byKey = new Map;
@@ -1349,18 +1346,6 @@ function renderCommentBody(finding) {
1349
1346
  return `${prefix}${finding.body}${reference}`;
1350
1347
  }
1351
1348
  // src/review/review-markdown.ts
1352
- import { z as z4 } from "zod";
1353
- var reviewThreadRecordSchema = z4.object({
1354
- id: z4.string().min(1),
1355
- file: z4.string().nullable().optional().transform((value) => value ?? undefined),
1356
- line: z4.number().int().positive().nullable().optional().transform((value) => value ?? undefined),
1357
- body: z4.string(),
1358
- author: z4.string().optional(),
1359
- resolved: z4.boolean(),
1360
- addressed: z4.boolean().optional(),
1361
- question: z4.boolean(),
1362
- replies: z4.array(z4.string()).optional()
1363
- });
1364
1349
  function renderReviewDoc(input) {
1365
1350
  const anchored = dedupeFindings(input.findings).filter((finding) => finding.line > 0);
1366
1351
  const lines = [`# Review: ${input.title}`, "", "## Metadata"];
@@ -1402,88 +1387,6 @@ function renderReviewDoc(input) {
1402
1387
  `).trimEnd()}
1403
1388
  `;
1404
1389
  }
1405
- function renderThreadArtifact(title, target, threads, options = {}) {
1406
- const records = threads.map((thread) => ({
1407
- id: thread.id,
1408
- file: thread.file,
1409
- line: thread.line,
1410
- body: thread.body,
1411
- author: thread.author,
1412
- resolved: thread.resolved,
1413
- addressed: thread.addressed,
1414
- question: thread.question,
1415
- replies: thread.replies
1416
- }));
1417
- const payload = Buffer.from(JSON.stringify(records), "utf8").toString("base64url");
1418
- const lines = [
1419
- `<!-- diffpi-threads:${payload} -->`,
1420
- `# Review threads: ${title}`,
1421
- "",
1422
- "## Metadata",
1423
- "",
1424
- `- Target: ${target}`,
1425
- `- Pulled: ${options.timestamp ?? new Date().toISOString()}`
1426
- ];
1427
- if (options.number !== undefined)
1428
- lines.push(`- PR/MR: #${options.number}${options.url ? ` — ${options.url}` : ""}`);
1429
- lines.push("", "## Replies", "");
1430
- for (const thread of threads) {
1431
- const id = Buffer.from(thread.id, "utf8").toString("base64url");
1432
- lines.push(`### ${thread.file ?? "review"}:${thread.line ?? "n/a"} (${thread.id})`, "", `<!-- diffpi-reply-start:${id} -->`, thread.reply ?? "", `<!-- diffpi-reply-end:${id} -->`, "");
1433
- }
1434
- lines.push("## Source comments", "");
1435
- for (const thread of threads) {
1436
- lines.push(`### ${thread.file ?? "review"}:${thread.line ?? "n/a"} — ${thread.author ?? "unknown"}`, "", `Thread: ${thread.id}`, `Status: ${thread.resolved ? "resolved" : thread.addressed ? "addressed" : "open"}`, "", thread.body, "", "---", "");
1437
- }
1438
- return `${lines.join(`
1439
- `).trimEnd()}
1440
- `;
1441
- }
1442
- function parseThreadArtifact(content) {
1443
- const payload = content.match(/^<!-- diffpi-threads:([A-Za-z0-9_-]+) -->$/m)?.[1];
1444
- if (!payload)
1445
- throw new Error("This file is not a Diffpi thread artifact.");
1446
- let threads;
1447
- try {
1448
- const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
1449
- threads = z4.array(reviewThreadRecordSchema).parse(decoded);
1450
- } catch {
1451
- throw new Error("Cannot parse the Diffpi thread artifact payload.");
1452
- }
1453
- const replies = content.split(/^## Source comments$/m, 1)[0] ?? "";
1454
- return threads.map((thread) => {
1455
- const id = Buffer.from(thread.id, "utf8").toString("base64url");
1456
- const startMarker = `<!-- diffpi-reply-start:${id} -->
1457
- `;
1458
- const endMarker = `
1459
- <!-- diffpi-reply-end:${id} -->`;
1460
- const start = replies.indexOf(startMarker);
1461
- const end = start < 0 ? -1 : replies.indexOf(endMarker, start + startMarker.length);
1462
- const reply = start >= 0 && end >= 0 ? replies.slice(start + startMarker.length, end).trim() : "";
1463
- return reply ? { ...thread, reply } : thread;
1464
- });
1465
- }
1466
- function upsertThreadReply(content, threadId, body, question, resolved) {
1467
- const threads = parseThreadArtifact(content);
1468
- const thread = threads.find((candidate) => candidate.id === threadId);
1469
- if (!thread)
1470
- throw new Error(`Review thread ${threadId} was not found in the local artifact.`);
1471
- thread.reply = body;
1472
- thread.addressed = true;
1473
- if (question !== undefined)
1474
- thread.question = question;
1475
- if (resolved !== undefined)
1476
- thread.resolved = resolved;
1477
- const title = content.match(/^# Review threads: (.+)$/m)?.[1] ?? "review";
1478
- const target = content.match(/^- Target: (.+)$/m)?.[1] ?? "local";
1479
- const timestamp = content.match(/^- Pulled: (.+)$/m)?.[1];
1480
- const pr = content.match(/^- PR\/MR: #(\d+)(?: — (.+))?$/m);
1481
- return renderThreadArtifact(title, target, threads, {
1482
- timestamp,
1483
- number: pr ? Number.parseInt(pr[1], 10) : undefined,
1484
- url: pr?.[2]
1485
- });
1486
- }
1487
1390
  // src/review/github-review-backend.ts
1488
1391
  function GitHubReviewBackend(vcs, number) {
1489
1392
  const pendingReview = async () => {
@@ -1545,9 +1448,6 @@ function GitHubReviewBackend(vcs, number) {
1545
1448
  id: thread.id,
1546
1449
  file: thread.path ?? undefined,
1547
1450
  line: thread.line ?? undefined,
1548
- rootCommentId: comment?.id,
1549
- commentIds: nodes.flatMap((node) => [node.id, node.databaseId?.toString()].filter((id) => Boolean(id))),
1550
- commentNodeIds: nodes.flatMap((node) => node.id ? [node.id] : []),
1551
1451
  body,
1552
1452
  author: comment?.author?.login,
1553
1453
  resolved: thread.isResolved,
@@ -1557,7 +1457,6 @@ function GitHubReviewBackend(vcs, number) {
1557
1457
  });
1558
1458
  };
1559
1459
  return {
1560
- kind: "remote",
1561
1460
  async stage(draft) {
1562
1461
  const pending = await pendingReview();
1563
1462
  if (!pending) {
@@ -1615,33 +1514,6 @@ function GitHubReviewBackend(vcs, number) {
1615
1514
  await ghChecked(args);
1616
1515
  }
1617
1516
  },
1618
- async readDraft() {
1619
- const pending = await pendingReview();
1620
- if (!pending)
1621
- return { comments: [], body: "" };
1622
- const review = await ghChecked(["api", `/repos/${vcs.owner}/${vcs.repo}/pulls/${number}/reviews/${pending.id}`]);
1623
- const comments = await ghChecked([
1624
- "api",
1625
- `/repos/${vcs.owner}/${vcs.repo}/pulls/${number}/reviews/${pending.id}/comments`
1626
- ]);
1627
- let reviewData;
1628
- let commentData;
1629
- try {
1630
- reviewData = JSON.parse(review.stdout);
1631
- commentData = JSON.parse(comments.stdout);
1632
- } catch {
1633
- throw new Error("Cannot parse the pending GitHub review as JSON.");
1634
- }
1635
- return {
1636
- body: reviewData.body ?? "",
1637
- comments: commentData.map((comment) => ({
1638
- file: comment.path,
1639
- line: comment.line ?? comment.original_line ?? undefined,
1640
- side: comment.side ?? undefined,
1641
- body: comment.body
1642
- }))
1643
- };
1644
- },
1645
1517
  listThreads,
1646
1518
  async reply(input) {
1647
1519
  const threads = await listThreads();
@@ -1658,32 +1530,14 @@ function GitHubReviewBackend(vcs, number) {
1658
1530
  `body=${input.body}`
1659
1531
  ]);
1660
1532
  }
1661
- if (input.resolve)
1662
- await this.setResolved?.(input.threadId, true);
1663
- },
1664
- async setResolved(threadId, resolved) {
1665
- await ghChecked([
1666
- "api",
1667
- "graphql",
1668
- "-f",
1669
- `query=${resolved ? GITHUB_RESOLVE_MUTATION : GITHUB_UNRESOLVE_MUTATION}`,
1670
- "-f",
1671
- `threadId=${threadId}`
1672
- ]);
1673
- },
1674
- async deleteThread(threadId) {
1675
- const thread = (await listThreads()).find((candidate) => candidate.id === threadId);
1676
- const commentNodeIds = thread?.commentNodeIds ?? (thread?.rootCommentId ? [thread.rootCommentId] : []);
1677
- if (!commentNodeIds.length)
1678
- throw new Error(`Cannot delete GitHub thread ${threadId}: comment node IDs are missing.`);
1679
- for (const commentId of commentNodeIds) {
1533
+ if (input.resolve) {
1680
1534
  await ghChecked([
1681
1535
  "api",
1682
1536
  "graphql",
1683
1537
  "-f",
1684
- `query=${GITHUB_DELETE_COMMENT_MUTATION}`,
1538
+ `query=${GITHUB_RESOLVE_MUTATION}`,
1685
1539
  "-f",
1686
- `commentId=${commentId}`
1540
+ `threadId=${input.threadId}`
1687
1541
  ]);
1688
1542
  }
1689
1543
  },
@@ -1702,13 +1556,11 @@ function GitHubReviewBackend(vcs, number) {
1702
1556
  function githubReviewSubmissionEndpoint(owner, repo, id, pendingReviewId) {
1703
1557
  return pendingReviewId ? `/repos/${owner}/${repo}/pulls/${id}/reviews/${pendingReviewId}/events` : `/repos/${owner}/${repo}/pulls/${id}/reviews`;
1704
1558
  }
1705
- var GITHUB_THREADS_QUERY = `query($owner:String!,$repo:String!,$number:Int!,$after:String){repository(owner:$owner,name:$repo){pullRequest(number:$number){reviewThreads(first:100,after:$after){nodes{id,isResolved,path,line,comments(first:100){nodes{id,databaseId,body,author{login}}}}pageInfo{hasNextPage,endCursor}}}}}`;
1559
+ var GITHUB_THREADS_QUERY = `query($owner:String!,$repo:String!,$number:Int!,$after:String){repository(owner:$owner,name:$repo){pullRequest(number:$number){reviewThreads(first:100,after:$after){nodes{id,isResolved,path,line,comments(first:100){nodes{body,author{login}}}}pageInfo{hasNextPage,endCursor}}}}}`;
1706
1560
  var GITHUB_ADD_THREAD_MUTATION = `mutation($reviewId:ID!,$body:String!,$path:String!,$line:Int!,$side:DiffSide!){addPullRequestReviewThread(input:{pullRequestReviewId:$reviewId,body:$body,path:$path,line:$line,side:$side}){thread{id}}}`;
1707
1561
  var GITHUB_ADD_FILE_THREAD_MUTATION = `mutation($reviewId:ID!,$body:String!,$path:String!){addPullRequestReviewThread(input:{pullRequestReviewId:$reviewId,body:$body,path:$path,subjectType:FILE}){thread{id}}}`;
1708
1562
  var GITHUB_REPLY_MUTATION = `mutation($threadId:ID!,$body:String!){addPullRequestReviewThreadReply(input:{pullRequestReviewThreadId:$threadId,body:$body}){comment{id}}}`;
1709
1563
  var GITHUB_RESOLVE_MUTATION = `mutation($threadId:ID!){resolveReviewThread(input:{threadId:$threadId}){thread{isResolved}}}`;
1710
- var GITHUB_UNRESOLVE_MUTATION = `mutation($threadId:ID!){unresolveReviewThread(input:{threadId:$threadId}){thread{isResolved}}}`;
1711
- var GITHUB_DELETE_COMMENT_MUTATION = `mutation($commentId:ID!){deletePullRequestReviewComment(input:{id:$commentId}){clientMutationId}}`;
1712
1564
 
1713
1565
  // src/review/gitlab-review-backend.ts
1714
1566
  function GitLabReviewBackend(vcs, number) {
@@ -1745,7 +1597,6 @@ function GitLabReviewBackend(vcs, number) {
1745
1597
  });
1746
1598
  };
1747
1599
  return {
1748
- kind: "remote",
1749
1600
  async stage(draft) {
1750
1601
  const endpoint = `${mergeRequestEndpoint()}/draft_notes`;
1751
1602
  if (draft.body.trim()) {
@@ -1774,36 +1625,6 @@ function GitLabReviewBackend(vcs, number) {
1774
1625
  });
1775
1626
  }
1776
1627
  },
1777
- async readDraft() {
1778
- const result = await glabChecked(["api", `${mergeRequestEndpoint()}/draft_notes`]);
1779
- let notes;
1780
- try {
1781
- notes = JSON.parse(result.stdout);
1782
- } catch {
1783
- throw new Error("Cannot parse GitLab draft notes as JSON.");
1784
- }
1785
- const comments = [];
1786
- const body = [];
1787
- for (const note of notes) {
1788
- if (!note.note)
1789
- continue;
1790
- const file = note.position?.new_path ?? note.position?.old_path;
1791
- const line = note.position?.new_line ?? note.position?.old_line;
1792
- if (file && line) {
1793
- comments.push({
1794
- file,
1795
- line,
1796
- side: note.position?.new_line ? "RIGHT" : "LEFT",
1797
- body: note.note
1798
- });
1799
- } else {
1800
- body.push(note.note);
1801
- }
1802
- }
1803
- return { comments, body: body.join(`
1804
-
1805
- `) };
1806
- },
1807
1628
  listThreads,
1808
1629
  async reply(input) {
1809
1630
  const endpoint = `${mergeRequestEndpoint()}/discussions/${encodeURIComponent(input.threadId)}`;
@@ -1815,15 +1636,7 @@ function GitLabReviewBackend(vcs, number) {
1815
1636
  });
1816
1637
  }
1817
1638
  if (input.resolve)
1818
- await this.setResolved?.(input.threadId, true);
1819
- },
1820
- async setResolved(threadId, resolved) {
1821
- const endpoint = `${mergeRequestEndpoint()}/discussions/${encodeURIComponent(threadId)}`;
1822
- await glabChecked(["api", "--method", "PUT", `${endpoint}?resolved=${resolved}`]);
1823
- },
1824
- async deleteThread(threadId) {
1825
- const endpoint = `${mergeRequestEndpoint()}/discussions/${encodeURIComponent(threadId)}`;
1826
- await glabChecked(["api", "--method", "DELETE", endpoint]);
1639
+ await glabChecked(["api", "--method", "PUT", `${endpoint}?resolved=true`]);
1827
1640
  },
1828
1641
  async publish(event) {
1829
1642
  assertReviewEventSupported(vcs.provider, event);
@@ -1863,12 +1676,105 @@ function hasGitlabDraftNotes(input) {
1863
1676
  }
1864
1677
  }
1865
1678
 
1866
- // src/review/local-review-backend.ts
1867
- import { readFile as readFile6, writeFile as writeFile3 } from "node:fs/promises";
1679
+ // src/review/review-backend.ts
1680
+ function createRemoteReviewBackend(vcs, number) {
1681
+ if (vcs.provider === "github")
1682
+ return GitHubReviewBackend(vcs, number);
1683
+ if (vcs.provider === "gitlab")
1684
+ return GitLabReviewBackend(vcs, number);
1685
+ throw new Error("A remote review backend requires a GitHub or GitLab repository.");
1686
+ }
1687
+ // src/review/local-reviews.ts
1688
+ import { mkdir as mkdir4, readdir as readdir2, readFile as readFile6, unlink as unlink2, writeFile as writeFile3 } from "node:fs/promises";
1689
+ import { join as join9 } from "node:path";
1690
+
1691
+ // src/store.ts
1692
+ import { createHash } from "node:crypto";
1693
+ import { lstat, mkdir as mkdir3, readlink, realpath as realpath2, symlink, unlink } from "node:fs/promises";
1694
+ import { homedir as homedir4 } from "node:os";
1695
+ import { dirname as dirname4, isAbsolute as isAbsolute2, join as join8, resolve as resolve2 } from "node:path";
1696
+ var STORE_LINK = ".diffpi";
1697
+ var LEGACY_STORE_LINK = join8(".pi", "diffpi");
1698
+ async function computeProjectSlug(cwd) {
1699
+ return projectSlug(await inspectGitRepository(cwd));
1700
+ }
1701
+ function storeGlobalRoot(homeDir = homedir4()) {
1702
+ return join8(homeDir, ".difflab", "diffpi", "projects");
1703
+ }
1704
+ async function ensureStore(cwd, homeDir = homedir4()) {
1705
+ const repository = await inspectGitRepository(cwd);
1706
+ const root = repository.root;
1707
+ const slug = projectSlug(repository);
1708
+ const dest = join8(storeGlobalRoot(homeDir), slug);
1709
+ const link = join8(root, STORE_LINK);
1710
+ await mkdir3(dest, { recursive: true });
1711
+ try {
1712
+ await assertStoreLink(link, dest);
1713
+ } catch (error) {
1714
+ if (error.code !== "ENOENT")
1715
+ throw error;
1716
+ await symlink(dest, link);
1717
+ }
1718
+ await removeLegacyStoreLink(join8(root, LEGACY_STORE_LINK), dest);
1719
+ return { slug, root, dest, link, linked: true };
1720
+ }
1721
+ async function storeDir(cwd, homeDir = homedir4()) {
1722
+ const store = await ensureStore(cwd, homeDir);
1723
+ return store.dest;
1724
+ }
1725
+ async function plansDir(cwd, homeDir = homedir4()) {
1726
+ const store = await ensureStore(cwd, homeDir);
1727
+ const dir = join8(store.link, "plan");
1728
+ await mkdir3(dir, { recursive: true });
1729
+ return dir;
1730
+ }
1731
+ async function reviewsDir(cwd, homeDir = homedir4()) {
1732
+ const store = await ensureStore(cwd, homeDir);
1733
+ const dir = join8(store.link, "review");
1734
+ await mkdir3(dir, { recursive: true });
1735
+ return dir;
1736
+ }
1737
+ async function assertStoreLink(path, dest) {
1738
+ const entry = await lstat(path);
1739
+ if (!entry.isSymbolicLink())
1740
+ throw new Error(`${path} exists and is not a symlink.`);
1741
+ const target = await symlinkTarget(path);
1742
+ if (target !== await canonicalPath2(dest))
1743
+ throw new Error(`${path} points to ${target}, not ${dest}.`);
1744
+ }
1745
+ async function removeLegacyStoreLink(path, dest) {
1746
+ try {
1747
+ const entry = await lstat(path);
1748
+ if (!entry.isSymbolicLink())
1749
+ return;
1750
+ const target = await symlinkTarget(path);
1751
+ if (target === await canonicalPath2(dest))
1752
+ await unlink(path);
1753
+ } catch (error) {
1754
+ if (error.code !== "ENOENT")
1755
+ throw error;
1756
+ }
1757
+ }
1758
+ async function symlinkTarget(path) {
1759
+ const target = await readlink(path);
1760
+ return canonicalPath2(isAbsolute2(target) ? target : resolve2(dirname4(path), target));
1761
+ }
1762
+ async function canonicalPath2(path) {
1763
+ try {
1764
+ return await realpath2(path);
1765
+ } catch {
1766
+ return resolve2(path);
1767
+ }
1768
+ }
1769
+ function projectSlug(repository) {
1770
+ const readable = repository.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "unnamed";
1771
+ const digest = createHash("sha256").update(repository.identity).digest("hex").slice(0, 12);
1772
+ return `${readable}-${digest}`;
1773
+ }
1868
1774
 
1869
1775
  // src/extensions/tuicrx.ts
1870
- import { readFile as readFile5, realpath as realpath2 } from "node:fs/promises";
1871
- import { resolve as resolve2 } from "node:path";
1776
+ import { readFile as readFile5, realpath as realpath3 } from "node:fs/promises";
1777
+ import { resolve as resolve3 } from "node:path";
1872
1778
  async function tuicrAvailable() {
1873
1779
  return Boolean(await findExecutable("tuicr"));
1874
1780
  }
@@ -1944,7 +1850,7 @@ async function resolvePrSession(cwd, owner, repo, number) {
1944
1850
  });
1945
1851
  }
1946
1852
  async function findMatchingSession(sessions, cwd, branch) {
1947
- const repository = await canonicalPath2(await gitToplevel(cwd));
1853
+ const repository = await canonicalPath3(await gitToplevel(cwd));
1948
1854
  for (const session of sessions) {
1949
1855
  if (session.kind !== "local")
1950
1856
  continue;
@@ -1952,7 +1858,7 @@ async function findMatchingSession(sessions, cwd, branch) {
1952
1858
  const data = await readSession(session.path);
1953
1859
  if (data.branch_name !== branch || !data.repo_path)
1954
1860
  continue;
1955
- if (await canonicalPath2(data.repo_path) === repository)
1861
+ if (await canonicalPath3(data.repo_path) === repository)
1956
1862
  return session;
1957
1863
  } catch {}
1958
1864
  }
@@ -1966,53 +1872,33 @@ async function readSession(path) {
1966
1872
  throw new Error(`Cannot parse tuicr session JSON: ${path}`);
1967
1873
  }
1968
1874
  }
1969
- function toFindings(session, options = {}) {
1970
- const comments = [];
1971
- const include = (comment) => (!options.agentOnly || commentAuthor(comment)?.startsWith("Agent: ")) && (!options.excludeLocalResponses || !isLocalResponse(comment.content));
1972
- const publishableBody = (comment) => {
1875
+ function toLocalReviewThreads(session) {
1876
+ const threads = [];
1877
+ const add = (comment, location = {}) => {
1973
1878
  const author = commentAuthor(comment);
1974
- const route = author?.match(/^Agent:\s*(.+)$/)?.[1];
1975
- return route ? withRemoteProvenance(comment.content, route) : comment.content;
1879
+ threads.push({
1880
+ id: comment.id ?? `local-${threads.length + 1}`,
1881
+ ...location,
1882
+ body: comment.content,
1883
+ ...author ? { author } : {},
1884
+ resolved: false,
1885
+ question: /\?\s*$/.test(comment.content.trim())
1886
+ });
1976
1887
  };
1977
- const bodyParts = (session.review_comments ?? []).flatMap((comment) => include(comment) ? [publishableBody(comment)] : []);
1888
+ for (const comment of session.review_comments ?? [])
1889
+ add(comment);
1978
1890
  for (const [file, entry] of Object.entries(session.files ?? {})) {
1979
- for (const fileComment of entry.file_comments ?? []) {
1980
- if (!include(fileComment))
1981
- continue;
1982
- const comment = {
1983
- file,
1984
- body: fileComment.content,
1985
- ...fileComment.id ? { sourceCommentId: String(fileComment.id) } : {}
1986
- };
1987
- const author = commentAuthor(fileComment);
1988
- if (author)
1989
- comment.author = author;
1990
- comments.push(comment);
1991
- }
1891
+ for (const comment of entry.file_comments ?? [])
1892
+ add(comment, { file });
1992
1893
  for (const [lineKey, lineComments] of Object.entries(entry.line_comments ?? {})) {
1993
1894
  const line = Number.parseInt(lineKey, 10);
1994
1895
  if (!Number.isFinite(line))
1995
1896
  continue;
1996
- for (const lineComment of lineComments) {
1997
- if (!include(lineComment))
1998
- continue;
1999
- const comment = {
2000
- file,
2001
- line,
2002
- side: lineComment.side === "old" ? "LEFT" : "RIGHT",
2003
- body: lineComment.content,
2004
- ...lineComment.id ? { sourceCommentId: String(lineComment.id) } : {}
2005
- };
2006
- const author = commentAuthor(lineComment);
2007
- if (author)
2008
- comment.author = author;
2009
- comments.push(comment);
2010
- }
1897
+ for (const comment of lineComments)
1898
+ add(comment, { file, line });
2011
1899
  }
2012
1900
  }
2013
- return { comments, body: bodyParts.join(`
2014
-
2015
- `) };
1901
+ return threads;
2016
1902
  }
2017
1903
  async function requireTuicr() {
2018
1904
  if (!await tuicrAvailable())
@@ -2021,207 +1907,70 @@ async function requireTuicr() {
2021
1907
  function commentAuthor(comment) {
2022
1908
  return comment.username ?? comment.author;
2023
1909
  }
2024
- async function canonicalPath2(path) {
1910
+ async function canonicalPath3(path) {
2025
1911
  try {
2026
- return await realpath2(path);
1912
+ return await realpath3(path);
2027
1913
  } catch {
2028
- return resolve2(path);
1914
+ return resolve3(path);
2029
1915
  }
2030
1916
  }
2031
1917
 
2032
- // src/review/local-review-backend.ts
2033
- function LocalReviewBackend(options) {
2034
- return {
2035
- kind: "local",
2036
- async stage(draft) {
2037
- for (const comment of draft.comments) {
2038
- await addComment(options.session, comment.body, {
2039
- targetFile: comment.file,
2040
- line: comment.line,
2041
- side: comment.side === "LEFT" ? "old" : "new",
2042
- username: options.author
2043
- });
2044
- }
2045
- if (draft.body.trim())
2046
- await addComment(options.session, draft.body, { username: options.author });
2047
- },
2048
- async readDraft() {
2049
- return toFindings(await readSession(options.session), { excludeLocalResponses: true });
2050
- },
2051
- async listThreads() {
2052
- try {
2053
- return parseThreadArtifact(await readFile6(options.artifactPath, "utf8"));
2054
- } catch (error) {
2055
- if (error.code === "ENOENT")
2056
- return [];
2057
- throw error;
2058
- }
2059
- },
2060
- async reply(input) {
2061
- const content = await readFile6(options.artifactPath, "utf8");
2062
- await writeFile3(options.artifactPath, upsertThreadReply(content, input.threadId, input.body, input.question, input.resolve), "utf8");
2063
- },
2064
- async publish() {
2065
- throw new Error("Promote a local draft through a remote review backend before publishing it.");
2066
- }
1918
+ // src/review/local-reviews.ts
1919
+ async function captureLocalReview(input) {
1920
+ const id = reviewSlug(input.branch) || "local-review";
1921
+ const dir = join9(await reviewsDir(input.cwd, input.homeDir), id);
1922
+ await mkdir4(dir, { recursive: true });
1923
+ const revision = await nextRevision(dir);
1924
+ const output = await readFile6(input.session.path, "utf8");
1925
+ const comments = toLocalReviewThreads(await readSession(input.session.path));
1926
+ const dump = {
1927
+ schemaVersion: 1,
1928
+ id,
1929
+ revision,
1930
+ branch: input.branch,
1931
+ base: input.base,
1932
+ createdAt: (input.now ?? (() => new Date))().toISOString(),
1933
+ diff: input.diff,
1934
+ comments,
1935
+ tuicr: { slug: input.session.slug, output }
2067
1936
  };
2068
- }
2069
-
2070
- // src/review/review-backend.ts
2071
- function createRemoteReviewBackend(vcs, number) {
2072
- if (vcs.provider === "github")
2073
- return GitHubReviewBackend(vcs, number);
2074
- if (vcs.provider === "gitlab")
2075
- return GitLabReviewBackend(vcs, number);
2076
- throw new Error("A remote review backend requires a GitHub or GitLab repository.");
2077
- }
2078
- function createLocalReviewBackend(options) {
2079
- return LocalReviewBackend(options);
2080
- }
2081
- // src/review/review-state.ts
2082
- import { createHash as createHash2 } from "node:crypto";
2083
- import { readFile as readFile7, rename, writeFile as writeFile4 } from "node:fs/promises";
2084
- import { join as join9 } from "node:path";
2085
- import { z as z5 } from "zod";
2086
-
2087
- // src/store.ts
2088
- import { createHash } from "node:crypto";
2089
- import { lstat, mkdir as mkdir3, readlink, realpath as realpath3, symlink, unlink } from "node:fs/promises";
2090
- import { homedir as homedir4 } from "node:os";
2091
- import { dirname as dirname4, isAbsolute as isAbsolute2, join as join8, resolve as resolve3 } from "node:path";
2092
- var STORE_LINK = ".diffpi";
2093
- var LEGACY_STORE_LINK = join8(".pi", "diffpi");
2094
- async function computeProjectSlug(cwd) {
2095
- return projectSlug(await inspectGitRepository(cwd));
2096
- }
2097
- function storeGlobalRoot(homeDir = homedir4()) {
2098
- return join8(homeDir, ".difflab", "diffpi", "projects");
2099
- }
2100
- async function ensureStore(cwd, homeDir = homedir4()) {
2101
- const repository = await inspectGitRepository(cwd);
2102
- const root = repository.root;
2103
- const slug = projectSlug(repository);
2104
- const dest = join8(storeGlobalRoot(homeDir), slug);
2105
- const link = join8(root, STORE_LINK);
2106
- await mkdir3(dest, { recursive: true });
1937
+ const path = join9(dir, `${revision}.json`);
1938
+ await writeFile3(path, `${JSON.stringify(dump, null, 2)}
1939
+ `, { encoding: "utf8", flag: "wx" });
1940
+ await unlink2(input.session.path);
1941
+ return { path, dump };
1942
+ }
1943
+ async function readLocalReview(cwd, branch, revision, homeDir) {
1944
+ const id = reviewSlug(branch) || "local-review";
1945
+ const dir = join9(await reviewsDir(cwd, homeDir), id);
1946
+ const selected = revision ?? await latestRevision(dir);
1947
+ if (!selected)
1948
+ return;
2107
1949
  try {
2108
- await assertStoreLink(link, dest);
1950
+ return JSON.parse(await readFile6(join9(dir, `${selected}.json`), "utf8"));
2109
1951
  } catch (error) {
2110
- if (error.code !== "ENOENT")
2111
- throw error;
2112
- await symlink(dest, link);
2113
- }
2114
- await removeLegacyStoreLink(join8(root, LEGACY_STORE_LINK), dest);
2115
- return { slug, root, dest, link, linked: true };
2116
- }
2117
- async function storeDir(cwd, homeDir = homedir4()) {
2118
- const store = await ensureStore(cwd, homeDir);
2119
- return store.dest;
2120
- }
2121
- async function plansDir(cwd, homeDir = homedir4()) {
2122
- const store = await ensureStore(cwd, homeDir);
2123
- const dir = join8(store.link, "plan");
2124
- await mkdir3(dir, { recursive: true });
2125
- return dir;
2126
- }
2127
- async function reviewsDir(cwd, homeDir = homedir4()) {
2128
- const store = await ensureStore(cwd, homeDir);
2129
- const dir = join8(store.link, "review");
2130
- await mkdir3(dir, { recursive: true });
2131
- return dir;
2132
- }
2133
- async function sessionsDir(cwd, homeDir = homedir4()) {
2134
- const store = await ensureStore(cwd, homeDir);
2135
- const dir = join8(store.link, "sessions");
2136
- await mkdir3(dir, { recursive: true });
2137
- return dir;
2138
- }
2139
- async function assertStoreLink(path, dest) {
2140
- const entry = await lstat(path);
2141
- if (!entry.isSymbolicLink())
2142
- throw new Error(`${path} exists and is not a symlink.`);
2143
- const target = await symlinkTarget(path);
2144
- if (target !== await canonicalPath3(dest))
2145
- throw new Error(`${path} points to ${target}, not ${dest}.`);
2146
- }
2147
- async function removeLegacyStoreLink(path, dest) {
2148
- try {
2149
- const entry = await lstat(path);
2150
- if (!entry.isSymbolicLink())
1952
+ if (error.code === "ENOENT")
2151
1953
  return;
2152
- const target = await symlinkTarget(path);
2153
- if (target === await canonicalPath3(dest))
2154
- await unlink(path);
2155
- } catch (error) {
2156
- if (error.code !== "ENOENT")
2157
- throw error;
1954
+ throw error;
2158
1955
  }
2159
1956
  }
2160
- async function symlinkTarget(path) {
2161
- const target = await readlink(path);
2162
- return canonicalPath3(isAbsolute2(target) ? target : resolve3(dirname4(path), target));
1957
+ async function nextRevision(dir) {
1958
+ return await latestRevision(dir) + 1;
2163
1959
  }
2164
- async function canonicalPath3(path) {
1960
+ async function latestRevision(dir) {
2165
1961
  try {
2166
- return await realpath3(path);
2167
- } catch {
2168
- return resolve3(path);
2169
- }
2170
- }
2171
- function projectSlug(repository) {
2172
- const readable = repository.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "unnamed";
2173
- const digest = createHash("sha256").update(repository.identity).digest("hex").slice(0, 12);
2174
- return `${readable}-${digest}`;
2175
- }
2176
-
2177
- // src/review/review-state.ts
2178
- var reviewPublicationStateSchema = z5.object({
2179
- target: z5.string().optional(),
2180
- bodies: z5.array(z5.string()).default([]),
2181
- comments: z5.array(z5.string()).default([]),
2182
- stagedComments: z5.array(z5.string()).default([]),
2183
- replies: z5.array(z5.string()).default([]),
2184
- overlayPath: z5.string().optional()
2185
- });
2186
- function reviewBodyFingerprint(body) {
2187
- return digest(body);
2188
- }
2189
- function reviewCommentFingerprint(comment) {
2190
- return digest([comment.file, String(comment.line), comment.side ?? "RIGHT", comment.body].join("\x00"));
2191
- }
2192
- function reviewReplyFingerprint(threadId, body) {
2193
- return digest(`${threadId}\x00${body}`);
2194
- }
2195
- function unpublishedReviewComments(comments, knownFingerprints) {
2196
- return comments.filter((comment) => !knownFingerprints.has(reviewCommentFingerprint(comment)));
2197
- }
2198
- async function loadReviewPublicationState(cwd, vcs, number, homeDir) {
2199
- const target = `${vcs.provider}:${vcs.owner}/${vcs.repo}#${number}`;
2200
- const path = join9(await sessionsDir(cwd, homeDir), `review-publish-${digest(target).slice(0, 16)}.json`);
2201
- try {
2202
- const parsed = reviewPublicationStateSchema.parse(JSON.parse(await readFile7(path, "utf8")));
2203
- return { path, state: { ...parsed, target } };
1962
+ return (await readdir2(dir)).reduce((latest, name) => {
1963
+ const match = name.match(/^(\d+)\.json$/);
1964
+ return match ? Math.max(latest, Number(match[1])) : latest;
1965
+ }, 0);
2204
1966
  } catch (error) {
2205
- if (error.code === "ENOENT") {
2206
- return { path, state: { target, bodies: [], comments: [], stagedComments: [], replies: [] } };
2207
- }
2208
- if (error instanceof SyntaxError || error instanceof z5.ZodError) {
2209
- throw new Error(`Cannot parse review publication state: ${path}`);
2210
- }
1967
+ if (error.code === "ENOENT")
1968
+ return 0;
2211
1969
  throw error;
2212
1970
  }
2213
1971
  }
2214
- async function saveReviewPublicationState(path, state) {
2215
- const temp = `${path}.${process.pid}.tmp`;
2216
- await writeFile4(temp, `${JSON.stringify(state, null, 2)}
2217
- `, "utf8");
2218
- await rename(temp, path);
2219
- }
2220
- function digest(value) {
2221
- return createHash2("sha256").update(value).digest("hex");
2222
- }
2223
1972
  // src/mcp.ts
2224
- import { mkdir as mkdir4, readFile as readFile8, writeFile as writeFile5 } from "node:fs/promises";
1973
+ import { mkdir as mkdir5, readFile as readFile7, writeFile as writeFile4 } from "node:fs/promises";
2225
1974
  import { homedir as homedir5 } from "node:os";
2226
1975
  import { dirname as dirname5, join as join10 } from "node:path";
2227
1976
  var mcp = {
@@ -2239,8 +1988,8 @@ var mcp = {
2239
1988
  const next = { ...current, mcpServers: nextServers };
2240
1989
  const changed = JSON.stringify(current) !== JSON.stringify(next);
2241
1990
  if (changed && !options.dryRun) {
2242
- await mkdir4(dirname5(path), { recursive: true });
2243
- await writeFile5(path, `${JSON.stringify(next, null, 2)}
1991
+ await mkdir5(dirname5(path), { recursive: true });
1992
+ await writeFile4(path, `${JSON.stringify(next, null, 2)}
2244
1993
  `, "utf8");
2245
1994
  }
2246
1995
  return { path, changed, existed: currentText !== undefined, planned: changed && options.dryRun === true };
@@ -2269,7 +2018,7 @@ function getParsedConfig(content, path) {
2269
2018
  }
2270
2019
  async function getOptionalFile2(path) {
2271
2020
  try {
2272
- return await readFile8(path, "utf8");
2021
+ return await readFile7(path, "utf8");
2273
2022
  } catch (error) {
2274
2023
  if (error instanceof Error && "code" in error && error.code === "ENOENT")
2275
2024
  return;
@@ -2284,7 +2033,7 @@ import {
2284
2033
  getAgentDir,
2285
2034
  parseFrontmatter as parseFrontmatter2
2286
2035
  } from "@earendil-works/pi-coding-agent";
2287
- import { readFile as readFile9 } from "node:fs/promises";
2036
+ import { readFile as readFile8 } from "node:fs/promises";
2288
2037
  import { homedir as homedir6 } from "node:os";
2289
2038
  import { basename as basename4, extname, join as join11 } from "node:path";
2290
2039
  async function discoverAgentModes(options) {
@@ -2483,7 +2232,7 @@ async function loadAgentModes(directory, source, modes, diagnostics, skillName)
2483
2232
  for (const entry of entries.filter((item) => item.isFile() && item.name.endsWith(".md")).sort((a, b) => a.name.localeCompare(b.name))) {
2484
2233
  const path = join11(directory, entry.name);
2485
2234
  try {
2486
- const content = await readFile9(path, "utf8");
2235
+ const content = await readFile8(path, "utf8");
2487
2236
  const { frontmatter, body } = parseFrontmatter2(content.startsWith("\uFEFF") ? content.slice(1) : content);
2488
2237
  if (frontmatter.enabled === false || frontmatter.inline === false)
2489
2238
  continue;
@@ -2539,7 +2288,7 @@ function isModeBaseline(value) {
2539
2288
  return (model === undefined || typeof model.provider === "string" && typeof model.id === "string") && candidate.thinkingLevel !== undefined && THINKING_LEVELS.has(candidate.thinkingLevel) && Array.isArray(candidate.tools) && candidate.tools.every((tool) => typeof tool === "string");
2540
2289
  }
2541
2290
  // src/pi.ts
2542
- import { mkdir as mkdir5, writeFile as writeFile6 } from "node:fs/promises";
2291
+ import { mkdir as mkdir6, writeFile as writeFile5 } from "node:fs/promises";
2543
2292
  import { homedir as homedir7 } from "node:os";
2544
2293
  import { dirname as dirname6, join as join12 } from "node:path";
2545
2294
  var pi = {
@@ -2558,8 +2307,8 @@ async function ensurePiAgent(filename, content, agentDir = resolvePiAgentDir(),
2558
2307
  const currentText = await readTextIfExists(path);
2559
2308
  const changed = currentText !== content;
2560
2309
  if (changed && !dryRun) {
2561
- await mkdir5(dirname6(path), { recursive: true });
2562
- await writeFile6(path, content, "utf8");
2310
+ await mkdir6(dirname6(path), { recursive: true });
2311
+ await writeFile5(path, content, "utf8");
2563
2312
  }
2564
2313
  return { path, changed, existed: currentText !== undefined, planned: changed && dryRun };
2565
2314
  }
@@ -2595,8 +2344,8 @@ async function ensurePiConfig(path, update, dryRun = false) {
2595
2344
  const next = update(current);
2596
2345
  const changed = JSON.stringify(current) !== JSON.stringify(next);
2597
2346
  if (changed && !dryRun) {
2598
- await mkdir5(dirname6(path), { recursive: true });
2599
- await writeFile6(path, `${JSON.stringify(next, null, 2)}
2347
+ await mkdir6(dirname6(path), { recursive: true });
2348
+ await writeFile5(path, `${JSON.stringify(next, null, 2)}
2600
2349
  `, "utf8");
2601
2350
  }
2602
2351
  return { path, changed, existed: currentText !== undefined, planned: changed && dryRun };
@@ -2629,16 +2378,16 @@ function parseJsonObject(content, path) {
2629
2378
  throw new Error(`Expected valid JSON object in ${path}.`);
2630
2379
  }
2631
2380
  // src/plan/store.ts
2632
- import { mkdir as mkdir7, readdir as readdir2, readFile as readFile12, realpath as realpath4, rm as rm2 } from "node:fs/promises";
2381
+ import { mkdir as mkdir8, readdir as readdir3, readFile as readFile11, realpath as realpath4, rm as rm2 } from "node:fs/promises";
2633
2382
  import { basename as basename6, dirname as dirname8, join as join16, resolve as resolve4, sep } from "node:path";
2634
2383
 
2635
2384
  // src/extensions/fsx.ts
2636
- import { mkdir as mkdir6, rename as rename2, rm, writeFile as writeFile7 } from "node:fs/promises";
2385
+ import { mkdir as mkdir7, rename, rm, writeFile as writeFile6 } from "node:fs/promises";
2637
2386
  import { basename as basename5, dirname as dirname7, join as join13 } from "node:path";
2638
2387
  async function atomicWrite(path, content) {
2639
2388
  const temp = join13(dirname7(path), `.${basename5(path)}.${crypto.randomUUID()}.tmp`);
2640
- await writeFile7(temp, content, { encoding: "utf8", mode: 384, flag: "wx" });
2641
- await rename2(temp, path);
2389
+ await writeFile6(temp, content, { encoding: "utf8", mode: 384, flag: "wx" });
2390
+ await rename(temp, path);
2642
2391
  }
2643
2392
  async function withDirectoryLock(lockDir, operation, options = {}) {
2644
2393
  const waitMs = options.waitMs ?? 5000;
@@ -2646,7 +2395,7 @@ async function withDirectoryLock(lockDir, operation, options = {}) {
2646
2395
  const started = Date.now();
2647
2396
  while (true) {
2648
2397
  try {
2649
- await mkdir6(lockDir);
2398
+ await mkdir7(lockDir);
2650
2399
  break;
2651
2400
  } catch (error) {
2652
2401
  if (error.code !== "EEXIST")
@@ -2668,7 +2417,7 @@ function sleep(ms) {
2668
2417
 
2669
2418
  // src/log.ts
2670
2419
  import { randomUUID } from "node:crypto";
2671
- import { appendFile, readFile as readFile10 } from "node:fs/promises";
2420
+ import { appendFile, readFile as readFile9 } from "node:fs/promises";
2672
2421
  async function appendLogEntry(logPath, event) {
2673
2422
  const entry = {
2674
2423
  ...event,
@@ -2682,7 +2431,7 @@ async function appendLogEntry(logPath, event) {
2682
2431
  }
2683
2432
 
2684
2433
  // src/templates.ts
2685
- import { readFile as readFile11 } from "node:fs/promises";
2434
+ import { readFile as readFile10 } from "node:fs/promises";
2686
2435
  import { homedir as homedir8 } from "node:os";
2687
2436
  import { join as join14, normalize } from "node:path";
2688
2437
  async function loadTemplate(name, options = {}) {
@@ -2709,7 +2458,7 @@ function templateRelativePath(name) {
2709
2458
  }
2710
2459
  async function readOptionalFile(path) {
2711
2460
  try {
2712
- return await readFile11(path, "utf8");
2461
+ return await readFile10(path, "utf8");
2713
2462
  } catch (error) {
2714
2463
  if (error.code === "ENOENT")
2715
2464
  return;
@@ -7793,7 +7542,7 @@ function planRecordName(shortSlug, date = new Date) {
7793
7542
  }
7794
7543
  async function resolvePlan(cwd, query, filters = {}, options = {}) {
7795
7544
  const root = await plansDir(cwd, options.homeDir);
7796
- const entries = await readdir2(root, { withFileTypes: true });
7545
+ const entries = await readdir3(root, { withFileTypes: true });
7797
7546
  const normalizedQuery = query ? normalizeQuery(query) : undefined;
7798
7547
  const records = [];
7799
7548
  for (const entry of entries) {
@@ -7830,7 +7579,7 @@ function createPlanStore(options = {}) {
7830
7579
  const dir = join16(root, id);
7831
7580
  await assertContained(root, dirname8(dir));
7832
7581
  try {
7833
- await mkdir7(dir);
7582
+ await mkdir8(dir);
7834
7583
  } catch (error) {
7835
7584
  if (error.code === "EEXIST")
7836
7585
  throw new Error(`Plan ${id} already exists.`);
@@ -7905,7 +7654,7 @@ function createPlanStore(options = {}) {
7905
7654
  async function readRecord(dir) {
7906
7655
  const planPath = join16(dir, "PLAN.md");
7907
7656
  const logPath = join16(dir, "logs.txt");
7908
- const source = await readFile12(planPath, "utf8");
7657
+ const source = await readFile11(planPath, "utf8");
7909
7658
  const document = parsePlanDocument(source, planPath);
7910
7659
  return {
7911
7660
  id: basename6(dir),
@@ -7919,7 +7668,7 @@ async function readRecord(dir) {
7919
7668
  }
7920
7669
  async function ensureImplementationFiles(dir, document, options) {
7921
7670
  const implementationDir = join16(dir, "implementation");
7922
- await mkdir7(implementationDir, { recursive: true });
7671
+ await mkdir8(implementationDir, { recursive: true });
7923
7672
  const template = await loadTemplate("plan/implementation", {
7924
7673
  homeDir: options.homeDir,
7925
7674
  bundledDir: options.bundledTemplatesDir
@@ -7927,7 +7676,7 @@ async function ensureImplementationFiles(dir, document, options) {
7927
7676
  for (const phase of document.phases) {
7928
7677
  const path = join16(implementationDir, `phase-${phase.id}.md`);
7929
7678
  try {
7930
- await readFile12(path, "utf8");
7679
+ await readFile11(path, "utf8");
7931
7680
  } catch (error) {
7932
7681
  if (error.code !== "ENOENT")
7933
7682
  throw error;
@@ -7976,7 +7725,7 @@ function titleFromSlug(value) {
7976
7725
  }
7977
7726
  // src/setup.ts
7978
7727
  import { parseFrontmatter as parseFrontmatter3 } from "@earendil-works/pi-coding-agent";
7979
- import { readdir as readdir3, readFile as readFile13 } from "node:fs/promises";
7728
+ import { readdir as readdir4, readFile as readFile12 } from "node:fs/promises";
7980
7729
  import { homedir as homedir9 } from "node:os";
7981
7730
  import { basename as basename7, join as join17 } from "node:path";
7982
7731
  var MISE_DEPENDENCIES = [
@@ -8079,11 +7828,11 @@ async function ensurePiAgents(options = {}) {
8079
7828
  const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
8080
7829
  const bundledAgentsDir = options.bundledAgentsDir ?? BUNDLED_AGENTS_DIR2;
8081
7830
  const userConfig = await loadDiffpiConfig({ homeDir: options.homeDir });
8082
- const entries = (await readdir3(bundledAgentsDir, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.startsWith("diffpi-") && entry.name.endsWith(".md")).sort((left, right) => left.name.localeCompare(right.name));
7831
+ const entries = (await readdir4(bundledAgentsDir, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.startsWith("diffpi-") && entry.name.endsWith(".md")).sort((left, right) => left.name.localeCompare(right.name));
8083
7832
  const actions = [];
8084
7833
  for (const entry of entries) {
8085
7834
  const id = basename7(entry.name, ".md").replace(/^diffpi-/, "");
8086
- const source = await readFile13(join17(bundledAgentsDir, entry.name), "utf8");
7835
+ const source = await readFile12(join17(bundledAgentsDir, entry.name), "utf8");
8087
7836
  const content = materializeAgentModels(source, id, userConfig.config, options.availableModels);
8088
7837
  const result = await pi.agentEnsure(entry.name, content, agentDir, options.dryRun);
8089
7838
  actions.push(getConfigSetupAction(`pi agent ${id}`, result));
@@ -8288,7 +8037,7 @@ function reportProgress(options, message) {
8288
8037
  options.onProgress?.(message);
8289
8038
  }
8290
8039
  async function packageVersion2() {
8291
- const source = await readFile13(join17(resolveBundledAgentsDir(), "..", "package.json"), "utf8");
8040
+ const source = await readFile12(join17(resolveBundledAgentsDir(), "..", "package.json"), "utf8");
8292
8041
  let value;
8293
8042
  try {
8294
8043
  value = JSON.parse(source);
@@ -8313,11 +8062,11 @@ export {
8313
8062
  addComment,
8314
8063
  assertGitHubMergeReady,
8315
8064
  assertReviewEventSupported,
8065
+ captureLocalReview,
8316
8066
  checkConventionalSubject,
8317
8067
  computeProjectSlug,
8318
8068
  createForge,
8319
8069
  createForgeBackend,
8320
- createLocalReviewBackend,
8321
8070
  createModeController,
8322
8071
  createPlanStore,
8323
8072
  createRemoteReviewBackend,
@@ -8350,7 +8099,6 @@ export {
8350
8099
  launch,
8351
8100
  listSessions,
8352
8101
  loadDiffpiConfig,
8353
- loadReviewPublicationState,
8354
8102
  loadTemplate,
8355
8103
  localReviewAuthor,
8356
8104
  mcp,
@@ -8359,37 +8107,28 @@ export {
8359
8107
  openInNewTab,
8360
8108
  parseGitlabDiffRefs,
8361
8109
  parseRemote,
8362
- parseThreadArtifact,
8363
8110
  pi,
8364
8111
  plansDir,
8112
+ readLocalReview,
8365
8113
  readSession,
8366
8114
  renderReviewDoc,
8367
8115
  renderTemplate,
8368
- renderThreadArtifact,
8369
8116
  resolveAgentMode,
8370
8117
  resolveAgentModelPreferences,
8371
8118
  resolvePrSession,
8372
8119
  resolveReviewSession,
8373
8120
  resolveSession,
8374
- reviewBodyFingerprint,
8375
- reviewCommentFingerprint,
8376
8121
  reviewRecordName,
8377
- reviewReplyFingerprint,
8378
8122
  reviewSlug,
8379
8123
  reviewsDir,
8380
8124
  runMiseGates,
8381
- saveReviewPublicationState,
8382
- sessionsDir,
8383
8125
  setupPi,
8384
8126
  severitySchema,
8385
8127
  storeDir,
8386
8128
  storeGlobalRoot,
8387
8129
  templateRelativePath,
8388
- toFindings,
8389
8130
  toReviewComments,
8390
8131
  tuicrAvailable,
8391
- unpublishedReviewComments,
8392
- upsertThreadReply,
8393
8132
  withRemoteProvenance,
8394
8133
  yymmdd,
8395
8134
  zedKeymapPath,