@difflab/pi 0.5.0-rc.202609231305.97666c2 → 0.5.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.
- package/README.md +5 -5
- package/agents/diffpi-reviewer.md +3 -3
- package/dist/commands/review.d.ts +1 -0
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/extensions/index.js +804 -296
- package/dist/extensions/tuicrx.d.ts +8 -1
- package/dist/extensions/tuicrx.d.ts.map +1 -1
- package/dist/index.d.ts +8 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +458 -197
- package/dist/review/github-review-backend.d.ts.map +1 -1
- package/dist/review/gitlab-review-backend.d.ts.map +1 -1
- package/dist/review/index.d.ts +6 -6
- package/dist/review/index.d.ts.map +1 -1
- package/dist/review/local-review-backend.d.ts +3 -0
- package/dist/review/local-review-backend.d.ts.map +1 -0
- package/dist/review/review-backend.d.ts +3 -2
- package/dist/review/review-backend.d.ts.map +1 -1
- package/dist/review/review-markdown.d.ts +4 -1
- package/dist/review/review-markdown.d.ts.map +1 -1
- package/dist/review/review-state.d.ts +20 -0
- package/dist/review/review-state.d.ts.map +1 -0
- package/dist/review/types.d.ts +28 -0
- package/dist/review/types.d.ts.map +1 -1
- package/dist/store.d.ts +2 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/tools/index.js +783 -185
- package/dist/tools/review.d.ts +30 -0
- package/dist/tools/review.d.ts.map +1 -1
- package/dist/workflow.d.ts +0 -6
- package/dist/workflow.d.ts.map +1 -1
- package/package.json +1 -1
- package/skills/review/SKILL.md +8 -6
- package/skills/review/references/workflows/address.md +11 -0
- package/skills/review/references/workflows/auto.md +18 -0
- package/skills/review/references/workflows/complete.md +7 -0
- package/skills/review/references/workflows/edit.md +7 -0
- package/skills/review/references/workflows/help.md +30 -0
- package/skills/review/references/workflows/merge.md +6 -0
- package/skills/review/references/workflows/new.md +19 -0
- package/skills/review/references/workflows/open.md +3 -0
- package/skills/review/references/workflows/publish.md +8 -0
- package/dist/review/local-reviews.d.ts +0 -31
- package/dist/review/local-reviews.d.ts.map +0 -1
- package/workflows/review/address.md +0 -8
- package/workflows/review/auto.md +0 -8
- package/workflows/review/complete.md +0 -7
- package/workflows/review/edit.md +0 -7
- package/workflows/review/help.md +0 -14
- package/workflows/review/merge.md +0 -6
- package/workflows/review/new.md +0 -7
- package/workflows/review/publish.md +0 -6
package/dist/extensions/index.js
CHANGED
|
@@ -16642,14 +16642,8 @@ async function rpc(events, method, payload, timeout = 5000) {
|
|
|
16642
16642
|
// src/workflow.ts
|
|
16643
16643
|
import { readFile as readFile3 } from "node:fs/promises";
|
|
16644
16644
|
import { join as join6 } from "node:path";
|
|
16645
|
-
function loadPlanWorkflow(name, args) {
|
|
16646
|
-
|
|
16647
|
-
}
|
|
16648
|
-
function loadReviewWorkflow(name, args) {
|
|
16649
|
-
return loadWorkflow("review", name, args);
|
|
16650
|
-
}
|
|
16651
|
-
async function loadWorkflow(scope, name, args) {
|
|
16652
|
-
const workflowDir = join6(resolveBundledWorkflowsDir(), scope);
|
|
16645
|
+
async function loadPlanWorkflow(name, args) {
|
|
16646
|
+
const workflowDir = join6(resolveBundledWorkflowsDir(), "plan");
|
|
16653
16647
|
const content = await readFile3(join6(workflowDir, `${name}.md`), "utf8");
|
|
16654
16648
|
return [`Workflow directory: ${workflowDir}`, content.trim(), `Arguments:
|
|
16655
16649
|
${JSON.stringify(args, null, 2)}`].join(`
|
|
@@ -16860,130 +16854,46 @@ function registerReloadCommand(pi) {
|
|
|
16860
16854
|
}
|
|
16861
16855
|
|
|
16862
16856
|
// src/commands/review.ts
|
|
16857
|
+
var REVIEWER_VERBS = new Set(["address", "auto", "launch"]);
|
|
16863
16858
|
function registerReviewCommand(pi, modes) {
|
|
16864
16859
|
pi.registerCommand("review", {
|
|
16865
16860
|
description: "Code review: auto, new, edit, address, publish, complete, merge (add --local or --bg)",
|
|
16866
|
-
handler: async (args, ctx) =>
|
|
16861
|
+
handler: async (args, ctx) => handleReviewCommand(args, ctx, pi, modes)
|
|
16867
16862
|
});
|
|
16868
16863
|
}
|
|
16869
|
-
async function
|
|
16870
|
-
|
|
16871
|
-
|
|
16872
|
-
|
|
16873
|
-
if (parsed._tag === "error")
|
|
16874
|
-
throw new Error(parsed.error);
|
|
16875
|
-
} catch (error) {
|
|
16876
|
-
const help = await generateCommandHelp(createReviewCommand(runtime));
|
|
16877
|
-
runtime.ctx.ui.notify(`${error instanceof Error ? error.message : String(error)}
|
|
16878
|
-
${help}`, "error");
|
|
16879
|
-
}
|
|
16880
|
-
}
|
|
16881
|
-
function createReviewCommand(runtime) {
|
|
16882
|
-
const invoke = (workflow) => async (args) => {
|
|
16883
|
-
await invokeReviewWorkflow(runtime, {
|
|
16884
|
-
workflow,
|
|
16885
|
-
invocation: serializeInvocation(args),
|
|
16886
|
-
background: args.background
|
|
16887
|
-
});
|
|
16888
|
-
};
|
|
16889
|
-
const commands = {
|
|
16890
|
-
auto: reviewSubcommand("auto", invoke("auto")),
|
|
16891
|
-
new: reviewSubcommand("new", invoke("new")),
|
|
16892
|
-
edit: reviewSubcommand("edit", invoke("edit")),
|
|
16893
|
-
address: reviewSubcommand("address", invoke("address")),
|
|
16894
|
-
publish: reviewSubcommand("publish", invoke("publish")),
|
|
16895
|
-
complete: reviewSubcommand("complete", invoke("complete")),
|
|
16896
|
-
merge: reviewSubcommand("merge", invoke("merge")),
|
|
16897
|
-
help: command({
|
|
16898
|
-
name: "help",
|
|
16899
|
-
args: {},
|
|
16900
|
-
handler: async () => invokeReviewWorkflow(runtime, { workflow: "help", invocation: "", background: false })
|
|
16901
|
-
})
|
|
16902
|
-
};
|
|
16903
|
-
return subcommands({
|
|
16904
|
-
name: "review",
|
|
16905
|
-
cmds: {
|
|
16906
|
-
...commands,
|
|
16907
|
-
open: commands.new,
|
|
16908
|
-
create: commands.new,
|
|
16909
|
-
draft: commands.new,
|
|
16910
|
-
launch: commands.auto,
|
|
16911
|
-
ready: commands.publish,
|
|
16912
|
-
close: commands.complete,
|
|
16913
|
-
land: commands.merge
|
|
16914
|
-
}
|
|
16915
|
-
});
|
|
16916
|
-
}
|
|
16917
|
-
async function invokeReviewWorkflow(runtime, invocation) {
|
|
16918
|
-
const { workflow, background } = invocation;
|
|
16919
|
-
const prompt = await loadReviewWorkflow(workflow, { invocation: invocation.invocation, background });
|
|
16864
|
+
async function handleReviewCommand(args, ctx, pi, modes) {
|
|
16865
|
+
const background = hasFlag2(args, "--bg");
|
|
16866
|
+
const invocation = withoutFlag(args, "--bg") || "help";
|
|
16867
|
+
const verb = reviewVerb(invocation);
|
|
16920
16868
|
if (background) {
|
|
16921
|
-
await launchBackgroundAgent(
|
|
16922
|
-
name: `Review ${
|
|
16869
|
+
await launchBackgroundAgent(pi.events, {
|
|
16870
|
+
name: `Review ${verb}`,
|
|
16923
16871
|
agent: "orchestrator",
|
|
16924
|
-
cwd:
|
|
16925
|
-
|
|
16926
|
-
prompt
|
|
16872
|
+
cwd: ctx.cwd,
|
|
16873
|
+
prompt: reviewPrompt(invocation, "orchestrator")
|
|
16927
16874
|
});
|
|
16928
16875
|
return;
|
|
16929
16876
|
}
|
|
16930
|
-
const agent =
|
|
16931
|
-
const activation = await
|
|
16932
|
-
|
|
16933
|
-
customType:
|
|
16877
|
+
const agent = REVIEWER_VERBS.has(verb) ? "reviewer" : "orchestrator";
|
|
16878
|
+
const activation = await modes.set(agent, ctx);
|
|
16879
|
+
pi.sendMessage({
|
|
16880
|
+
customType: "diffpi-review-command",
|
|
16934
16881
|
display: false,
|
|
16935
|
-
content: `${
|
|
16936
|
-
|
|
16937
|
-
${activation.message}`
|
|
16882
|
+
content: `${reviewPrompt(invocation, agent)} ${activation.message}`
|
|
16938
16883
|
}, { triggerTurn: true });
|
|
16939
16884
|
}
|
|
16940
|
-
function
|
|
16941
|
-
return
|
|
16942
|
-
|
|
16943
|
-
|
|
16944
|
-
|
|
16945
|
-
|
|
16946
|
-
|
|
16947
|
-
|
|
16948
|
-
|
|
16949
|
-
base: option({ long: "base", type: optional(string5), defaultValue: () => {
|
|
16950
|
-
return;
|
|
16951
|
-
} }),
|
|
16952
|
-
comment: flag({ long: "comment" }),
|
|
16953
|
-
approve: flag({ long: "approve" }),
|
|
16954
|
-
requestChanges: flag({ long: "request-changes" }),
|
|
16955
|
-
close: flag({ long: "close" }),
|
|
16956
|
-
reject: flag({ long: "reject" }),
|
|
16957
|
-
abandon: flag({ long: "abandon" }),
|
|
16958
|
-
values: restPositionals({ type: string5 })
|
|
16959
|
-
},
|
|
16960
|
-
handler
|
|
16961
|
-
});
|
|
16885
|
+
function reviewVerb(invocation) {
|
|
16886
|
+
return invocation.split(/\s+/).find((token) => token && !token.startsWith("-")) ?? "help";
|
|
16887
|
+
}
|
|
16888
|
+
function reviewPrompt(invocation, agent) {
|
|
16889
|
+
const routing = agent === "reviewer" ? "Coordinate this auto/address workflow. For address, classify every thread and delegate bounded edits to lightweight worker agents before collecting outcomes." : "Own this lifecycle workflow. Delegate review judgment to the reviewer only if the selected workflow requires it.";
|
|
16890
|
+
return `The user ran /review ${invocation}. Active inline agent: ${agent}. ${routing} Follow the review skill dispatcher. Call review_context first, then the matching review_* tools. Do not perform unrelated work.`;
|
|
16891
|
+
}
|
|
16892
|
+
function hasFlag2(input, flag) {
|
|
16893
|
+
return input.split(/\s+/).includes(flag);
|
|
16962
16894
|
}
|
|
16963
|
-
function
|
|
16964
|
-
|
|
16965
|
-
if (args.intent)
|
|
16966
|
-
values.push("--intent", quoteArg(args.intent));
|
|
16967
|
-
if (args.base)
|
|
16968
|
-
values.push("--base", quoteArg(args.base));
|
|
16969
|
-
if (args.local)
|
|
16970
|
-
values.push("--local");
|
|
16971
|
-
if (args.comment)
|
|
16972
|
-
values.push("--comment");
|
|
16973
|
-
if (args.approve)
|
|
16974
|
-
values.push("--approve");
|
|
16975
|
-
if (args.requestChanges)
|
|
16976
|
-
values.push("--request-changes");
|
|
16977
|
-
if (args.close)
|
|
16978
|
-
values.push("--close");
|
|
16979
|
-
if (args.reject)
|
|
16980
|
-
values.push("--reject");
|
|
16981
|
-
if (args.abandon)
|
|
16982
|
-
values.push("--abandon");
|
|
16983
|
-
return values.join(" ");
|
|
16984
|
-
}
|
|
16985
|
-
function quoteArg(value) {
|
|
16986
|
-
return /\s/.test(value) ? JSON.stringify(value) : value;
|
|
16895
|
+
function withoutFlag(input, flag) {
|
|
16896
|
+
return input.split(/\s+/).filter((token) => token && token !== flag).join(" ").trim();
|
|
16987
16897
|
}
|
|
16988
16898
|
|
|
16989
16899
|
// src/commands/index.ts
|
|
@@ -17864,6 +17774,18 @@ async function reviewsDir(cwd, homeDir = homedir5()) {
|
|
|
17864
17774
|
await mkdir2(dir, { recursive: true });
|
|
17865
17775
|
return dir;
|
|
17866
17776
|
}
|
|
17777
|
+
async function completedReviewsDir(cwd, homeDir = homedir5()) {
|
|
17778
|
+
const store = await ensureStore(cwd, homeDir);
|
|
17779
|
+
const dir = join11(store.link, "reviews");
|
|
17780
|
+
await mkdir2(dir, { recursive: true });
|
|
17781
|
+
return dir;
|
|
17782
|
+
}
|
|
17783
|
+
async function sessionsDir(cwd, homeDir = homedir5()) {
|
|
17784
|
+
const store = await ensureStore(cwd, homeDir);
|
|
17785
|
+
const dir = join11(store.link, "sessions");
|
|
17786
|
+
await mkdir2(dir, { recursive: true });
|
|
17787
|
+
return dir;
|
|
17788
|
+
}
|
|
17867
17789
|
async function assertStoreLink(path, dest) {
|
|
17868
17790
|
const entry = await lstat(path);
|
|
17869
17791
|
if (!entry.isSymbolicLink())
|
|
@@ -24686,10 +24608,10 @@ function result2(text, details = {}) {
|
|
|
24686
24608
|
|
|
24687
24609
|
// src/tools/review.ts
|
|
24688
24610
|
import { existsSync as existsSync3 } from "node:fs";
|
|
24689
|
-
import { mkdir as
|
|
24611
|
+
import { mkdir as mkdir7, readdir as readdir3, readFile as readFile14, rename as rename3, stat, unlink as unlink2, writeFile as writeFile7 } from "node:fs/promises";
|
|
24690
24612
|
import { join as join21 } from "node:path";
|
|
24691
24613
|
import { defineTool as defineTool6 } from "@earendil-works/pi-coding-agent";
|
|
24692
|
-
import { z as
|
|
24614
|
+
import { z as z13 } from "zod";
|
|
24693
24615
|
|
|
24694
24616
|
// src/review/types.ts
|
|
24695
24617
|
import { z as z10 } from "zod";
|
|
@@ -24714,6 +24636,21 @@ function yymmdd(date = new Date) {
|
|
|
24714
24636
|
function reviewRecordName(target, date = new Date) {
|
|
24715
24637
|
return `${yymmdd(date)}-${reviewSlug(target) || "uncommitted"}`;
|
|
24716
24638
|
}
|
|
24639
|
+
function localResponseMarker(threadId) {
|
|
24640
|
+
return `<!-- diffpi -->`;
|
|
24641
|
+
}
|
|
24642
|
+
function isLocalResponse(body) {
|
|
24643
|
+
return /^<!-- diffpi(?:-local-response:[A-Za-z0-9_-]+)? -->\n/m.test(body);
|
|
24644
|
+
}
|
|
24645
|
+
function parseReviewThreadAction(body) {
|
|
24646
|
+
const match = body.match(/^\[(REOPEN|RESOLVE|DELETE)\]\s*/i);
|
|
24647
|
+
if (!match)
|
|
24648
|
+
return { body };
|
|
24649
|
+
return {
|
|
24650
|
+
action: match[1].toLowerCase(),
|
|
24651
|
+
body: body.slice(match[0].length)
|
|
24652
|
+
};
|
|
24653
|
+
}
|
|
24717
24654
|
function dedupeFindings(findings) {
|
|
24718
24655
|
const rank = { BLOCKING: 3, CONSIDER: 2, NOTE: 1 };
|
|
24719
24656
|
const byKey = new Map;
|
|
@@ -24759,6 +24696,18 @@ function renderCommentBody(finding) {
|
|
|
24759
24696
|
return `${prefix}${finding.body}${reference}`;
|
|
24760
24697
|
}
|
|
24761
24698
|
// src/review/review-markdown.ts
|
|
24699
|
+
import { z as z11 } from "zod";
|
|
24700
|
+
var reviewThreadRecordSchema = z11.object({
|
|
24701
|
+
id: z11.string().min(1),
|
|
24702
|
+
file: z11.string().nullable().optional().transform((value) => value ?? undefined),
|
|
24703
|
+
line: z11.number().int().positive().nullable().optional().transform((value) => value ?? undefined),
|
|
24704
|
+
body: z11.string(),
|
|
24705
|
+
author: z11.string().optional(),
|
|
24706
|
+
resolved: z11.boolean(),
|
|
24707
|
+
addressed: z11.boolean().optional(),
|
|
24708
|
+
question: z11.boolean(),
|
|
24709
|
+
replies: z11.array(z11.string()).optional()
|
|
24710
|
+
});
|
|
24762
24711
|
function renderReviewDoc(input) {
|
|
24763
24712
|
const anchored = dedupeFindings(input.findings).filter((finding) => finding.line > 0);
|
|
24764
24713
|
const lines = [`# Review: ${input.title}`, "", "## Metadata"];
|
|
@@ -24800,6 +24749,88 @@ function renderReviewDoc(input) {
|
|
|
24800
24749
|
`).trimEnd()}
|
|
24801
24750
|
`;
|
|
24802
24751
|
}
|
|
24752
|
+
function renderThreadArtifact(title, target, threads, options = {}) {
|
|
24753
|
+
const records = threads.map((thread) => ({
|
|
24754
|
+
id: thread.id,
|
|
24755
|
+
file: thread.file,
|
|
24756
|
+
line: thread.line,
|
|
24757
|
+
body: thread.body,
|
|
24758
|
+
author: thread.author,
|
|
24759
|
+
resolved: thread.resolved,
|
|
24760
|
+
addressed: thread.addressed,
|
|
24761
|
+
question: thread.question,
|
|
24762
|
+
replies: thread.replies
|
|
24763
|
+
}));
|
|
24764
|
+
const payload = Buffer.from(JSON.stringify(records), "utf8").toString("base64url");
|
|
24765
|
+
const lines = [
|
|
24766
|
+
`<!-- diffpi-threads:${payload} -->`,
|
|
24767
|
+
`# Review threads: ${title}`,
|
|
24768
|
+
"",
|
|
24769
|
+
"## Metadata",
|
|
24770
|
+
"",
|
|
24771
|
+
`- Target: ${target}`,
|
|
24772
|
+
`- Pulled: ${options.timestamp ?? new Date().toISOString()}`
|
|
24773
|
+
];
|
|
24774
|
+
if (options.number !== undefined)
|
|
24775
|
+
lines.push(`- PR/MR: #${options.number}${options.url ? ` — ${options.url}` : ""}`);
|
|
24776
|
+
lines.push("", "## Replies", "");
|
|
24777
|
+
for (const thread of threads) {
|
|
24778
|
+
const id = Buffer.from(thread.id, "utf8").toString("base64url");
|
|
24779
|
+
lines.push(`### ${thread.file ?? "review"}:${thread.line ?? "n/a"} (${thread.id})`, "", `<!-- diffpi-reply-start:${id} -->`, thread.reply ?? "", `<!-- diffpi-reply-end:${id} -->`, "");
|
|
24780
|
+
}
|
|
24781
|
+
lines.push("## Source comments", "");
|
|
24782
|
+
for (const thread of threads) {
|
|
24783
|
+
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, "", "---", "");
|
|
24784
|
+
}
|
|
24785
|
+
return `${lines.join(`
|
|
24786
|
+
`).trimEnd()}
|
|
24787
|
+
`;
|
|
24788
|
+
}
|
|
24789
|
+
function parseThreadArtifact(content) {
|
|
24790
|
+
const payload = content.match(/^<!-- diffpi-threads:([A-Za-z0-9_-]+) -->$/m)?.[1];
|
|
24791
|
+
if (!payload)
|
|
24792
|
+
throw new Error("This file is not a Diffpi thread artifact.");
|
|
24793
|
+
let threads;
|
|
24794
|
+
try {
|
|
24795
|
+
const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
24796
|
+
threads = z11.array(reviewThreadRecordSchema).parse(decoded);
|
|
24797
|
+
} catch {
|
|
24798
|
+
throw new Error("Cannot parse the Diffpi thread artifact payload.");
|
|
24799
|
+
}
|
|
24800
|
+
const replies = content.split(/^## Source comments$/m, 1)[0] ?? "";
|
|
24801
|
+
return threads.map((thread) => {
|
|
24802
|
+
const id = Buffer.from(thread.id, "utf8").toString("base64url");
|
|
24803
|
+
const startMarker = `<!-- diffpi-reply-start:${id} -->
|
|
24804
|
+
`;
|
|
24805
|
+
const endMarker = `
|
|
24806
|
+
<!-- diffpi-reply-end:${id} -->`;
|
|
24807
|
+
const start = replies.indexOf(startMarker);
|
|
24808
|
+
const end = start < 0 ? -1 : replies.indexOf(endMarker, start + startMarker.length);
|
|
24809
|
+
const reply = start >= 0 && end >= 0 ? replies.slice(start + startMarker.length, end).trim() : "";
|
|
24810
|
+
return reply ? { ...thread, reply } : thread;
|
|
24811
|
+
});
|
|
24812
|
+
}
|
|
24813
|
+
function upsertThreadReply(content, threadId, body, question, resolved) {
|
|
24814
|
+
const threads = parseThreadArtifact(content);
|
|
24815
|
+
const thread = threads.find((candidate) => candidate.id === threadId);
|
|
24816
|
+
if (!thread)
|
|
24817
|
+
throw new Error(`Review thread ${threadId} was not found in the local artifact.`);
|
|
24818
|
+
thread.reply = body;
|
|
24819
|
+
thread.addressed = true;
|
|
24820
|
+
if (question !== undefined)
|
|
24821
|
+
thread.question = question;
|
|
24822
|
+
if (resolved !== undefined)
|
|
24823
|
+
thread.resolved = resolved;
|
|
24824
|
+
const title = content.match(/^# Review threads: (.+)$/m)?.[1] ?? "review";
|
|
24825
|
+
const target = content.match(/^- Target: (.+)$/m)?.[1] ?? "local";
|
|
24826
|
+
const timestamp = content.match(/^- Pulled: (.+)$/m)?.[1];
|
|
24827
|
+
const pr = content.match(/^- PR\/MR: #(\d+)(?: — (.+))?$/m);
|
|
24828
|
+
return renderThreadArtifact(title, target, threads, {
|
|
24829
|
+
timestamp,
|
|
24830
|
+
number: pr ? Number.parseInt(pr[1], 10) : undefined,
|
|
24831
|
+
url: pr?.[2]
|
|
24832
|
+
});
|
|
24833
|
+
}
|
|
24803
24834
|
// src/review/github-review-backend.ts
|
|
24804
24835
|
function GitHubReviewBackend(vcs, number) {
|
|
24805
24836
|
const pendingReview = async () => {
|
|
@@ -24861,6 +24892,9 @@ function GitHubReviewBackend(vcs, number) {
|
|
|
24861
24892
|
id: thread.id,
|
|
24862
24893
|
file: thread.path ?? undefined,
|
|
24863
24894
|
line: thread.line ?? undefined,
|
|
24895
|
+
rootCommentId: comment?.id,
|
|
24896
|
+
commentIds: nodes.flatMap((node) => [node.id, node.databaseId?.toString()].filter((id) => Boolean(id))),
|
|
24897
|
+
commentNodeIds: nodes.flatMap((node) => node.id ? [node.id] : []),
|
|
24864
24898
|
body,
|
|
24865
24899
|
author: comment?.author?.login,
|
|
24866
24900
|
resolved: thread.isResolved,
|
|
@@ -24870,6 +24904,7 @@ function GitHubReviewBackend(vcs, number) {
|
|
|
24870
24904
|
});
|
|
24871
24905
|
};
|
|
24872
24906
|
return {
|
|
24907
|
+
kind: "remote",
|
|
24873
24908
|
async stage(draft) {
|
|
24874
24909
|
const pending = await pendingReview();
|
|
24875
24910
|
if (!pending) {
|
|
@@ -24927,6 +24962,33 @@ function GitHubReviewBackend(vcs, number) {
|
|
|
24927
24962
|
await ghChecked(args);
|
|
24928
24963
|
}
|
|
24929
24964
|
},
|
|
24965
|
+
async readDraft() {
|
|
24966
|
+
const pending = await pendingReview();
|
|
24967
|
+
if (!pending)
|
|
24968
|
+
return { comments: [], body: "" };
|
|
24969
|
+
const review = await ghChecked(["api", `/repos/${vcs.owner}/${vcs.repo}/pulls/${number}/reviews/${pending.id}`]);
|
|
24970
|
+
const comments = await ghChecked([
|
|
24971
|
+
"api",
|
|
24972
|
+
`/repos/${vcs.owner}/${vcs.repo}/pulls/${number}/reviews/${pending.id}/comments`
|
|
24973
|
+
]);
|
|
24974
|
+
let reviewData;
|
|
24975
|
+
let commentData;
|
|
24976
|
+
try {
|
|
24977
|
+
reviewData = JSON.parse(review.stdout);
|
|
24978
|
+
commentData = JSON.parse(comments.stdout);
|
|
24979
|
+
} catch {
|
|
24980
|
+
throw new Error("Cannot parse the pending GitHub review as JSON.");
|
|
24981
|
+
}
|
|
24982
|
+
return {
|
|
24983
|
+
body: reviewData.body ?? "",
|
|
24984
|
+
comments: commentData.map((comment) => ({
|
|
24985
|
+
file: comment.path,
|
|
24986
|
+
line: comment.line ?? comment.original_line ?? undefined,
|
|
24987
|
+
side: comment.side ?? undefined,
|
|
24988
|
+
body: comment.body
|
|
24989
|
+
}))
|
|
24990
|
+
};
|
|
24991
|
+
},
|
|
24930
24992
|
listThreads,
|
|
24931
24993
|
async reply(input) {
|
|
24932
24994
|
const threads = await listThreads();
|
|
@@ -24943,14 +25005,32 @@ function GitHubReviewBackend(vcs, number) {
|
|
|
24943
25005
|
`body=${input.body}`
|
|
24944
25006
|
]);
|
|
24945
25007
|
}
|
|
24946
|
-
if (input.resolve)
|
|
25008
|
+
if (input.resolve)
|
|
25009
|
+
await this.setResolved?.(input.threadId, true);
|
|
25010
|
+
},
|
|
25011
|
+
async setResolved(threadId, resolved) {
|
|
25012
|
+
await ghChecked([
|
|
25013
|
+
"api",
|
|
25014
|
+
"graphql",
|
|
25015
|
+
"-f",
|
|
25016
|
+
`query=${resolved ? GITHUB_RESOLVE_MUTATION : GITHUB_UNRESOLVE_MUTATION}`,
|
|
25017
|
+
"-f",
|
|
25018
|
+
`threadId=${threadId}`
|
|
25019
|
+
]);
|
|
25020
|
+
},
|
|
25021
|
+
async deleteThread(threadId) {
|
|
25022
|
+
const thread = (await listThreads()).find((candidate) => candidate.id === threadId);
|
|
25023
|
+
const commentNodeIds = thread?.commentNodeIds ?? (thread?.rootCommentId ? [thread.rootCommentId] : []);
|
|
25024
|
+
if (!commentNodeIds.length)
|
|
25025
|
+
throw new Error(`Cannot delete GitHub thread ${threadId}: comment node IDs are missing.`);
|
|
25026
|
+
for (const commentId of commentNodeIds) {
|
|
24947
25027
|
await ghChecked([
|
|
24948
25028
|
"api",
|
|
24949
25029
|
"graphql",
|
|
24950
25030
|
"-f",
|
|
24951
|
-
`query=${
|
|
25031
|
+
`query=${GITHUB_DELETE_COMMENT_MUTATION}`,
|
|
24952
25032
|
"-f",
|
|
24953
|
-
`
|
|
25033
|
+
`commentId=${commentId}`
|
|
24954
25034
|
]);
|
|
24955
25035
|
}
|
|
24956
25036
|
},
|
|
@@ -24969,11 +25049,13 @@ function GitHubReviewBackend(vcs, number) {
|
|
|
24969
25049
|
function githubReviewSubmissionEndpoint(owner, repo, id, pendingReviewId) {
|
|
24970
25050
|
return pendingReviewId ? `/repos/${owner}/${repo}/pulls/${id}/reviews/${pendingReviewId}/events` : `/repos/${owner}/${repo}/pulls/${id}/reviews`;
|
|
24971
25051
|
}
|
|
24972
|
-
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}}}}}`;
|
|
25052
|
+
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}}}}}`;
|
|
24973
25053
|
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}}}`;
|
|
24974
25054
|
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}}}`;
|
|
24975
25055
|
var GITHUB_REPLY_MUTATION = `mutation($threadId:ID!,$body:String!){addPullRequestReviewThreadReply(input:{pullRequestReviewThreadId:$threadId,body:$body}){comment{id}}}`;
|
|
24976
25056
|
var GITHUB_RESOLVE_MUTATION = `mutation($threadId:ID!){resolveReviewThread(input:{threadId:$threadId}){thread{isResolved}}}`;
|
|
25057
|
+
var GITHUB_UNRESOLVE_MUTATION = `mutation($threadId:ID!){unresolveReviewThread(input:{threadId:$threadId}){thread{isResolved}}}`;
|
|
25058
|
+
var GITHUB_DELETE_COMMENT_MUTATION = `mutation($commentId:ID!){deletePullRequestReviewComment(input:{id:$commentId}){clientMutationId}}`;
|
|
24977
25059
|
|
|
24978
25060
|
// src/review/gitlab-review-backend.ts
|
|
24979
25061
|
function GitLabReviewBackend(vcs, number) {
|
|
@@ -25010,6 +25092,7 @@ function GitLabReviewBackend(vcs, number) {
|
|
|
25010
25092
|
});
|
|
25011
25093
|
};
|
|
25012
25094
|
return {
|
|
25095
|
+
kind: "remote",
|
|
25013
25096
|
async stage(draft) {
|
|
25014
25097
|
const endpoint = `${mergeRequestEndpoint()}/draft_notes`;
|
|
25015
25098
|
if (draft.body.trim()) {
|
|
@@ -25038,6 +25121,36 @@ function GitLabReviewBackend(vcs, number) {
|
|
|
25038
25121
|
});
|
|
25039
25122
|
}
|
|
25040
25123
|
},
|
|
25124
|
+
async readDraft() {
|
|
25125
|
+
const result = await glabChecked(["api", `${mergeRequestEndpoint()}/draft_notes`]);
|
|
25126
|
+
let notes;
|
|
25127
|
+
try {
|
|
25128
|
+
notes = JSON.parse(result.stdout);
|
|
25129
|
+
} catch {
|
|
25130
|
+
throw new Error("Cannot parse GitLab draft notes as JSON.");
|
|
25131
|
+
}
|
|
25132
|
+
const comments = [];
|
|
25133
|
+
const body = [];
|
|
25134
|
+
for (const note of notes) {
|
|
25135
|
+
if (!note.note)
|
|
25136
|
+
continue;
|
|
25137
|
+
const file = note.position?.new_path ?? note.position?.old_path;
|
|
25138
|
+
const line = note.position?.new_line ?? note.position?.old_line;
|
|
25139
|
+
if (file && line) {
|
|
25140
|
+
comments.push({
|
|
25141
|
+
file,
|
|
25142
|
+
line,
|
|
25143
|
+
side: note.position?.new_line ? "RIGHT" : "LEFT",
|
|
25144
|
+
body: note.note
|
|
25145
|
+
});
|
|
25146
|
+
} else {
|
|
25147
|
+
body.push(note.note);
|
|
25148
|
+
}
|
|
25149
|
+
}
|
|
25150
|
+
return { comments, body: body.join(`
|
|
25151
|
+
|
|
25152
|
+
`) };
|
|
25153
|
+
},
|
|
25041
25154
|
listThreads,
|
|
25042
25155
|
async reply(input) {
|
|
25043
25156
|
const endpoint = `${mergeRequestEndpoint()}/discussions/${encodeURIComponent(input.threadId)}`;
|
|
@@ -25049,7 +25162,15 @@ function GitLabReviewBackend(vcs, number) {
|
|
|
25049
25162
|
});
|
|
25050
25163
|
}
|
|
25051
25164
|
if (input.resolve)
|
|
25052
|
-
await
|
|
25165
|
+
await this.setResolved?.(input.threadId, true);
|
|
25166
|
+
},
|
|
25167
|
+
async setResolved(threadId, resolved) {
|
|
25168
|
+
const endpoint = `${mergeRequestEndpoint()}/discussions/${encodeURIComponent(threadId)}`;
|
|
25169
|
+
await glabChecked(["api", "--method", "PUT", `${endpoint}?resolved=${resolved}`]);
|
|
25170
|
+
},
|
|
25171
|
+
async deleteThread(threadId) {
|
|
25172
|
+
const endpoint = `${mergeRequestEndpoint()}/discussions/${encodeURIComponent(threadId)}`;
|
|
25173
|
+
await glabChecked(["api", "--method", "DELETE", endpoint]);
|
|
25053
25174
|
},
|
|
25054
25175
|
async publish(event) {
|
|
25055
25176
|
assertReviewEventSupported(vcs.provider, event);
|
|
@@ -25089,17 +25210,8 @@ function hasGitlabDraftNotes(input) {
|
|
|
25089
25210
|
}
|
|
25090
25211
|
}
|
|
25091
25212
|
|
|
25092
|
-
// src/review/review-backend.ts
|
|
25093
|
-
|
|
25094
|
-
if (vcs.provider === "github")
|
|
25095
|
-
return GitHubReviewBackend(vcs, number);
|
|
25096
|
-
if (vcs.provider === "gitlab")
|
|
25097
|
-
return GitLabReviewBackend(vcs, number);
|
|
25098
|
-
throw new Error("A remote review backend requires a GitHub or GitLab repository.");
|
|
25099
|
-
}
|
|
25100
|
-
// src/review/local-reviews.ts
|
|
25101
|
-
import { mkdir as mkdir7, readdir as readdir3, readFile as readFile12, unlink as unlink2, writeFile as writeFile5 } from "node:fs/promises";
|
|
25102
|
-
import { join as join20 } from "node:path";
|
|
25213
|
+
// src/review/local-review-backend.ts
|
|
25214
|
+
import { readFile as readFile12, writeFile as writeFile5 } from "node:fs/promises";
|
|
25103
25215
|
|
|
25104
25216
|
// src/extensions/tuicrx.ts
|
|
25105
25217
|
import { readFile as readFile11, realpath as realpath4 } from "node:fs/promises";
|
|
@@ -25170,6 +25282,11 @@ async function resolveReviewSession(cwd, target) {
|
|
|
25170
25282
|
}
|
|
25171
25283
|
return resolveSession(cwd, target.branch);
|
|
25172
25284
|
}
|
|
25285
|
+
async function resolvePublishSession(cwd, target) {
|
|
25286
|
+
if (target.owner && target.repo && target.number !== undefined)
|
|
25287
|
+
return resolvePrSession(cwd, target.owner, target.repo, target.number);
|
|
25288
|
+
return resolveSession(cwd, target.branch);
|
|
25289
|
+
}
|
|
25173
25290
|
async function resolvePrSession(cwd, owner, repo, number) {
|
|
25174
25291
|
const coordinate = `${owner}/${repo}`.toLowerCase();
|
|
25175
25292
|
const sessions = await listSessions(cwd);
|
|
@@ -25204,6 +25321,8 @@ async function readSession(path) {
|
|
|
25204
25321
|
function toLocalReviewThreads(session) {
|
|
25205
25322
|
const threads = [];
|
|
25206
25323
|
const add = (comment, location = {}) => {
|
|
25324
|
+
if (isLocalResponse(comment.content))
|
|
25325
|
+
return;
|
|
25207
25326
|
const author = commentAuthor(comment);
|
|
25208
25327
|
threads.push({
|
|
25209
25328
|
id: comment.id ?? `local-${threads.length + 1}`,
|
|
@@ -25229,6 +25348,54 @@ function toLocalReviewThreads(session) {
|
|
|
25229
25348
|
}
|
|
25230
25349
|
return threads;
|
|
25231
25350
|
}
|
|
25351
|
+
function toFindings(session, options = {}) {
|
|
25352
|
+
const comments = [];
|
|
25353
|
+
const include = (comment) => (!options.agentOnly || commentAuthor(comment)?.startsWith("Agent: ")) && (!options.excludeLocalResponses || !isLocalResponse(comment.content));
|
|
25354
|
+
const publishableBody = (comment) => {
|
|
25355
|
+
const author = commentAuthor(comment);
|
|
25356
|
+
const route = author?.match(/^Agent:\s*(.+)$/)?.[1];
|
|
25357
|
+
return route ? withRemoteProvenance(comment.content, route) : comment.content;
|
|
25358
|
+
};
|
|
25359
|
+
const bodyParts = (session.review_comments ?? []).flatMap((comment) => include(comment) ? [publishableBody(comment)] : []);
|
|
25360
|
+
for (const [file, entry] of Object.entries(session.files ?? {})) {
|
|
25361
|
+
for (const fileComment of entry.file_comments ?? []) {
|
|
25362
|
+
if (!include(fileComment))
|
|
25363
|
+
continue;
|
|
25364
|
+
const comment = {
|
|
25365
|
+
file,
|
|
25366
|
+
body: fileComment.content,
|
|
25367
|
+
...fileComment.id ? { sourceCommentId: String(fileComment.id) } : {}
|
|
25368
|
+
};
|
|
25369
|
+
const author = commentAuthor(fileComment);
|
|
25370
|
+
if (author)
|
|
25371
|
+
comment.author = author;
|
|
25372
|
+
comments.push(comment);
|
|
25373
|
+
}
|
|
25374
|
+
for (const [lineKey, lineComments] of Object.entries(entry.line_comments ?? {})) {
|
|
25375
|
+
const line = Number.parseInt(lineKey, 10);
|
|
25376
|
+
if (!Number.isFinite(line))
|
|
25377
|
+
continue;
|
|
25378
|
+
for (const lineComment of lineComments) {
|
|
25379
|
+
if (!include(lineComment))
|
|
25380
|
+
continue;
|
|
25381
|
+
const comment = {
|
|
25382
|
+
file,
|
|
25383
|
+
line,
|
|
25384
|
+
side: lineComment.side === "old" ? "LEFT" : "RIGHT",
|
|
25385
|
+
body: lineComment.content,
|
|
25386
|
+
...lineComment.id ? { sourceCommentId: String(lineComment.id) } : {}
|
|
25387
|
+
};
|
|
25388
|
+
const author = commentAuthor(lineComment);
|
|
25389
|
+
if (author)
|
|
25390
|
+
comment.author = author;
|
|
25391
|
+
comments.push(comment);
|
|
25392
|
+
}
|
|
25393
|
+
}
|
|
25394
|
+
}
|
|
25395
|
+
return { comments, body: bodyParts.join(`
|
|
25396
|
+
|
|
25397
|
+
`) };
|
|
25398
|
+
}
|
|
25232
25399
|
async function requireTuicr() {
|
|
25233
25400
|
if (!await tuicrAvailable())
|
|
25234
25401
|
throw new Error("tuicr is not installed or is not available on PATH.");
|
|
@@ -25244,83 +25411,167 @@ async function canonicalPath3(path) {
|
|
|
25244
25411
|
}
|
|
25245
25412
|
}
|
|
25246
25413
|
|
|
25247
|
-
// src/review/local-
|
|
25248
|
-
|
|
25249
|
-
|
|
25250
|
-
|
|
25251
|
-
|
|
25252
|
-
|
|
25253
|
-
|
|
25254
|
-
|
|
25255
|
-
|
|
25256
|
-
|
|
25257
|
-
|
|
25258
|
-
|
|
25259
|
-
|
|
25260
|
-
|
|
25261
|
-
|
|
25262
|
-
|
|
25263
|
-
|
|
25264
|
-
|
|
25414
|
+
// src/review/local-review-backend.ts
|
|
25415
|
+
function LocalReviewBackend(options) {
|
|
25416
|
+
return {
|
|
25417
|
+
kind: "local",
|
|
25418
|
+
async stage(draft) {
|
|
25419
|
+
for (const comment of draft.comments) {
|
|
25420
|
+
await addComment(options.session, comment.body, {
|
|
25421
|
+
targetFile: comment.file,
|
|
25422
|
+
line: comment.line,
|
|
25423
|
+
side: comment.side === "LEFT" ? "old" : "new",
|
|
25424
|
+
username: options.author
|
|
25425
|
+
});
|
|
25426
|
+
}
|
|
25427
|
+
if (draft.body.trim())
|
|
25428
|
+
await addComment(options.session, draft.body, { username: options.author });
|
|
25429
|
+
},
|
|
25430
|
+
async readDraft() {
|
|
25431
|
+
return toFindings(await readSession(options.session), { excludeLocalResponses: true });
|
|
25432
|
+
},
|
|
25433
|
+
async listThreads() {
|
|
25434
|
+
try {
|
|
25435
|
+
return parseThreadArtifact(await readFile12(options.artifactPath, "utf8"));
|
|
25436
|
+
} catch (error) {
|
|
25437
|
+
if (error.code === "ENOENT")
|
|
25438
|
+
return [];
|
|
25439
|
+
throw error;
|
|
25440
|
+
}
|
|
25441
|
+
},
|
|
25442
|
+
async reply(input) {
|
|
25443
|
+
const content = await readFile12(options.artifactPath, "utf8");
|
|
25444
|
+
await writeFile5(options.artifactPath, upsertThreadReply(content, input.threadId, input.body, input.question, input.resolve), "utf8");
|
|
25445
|
+
},
|
|
25446
|
+
async publish() {
|
|
25447
|
+
throw new Error("Promote a local draft through a remote review backend before publishing it.");
|
|
25448
|
+
}
|
|
25265
25449
|
};
|
|
25266
|
-
const path = join20(dir, `${revision}.json`);
|
|
25267
|
-
await writeFile5(path, `${JSON.stringify(dump, null, 2)}
|
|
25268
|
-
`, { encoding: "utf8", flag: "wx" });
|
|
25269
|
-
await unlink2(input.session.path);
|
|
25270
|
-
return { path, dump };
|
|
25271
25450
|
}
|
|
25272
|
-
|
|
25273
|
-
|
|
25451
|
+
|
|
25452
|
+
// src/review/review-backend.ts
|
|
25453
|
+
function createRemoteReviewBackend(vcs, number) {
|
|
25454
|
+
if (vcs.provider === "github")
|
|
25455
|
+
return GitHubReviewBackend(vcs, number);
|
|
25456
|
+
if (vcs.provider === "gitlab")
|
|
25457
|
+
return GitLabReviewBackend(vcs, number);
|
|
25458
|
+
throw new Error("A remote review backend requires a GitHub or GitLab repository.");
|
|
25459
|
+
}
|
|
25460
|
+
function createLocalReviewBackend(options) {
|
|
25461
|
+
return LocalReviewBackend(options);
|
|
25462
|
+
}
|
|
25463
|
+
// src/review/review-state.ts
|
|
25464
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
25465
|
+
import { readFile as readFile13, rename as rename2, writeFile as writeFile6 } from "node:fs/promises";
|
|
25466
|
+
import { join as join20 } from "node:path";
|
|
25467
|
+
import { z as z12 } from "zod";
|
|
25468
|
+
var reviewPublicationStateSchema = z12.object({
|
|
25469
|
+
target: z12.string().optional(),
|
|
25470
|
+
bodies: z12.array(z12.string()).default([]),
|
|
25471
|
+
comments: z12.array(z12.string()).default([]),
|
|
25472
|
+
stagedComments: z12.array(z12.string()).default([]),
|
|
25473
|
+
replies: z12.array(z12.string()).default([]),
|
|
25474
|
+
overlayPath: z12.string().optional()
|
|
25475
|
+
});
|
|
25476
|
+
function reviewBodyFingerprint(body) {
|
|
25477
|
+
return digest(body);
|
|
25478
|
+
}
|
|
25479
|
+
function reviewCommentFingerprint(comment) {
|
|
25480
|
+
return digest([comment.file, String(comment.line), comment.side ?? "RIGHT", comment.body].join("\x00"));
|
|
25481
|
+
}
|
|
25482
|
+
function reviewReplyFingerprint(threadId, body) {
|
|
25483
|
+
return digest(`${threadId}\x00${body}`);
|
|
25274
25484
|
}
|
|
25275
|
-
|
|
25485
|
+
function unpublishedReviewComments(comments, knownFingerprints) {
|
|
25486
|
+
return comments.filter((comment) => !knownFingerprints.has(reviewCommentFingerprint(comment)));
|
|
25487
|
+
}
|
|
25488
|
+
async function loadReviewPublicationState(cwd, vcs, number, homeDir) {
|
|
25489
|
+
const target = `${vcs.provider}:${vcs.owner}/${vcs.repo}#${number}`;
|
|
25490
|
+
const path = join20(await sessionsDir(cwd, homeDir), `review-publish-${digest(target).slice(0, 16)}.json`);
|
|
25276
25491
|
try {
|
|
25277
|
-
|
|
25278
|
-
|
|
25279
|
-
return match ? Math.max(latest, Number(match[1])) : latest;
|
|
25280
|
-
}, 0);
|
|
25492
|
+
const parsed = reviewPublicationStateSchema.parse(JSON.parse(await readFile13(path, "utf8")));
|
|
25493
|
+
return { path, state: { ...parsed, target } };
|
|
25281
25494
|
} catch (error) {
|
|
25282
|
-
if (error.code === "ENOENT")
|
|
25283
|
-
return
|
|
25495
|
+
if (error.code === "ENOENT") {
|
|
25496
|
+
return { path, state: { target, bodies: [], comments: [], stagedComments: [], replies: [] } };
|
|
25497
|
+
}
|
|
25498
|
+
if (error instanceof SyntaxError || error instanceof z12.ZodError) {
|
|
25499
|
+
throw new Error(`Cannot parse review publication state: ${path}`);
|
|
25500
|
+
}
|
|
25284
25501
|
throw error;
|
|
25285
25502
|
}
|
|
25286
25503
|
}
|
|
25504
|
+
async function saveReviewPublicationState(path, state) {
|
|
25505
|
+
const temp = `${path}.${process.pid}.tmp`;
|
|
25506
|
+
await writeFile6(temp, `${JSON.stringify(state, null, 2)}
|
|
25507
|
+
`, "utf8");
|
|
25508
|
+
await rename2(temp, path);
|
|
25509
|
+
}
|
|
25510
|
+
function digest(value) {
|
|
25511
|
+
return createHash2("sha256").update(value).digest("hex");
|
|
25512
|
+
}
|
|
25287
25513
|
// src/tools/review.ts
|
|
25288
|
-
var contextSchema =
|
|
25289
|
-
cwd:
|
|
25290
|
-
target:
|
|
25291
|
-
workingTree:
|
|
25514
|
+
var contextSchema = z13.object({
|
|
25515
|
+
cwd: z13.string().optional(),
|
|
25516
|
+
target: z13.string().optional(),
|
|
25517
|
+
workingTree: z13.boolean().optional()
|
|
25292
25518
|
});
|
|
25293
|
-
var localSchema = contextSchema.extend({ local:
|
|
25519
|
+
var localSchema = contextSchema.extend({ local: z13.boolean().optional() });
|
|
25294
25520
|
var openSchema = localSchema.extend({
|
|
25295
|
-
title:
|
|
25296
|
-
intent:
|
|
25297
|
-
issueUrl:
|
|
25298
|
-
base:
|
|
25521
|
+
title: z13.string().optional(),
|
|
25522
|
+
intent: z13.string().optional(),
|
|
25523
|
+
issueUrl: z13.string().url().optional(),
|
|
25524
|
+
base: z13.string().optional()
|
|
25299
25525
|
});
|
|
25300
25526
|
var submitSchema = localSchema.extend({
|
|
25301
25527
|
findings: findingsSchema,
|
|
25302
|
-
overallIssues:
|
|
25303
|
-
notVerified:
|
|
25304
|
-
title:
|
|
25528
|
+
overallIssues: z13.array(z13.string()).optional(),
|
|
25529
|
+
notVerified: z13.array(z13.string()).optional(),
|
|
25530
|
+
title: z13.string().optional()
|
|
25305
25531
|
});
|
|
25306
25532
|
var addCommentSchema = localSchema.extend({
|
|
25307
|
-
body:
|
|
25308
|
-
file:
|
|
25309
|
-
line:
|
|
25310
|
-
side:
|
|
25533
|
+
body: z13.string().min(1),
|
|
25534
|
+
file: z13.string().min(1),
|
|
25535
|
+
line: z13.number().int().positive(),
|
|
25536
|
+
side: z13.enum(["LEFT", "RIGHT"]).optional()
|
|
25311
25537
|
});
|
|
25312
25538
|
var respondSchema = localSchema.extend({
|
|
25313
|
-
threadId:
|
|
25314
|
-
body:
|
|
25315
|
-
question:
|
|
25316
|
-
resolve:
|
|
25539
|
+
threadId: z13.string().min(1),
|
|
25540
|
+
body: z13.string().min(1),
|
|
25541
|
+
question: z13.boolean().optional(),
|
|
25542
|
+
resolve: z13.boolean().optional()
|
|
25317
25543
|
});
|
|
25318
25544
|
var publishSchema = localSchema.extend({
|
|
25319
|
-
status:
|
|
25545
|
+
status: z13.enum(["COMMENT", "APPROVE", "REQUEST_CHANGES", "CLOSE"]).optional()
|
|
25320
25546
|
});
|
|
25321
25547
|
var completeSchema = localSchema.extend({
|
|
25322
|
-
action:
|
|
25548
|
+
action: z13.enum(["approve", "reject", "abandon"]).optional()
|
|
25323
25549
|
});
|
|
25550
|
+
function hasReviewDraft(comments, body) {
|
|
25551
|
+
return comments.length > 0 || body.trim().length > 0;
|
|
25552
|
+
}
|
|
25553
|
+
function partitionReviewComments(comments, knownFingerprints, model) {
|
|
25554
|
+
const normalized = comments.map((comment) => {
|
|
25555
|
+
const published = { ...comment, body: withCommentProvenance(comment, model) };
|
|
25556
|
+
return { source: comment, published, fingerprint: reviewCommentFingerprint(published) };
|
|
25557
|
+
});
|
|
25558
|
+
return {
|
|
25559
|
+
checkpointed: normalized.filter((comment) => knownFingerprints.has(comment.fingerprint)),
|
|
25560
|
+
candidates: normalized.filter((comment) => !knownFingerprints.has(comment.fingerprint))
|
|
25561
|
+
};
|
|
25562
|
+
}
|
|
25563
|
+
function resolveReviewThread(comment, threads) {
|
|
25564
|
+
const sourceCommentId = comment.sourceCommentId;
|
|
25565
|
+
if (sourceCommentId) {
|
|
25566
|
+
const identified = threads.find((thread) => thread.id === sourceCommentId || thread.rootCommentId === sourceCommentId || thread.commentIds?.includes(sourceCommentId));
|
|
25567
|
+
if (identified)
|
|
25568
|
+
return identified;
|
|
25569
|
+
}
|
|
25570
|
+
const matches = threads.filter((thread) => thread.file === comment.file && (thread.line ?? undefined) === (comment.line ?? undefined));
|
|
25571
|
+
if (matches.length > 1)
|
|
25572
|
+
throw new Error(`Ambiguous remote review threads at ${comment.file}:${comment.line ?? "file"}: ${matches.map((thread) => thread.id).join(", ")}.`);
|
|
25573
|
+
return matches[0];
|
|
25574
|
+
}
|
|
25324
25575
|
async function workingTreeDiff(cwd) {
|
|
25325
25576
|
const tracked = await run2("git", ["-C", cwd, "diff", "HEAD"], { capture: "unbounded" });
|
|
25326
25577
|
if (tracked.code !== 0)
|
|
@@ -25516,7 +25767,7 @@ ${openMessage}`, {
|
|
|
25516
25767
|
const gates = await runMiseGates(review.cwd);
|
|
25517
25768
|
const baseRef = review.pr?.baseRef ?? (review.forge ? await review.forge.defaultBranch() : "local");
|
|
25518
25769
|
const artifact = await newReviewArtifactPath(review.cwd, params.local || params.workingTree || !review.pr ? "uncommitted" : await reviewTargetId(review));
|
|
25519
|
-
await
|
|
25770
|
+
await writeFile7(artifact, renderReviewDoc({
|
|
25520
25771
|
title: params.title ?? review.pr?.title ?? review.vcs.branch,
|
|
25521
25772
|
number: useLocalBackend ? undefined : review.pr?.number,
|
|
25522
25773
|
url: useLocalBackend ? undefined : review.pr?.url,
|
|
@@ -25539,21 +25790,17 @@ ${openMessage}`, {
|
|
|
25539
25790
|
count: findings.length
|
|
25540
25791
|
});
|
|
25541
25792
|
}
|
|
25542
|
-
|
|
25543
|
-
|
|
25544
|
-
|
|
25545
|
-
|
|
25546
|
-
|
|
25547
|
-
|
|
25548
|
-
});
|
|
25549
|
-
}
|
|
25550
|
-
if (body.trim())
|
|
25551
|
-
await addComment(session.path, body, { username: localReviewAuthor(model) });
|
|
25793
|
+
const backend = createLocalReviewBackend({
|
|
25794
|
+
session: session.path,
|
|
25795
|
+
artifactPath: artifact,
|
|
25796
|
+
author: localReviewAuthor(model)
|
|
25797
|
+
});
|
|
25798
|
+
await backend.stage({ comments, body });
|
|
25552
25799
|
return result3(`Local review staged in tuicr: ${artifact}`, { artifact, count: findings.length, session });
|
|
25553
25800
|
}
|
|
25554
25801
|
if (!review.pr)
|
|
25555
25802
|
return result3(`Review written: ${artifact}. No PR/MR matches this remote target.`, { artifact });
|
|
25556
|
-
const hasDraft = comments
|
|
25803
|
+
const hasDraft = hasReviewDraft(comments, body);
|
|
25557
25804
|
if (hasDraft) {
|
|
25558
25805
|
await createRemoteReviewBackend(review.vcs, review.pr.number).stage({ comments, body });
|
|
25559
25806
|
}
|
|
@@ -25589,12 +25836,12 @@ ${openMessage}`, {
|
|
|
25589
25836
|
const session = await resolveTuicrSession(review, Boolean(params.local || params.workingTree));
|
|
25590
25837
|
if (!session)
|
|
25591
25838
|
return result3("No matching tuicr session. Open review_new or review_launch_ui first.");
|
|
25592
|
-
|
|
25593
|
-
|
|
25594
|
-
|
|
25595
|
-
|
|
25596
|
-
username: localReviewAuthor(model)
|
|
25839
|
+
const backend = createLocalReviewBackend({
|
|
25840
|
+
session: session.path,
|
|
25841
|
+
artifactPath: "",
|
|
25842
|
+
author: localReviewAuthor(model)
|
|
25597
25843
|
});
|
|
25844
|
+
await backend.stage({ comments: [comment], body: "" });
|
|
25598
25845
|
return result3(`Comment added to tuicr session ${session.slug}.`, { session, comment });
|
|
25599
25846
|
}
|
|
25600
25847
|
if (!review.pr)
|
|
@@ -25606,70 +25853,88 @@ ${openMessage}`, {
|
|
|
25606
25853
|
defineTool6({
|
|
25607
25854
|
name: "review_comments",
|
|
25608
25855
|
label: "review comments",
|
|
25609
|
-
description: "
|
|
25610
|
-
promptSnippet: "Call review_comments before addressing
|
|
25611
|
-
promptGuidelines: ["
|
|
25856
|
+
description: "Pull review threads or local tuicr comments and write a target-named artifact.",
|
|
25857
|
+
promptSnippet: "Call review_comments before addressing findings",
|
|
25858
|
+
promptGuidelines: ["Pass local=true to prepare the local reply overlay."],
|
|
25612
25859
|
parameters: parameters6(localSchema),
|
|
25613
|
-
executionMode: "
|
|
25860
|
+
executionMode: "sequential",
|
|
25614
25861
|
async execute(_id, input) {
|
|
25615
25862
|
const params = localSchema.parse(input);
|
|
25616
|
-
if (params.local || params.workingTree)
|
|
25617
|
-
return result3("Use review_dump for a local tuicr review.");
|
|
25618
25863
|
const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
|
|
25619
|
-
|
|
25864
|
+
let threads;
|
|
25865
|
+
let artifact;
|
|
25866
|
+
if (params.local || params.workingTree) {
|
|
25867
|
+
const session = await resolveTuicrSession(review, true);
|
|
25868
|
+
if (!session)
|
|
25869
|
+
return result3("No matching tuicr session found.");
|
|
25870
|
+
artifact = await localThreadArtifactPath(review.cwd, session.slug);
|
|
25871
|
+
threads = await syncLocalThreadArtifact(artifact, review.pr?.title ?? review.vcs.branch, session.slug, toLocalReviewThreads(await readSession(session.path)));
|
|
25872
|
+
} else if (review.pr && review.forge) {
|
|
25873
|
+
threads = await createRemoteReviewBackend(review.vcs, review.pr.number).listThreads();
|
|
25874
|
+
const target = await reviewTargetId(review);
|
|
25875
|
+
artifact = await newReviewArtifactPath(review.cwd, target);
|
|
25876
|
+
await writeFile7(artifact, renderThreadArtifact(review.pr.title, target, threads, { number: review.pr.number, url: review.pr.url }), "utf8");
|
|
25877
|
+
} else {
|
|
25620
25878
|
return result3(review.forge ? "No PR/MR matches this remote review target." : unsupportedForgeMessage());
|
|
25621
|
-
|
|
25879
|
+
}
|
|
25622
25880
|
return result3(threads.map((thread) => `${thread.id} ${thread.file ?? "review"}:${thread.line ?? "-"} — ${thread.body}`).join(`
|
|
25623
|
-
`) || "No comments.", {
|
|
25624
|
-
}
|
|
25625
|
-
}),
|
|
25626
|
-
defineTool6({
|
|
25627
|
-
name: "review_dump",
|
|
25628
|
-
label: "review dump",
|
|
25629
|
-
description: "Save one immutable local tuicr review revision and remove its completed session.",
|
|
25630
|
-
promptSnippet: "Call review_dump before applying local review feedback",
|
|
25631
|
-
promptGuidelines: ["Each dump advances the local review revision."],
|
|
25632
|
-
parameters: parameters6(localSchema),
|
|
25633
|
-
executionMode: "sequential",
|
|
25634
|
-
async execute(_id, input) {
|
|
25635
|
-
const params = localSchema.parse(input);
|
|
25636
|
-
const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
|
|
25637
|
-
const session = await resolveTuicrSession(review, true);
|
|
25638
|
-
if (!session)
|
|
25639
|
-
return result3("No matching local tuicr session found.");
|
|
25640
|
-
const base = review.pr?.baseRef ?? (review.forge ? await review.forge.defaultBranch() : "HEAD");
|
|
25641
|
-
const captured = await captureLocalReview({
|
|
25642
|
-
cwd: review.cwd,
|
|
25643
|
-
branch: review.vcs.branch,
|
|
25644
|
-
base,
|
|
25645
|
-
diff: await workingTreeDiff(review.cwd),
|
|
25646
|
-
session
|
|
25647
|
-
});
|
|
25648
|
-
return result3(captured.dump.comments.map((thread) => `${thread.id} ${thread.file ?? "review"}:${thread.line ?? "-"} — ${thread.body}`).join(`
|
|
25649
|
-
`) || "No comments.", { artifact: captured.path, review: captured.dump, threads: captured.dump.comments });
|
|
25881
|
+
`) || "No comments.", { artifact, threads });
|
|
25650
25882
|
}
|
|
25651
25883
|
}),
|
|
25652
25884
|
defineTool6({
|
|
25653
25885
|
name: "review_respond",
|
|
25654
25886
|
label: "review respond",
|
|
25655
|
-
description: "
|
|
25656
|
-
promptSnippet: "Call review_respond after addressing a
|
|
25657
|
-
promptGuidelines: ["
|
|
25887
|
+
description: "Post local question replies to tuicr and record their publish overlay, or reply to remote threads.",
|
|
25888
|
+
promptSnippet: "Call review_respond after addressing a comment",
|
|
25889
|
+
promptGuidelines: ["Question replies remain unresolved."],
|
|
25658
25890
|
parameters: parameters6(respondSchema),
|
|
25659
25891
|
executionMode: "sequential",
|
|
25660
25892
|
async execute(_id, input, _signal, _onUpdate, ctx) {
|
|
25661
25893
|
const params = respondSchema.parse(input);
|
|
25662
|
-
if (params.local)
|
|
25663
|
-
return result3("Local reviews use immutable revision dumps, not thread replies.");
|
|
25664
25894
|
const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
|
|
25895
|
+
const model = modelRoute(ctx);
|
|
25896
|
+
if (params.local) {
|
|
25897
|
+
const tuicrSession = await resolveTuicrSession(review, true);
|
|
25898
|
+
if (!tuicrSession)
|
|
25899
|
+
return result3("No matching tuicr session. Open review_new or review_launch_ui first.");
|
|
25900
|
+
const artifact = await localThreadArtifactPath(review.cwd, tuicrSession.slug);
|
|
25901
|
+
if (!existsSync3(artifact))
|
|
25902
|
+
return result3("No local review artifact. Run review_comments with local=true first.");
|
|
25903
|
+
const threads = await readThreadArtifact(artifact);
|
|
25904
|
+
const known = threads.find((thread) => thread.id === params.threadId);
|
|
25905
|
+
const question = known?.question === true || params.question === true || !known && params.question === undefined;
|
|
25906
|
+
const awaitUserDeletion = !question && params.resolve === true;
|
|
25907
|
+
await addComment(tuicrSession.path, `${localResponseMarker(params.threadId)}
|
|
25908
|
+
${params.body}`, {
|
|
25909
|
+
targetFile: known?.file,
|
|
25910
|
+
line: known?.line,
|
|
25911
|
+
side: "new",
|
|
25912
|
+
username: localReviewAuthor(model)
|
|
25913
|
+
});
|
|
25914
|
+
await createLocalReviewBackend({
|
|
25915
|
+
session: tuicrSession.path,
|
|
25916
|
+
artifactPath: artifact,
|
|
25917
|
+
author: localReviewAuthor(model)
|
|
25918
|
+
}).reply({
|
|
25919
|
+
threadId: params.threadId,
|
|
25920
|
+
body: params.body,
|
|
25921
|
+
resolve: false,
|
|
25922
|
+
question
|
|
25923
|
+
});
|
|
25924
|
+
return result3(`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 });
|
|
25925
|
+
}
|
|
25665
25926
|
if (!review.pr || !review.forge)
|
|
25666
25927
|
return result3("No remote PR/MR for this reply.");
|
|
25667
|
-
const
|
|
25668
|
-
await
|
|
25928
|
+
const remote = createRemoteReviewBackend(review.vcs, review.pr.number);
|
|
25929
|
+
const remoteThreads = await remote.listThreads();
|
|
25930
|
+
const known = remoteThreads.find((thread) => thread.id === params.threadId);
|
|
25931
|
+
const question = known?.question === true || params.question === true || !known && params.question === undefined;
|
|
25932
|
+
const resolve = question ? false : params.resolve ?? true;
|
|
25933
|
+
await remote.reply({
|
|
25669
25934
|
threadId: params.threadId,
|
|
25670
|
-
body: withRemoteProvenance(params.body,
|
|
25935
|
+
body: withRemoteProvenance(params.body, model),
|
|
25671
25936
|
resolve,
|
|
25672
|
-
question
|
|
25937
|
+
question
|
|
25673
25938
|
});
|
|
25674
25939
|
return result3(`Replied to ${params.threadId}${resolve ? " and resolved it" : " and left it open"}.`, {
|
|
25675
25940
|
pr: review.pr,
|
|
@@ -25680,40 +25945,51 @@ ${openMessage}`, {
|
|
|
25680
25945
|
defineTool6({
|
|
25681
25946
|
name: "review_publish",
|
|
25682
25947
|
label: "review publish",
|
|
25683
|
-
description: "Publish pending remote review comments and status.",
|
|
25948
|
+
description: "Publish pending remote draft PR/MR review comments and status, or promote a local tuicr draft before publishing.",
|
|
25684
25949
|
promptSnippet: "Call review_publish to make remote review work public",
|
|
25685
|
-
promptGuidelines: ["
|
|
25950
|
+
promptGuidelines: ["Statuses are COMMENT, APPROVE, REQUEST_CHANGES, or CLOSE."],
|
|
25686
25951
|
parameters: parameters6(publishSchema),
|
|
25687
25952
|
executionMode: "sequential",
|
|
25688
|
-
async execute(_id, input) {
|
|
25953
|
+
async execute(_id, input, _signal, _onUpdate, ctx) {
|
|
25689
25954
|
const params = publishSchema.parse(input);
|
|
25690
|
-
if (params.local)
|
|
25691
|
-
return result3("Local reviews use immutable revision dumps and have no publish step.");
|
|
25692
25955
|
const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
|
|
25693
25956
|
if (!review.pr || !review.forge)
|
|
25694
25957
|
return result3("No remote PR/MR to publish.");
|
|
25695
|
-
return publishResolvedReview(review, params);
|
|
25958
|
+
return publishResolvedReview(review, params, modelRoute(ctx));
|
|
25696
25959
|
}
|
|
25697
25960
|
}),
|
|
25698
25961
|
defineTool6({
|
|
25699
25962
|
name: "review_complete",
|
|
25700
25963
|
label: "review complete",
|
|
25701
|
-
description: "Approve, reject, or abandon a remote review.",
|
|
25702
|
-
promptSnippet: "Call review_complete to finish a
|
|
25703
|
-
promptGuidelines: ["
|
|
25964
|
+
description: "Approve, reject, or abandon a remote review, or archive a local review artifact.",
|
|
25965
|
+
promptSnippet: "Call review_complete to finish a review without merging it.",
|
|
25966
|
+
promptGuidelines: ["A local completion archives the Diffpi artifact and deletes the matching tuicr session."],
|
|
25704
25967
|
parameters: parameters6(completeSchema),
|
|
25705
25968
|
executionMode: "sequential",
|
|
25706
|
-
async execute(_id, input) {
|
|
25969
|
+
async execute(_id, input, _signal, _onUpdate, ctx) {
|
|
25707
25970
|
const params = completeSchema.parse(input);
|
|
25708
|
-
if (params.local)
|
|
25709
|
-
return result3("Use review_dump to finish a local review revision.");
|
|
25710
25971
|
const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
|
|
25972
|
+
if (params.local) {
|
|
25973
|
+
const session = await resolveTuicrSession(review, true);
|
|
25974
|
+
if (!session)
|
|
25975
|
+
return result3("No matching local tuicr session to close.");
|
|
25976
|
+
const artifact = await localThreadArtifactPath(review.cwd, session.slug);
|
|
25977
|
+
if (!existsSync3(artifact))
|
|
25978
|
+
return result3("No local review artifact. Run review_comments with local=true first.");
|
|
25979
|
+
const archived = await uniqueRecordPath(await completedReviewsDir(review.cwd), reviewSlug(session.slug));
|
|
25980
|
+
await rename3(artifact, archived);
|
|
25981
|
+
await unlink2(session.path);
|
|
25982
|
+
return result3(`Archived ${archived} and deleted the tuicr session ${session.slug}.`, {
|
|
25983
|
+
artifact: archived,
|
|
25984
|
+
session
|
|
25985
|
+
});
|
|
25986
|
+
}
|
|
25711
25987
|
if (!review.pr || !review.forge)
|
|
25712
25988
|
return result3("No remote PR/MR to complete.");
|
|
25713
25989
|
if (!params.action)
|
|
25714
25990
|
return result3("Choose a complete action: approve, reject, or abandon.");
|
|
25715
25991
|
const status = { approve: "APPROVE", reject: "REQUEST_CHANGES", abandon: "CLOSE" }[params.action];
|
|
25716
|
-
return publishResolvedReview(review, { ...params, status });
|
|
25992
|
+
return publishResolvedReview(review, { ...params, status }, modelRoute(ctx));
|
|
25717
25993
|
}
|
|
25718
25994
|
}),
|
|
25719
25995
|
defineTool6({
|
|
@@ -25722,10 +25998,10 @@ ${openMessage}`, {
|
|
|
25722
25998
|
description: "Squash-merge an open, ready GitHub PR after checking its conventional subject.",
|
|
25723
25999
|
promptSnippet: "Call review_merge after confirming the PR is open, ready, and CI is settled; approval is optional",
|
|
25724
26000
|
promptGuidelines: ["This is intentionally GitHub-only until GitLab merge support is added."],
|
|
25725
|
-
parameters: parameters6(contextSchema.extend({ subject:
|
|
26001
|
+
parameters: parameters6(contextSchema.extend({ subject: z13.string().optional() })),
|
|
25726
26002
|
executionMode: "sequential",
|
|
25727
26003
|
async execute(_id, input) {
|
|
25728
|
-
const params = contextSchema.extend({ subject:
|
|
26004
|
+
const params = contextSchema.extend({ subject: z13.string().optional() }).parse(input);
|
|
25729
26005
|
const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
|
|
25730
26006
|
if (review.vcs.provider !== "github" || !review.forge)
|
|
25731
26007
|
return result3("review_merge currently supports GitHub only.");
|
|
@@ -25758,8 +26034,32 @@ ${openMessage}`, {
|
|
|
25758
26034
|
})
|
|
25759
26035
|
];
|
|
25760
26036
|
}
|
|
26037
|
+
async function syncLocalThreadArtifact(path, title, sessionSlug, threads) {
|
|
26038
|
+
let previous = [];
|
|
26039
|
+
try {
|
|
26040
|
+
previous = parseThreadArtifact(await readFile14(path, "utf8"));
|
|
26041
|
+
} catch (error) {
|
|
26042
|
+
if (error.code !== "ENOENT")
|
|
26043
|
+
throw error;
|
|
26044
|
+
}
|
|
26045
|
+
const previousById = new Map(previous.map((thread) => [thread.id, thread]));
|
|
26046
|
+
const currentIds = new Set(threads.map((thread) => thread.id));
|
|
26047
|
+
const current = threads.map((thread) => {
|
|
26048
|
+
const existing = previousById.get(thread.id);
|
|
26049
|
+
return {
|
|
26050
|
+
...thread,
|
|
26051
|
+
resolved: false,
|
|
26052
|
+
addressed: existing?.addressed ?? Boolean(existing?.reply),
|
|
26053
|
+
reply: existing?.reply
|
|
26054
|
+
};
|
|
26055
|
+
});
|
|
26056
|
+
const removed = previous.filter((thread) => !currentIds.has(thread.id)).map((thread) => ({ ...thread, resolved: true }));
|
|
26057
|
+
const merged = [...current, ...removed];
|
|
26058
|
+
await writeFile7(path, renderThreadArtifact(title, sessionSlug, merged), "utf8");
|
|
26059
|
+
return merged;
|
|
26060
|
+
}
|
|
25761
26061
|
function parameters6(schema) {
|
|
25762
|
-
return
|
|
26062
|
+
return z13.toJSONSchema(schema, { io: "input" });
|
|
25763
26063
|
}
|
|
25764
26064
|
function resolveWorkingDirectory(params) {
|
|
25765
26065
|
return params.cwd ?? process.cwd();
|
|
@@ -25775,17 +26075,159 @@ function modelRoute(ctx) {
|
|
|
25775
26075
|
throw new Error("Cannot record review provenance because Pi did not provide an active model route.");
|
|
25776
26076
|
return `${ctx.model.provider}/${ctx.model.id}`;
|
|
25777
26077
|
}
|
|
25778
|
-
async function publishResolvedReview(review, params) {
|
|
26078
|
+
async function publishResolvedReview(review, params, model) {
|
|
25779
26079
|
const status = params.status ?? "COMMENT";
|
|
25780
26080
|
const event = status === "CLOSE" ? "COMMENT" : status;
|
|
25781
26081
|
assertReviewEventSupported(review.vcs.provider, event);
|
|
26082
|
+
const remote = createRemoteReviewBackend(review.vcs, review.pr.number);
|
|
26083
|
+
const promotion = params.local ? await promoteLocalReview(review, remote, model) : undefined;
|
|
25782
26084
|
if (status !== "CLOSE" && review.pr.isDraft)
|
|
25783
26085
|
await review.forge.markReady(review.pr.number);
|
|
25784
|
-
await
|
|
26086
|
+
await remote.publish(event);
|
|
26087
|
+
if (promotion) {
|
|
26088
|
+
promotion.publication.state.bodies = [
|
|
26089
|
+
...new Set([...promotion.publication.state.bodies, ...promotion.bodyFingerprints])
|
|
26090
|
+
];
|
|
26091
|
+
promotion.publication.state.comments = [
|
|
26092
|
+
...new Set([...promotion.publication.state.comments, ...promotion.commentFingerprints])
|
|
26093
|
+
];
|
|
26094
|
+
const published = new Set(promotion.commentFingerprints);
|
|
26095
|
+
promotion.publication.state.stagedComments = promotion.publication.state.stagedComments.filter((fingerprint) => !published.has(fingerprint));
|
|
26096
|
+
await saveReviewPublicationState(promotion.publication.path, promotion.publication.state);
|
|
26097
|
+
}
|
|
25785
26098
|
if (status === "CLOSE")
|
|
25786
26099
|
await review.forge.closePr(review.pr.number);
|
|
25787
26100
|
const finalPr = await review.forge.viewPr(String(review.pr.number));
|
|
25788
|
-
|
|
26101
|
+
const promotedBodies = promotion?.promotedBodies ?? 0;
|
|
26102
|
+
const promotedComments = promotion?.promotedComments ?? 0;
|
|
26103
|
+
const promotedReplies = promotion?.promotedReplies ?? 0;
|
|
26104
|
+
return result3(`Published #${review.pr.number} (${status}); promoted ${promotedBodies} review bodies, ${promotedComments} comments, and ${promotedReplies} replies.`, { pr: finalPr ?? review.pr, status, promotedBodies, promotedComments, promotedReplies });
|
|
26105
|
+
}
|
|
26106
|
+
async function promoteLocalReview(review, remote, model) {
|
|
26107
|
+
const publication = await loadReviewPublicationState(review.cwd, review.vcs, review.pr.number);
|
|
26108
|
+
const session = await resolvePublishSession(review.cwd, {
|
|
26109
|
+
branch: review.vcs.branch,
|
|
26110
|
+
owner: review.vcs.provider === "none" ? undefined : review.vcs.owner,
|
|
26111
|
+
repo: review.vcs.provider === "none" ? undefined : review.vcs.repo,
|
|
26112
|
+
number: review.pr?.number
|
|
26113
|
+
});
|
|
26114
|
+
if (!session)
|
|
26115
|
+
throw new Error("No matching tuicr session to publish.");
|
|
26116
|
+
const sessionIsWorkingTree = session.kind === "local";
|
|
26117
|
+
const local = createLocalReviewBackend({
|
|
26118
|
+
session: session.path,
|
|
26119
|
+
artifactPath: "",
|
|
26120
|
+
author: localReviewAuthor(model)
|
|
26121
|
+
});
|
|
26122
|
+
const draft = await local.readDraft();
|
|
26123
|
+
const remoteDraft = await remote.readDraft();
|
|
26124
|
+
const knownRootComments = new Set([
|
|
26125
|
+
...publication.state.comments,
|
|
26126
|
+
...publication.state.stagedComments,
|
|
26127
|
+
...remoteDraft.comments.map(reviewCommentFingerprint)
|
|
26128
|
+
]);
|
|
26129
|
+
const { checkpointed, candidates } = partitionReviewComments(draft.comments, knownRootComments, model);
|
|
26130
|
+
const remoteThreads = await remote.listThreads();
|
|
26131
|
+
const threadReplies = sessionIsWorkingTree ? [] : candidates.flatMap(({ source: comment }) => {
|
|
26132
|
+
const thread = resolveReviewThread(comment, remoteThreads);
|
|
26133
|
+
return thread ? [{ comment, thread }] : [];
|
|
26134
|
+
});
|
|
26135
|
+
const unmatchedActions = sessionIsWorkingTree ? [] : candidates.map(({ source }) => source).filter((comment) => parseReviewThreadAction(comment.body).action && !threadReplies.some((reply) => reply.comment === comment));
|
|
26136
|
+
if (unmatchedActions.length > 0) {
|
|
26137
|
+
const locations = unmatchedActions.map(({ file, line }) => `${file}:${line ?? "?"}`).join(", ");
|
|
26138
|
+
throw new Error(`Cannot apply review thread action: no matching remote thread at ${locations}.`);
|
|
26139
|
+
}
|
|
26140
|
+
const comments = candidates.filter(({ source }) => !threadReplies.some((reply) => reply.comment === source)).map(({ published }) => published);
|
|
26141
|
+
const body = draft.body.trim();
|
|
26142
|
+
const bodyFingerprints = body ? [reviewBodyFingerprint(body)] : [];
|
|
26143
|
+
const commentFingerprints = [
|
|
26144
|
+
...checkpointed.map(({ fingerprint }) => fingerprint),
|
|
26145
|
+
...comments.map(reviewCommentFingerprint)
|
|
26146
|
+
];
|
|
26147
|
+
const knownBodies = new Set(publication.state.bodies);
|
|
26148
|
+
if (remoteDraft.body.trim())
|
|
26149
|
+
knownBodies.add(reviewBodyFingerprint(remoteDraft.body));
|
|
26150
|
+
const unpublishedBody = body && !knownBodies.has(reviewBodyFingerprint(body)) ? body : "";
|
|
26151
|
+
const unpublished = unpublishedReviewComments(comments, knownRootComments);
|
|
26152
|
+
if (unpublishedBody)
|
|
26153
|
+
await remote.stage({ comments: [], body: unpublishedBody });
|
|
26154
|
+
let promotedComments = 0;
|
|
26155
|
+
for (const comment of unpublished) {
|
|
26156
|
+
await remote.stage({ comments: [comment], body: "" });
|
|
26157
|
+
publication.state.stagedComments.push(reviewCommentFingerprint(comment));
|
|
26158
|
+
await saveReviewPublicationState(publication.path, publication.state);
|
|
26159
|
+
promotedComments += 1;
|
|
26160
|
+
}
|
|
26161
|
+
const promotedReplies = await promoteLocalReplies(review, remote, publication, model, sessionIsWorkingTree) + await promoteLocationReplies(remote, publication, threadReplies, model);
|
|
26162
|
+
return {
|
|
26163
|
+
publication,
|
|
26164
|
+
bodyFingerprints,
|
|
26165
|
+
commentFingerprints,
|
|
26166
|
+
promotedBodies: unpublishedBody ? 1 : 0,
|
|
26167
|
+
promotedComments,
|
|
26168
|
+
promotedReplies
|
|
26169
|
+
};
|
|
26170
|
+
}
|
|
26171
|
+
async function promoteLocationReplies(remote, publication, replies, model) {
|
|
26172
|
+
let count = 0;
|
|
26173
|
+
for (const { comment, thread } of replies) {
|
|
26174
|
+
const parsed = parseReviewThreadAction(comment.body);
|
|
26175
|
+
const body = parsed.body.trim();
|
|
26176
|
+
const action = parsed.action;
|
|
26177
|
+
const fingerprint = reviewReplyFingerprint(thread.id, `${action ?? "reply"}\x00${body}`);
|
|
26178
|
+
if (publication.state.replies.includes(fingerprint))
|
|
26179
|
+
continue;
|
|
26180
|
+
if (action === "delete") {
|
|
26181
|
+
if (!remote.deleteThread)
|
|
26182
|
+
throw new Error(`The review backend cannot delete thread ${thread.id}.`);
|
|
26183
|
+
await remote.deleteThread(thread.id);
|
|
26184
|
+
publication.state.replies.push(fingerprint);
|
|
26185
|
+
await saveReviewPublicationState(publication.path, publication.state);
|
|
26186
|
+
count += 1;
|
|
26187
|
+
continue;
|
|
26188
|
+
}
|
|
26189
|
+
const remoteBody = withCommentProvenance({ ...comment, body }, model);
|
|
26190
|
+
if (body && !thread.replies?.some((reply) => reply === body || reply === remoteBody)) {
|
|
26191
|
+
await remote.reply({
|
|
26192
|
+
threadId: thread.id,
|
|
26193
|
+
body: remoteBody,
|
|
26194
|
+
resolve: false
|
|
26195
|
+
});
|
|
26196
|
+
}
|
|
26197
|
+
if (action && remote.setResolved)
|
|
26198
|
+
await remote.setResolved(thread.id, action === "resolve");
|
|
26199
|
+
publication.state.replies.push(fingerprint);
|
|
26200
|
+
await saveReviewPublicationState(publication.path, publication.state);
|
|
26201
|
+
count += 1;
|
|
26202
|
+
}
|
|
26203
|
+
return count;
|
|
26204
|
+
}
|
|
26205
|
+
async function promoteLocalReplies(review, remote, publication, model, workingTree) {
|
|
26206
|
+
const target = workingTree ? "uncommitted" : await reviewTargetId(review);
|
|
26207
|
+
const artifact = workingTree ? await latestThreadArtifact(review.cwd, review, target) : publication.state.overlayPath ?? await latestThreadArtifact(review.cwd, review, target);
|
|
26208
|
+
if (!artifact)
|
|
26209
|
+
return 0;
|
|
26210
|
+
if (!workingTree)
|
|
26211
|
+
publication.state.overlayPath = artifact;
|
|
26212
|
+
let count = 0;
|
|
26213
|
+
for (const thread of await readThreadArtifact(artifact)) {
|
|
26214
|
+
if (!thread.reply)
|
|
26215
|
+
continue;
|
|
26216
|
+
const body = withRemoteProvenance(thread.reply, model);
|
|
26217
|
+
const fingerprint = reviewReplyFingerprint(thread.id, body);
|
|
26218
|
+
if (publication.state.replies.includes(fingerprint))
|
|
26219
|
+
continue;
|
|
26220
|
+
await remote.reply({
|
|
26221
|
+
threadId: thread.id,
|
|
26222
|
+
body,
|
|
26223
|
+
resolve: !thread.question,
|
|
26224
|
+
question: thread.question
|
|
26225
|
+
});
|
|
26226
|
+
publication.state.replies.push(fingerprint);
|
|
26227
|
+
await saveReviewPublicationState(publication.path, publication.state);
|
|
26228
|
+
count += 1;
|
|
26229
|
+
}
|
|
26230
|
+
return count;
|
|
25789
26231
|
}
|
|
25790
26232
|
async function resolveReviewContext(cwd, target) {
|
|
25791
26233
|
const vcs = await detectVcs(cwd);
|
|
@@ -25826,6 +26268,10 @@ async function assertRemoteBranchReady(cwd) {
|
|
|
25826
26268
|
if (head.stdout.trim() !== pushed.stdout.trim())
|
|
25827
26269
|
throw new Error("Push the current branch before opening a remote review.");
|
|
25828
26270
|
}
|
|
26271
|
+
function withCommentProvenance(comment, fallbackModel) {
|
|
26272
|
+
const route = comment.author?.match(/^Agent:\s*(.+)$/)?.[1];
|
|
26273
|
+
return route ? withRemoteProvenance(comment.body, route || fallbackModel) : comment.body;
|
|
26274
|
+
}
|
|
25829
26275
|
async function reviewTargetId(review) {
|
|
25830
26276
|
if (!review.pr)
|
|
25831
26277
|
return "uncommitted";
|
|
@@ -25842,9 +26288,60 @@ async function headSha(cwd) {
|
|
|
25842
26288
|
}
|
|
25843
26289
|
async function newReviewArtifactPath(cwd, target) {
|
|
25844
26290
|
const dir = await reviewsDir(cwd);
|
|
25845
|
-
await
|
|
26291
|
+
await mkdir7(dir, { recursive: true });
|
|
25846
26292
|
return uniqueRecordPath(dir, reviewRecordName(target));
|
|
25847
26293
|
}
|
|
26294
|
+
async function localThreadArtifactPath(cwd, sessionSlug) {
|
|
26295
|
+
const dir = await reviewsDir(cwd);
|
|
26296
|
+
return join21(dir, `${reviewSlug(sessionSlug) || "local-review"}.md`);
|
|
26297
|
+
}
|
|
26298
|
+
async function readThreadArtifact(path) {
|
|
26299
|
+
try {
|
|
26300
|
+
return parseThreadArtifact(await readFile14(path, "utf8"));
|
|
26301
|
+
} catch (error) {
|
|
26302
|
+
if (error.code === "ENOENT") {
|
|
26303
|
+
throw new Error(`Expected local reply overlay is missing: ${path}`);
|
|
26304
|
+
}
|
|
26305
|
+
throw error;
|
|
26306
|
+
}
|
|
26307
|
+
}
|
|
26308
|
+
async function latestThreadArtifact(cwd, review, target) {
|
|
26309
|
+
try {
|
|
26310
|
+
return await findLatestThreadArtifact(cwd, review, target);
|
|
26311
|
+
} catch (error) {
|
|
26312
|
+
throw new Error(`Cannot locate the latest review thread artifact for ${target}.`, { cause: error });
|
|
26313
|
+
}
|
|
26314
|
+
}
|
|
26315
|
+
async function findLatestThreadArtifact(cwd, review, target) {
|
|
26316
|
+
const dir = await reviewsDir(cwd);
|
|
26317
|
+
const suffix = reviewSlug(target) || "uncommitted";
|
|
26318
|
+
const names = await listReviewArtifactNames(dir);
|
|
26319
|
+
const candidates = await Promise.all(names.flatMap((name) => {
|
|
26320
|
+
if (!name.endsWith(".md"))
|
|
26321
|
+
return [];
|
|
26322
|
+
return [
|
|
26323
|
+
(async () => {
|
|
26324
|
+
const path = join21(dir, name);
|
|
26325
|
+
const info = await stat(path);
|
|
26326
|
+
return { path, name, content: await readFile14(path, "utf8"), modified: info.mtimeMs };
|
|
26327
|
+
})()
|
|
26328
|
+
];
|
|
26329
|
+
}));
|
|
26330
|
+
return candidates.filter(({ name, content }) => {
|
|
26331
|
+
if (!content.startsWith("<!-- diffpi-threads:"))
|
|
26332
|
+
return false;
|
|
26333
|
+
if (review.pr && target !== "local" && target !== "uncommitted")
|
|
26334
|
+
return content.includes(`- PR/MR: #${review.pr.number}`);
|
|
26335
|
+
return artifactNameMatches(name, suffix);
|
|
26336
|
+
}).sort((a, b) => b.modified - a.modified)[0]?.path;
|
|
26337
|
+
}
|
|
26338
|
+
async function listReviewArtifactNames(dir) {
|
|
26339
|
+
try {
|
|
26340
|
+
return await readdir3(dir);
|
|
26341
|
+
} catch (error) {
|
|
26342
|
+
throw new Error(`Cannot read review artifacts in ${dir}.`, { cause: error });
|
|
26343
|
+
}
|
|
26344
|
+
}
|
|
25848
26345
|
function deriveTitle(branch) {
|
|
25849
26346
|
return branch.replace(/^(feature|feat|fix|bug|chore)\//, "").replace(/^eng-\d+-/i, "").replace(/[-_]+/g, " ").replace(/^\w/, (char) => char.toUpperCase());
|
|
25850
26347
|
}
|
|
@@ -25855,19 +26352,30 @@ function uniqueRecordPath(dir, base) {
|
|
|
25855
26352
|
path = join21(dir, `${base}-${count++}.md`);
|
|
25856
26353
|
return path;
|
|
25857
26354
|
}
|
|
26355
|
+
function artifactNameMatches(name, suffix) {
|
|
26356
|
+
if (!name.endsWith(".md"))
|
|
26357
|
+
return false;
|
|
26358
|
+
const stem = name.slice(0, -3);
|
|
26359
|
+
const marker = `-${suffix}`;
|
|
26360
|
+
const markerIndex = stem.lastIndexOf(marker);
|
|
26361
|
+
if (markerIndex < 0)
|
|
26362
|
+
return false;
|
|
26363
|
+
const tail = stem.slice(markerIndex + marker.length);
|
|
26364
|
+
return tail === "" || /^-\d+$/.test(tail);
|
|
26365
|
+
}
|
|
25858
26366
|
|
|
25859
26367
|
// src/tools/setup.ts
|
|
25860
26368
|
import { defineTool as defineTool7 } from "@earendil-works/pi-coding-agent";
|
|
25861
|
-
import { z as
|
|
26369
|
+
import { z as z14 } from "zod";
|
|
25862
26370
|
|
|
25863
26371
|
// src/setup.ts
|
|
25864
26372
|
import { parseFrontmatter as parseFrontmatter3 } from "@earendil-works/pi-coding-agent";
|
|
25865
|
-
import { readdir as readdir4, readFile as
|
|
26373
|
+
import { readdir as readdir4, readFile as readFile16 } from "node:fs/promises";
|
|
25866
26374
|
import { homedir as homedir10 } from "node:os";
|
|
25867
26375
|
import { basename as basename7, join as join24 } from "node:path";
|
|
25868
26376
|
|
|
25869
26377
|
// src/mcp.ts
|
|
25870
|
-
import { mkdir as
|
|
26378
|
+
import { mkdir as mkdir8, readFile as readFile15, writeFile as writeFile8 } from "node:fs/promises";
|
|
25871
26379
|
import { homedir as homedir8 } from "node:os";
|
|
25872
26380
|
import { dirname as dirname9, join as join22 } from "node:path";
|
|
25873
26381
|
var mcp = {
|
|
@@ -25885,8 +26393,8 @@ var mcp = {
|
|
|
25885
26393
|
const next = { ...current, mcpServers: nextServers };
|
|
25886
26394
|
const changed = JSON.stringify(current) !== JSON.stringify(next);
|
|
25887
26395
|
if (changed && !options.dryRun) {
|
|
25888
|
-
await
|
|
25889
|
-
await
|
|
26396
|
+
await mkdir8(dirname9(path), { recursive: true });
|
|
26397
|
+
await writeFile8(path, `${JSON.stringify(next, null, 2)}
|
|
25890
26398
|
`, "utf8");
|
|
25891
26399
|
}
|
|
25892
26400
|
return { path, changed, existed: currentText !== undefined, planned: changed && options.dryRun === true };
|
|
@@ -25915,7 +26423,7 @@ function getParsedConfig(content, path) {
|
|
|
25915
26423
|
}
|
|
25916
26424
|
async function getOptionalFile2(path) {
|
|
25917
26425
|
try {
|
|
25918
|
-
return await
|
|
26426
|
+
return await readFile15(path, "utf8");
|
|
25919
26427
|
} catch (error) {
|
|
25920
26428
|
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
25921
26429
|
return;
|
|
@@ -25927,7 +26435,7 @@ function isRecord(value) {
|
|
|
25927
26435
|
}
|
|
25928
26436
|
|
|
25929
26437
|
// src/pi.ts
|
|
25930
|
-
import { mkdir as
|
|
26438
|
+
import { mkdir as mkdir9, writeFile as writeFile9 } from "node:fs/promises";
|
|
25931
26439
|
import { homedir as homedir9 } from "node:os";
|
|
25932
26440
|
import { dirname as dirname10, join as join23 } from "node:path";
|
|
25933
26441
|
var pi = {
|
|
@@ -25946,8 +26454,8 @@ async function ensurePiAgent(filename, content, agentDir = resolvePiAgentDir(),
|
|
|
25946
26454
|
const currentText = await readTextIfExists(path);
|
|
25947
26455
|
const changed = currentText !== content;
|
|
25948
26456
|
if (changed && !dryRun) {
|
|
25949
|
-
await
|
|
25950
|
-
await
|
|
26457
|
+
await mkdir9(dirname10(path), { recursive: true });
|
|
26458
|
+
await writeFile9(path, content, "utf8");
|
|
25951
26459
|
}
|
|
25952
26460
|
return { path, changed, existed: currentText !== undefined, planned: changed && dryRun };
|
|
25953
26461
|
}
|
|
@@ -25983,8 +26491,8 @@ async function ensurePiConfig(path, update, dryRun = false) {
|
|
|
25983
26491
|
const next = update(current);
|
|
25984
26492
|
const changed = JSON.stringify(current) !== JSON.stringify(next);
|
|
25985
26493
|
if (changed && !dryRun) {
|
|
25986
|
-
await
|
|
25987
|
-
await
|
|
26494
|
+
await mkdir9(dirname10(path), { recursive: true });
|
|
26495
|
+
await writeFile9(path, `${JSON.stringify(next, null, 2)}
|
|
25988
26496
|
`, "utf8");
|
|
25989
26497
|
}
|
|
25990
26498
|
return { path, changed, existed: currentText !== undefined, planned: changed && dryRun };
|
|
@@ -26122,7 +26630,7 @@ async function ensurePiAgents(options = {}) {
|
|
|
26122
26630
|
const actions = [];
|
|
26123
26631
|
for (const entry of entries) {
|
|
26124
26632
|
const id = basename7(entry.name, ".md").replace(/^diffpi-/, "");
|
|
26125
|
-
const source = await
|
|
26633
|
+
const source = await readFile16(join24(bundledAgentsDir, entry.name), "utf8");
|
|
26126
26634
|
const content = materializeAgentModels(source, id, userConfig.config, options.availableModels);
|
|
26127
26635
|
const result = await pi.agentEnsure(entry.name, content, agentDir, options.dryRun);
|
|
26128
26636
|
actions.push(getConfigSetupAction(`pi agent ${id}`, result));
|
|
@@ -26327,7 +26835,7 @@ function reportProgress(options, message) {
|
|
|
26327
26835
|
options.onProgress?.(message);
|
|
26328
26836
|
}
|
|
26329
26837
|
async function packageVersion2() {
|
|
26330
|
-
const source = await
|
|
26838
|
+
const source = await readFile16(join24(resolveBundledAgentsDir(), "..", "package.json"), "utf8");
|
|
26331
26839
|
let value;
|
|
26332
26840
|
try {
|
|
26333
26841
|
value = JSON.parse(source);
|
|
@@ -26343,13 +26851,13 @@ function getRecord(value) {
|
|
|
26343
26851
|
}
|
|
26344
26852
|
|
|
26345
26853
|
// src/tools/setup.ts
|
|
26346
|
-
var setupParametersSchema =
|
|
26347
|
-
issueTracker:
|
|
26348
|
-
forges:
|
|
26349
|
-
forge:
|
|
26350
|
-
bindZedKey:
|
|
26854
|
+
var setupParametersSchema = z14.object({
|
|
26855
|
+
issueTracker: z14.enum(["none", "linear", "jira"]).default("none").describe("Issue tracker MCP server to configure. Use none unless the user explicitly selects Linear or Jira."),
|
|
26856
|
+
forges: z14.array(z14.enum(["github", "gitlab"])).default([]).describe("Hosted VCS MCP servers to configure. Installs matching CLIs and registers each MCP server."),
|
|
26857
|
+
forge: z14.enum(["none", "github", "gitlab"]).optional().describe("Deprecated single-host alias. Prefer forges for one or more VCS MCP servers."),
|
|
26858
|
+
bindZedKey: z14.boolean().default(false).describe("Opt in to a Zed keybinding for the tuicr review task.")
|
|
26351
26859
|
});
|
|
26352
|
-
var setupParameters =
|
|
26860
|
+
var setupParameters = z14.toJSONSchema(setupParametersSchema, { io: "input" });
|
|
26353
26861
|
var diffpiSetupTool = defineTool7({
|
|
26354
26862
|
name: "diffpi_setup",
|
|
26355
26863
|
label: "diffpi setup",
|
|
@@ -26422,11 +26930,11 @@ ${lines.join(`
|
|
|
26422
26930
|
|
|
26423
26931
|
// src/tools/templates.ts
|
|
26424
26932
|
import { defineTool as defineTool8 } from "@earendil-works/pi-coding-agent";
|
|
26425
|
-
import { z as
|
|
26426
|
-
var templateSchema =
|
|
26427
|
-
name:
|
|
26428
|
-
variables:
|
|
26429
|
-
homeDir:
|
|
26933
|
+
import { z as z15 } from "zod";
|
|
26934
|
+
var templateSchema = z15.object({
|
|
26935
|
+
name: z15.string().min(1),
|
|
26936
|
+
variables: z15.record(z15.string(), z15.string()).optional(),
|
|
26937
|
+
homeDir: z15.string().optional()
|
|
26430
26938
|
});
|
|
26431
26939
|
var diffpiTemplateTool = defineTool8({
|
|
26432
26940
|
name: "diffpi_template",
|
|
@@ -26434,7 +26942,7 @@ var diffpiTemplateTool = defineTool8({
|
|
|
26434
26942
|
description: "Load a bundled Diffpi template or a user override and render named variables.",
|
|
26435
26943
|
promptSnippet: "Use diffpi_template for package workflow templates",
|
|
26436
26944
|
promptGuidelines: ["User overrides live under ~/.difflab/diffpi/templates."],
|
|
26437
|
-
parameters:
|
|
26945
|
+
parameters: z15.toJSONSchema(templateSchema, { io: "input" }),
|
|
26438
26946
|
executionMode: "parallel",
|
|
26439
26947
|
async execute(_id, input) {
|
|
26440
26948
|
const params = templateSchema.parse(input);
|