@difflab/pi 0.5.0-rc.202609231313.eaf7fa0 → 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-copilot.md +3 -6
- 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 +810 -344
- 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 -47
- 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,172 +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
|
-
description: "Code review: auto, new, edit, address, publish, complete, merge
|
|
16866
|
-
handler: async (args, ctx) =>
|
|
16860
|
+
description: "Code review: auto, new, edit, address, publish, complete, merge (add --local or --bg)",
|
|
16861
|
+
handler: async (args, ctx) => handleReviewCommand(args, ctx, pi, modes)
|
|
16867
16862
|
});
|
|
16868
16863
|
}
|
|
16869
|
-
async function
|
|
16870
|
-
|
|
16871
|
-
|
|
16872
|
-
|
|
16873
|
-
|
|
16874
|
-
|
|
16875
|
-
|
|
16876
|
-
|
|
16877
|
-
|
|
16878
|
-
|
|
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);
|
|
16868
|
+
if (background) {
|
|
16869
|
+
await launchBackgroundAgent(pi.events, {
|
|
16870
|
+
name: `Review ${verb}`,
|
|
16871
|
+
agent: "orchestrator",
|
|
16872
|
+
cwd: ctx.cwd,
|
|
16873
|
+
prompt: reviewPrompt(invocation, "orchestrator")
|
|
16874
|
+
});
|
|
16875
|
+
return;
|
|
16879
16876
|
}
|
|
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",
|
|
16881
|
+
display: false,
|
|
16882
|
+
content: `${reviewPrompt(invocation, agent)} ${activation.message}`
|
|
16883
|
+
}, { triggerTurn: true });
|
|
16880
16884
|
}
|
|
16881
|
-
function
|
|
16882
|
-
|
|
16883
|
-
const auto = command({
|
|
16884
|
-
name: "auto",
|
|
16885
|
-
args: { target: targetArgument(), local: flag({ long: "local" }), background: flag({ long: "bg" }) },
|
|
16886
|
-
handler: async ({ target, local, background }) => invokeWorkflow({
|
|
16887
|
-
workflow: "auto",
|
|
16888
|
-
arguments: { target, local, background },
|
|
16889
|
-
agent: "reviewer",
|
|
16890
|
-
isBackground: background
|
|
16891
|
-
})
|
|
16892
|
-
});
|
|
16893
|
-
const create = command({
|
|
16894
|
-
name: "new",
|
|
16895
|
-
args: {
|
|
16896
|
-
intent: option({ long: "intent", type: optional(string5), defaultValue: () => {
|
|
16897
|
-
return;
|
|
16898
|
-
} }),
|
|
16899
|
-
base: option({ long: "base", type: optional(string5), defaultValue: () => {
|
|
16900
|
-
return;
|
|
16901
|
-
} }),
|
|
16902
|
-
local: flag({ long: "local" }),
|
|
16903
|
-
background: flag({ long: "bg" }),
|
|
16904
|
-
title: restPositionals({ type: string5 })
|
|
16905
|
-
},
|
|
16906
|
-
handler: async ({ title, intent, base, local, background }) => invokeWorkflow({
|
|
16907
|
-
workflow: "new",
|
|
16908
|
-
arguments: { title: title.length ? title.join(" ") : undefined, intent, base, local, background },
|
|
16909
|
-
agent: "orchestrator",
|
|
16910
|
-
isBackground: background
|
|
16911
|
-
})
|
|
16912
|
-
});
|
|
16913
|
-
const edit = command({
|
|
16914
|
-
name: "edit",
|
|
16915
|
-
args: { target: targetArgument(), local: flag({ long: "local" }), background: flag({ long: "bg" }) },
|
|
16916
|
-
handler: async ({ target, local, background }) => invokeWorkflow({
|
|
16917
|
-
workflow: "edit",
|
|
16918
|
-
arguments: { target, local, background },
|
|
16919
|
-
agent: "orchestrator",
|
|
16920
|
-
isBackground: background
|
|
16921
|
-
})
|
|
16922
|
-
});
|
|
16923
|
-
const address = command({
|
|
16924
|
-
name: "address",
|
|
16925
|
-
args: { target: targetArgument(), local: flag({ long: "local" }), background: flag({ long: "bg" }) },
|
|
16926
|
-
handler: async ({ target, local, background }) => invokeWorkflow({
|
|
16927
|
-
workflow: "address",
|
|
16928
|
-
arguments: { target, local, background },
|
|
16929
|
-
agent: "reviewer",
|
|
16930
|
-
isBackground: background
|
|
16931
|
-
})
|
|
16932
|
-
});
|
|
16933
|
-
const publish = command({
|
|
16934
|
-
name: "publish",
|
|
16935
|
-
args: {
|
|
16936
|
-
target: targetArgument(),
|
|
16937
|
-
comment: flag({ long: "comment" }),
|
|
16938
|
-
approve: flag({ long: "approve" }),
|
|
16939
|
-
requestChanges: flag({ long: "request-changes" }),
|
|
16940
|
-
close: flag({ long: "close" }),
|
|
16941
|
-
background: flag({ long: "bg" })
|
|
16942
|
-
},
|
|
16943
|
-
handler: async ({ target, comment, approve, requestChanges, close, background }) => invokeWorkflow({
|
|
16944
|
-
workflow: "publish",
|
|
16945
|
-
arguments: { target, comment, approve, requestChanges, close, background },
|
|
16946
|
-
agent: "orchestrator",
|
|
16947
|
-
isBackground: background
|
|
16948
|
-
})
|
|
16949
|
-
});
|
|
16950
|
-
const complete = command({
|
|
16951
|
-
name: "complete",
|
|
16952
|
-
args: {
|
|
16953
|
-
target: targetArgument(),
|
|
16954
|
-
local: flag({ long: "local" }),
|
|
16955
|
-
approve: flag({ long: "approve" }),
|
|
16956
|
-
reject: flag({ long: "reject" }),
|
|
16957
|
-
abandon: flag({ long: "abandon" }),
|
|
16958
|
-
background: flag({ long: "bg" })
|
|
16959
|
-
},
|
|
16960
|
-
handler: async ({ target, local, approve, reject, abandon, background }) => invokeWorkflow({
|
|
16961
|
-
workflow: "complete",
|
|
16962
|
-
arguments: { target, local, approve, reject, abandon, background },
|
|
16963
|
-
agent: "orchestrator",
|
|
16964
|
-
isBackground: background
|
|
16965
|
-
})
|
|
16966
|
-
});
|
|
16967
|
-
const merge = command({
|
|
16968
|
-
name: "merge",
|
|
16969
|
-
args: { target: targetArgument(), background: flag({ long: "bg" }) },
|
|
16970
|
-
handler: async ({ target, background }) => invokeWorkflow({
|
|
16971
|
-
workflow: "merge",
|
|
16972
|
-
arguments: { target, background },
|
|
16973
|
-
agent: "orchestrator",
|
|
16974
|
-
isBackground: background
|
|
16975
|
-
})
|
|
16976
|
-
});
|
|
16977
|
-
const help = command({
|
|
16978
|
-
name: "help",
|
|
16979
|
-
args: {},
|
|
16980
|
-
handler: async () => invokeWorkflow({ workflow: "help", arguments: {}, agent: "orchestrator" })
|
|
16981
|
-
});
|
|
16982
|
-
return subcommands({
|
|
16983
|
-
name: "review",
|
|
16984
|
-
cmds: {
|
|
16985
|
-
auto,
|
|
16986
|
-
new: create,
|
|
16987
|
-
edit,
|
|
16988
|
-
address,
|
|
16989
|
-
publish,
|
|
16990
|
-
complete,
|
|
16991
|
-
merge,
|
|
16992
|
-
help,
|
|
16993
|
-
open: create,
|
|
16994
|
-
create,
|
|
16995
|
-
draft: create,
|
|
16996
|
-
launch: auto,
|
|
16997
|
-
ready: publish,
|
|
16998
|
-
close: complete,
|
|
16999
|
-
land: merge
|
|
17000
|
-
}
|
|
17001
|
-
});
|
|
16885
|
+
function reviewVerb(invocation) {
|
|
16886
|
+
return invocation.split(/\s+/).find((token) => token && !token.startsWith("-")) ?? "help";
|
|
17002
16887
|
}
|
|
17003
|
-
function
|
|
17004
|
-
|
|
17005
|
-
|
|
17006
|
-
const prompt = await loadReviewWorkflow(workflow, args);
|
|
17007
|
-
if (isBackground) {
|
|
17008
|
-
await launchBackgroundAgent(runtime.pi.events, {
|
|
17009
|
-
name: `Review ${workflow}`,
|
|
17010
|
-
agent: "orchestrator",
|
|
17011
|
-
cwd: runtime.ctx.cwd,
|
|
17012
|
-
inheritContext: true,
|
|
17013
|
-
prompt
|
|
17014
|
-
});
|
|
17015
|
-
return;
|
|
17016
|
-
}
|
|
17017
|
-
const activation = await runtime.modes.set(agent, runtime.ctx);
|
|
17018
|
-
runtime.pi.sendMessage({
|
|
17019
|
-
customType: `diffpi-review-${workflow}-command`,
|
|
17020
|
-
display: false,
|
|
17021
|
-
content: `${prompt}
|
|
17022
|
-
|
|
17023
|
-
${activation.message}`
|
|
17024
|
-
}, { triggerTurn: true });
|
|
17025
|
-
};
|
|
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.`;
|
|
17026
16891
|
}
|
|
17027
|
-
function
|
|
17028
|
-
return
|
|
16892
|
+
function hasFlag2(input, flag) {
|
|
16893
|
+
return input.split(/\s+/).includes(flag);
|
|
16894
|
+
}
|
|
16895
|
+
function withoutFlag(input, flag) {
|
|
16896
|
+
return input.split(/\s+/).filter((token) => token && token !== flag).join(" ").trim();
|
|
17029
16897
|
}
|
|
17030
16898
|
|
|
17031
16899
|
// src/commands/index.ts
|
|
@@ -17906,6 +17774,18 @@ async function reviewsDir(cwd, homeDir = homedir5()) {
|
|
|
17906
17774
|
await mkdir2(dir, { recursive: true });
|
|
17907
17775
|
return dir;
|
|
17908
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
|
+
}
|
|
17909
17789
|
async function assertStoreLink(path, dest) {
|
|
17910
17790
|
const entry = await lstat(path);
|
|
17911
17791
|
if (!entry.isSymbolicLink())
|
|
@@ -24728,10 +24608,10 @@ function result2(text, details = {}) {
|
|
|
24728
24608
|
|
|
24729
24609
|
// src/tools/review.ts
|
|
24730
24610
|
import { existsSync as existsSync3 } from "node:fs";
|
|
24731
|
-
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";
|
|
24732
24612
|
import { join as join21 } from "node:path";
|
|
24733
24613
|
import { defineTool as defineTool6 } from "@earendil-works/pi-coding-agent";
|
|
24734
|
-
import { z as
|
|
24614
|
+
import { z as z13 } from "zod";
|
|
24735
24615
|
|
|
24736
24616
|
// src/review/types.ts
|
|
24737
24617
|
import { z as z10 } from "zod";
|
|
@@ -24756,6 +24636,21 @@ function yymmdd(date = new Date) {
|
|
|
24756
24636
|
function reviewRecordName(target, date = new Date) {
|
|
24757
24637
|
return `${yymmdd(date)}-${reviewSlug(target) || "uncommitted"}`;
|
|
24758
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
|
+
}
|
|
24759
24654
|
function dedupeFindings(findings) {
|
|
24760
24655
|
const rank = { BLOCKING: 3, CONSIDER: 2, NOTE: 1 };
|
|
24761
24656
|
const byKey = new Map;
|
|
@@ -24801,6 +24696,18 @@ function renderCommentBody(finding) {
|
|
|
24801
24696
|
return `${prefix}${finding.body}${reference}`;
|
|
24802
24697
|
}
|
|
24803
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
|
+
});
|
|
24804
24711
|
function renderReviewDoc(input) {
|
|
24805
24712
|
const anchored = dedupeFindings(input.findings).filter((finding) => finding.line > 0);
|
|
24806
24713
|
const lines = [`# Review: ${input.title}`, "", "## Metadata"];
|
|
@@ -24842,6 +24749,88 @@ function renderReviewDoc(input) {
|
|
|
24842
24749
|
`).trimEnd()}
|
|
24843
24750
|
`;
|
|
24844
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
|
+
}
|
|
24845
24834
|
// src/review/github-review-backend.ts
|
|
24846
24835
|
function GitHubReviewBackend(vcs, number) {
|
|
24847
24836
|
const pendingReview = async () => {
|
|
@@ -24903,6 +24892,9 @@ function GitHubReviewBackend(vcs, number) {
|
|
|
24903
24892
|
id: thread.id,
|
|
24904
24893
|
file: thread.path ?? undefined,
|
|
24905
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] : []),
|
|
24906
24898
|
body,
|
|
24907
24899
|
author: comment?.author?.login,
|
|
24908
24900
|
resolved: thread.isResolved,
|
|
@@ -24912,6 +24904,7 @@ function GitHubReviewBackend(vcs, number) {
|
|
|
24912
24904
|
});
|
|
24913
24905
|
};
|
|
24914
24906
|
return {
|
|
24907
|
+
kind: "remote",
|
|
24915
24908
|
async stage(draft) {
|
|
24916
24909
|
const pending = await pendingReview();
|
|
24917
24910
|
if (!pending) {
|
|
@@ -24969,6 +24962,33 @@ function GitHubReviewBackend(vcs, number) {
|
|
|
24969
24962
|
await ghChecked(args);
|
|
24970
24963
|
}
|
|
24971
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
|
+
},
|
|
24972
24992
|
listThreads,
|
|
24973
24993
|
async reply(input) {
|
|
24974
24994
|
const threads = await listThreads();
|
|
@@ -24985,14 +25005,32 @@ function GitHubReviewBackend(vcs, number) {
|
|
|
24985
25005
|
`body=${input.body}`
|
|
24986
25006
|
]);
|
|
24987
25007
|
}
|
|
24988
|
-
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) {
|
|
24989
25027
|
await ghChecked([
|
|
24990
25028
|
"api",
|
|
24991
25029
|
"graphql",
|
|
24992
25030
|
"-f",
|
|
24993
|
-
`query=${
|
|
25031
|
+
`query=${GITHUB_DELETE_COMMENT_MUTATION}`,
|
|
24994
25032
|
"-f",
|
|
24995
|
-
`
|
|
25033
|
+
`commentId=${commentId}`
|
|
24996
25034
|
]);
|
|
24997
25035
|
}
|
|
24998
25036
|
},
|
|
@@ -25011,11 +25049,13 @@ function GitHubReviewBackend(vcs, number) {
|
|
|
25011
25049
|
function githubReviewSubmissionEndpoint(owner, repo, id, pendingReviewId) {
|
|
25012
25050
|
return pendingReviewId ? `/repos/${owner}/${repo}/pulls/${id}/reviews/${pendingReviewId}/events` : `/repos/${owner}/${repo}/pulls/${id}/reviews`;
|
|
25013
25051
|
}
|
|
25014
|
-
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}}}}}`;
|
|
25015
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}}}`;
|
|
25016
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}}}`;
|
|
25017
25055
|
var GITHUB_REPLY_MUTATION = `mutation($threadId:ID!,$body:String!){addPullRequestReviewThreadReply(input:{pullRequestReviewThreadId:$threadId,body:$body}){comment{id}}}`;
|
|
25018
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}}`;
|
|
25019
25059
|
|
|
25020
25060
|
// src/review/gitlab-review-backend.ts
|
|
25021
25061
|
function GitLabReviewBackend(vcs, number) {
|
|
@@ -25052,6 +25092,7 @@ function GitLabReviewBackend(vcs, number) {
|
|
|
25052
25092
|
});
|
|
25053
25093
|
};
|
|
25054
25094
|
return {
|
|
25095
|
+
kind: "remote",
|
|
25055
25096
|
async stage(draft) {
|
|
25056
25097
|
const endpoint = `${mergeRequestEndpoint()}/draft_notes`;
|
|
25057
25098
|
if (draft.body.trim()) {
|
|
@@ -25080,6 +25121,36 @@ function GitLabReviewBackend(vcs, number) {
|
|
|
25080
25121
|
});
|
|
25081
25122
|
}
|
|
25082
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
|
+
},
|
|
25083
25154
|
listThreads,
|
|
25084
25155
|
async reply(input) {
|
|
25085
25156
|
const endpoint = `${mergeRequestEndpoint()}/discussions/${encodeURIComponent(input.threadId)}`;
|
|
@@ -25091,7 +25162,15 @@ function GitLabReviewBackend(vcs, number) {
|
|
|
25091
25162
|
});
|
|
25092
25163
|
}
|
|
25093
25164
|
if (input.resolve)
|
|
25094
|
-
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]);
|
|
25095
25174
|
},
|
|
25096
25175
|
async publish(event) {
|
|
25097
25176
|
assertReviewEventSupported(vcs.provider, event);
|
|
@@ -25131,17 +25210,8 @@ function hasGitlabDraftNotes(input) {
|
|
|
25131
25210
|
}
|
|
25132
25211
|
}
|
|
25133
25212
|
|
|
25134
|
-
// src/review/review-backend.ts
|
|
25135
|
-
|
|
25136
|
-
if (vcs.provider === "github")
|
|
25137
|
-
return GitHubReviewBackend(vcs, number);
|
|
25138
|
-
if (vcs.provider === "gitlab")
|
|
25139
|
-
return GitLabReviewBackend(vcs, number);
|
|
25140
|
-
throw new Error("A remote review backend requires a GitHub or GitLab repository.");
|
|
25141
|
-
}
|
|
25142
|
-
// src/review/local-reviews.ts
|
|
25143
|
-
import { mkdir as mkdir7, readdir as readdir3, readFile as readFile12, unlink as unlink2, writeFile as writeFile5 } from "node:fs/promises";
|
|
25144
|
-
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";
|
|
25145
25215
|
|
|
25146
25216
|
// src/extensions/tuicrx.ts
|
|
25147
25217
|
import { readFile as readFile11, realpath as realpath4 } from "node:fs/promises";
|
|
@@ -25212,6 +25282,11 @@ async function resolveReviewSession(cwd, target) {
|
|
|
25212
25282
|
}
|
|
25213
25283
|
return resolveSession(cwd, target.branch);
|
|
25214
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
|
+
}
|
|
25215
25290
|
async function resolvePrSession(cwd, owner, repo, number) {
|
|
25216
25291
|
const coordinate = `${owner}/${repo}`.toLowerCase();
|
|
25217
25292
|
const sessions = await listSessions(cwd);
|
|
@@ -25246,6 +25321,8 @@ async function readSession(path) {
|
|
|
25246
25321
|
function toLocalReviewThreads(session) {
|
|
25247
25322
|
const threads = [];
|
|
25248
25323
|
const add = (comment, location = {}) => {
|
|
25324
|
+
if (isLocalResponse(comment.content))
|
|
25325
|
+
return;
|
|
25249
25326
|
const author = commentAuthor(comment);
|
|
25250
25327
|
threads.push({
|
|
25251
25328
|
id: comment.id ?? `local-${threads.length + 1}`,
|
|
@@ -25271,6 +25348,54 @@ function toLocalReviewThreads(session) {
|
|
|
25271
25348
|
}
|
|
25272
25349
|
return threads;
|
|
25273
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
|
+
}
|
|
25274
25399
|
async function requireTuicr() {
|
|
25275
25400
|
if (!await tuicrAvailable())
|
|
25276
25401
|
throw new Error("tuicr is not installed or is not available on PATH.");
|
|
@@ -25286,83 +25411,167 @@ async function canonicalPath3(path) {
|
|
|
25286
25411
|
}
|
|
25287
25412
|
}
|
|
25288
25413
|
|
|
25289
|
-
// src/review/local-
|
|
25290
|
-
|
|
25291
|
-
|
|
25292
|
-
|
|
25293
|
-
|
|
25294
|
-
|
|
25295
|
-
|
|
25296
|
-
|
|
25297
|
-
|
|
25298
|
-
|
|
25299
|
-
|
|
25300
|
-
|
|
25301
|
-
|
|
25302
|
-
|
|
25303
|
-
|
|
25304
|
-
|
|
25305
|
-
|
|
25306
|
-
|
|
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
|
+
}
|
|
25307
25449
|
};
|
|
25308
|
-
const path = join20(dir, `${revision}.json`);
|
|
25309
|
-
await writeFile5(path, `${JSON.stringify(dump, null, 2)}
|
|
25310
|
-
`, { encoding: "utf8", flag: "wx" });
|
|
25311
|
-
await unlink2(input.session.path);
|
|
25312
|
-
return { path, dump };
|
|
25313
25450
|
}
|
|
25314
|
-
|
|
25315
|
-
|
|
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);
|
|
25316
25462
|
}
|
|
25317
|
-
|
|
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}`);
|
|
25484
|
+
}
|
|
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`);
|
|
25318
25491
|
try {
|
|
25319
|
-
|
|
25320
|
-
|
|
25321
|
-
return match ? Math.max(latest, Number(match[1])) : latest;
|
|
25322
|
-
}, 0);
|
|
25492
|
+
const parsed = reviewPublicationStateSchema.parse(JSON.parse(await readFile13(path, "utf8")));
|
|
25493
|
+
return { path, state: { ...parsed, target } };
|
|
25323
25494
|
} catch (error) {
|
|
25324
|
-
if (error.code === "ENOENT")
|
|
25325
|
-
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
|
+
}
|
|
25326
25501
|
throw error;
|
|
25327
25502
|
}
|
|
25328
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
|
+
}
|
|
25329
25513
|
// src/tools/review.ts
|
|
25330
|
-
var contextSchema =
|
|
25331
|
-
cwd:
|
|
25332
|
-
target:
|
|
25333
|
-
workingTree:
|
|
25514
|
+
var contextSchema = z13.object({
|
|
25515
|
+
cwd: z13.string().optional(),
|
|
25516
|
+
target: z13.string().optional(),
|
|
25517
|
+
workingTree: z13.boolean().optional()
|
|
25334
25518
|
});
|
|
25335
|
-
var localSchema = contextSchema.extend({ local:
|
|
25519
|
+
var localSchema = contextSchema.extend({ local: z13.boolean().optional() });
|
|
25336
25520
|
var openSchema = localSchema.extend({
|
|
25337
|
-
title:
|
|
25338
|
-
intent:
|
|
25339
|
-
issueUrl:
|
|
25340
|
-
base:
|
|
25521
|
+
title: z13.string().optional(),
|
|
25522
|
+
intent: z13.string().optional(),
|
|
25523
|
+
issueUrl: z13.string().url().optional(),
|
|
25524
|
+
base: z13.string().optional()
|
|
25341
25525
|
});
|
|
25342
25526
|
var submitSchema = localSchema.extend({
|
|
25343
25527
|
findings: findingsSchema,
|
|
25344
|
-
overallIssues:
|
|
25345
|
-
notVerified:
|
|
25346
|
-
title:
|
|
25528
|
+
overallIssues: z13.array(z13.string()).optional(),
|
|
25529
|
+
notVerified: z13.array(z13.string()).optional(),
|
|
25530
|
+
title: z13.string().optional()
|
|
25347
25531
|
});
|
|
25348
25532
|
var addCommentSchema = localSchema.extend({
|
|
25349
|
-
body:
|
|
25350
|
-
file:
|
|
25351
|
-
line:
|
|
25352
|
-
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()
|
|
25353
25537
|
});
|
|
25354
25538
|
var respondSchema = localSchema.extend({
|
|
25355
|
-
threadId:
|
|
25356
|
-
body:
|
|
25357
|
-
question:
|
|
25358
|
-
resolve:
|
|
25539
|
+
threadId: z13.string().min(1),
|
|
25540
|
+
body: z13.string().min(1),
|
|
25541
|
+
question: z13.boolean().optional(),
|
|
25542
|
+
resolve: z13.boolean().optional()
|
|
25359
25543
|
});
|
|
25360
25544
|
var publishSchema = localSchema.extend({
|
|
25361
|
-
status:
|
|
25545
|
+
status: z13.enum(["COMMENT", "APPROVE", "REQUEST_CHANGES", "CLOSE"]).optional()
|
|
25362
25546
|
});
|
|
25363
25547
|
var completeSchema = localSchema.extend({
|
|
25364
|
-
action:
|
|
25548
|
+
action: z13.enum(["approve", "reject", "abandon"]).optional()
|
|
25365
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
|
+
}
|
|
25366
25575
|
async function workingTreeDiff(cwd) {
|
|
25367
25576
|
const tracked = await run2("git", ["-C", cwd, "diff", "HEAD"], { capture: "unbounded" });
|
|
25368
25577
|
if (tracked.code !== 0)
|
|
@@ -25558,7 +25767,7 @@ ${openMessage}`, {
|
|
|
25558
25767
|
const gates = await runMiseGates(review.cwd);
|
|
25559
25768
|
const baseRef = review.pr?.baseRef ?? (review.forge ? await review.forge.defaultBranch() : "local");
|
|
25560
25769
|
const artifact = await newReviewArtifactPath(review.cwd, params.local || params.workingTree || !review.pr ? "uncommitted" : await reviewTargetId(review));
|
|
25561
|
-
await
|
|
25770
|
+
await writeFile7(artifact, renderReviewDoc({
|
|
25562
25771
|
title: params.title ?? review.pr?.title ?? review.vcs.branch,
|
|
25563
25772
|
number: useLocalBackend ? undefined : review.pr?.number,
|
|
25564
25773
|
url: useLocalBackend ? undefined : review.pr?.url,
|
|
@@ -25581,21 +25790,17 @@ ${openMessage}`, {
|
|
|
25581
25790
|
count: findings.length
|
|
25582
25791
|
});
|
|
25583
25792
|
}
|
|
25584
|
-
|
|
25585
|
-
|
|
25586
|
-
|
|
25587
|
-
|
|
25588
|
-
|
|
25589
|
-
|
|
25590
|
-
});
|
|
25591
|
-
}
|
|
25592
|
-
if (body.trim())
|
|
25593
|
-
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 });
|
|
25594
25799
|
return result3(`Local review staged in tuicr: ${artifact}`, { artifact, count: findings.length, session });
|
|
25595
25800
|
}
|
|
25596
25801
|
if (!review.pr)
|
|
25597
25802
|
return result3(`Review written: ${artifact}. No PR/MR matches this remote target.`, { artifact });
|
|
25598
|
-
const hasDraft = comments
|
|
25803
|
+
const hasDraft = hasReviewDraft(comments, body);
|
|
25599
25804
|
if (hasDraft) {
|
|
25600
25805
|
await createRemoteReviewBackend(review.vcs, review.pr.number).stage({ comments, body });
|
|
25601
25806
|
}
|
|
@@ -25631,12 +25836,12 @@ ${openMessage}`, {
|
|
|
25631
25836
|
const session = await resolveTuicrSession(review, Boolean(params.local || params.workingTree));
|
|
25632
25837
|
if (!session)
|
|
25633
25838
|
return result3("No matching tuicr session. Open review_new or review_launch_ui first.");
|
|
25634
|
-
|
|
25635
|
-
|
|
25636
|
-
|
|
25637
|
-
|
|
25638
|
-
username: localReviewAuthor(model)
|
|
25839
|
+
const backend = createLocalReviewBackend({
|
|
25840
|
+
session: session.path,
|
|
25841
|
+
artifactPath: "",
|
|
25842
|
+
author: localReviewAuthor(model)
|
|
25639
25843
|
});
|
|
25844
|
+
await backend.stage({ comments: [comment], body: "" });
|
|
25640
25845
|
return result3(`Comment added to tuicr session ${session.slug}.`, { session, comment });
|
|
25641
25846
|
}
|
|
25642
25847
|
if (!review.pr)
|
|
@@ -25648,70 +25853,88 @@ ${openMessage}`, {
|
|
|
25648
25853
|
defineTool6({
|
|
25649
25854
|
name: "review_comments",
|
|
25650
25855
|
label: "review comments",
|
|
25651
|
-
description: "
|
|
25652
|
-
promptSnippet: "Call review_comments before addressing
|
|
25653
|
-
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."],
|
|
25654
25859
|
parameters: parameters6(localSchema),
|
|
25655
|
-
executionMode: "
|
|
25860
|
+
executionMode: "sequential",
|
|
25656
25861
|
async execute(_id, input) {
|
|
25657
25862
|
const params = localSchema.parse(input);
|
|
25658
|
-
if (params.local || params.workingTree)
|
|
25659
|
-
return result3("Use review_dump for a local tuicr review.");
|
|
25660
25863
|
const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
|
|
25661
|
-
|
|
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 {
|
|
25662
25878
|
return result3(review.forge ? "No PR/MR matches this remote review target." : unsupportedForgeMessage());
|
|
25663
|
-
|
|
25879
|
+
}
|
|
25664
25880
|
return result3(threads.map((thread) => `${thread.id} ${thread.file ?? "review"}:${thread.line ?? "-"} — ${thread.body}`).join(`
|
|
25665
|
-
`) || "No comments.", {
|
|
25666
|
-
}
|
|
25667
|
-
}),
|
|
25668
|
-
defineTool6({
|
|
25669
|
-
name: "review_dump",
|
|
25670
|
-
label: "review dump",
|
|
25671
|
-
description: "Save one immutable local tuicr review revision and remove its completed session.",
|
|
25672
|
-
promptSnippet: "Call review_dump before applying local review feedback",
|
|
25673
|
-
promptGuidelines: ["Each dump advances the local review revision."],
|
|
25674
|
-
parameters: parameters6(localSchema),
|
|
25675
|
-
executionMode: "sequential",
|
|
25676
|
-
async execute(_id, input) {
|
|
25677
|
-
const params = localSchema.parse(input);
|
|
25678
|
-
const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
|
|
25679
|
-
const session = await resolveTuicrSession(review, true);
|
|
25680
|
-
if (!session)
|
|
25681
|
-
return result3("No matching local tuicr session found.");
|
|
25682
|
-
const base = review.pr?.baseRef ?? (review.forge ? await review.forge.defaultBranch() : "HEAD");
|
|
25683
|
-
const captured = await captureLocalReview({
|
|
25684
|
-
cwd: review.cwd,
|
|
25685
|
-
branch: review.vcs.branch,
|
|
25686
|
-
base,
|
|
25687
|
-
diff: await workingTreeDiff(review.cwd),
|
|
25688
|
-
session
|
|
25689
|
-
});
|
|
25690
|
-
return result3(captured.dump.comments.map((thread) => `${thread.id} ${thread.file ?? "review"}:${thread.line ?? "-"} — ${thread.body}`).join(`
|
|
25691
|
-
`) || "No comments.", { artifact: captured.path, review: captured.dump, threads: captured.dump.comments });
|
|
25881
|
+
`) || "No comments.", { artifact, threads });
|
|
25692
25882
|
}
|
|
25693
25883
|
}),
|
|
25694
25884
|
defineTool6({
|
|
25695
25885
|
name: "review_respond",
|
|
25696
25886
|
label: "review respond",
|
|
25697
|
-
description: "
|
|
25698
|
-
promptSnippet: "Call review_respond after addressing a
|
|
25699
|
-
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."],
|
|
25700
25890
|
parameters: parameters6(respondSchema),
|
|
25701
25891
|
executionMode: "sequential",
|
|
25702
25892
|
async execute(_id, input, _signal, _onUpdate, ctx) {
|
|
25703
25893
|
const params = respondSchema.parse(input);
|
|
25704
|
-
if (params.local)
|
|
25705
|
-
return result3("Local reviews use immutable revision dumps, not thread replies.");
|
|
25706
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
|
+
}
|
|
25707
25926
|
if (!review.pr || !review.forge)
|
|
25708
25927
|
return result3("No remote PR/MR for this reply.");
|
|
25709
|
-
const
|
|
25710
|
-
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({
|
|
25711
25934
|
threadId: params.threadId,
|
|
25712
|
-
body: withRemoteProvenance(params.body,
|
|
25935
|
+
body: withRemoteProvenance(params.body, model),
|
|
25713
25936
|
resolve,
|
|
25714
|
-
question
|
|
25937
|
+
question
|
|
25715
25938
|
});
|
|
25716
25939
|
return result3(`Replied to ${params.threadId}${resolve ? " and resolved it" : " and left it open"}.`, {
|
|
25717
25940
|
pr: review.pr,
|
|
@@ -25722,40 +25945,51 @@ ${openMessage}`, {
|
|
|
25722
25945
|
defineTool6({
|
|
25723
25946
|
name: "review_publish",
|
|
25724
25947
|
label: "review publish",
|
|
25725
|
-
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.",
|
|
25726
25949
|
promptSnippet: "Call review_publish to make remote review work public",
|
|
25727
|
-
promptGuidelines: ["
|
|
25950
|
+
promptGuidelines: ["Statuses are COMMENT, APPROVE, REQUEST_CHANGES, or CLOSE."],
|
|
25728
25951
|
parameters: parameters6(publishSchema),
|
|
25729
25952
|
executionMode: "sequential",
|
|
25730
|
-
async execute(_id, input) {
|
|
25953
|
+
async execute(_id, input, _signal, _onUpdate, ctx) {
|
|
25731
25954
|
const params = publishSchema.parse(input);
|
|
25732
|
-
if (params.local)
|
|
25733
|
-
return result3("Local reviews use immutable revision dumps and have no publish step.");
|
|
25734
25955
|
const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
|
|
25735
25956
|
if (!review.pr || !review.forge)
|
|
25736
25957
|
return result3("No remote PR/MR to publish.");
|
|
25737
|
-
return publishResolvedReview(review, params);
|
|
25958
|
+
return publishResolvedReview(review, params, modelRoute(ctx));
|
|
25738
25959
|
}
|
|
25739
25960
|
}),
|
|
25740
25961
|
defineTool6({
|
|
25741
25962
|
name: "review_complete",
|
|
25742
25963
|
label: "review complete",
|
|
25743
|
-
description: "Approve, reject, or abandon a remote review.",
|
|
25744
|
-
promptSnippet: "Call review_complete to finish a
|
|
25745
|
-
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."],
|
|
25746
25967
|
parameters: parameters6(completeSchema),
|
|
25747
25968
|
executionMode: "sequential",
|
|
25748
|
-
async execute(_id, input) {
|
|
25969
|
+
async execute(_id, input, _signal, _onUpdate, ctx) {
|
|
25749
25970
|
const params = completeSchema.parse(input);
|
|
25750
|
-
if (params.local)
|
|
25751
|
-
return result3("Use review_dump to finish a local review revision.");
|
|
25752
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
|
+
}
|
|
25753
25987
|
if (!review.pr || !review.forge)
|
|
25754
25988
|
return result3("No remote PR/MR to complete.");
|
|
25755
25989
|
if (!params.action)
|
|
25756
25990
|
return result3("Choose a complete action: approve, reject, or abandon.");
|
|
25757
25991
|
const status = { approve: "APPROVE", reject: "REQUEST_CHANGES", abandon: "CLOSE" }[params.action];
|
|
25758
|
-
return publishResolvedReview(review, { ...params, status });
|
|
25992
|
+
return publishResolvedReview(review, { ...params, status }, modelRoute(ctx));
|
|
25759
25993
|
}
|
|
25760
25994
|
}),
|
|
25761
25995
|
defineTool6({
|
|
@@ -25764,10 +25998,10 @@ ${openMessage}`, {
|
|
|
25764
25998
|
description: "Squash-merge an open, ready GitHub PR after checking its conventional subject.",
|
|
25765
25999
|
promptSnippet: "Call review_merge after confirming the PR is open, ready, and CI is settled; approval is optional",
|
|
25766
26000
|
promptGuidelines: ["This is intentionally GitHub-only until GitLab merge support is added."],
|
|
25767
|
-
parameters: parameters6(contextSchema.extend({ subject:
|
|
26001
|
+
parameters: parameters6(contextSchema.extend({ subject: z13.string().optional() })),
|
|
25768
26002
|
executionMode: "sequential",
|
|
25769
26003
|
async execute(_id, input) {
|
|
25770
|
-
const params = contextSchema.extend({ subject:
|
|
26004
|
+
const params = contextSchema.extend({ subject: z13.string().optional() }).parse(input);
|
|
25771
26005
|
const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
|
|
25772
26006
|
if (review.vcs.provider !== "github" || !review.forge)
|
|
25773
26007
|
return result3("review_merge currently supports GitHub only.");
|
|
@@ -25800,8 +26034,32 @@ ${openMessage}`, {
|
|
|
25800
26034
|
})
|
|
25801
26035
|
];
|
|
25802
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
|
+
}
|
|
25803
26061
|
function parameters6(schema) {
|
|
25804
|
-
return
|
|
26062
|
+
return z13.toJSONSchema(schema, { io: "input" });
|
|
25805
26063
|
}
|
|
25806
26064
|
function resolveWorkingDirectory(params) {
|
|
25807
26065
|
return params.cwd ?? process.cwd();
|
|
@@ -25817,17 +26075,159 @@ function modelRoute(ctx) {
|
|
|
25817
26075
|
throw new Error("Cannot record review provenance because Pi did not provide an active model route.");
|
|
25818
26076
|
return `${ctx.model.provider}/${ctx.model.id}`;
|
|
25819
26077
|
}
|
|
25820
|
-
async function publishResolvedReview(review, params) {
|
|
26078
|
+
async function publishResolvedReview(review, params, model) {
|
|
25821
26079
|
const status = params.status ?? "COMMENT";
|
|
25822
26080
|
const event = status === "CLOSE" ? "COMMENT" : status;
|
|
25823
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;
|
|
25824
26084
|
if (status !== "CLOSE" && review.pr.isDraft)
|
|
25825
26085
|
await review.forge.markReady(review.pr.number);
|
|
25826
|
-
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
|
+
}
|
|
25827
26098
|
if (status === "CLOSE")
|
|
25828
26099
|
await review.forge.closePr(review.pr.number);
|
|
25829
26100
|
const finalPr = await review.forge.viewPr(String(review.pr.number));
|
|
25830
|
-
|
|
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;
|
|
25831
26231
|
}
|
|
25832
26232
|
async function resolveReviewContext(cwd, target) {
|
|
25833
26233
|
const vcs = await detectVcs(cwd);
|
|
@@ -25868,6 +26268,10 @@ async function assertRemoteBranchReady(cwd) {
|
|
|
25868
26268
|
if (head.stdout.trim() !== pushed.stdout.trim())
|
|
25869
26269
|
throw new Error("Push the current branch before opening a remote review.");
|
|
25870
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
|
+
}
|
|
25871
26275
|
async function reviewTargetId(review) {
|
|
25872
26276
|
if (!review.pr)
|
|
25873
26277
|
return "uncommitted";
|
|
@@ -25884,9 +26288,60 @@ async function headSha(cwd) {
|
|
|
25884
26288
|
}
|
|
25885
26289
|
async function newReviewArtifactPath(cwd, target) {
|
|
25886
26290
|
const dir = await reviewsDir(cwd);
|
|
25887
|
-
await
|
|
26291
|
+
await mkdir7(dir, { recursive: true });
|
|
25888
26292
|
return uniqueRecordPath(dir, reviewRecordName(target));
|
|
25889
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
|
+
}
|
|
25890
26345
|
function deriveTitle(branch) {
|
|
25891
26346
|
return branch.replace(/^(feature|feat|fix|bug|chore)\//, "").replace(/^eng-\d+-/i, "").replace(/[-_]+/g, " ").replace(/^\w/, (char) => char.toUpperCase());
|
|
25892
26347
|
}
|
|
@@ -25897,19 +26352,30 @@ function uniqueRecordPath(dir, base) {
|
|
|
25897
26352
|
path = join21(dir, `${base}-${count++}.md`);
|
|
25898
26353
|
return path;
|
|
25899
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
|
+
}
|
|
25900
26366
|
|
|
25901
26367
|
// src/tools/setup.ts
|
|
25902
26368
|
import { defineTool as defineTool7 } from "@earendil-works/pi-coding-agent";
|
|
25903
|
-
import { z as
|
|
26369
|
+
import { z as z14 } from "zod";
|
|
25904
26370
|
|
|
25905
26371
|
// src/setup.ts
|
|
25906
26372
|
import { parseFrontmatter as parseFrontmatter3 } from "@earendil-works/pi-coding-agent";
|
|
25907
|
-
import { readdir as readdir4, readFile as
|
|
26373
|
+
import { readdir as readdir4, readFile as readFile16 } from "node:fs/promises";
|
|
25908
26374
|
import { homedir as homedir10 } from "node:os";
|
|
25909
26375
|
import { basename as basename7, join as join24 } from "node:path";
|
|
25910
26376
|
|
|
25911
26377
|
// src/mcp.ts
|
|
25912
|
-
import { mkdir as
|
|
26378
|
+
import { mkdir as mkdir8, readFile as readFile15, writeFile as writeFile8 } from "node:fs/promises";
|
|
25913
26379
|
import { homedir as homedir8 } from "node:os";
|
|
25914
26380
|
import { dirname as dirname9, join as join22 } from "node:path";
|
|
25915
26381
|
var mcp = {
|
|
@@ -25927,8 +26393,8 @@ var mcp = {
|
|
|
25927
26393
|
const next = { ...current, mcpServers: nextServers };
|
|
25928
26394
|
const changed = JSON.stringify(current) !== JSON.stringify(next);
|
|
25929
26395
|
if (changed && !options.dryRun) {
|
|
25930
|
-
await
|
|
25931
|
-
await
|
|
26396
|
+
await mkdir8(dirname9(path), { recursive: true });
|
|
26397
|
+
await writeFile8(path, `${JSON.stringify(next, null, 2)}
|
|
25932
26398
|
`, "utf8");
|
|
25933
26399
|
}
|
|
25934
26400
|
return { path, changed, existed: currentText !== undefined, planned: changed && options.dryRun === true };
|
|
@@ -25957,7 +26423,7 @@ function getParsedConfig(content, path) {
|
|
|
25957
26423
|
}
|
|
25958
26424
|
async function getOptionalFile2(path) {
|
|
25959
26425
|
try {
|
|
25960
|
-
return await
|
|
26426
|
+
return await readFile15(path, "utf8");
|
|
25961
26427
|
} catch (error) {
|
|
25962
26428
|
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
25963
26429
|
return;
|
|
@@ -25969,7 +26435,7 @@ function isRecord(value) {
|
|
|
25969
26435
|
}
|
|
25970
26436
|
|
|
25971
26437
|
// src/pi.ts
|
|
25972
|
-
import { mkdir as
|
|
26438
|
+
import { mkdir as mkdir9, writeFile as writeFile9 } from "node:fs/promises";
|
|
25973
26439
|
import { homedir as homedir9 } from "node:os";
|
|
25974
26440
|
import { dirname as dirname10, join as join23 } from "node:path";
|
|
25975
26441
|
var pi = {
|
|
@@ -25988,8 +26454,8 @@ async function ensurePiAgent(filename, content, agentDir = resolvePiAgentDir(),
|
|
|
25988
26454
|
const currentText = await readTextIfExists(path);
|
|
25989
26455
|
const changed = currentText !== content;
|
|
25990
26456
|
if (changed && !dryRun) {
|
|
25991
|
-
await
|
|
25992
|
-
await
|
|
26457
|
+
await mkdir9(dirname10(path), { recursive: true });
|
|
26458
|
+
await writeFile9(path, content, "utf8");
|
|
25993
26459
|
}
|
|
25994
26460
|
return { path, changed, existed: currentText !== undefined, planned: changed && dryRun };
|
|
25995
26461
|
}
|
|
@@ -26025,8 +26491,8 @@ async function ensurePiConfig(path, update, dryRun = false) {
|
|
|
26025
26491
|
const next = update(current);
|
|
26026
26492
|
const changed = JSON.stringify(current) !== JSON.stringify(next);
|
|
26027
26493
|
if (changed && !dryRun) {
|
|
26028
|
-
await
|
|
26029
|
-
await
|
|
26494
|
+
await mkdir9(dirname10(path), { recursive: true });
|
|
26495
|
+
await writeFile9(path, `${JSON.stringify(next, null, 2)}
|
|
26030
26496
|
`, "utf8");
|
|
26031
26497
|
}
|
|
26032
26498
|
return { path, changed, existed: currentText !== undefined, planned: changed && dryRun };
|
|
@@ -26164,7 +26630,7 @@ async function ensurePiAgents(options = {}) {
|
|
|
26164
26630
|
const actions = [];
|
|
26165
26631
|
for (const entry of entries) {
|
|
26166
26632
|
const id = basename7(entry.name, ".md").replace(/^diffpi-/, "");
|
|
26167
|
-
const source = await
|
|
26633
|
+
const source = await readFile16(join24(bundledAgentsDir, entry.name), "utf8");
|
|
26168
26634
|
const content = materializeAgentModels(source, id, userConfig.config, options.availableModels);
|
|
26169
26635
|
const result = await pi.agentEnsure(entry.name, content, agentDir, options.dryRun);
|
|
26170
26636
|
actions.push(getConfigSetupAction(`pi agent ${id}`, result));
|
|
@@ -26369,7 +26835,7 @@ function reportProgress(options, message) {
|
|
|
26369
26835
|
options.onProgress?.(message);
|
|
26370
26836
|
}
|
|
26371
26837
|
async function packageVersion2() {
|
|
26372
|
-
const source = await
|
|
26838
|
+
const source = await readFile16(join24(resolveBundledAgentsDir(), "..", "package.json"), "utf8");
|
|
26373
26839
|
let value;
|
|
26374
26840
|
try {
|
|
26375
26841
|
value = JSON.parse(source);
|
|
@@ -26385,13 +26851,13 @@ function getRecord(value) {
|
|
|
26385
26851
|
}
|
|
26386
26852
|
|
|
26387
26853
|
// src/tools/setup.ts
|
|
26388
|
-
var setupParametersSchema =
|
|
26389
|
-
issueTracker:
|
|
26390
|
-
forges:
|
|
26391
|
-
forge:
|
|
26392
|
-
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.")
|
|
26393
26859
|
});
|
|
26394
|
-
var setupParameters =
|
|
26860
|
+
var setupParameters = z14.toJSONSchema(setupParametersSchema, { io: "input" });
|
|
26395
26861
|
var diffpiSetupTool = defineTool7({
|
|
26396
26862
|
name: "diffpi_setup",
|
|
26397
26863
|
label: "diffpi setup",
|
|
@@ -26464,11 +26930,11 @@ ${lines.join(`
|
|
|
26464
26930
|
|
|
26465
26931
|
// src/tools/templates.ts
|
|
26466
26932
|
import { defineTool as defineTool8 } from "@earendil-works/pi-coding-agent";
|
|
26467
|
-
import { z as
|
|
26468
|
-
var templateSchema =
|
|
26469
|
-
name:
|
|
26470
|
-
variables:
|
|
26471
|
-
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()
|
|
26472
26938
|
});
|
|
26473
26939
|
var diffpiTemplateTool = defineTool8({
|
|
26474
26940
|
name: "diffpi_template",
|
|
@@ -26476,7 +26942,7 @@ var diffpiTemplateTool = defineTool8({
|
|
|
26476
26942
|
description: "Load a bundled Diffpi template or a user override and render named variables.",
|
|
26477
26943
|
promptSnippet: "Use diffpi_template for package workflow templates",
|
|
26478
26944
|
promptGuidelines: ["User overrides live under ~/.difflab/diffpi/templates."],
|
|
26479
|
-
parameters:
|
|
26945
|
+
parameters: z15.toJSONSchema(templateSchema, { io: "input" }),
|
|
26480
26946
|
executionMode: "parallel",
|
|
26481
26947
|
async execute(_id, input) {
|
|
26482
26948
|
const params = templateSchema.parse(input);
|