@difflab/pi 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/agents/diffpi-copilot.md +6 -3
- package/agents/diffpi-reviewer.md +3 -3
- package/dist/commands/review.d.ts +0 -1
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/extensions/index.js +344 -810
- package/dist/extensions/tuicrx.d.ts +1 -8
- package/dist/extensions/tuicrx.d.ts.map +1 -1
- package/dist/index.d.ts +7 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +197 -458
- 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-reviews.d.ts +31 -0
- package/dist/review/local-reviews.d.ts.map +1 -0
- package/dist/review/review-backend.d.ts +2 -3
- package/dist/review/review-backend.d.ts.map +1 -1
- package/dist/review/review-markdown.d.ts +1 -4
- package/dist/review/review-markdown.d.ts.map +1 -1
- package/dist/review/types.d.ts +0 -28
- package/dist/review/types.d.ts.map +1 -1
- package/dist/store.d.ts +0 -2
- package/dist/store.d.ts.map +1 -1
- package/dist/tools/index.js +185 -783
- package/dist/tools/review.d.ts +0 -30
- package/dist/tools/review.d.ts.map +1 -1
- package/dist/workflow.d.ts +47 -0
- package/dist/workflow.d.ts.map +1 -1
- package/package.json +1 -1
- package/skills/review/SKILL.md +6 -8
- package/workflows/review/address.md +8 -0
- package/workflows/review/auto.md +8 -0
- package/workflows/review/complete.md +7 -0
- package/workflows/review/edit.md +7 -0
- package/workflows/review/help.md +14 -0
- package/workflows/review/merge.md +6 -0
- package/workflows/review/new.md +7 -0
- package/workflows/review/publish.md +6 -0
- package/dist/review/local-review-backend.d.ts +0 -3
- package/dist/review/local-review-backend.d.ts.map +0 -1
- package/dist/review/review-state.d.ts +0 -20
- package/dist/review/review-state.d.ts.map +0 -1
- package/skills/review/references/workflows/address.md +0 -11
- package/skills/review/references/workflows/auto.md +0 -18
- package/skills/review/references/workflows/complete.md +0 -7
- package/skills/review/references/workflows/edit.md +0 -7
- package/skills/review/references/workflows/help.md +0 -30
- package/skills/review/references/workflows/merge.md +0 -6
- package/skills/review/references/workflows/new.md +0 -19
- package/skills/review/references/workflows/open.md +0 -3
- package/skills/review/references/workflows/publish.md +0 -8
package/dist/extensions/index.js
CHANGED
|
@@ -16642,8 +16642,14 @@ 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
|
-
|
|
16646
|
-
|
|
16645
|
+
function loadPlanWorkflow(name, args) {
|
|
16646
|
+
return loadWorkflow("plan", name, args);
|
|
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);
|
|
16647
16653
|
const content = await readFile3(join6(workflowDir, `${name}.md`), "utf8");
|
|
16648
16654
|
return [`Workflow directory: ${workflowDir}`, content.trim(), `Arguments:
|
|
16649
16655
|
${JSON.stringify(args, null, 2)}`].join(`
|
|
@@ -16854,46 +16860,172 @@ function registerReloadCommand(pi) {
|
|
|
16854
16860
|
}
|
|
16855
16861
|
|
|
16856
16862
|
// src/commands/review.ts
|
|
16857
|
-
var REVIEWER_VERBS = new Set(["address", "auto", "launch"]);
|
|
16858
16863
|
function registerReviewCommand(pi, modes) {
|
|
16859
16864
|
pi.registerCommand("review", {
|
|
16860
|
-
description: "Code review: auto, new, edit, address, publish, complete, merge
|
|
16861
|
-
handler: async (args, ctx) =>
|
|
16865
|
+
description: "Code review: auto, new, edit, address, publish, complete, merge, help",
|
|
16866
|
+
handler: async (args, ctx) => runReviewCommand(args, { pi, ctx, modes })
|
|
16862
16867
|
});
|
|
16863
16868
|
}
|
|
16864
|
-
async function
|
|
16865
|
-
|
|
16866
|
-
|
|
16867
|
-
|
|
16868
|
-
|
|
16869
|
-
|
|
16870
|
-
|
|
16871
|
-
|
|
16872
|
-
|
|
16873
|
-
|
|
16874
|
-
});
|
|
16875
|
-
return;
|
|
16869
|
+
async function runReviewCommand(raw, runtime) {
|
|
16870
|
+
try {
|
|
16871
|
+
const reviewCommand = createReviewCommand(runtime);
|
|
16872
|
+
const parsed = await dryRun(reviewCommand, tokenizeCommandArgs(raw));
|
|
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");
|
|
16876
16879
|
}
|
|
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 });
|
|
16884
|
-
}
|
|
16885
|
-
function reviewVerb(invocation) {
|
|
16886
|
-
return invocation.split(/\s+/).find((token) => token && !token.startsWith("-")) ?? "help";
|
|
16887
16880
|
}
|
|
16888
|
-
function
|
|
16889
|
-
const
|
|
16890
|
-
|
|
16881
|
+
function createReviewCommand(runtime) {
|
|
16882
|
+
const invokeWorkflow = createReviewWorkflowInvoker(runtime);
|
|
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
|
+
});
|
|
16891
17002
|
}
|
|
16892
|
-
function
|
|
16893
|
-
return
|
|
17003
|
+
function createReviewWorkflowInvoker(runtime) {
|
|
17004
|
+
return async function invokeWorkflow(invocation) {
|
|
17005
|
+
const { workflow, arguments: args, agent, isBackground = false } = invocation;
|
|
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
|
+
};
|
|
16894
17026
|
}
|
|
16895
|
-
function
|
|
16896
|
-
return
|
|
17027
|
+
function targetArgument() {
|
|
17028
|
+
return positional({ type: optional(string5), displayName: "target" });
|
|
16897
17029
|
}
|
|
16898
17030
|
|
|
16899
17031
|
// src/commands/index.ts
|
|
@@ -17774,18 +17906,6 @@ async function reviewsDir(cwd, homeDir = homedir5()) {
|
|
|
17774
17906
|
await mkdir2(dir, { recursive: true });
|
|
17775
17907
|
return dir;
|
|
17776
17908
|
}
|
|
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
|
-
}
|
|
17789
17909
|
async function assertStoreLink(path, dest) {
|
|
17790
17910
|
const entry = await lstat(path);
|
|
17791
17911
|
if (!entry.isSymbolicLink())
|
|
@@ -24608,10 +24728,10 @@ function result2(text, details = {}) {
|
|
|
24608
24728
|
|
|
24609
24729
|
// src/tools/review.ts
|
|
24610
24730
|
import { existsSync as existsSync3 } from "node:fs";
|
|
24611
|
-
import { mkdir as
|
|
24731
|
+
import { mkdir as mkdir8, writeFile as writeFile6 } from "node:fs/promises";
|
|
24612
24732
|
import { join as join21 } from "node:path";
|
|
24613
24733
|
import { defineTool as defineTool6 } from "@earendil-works/pi-coding-agent";
|
|
24614
|
-
import { z as
|
|
24734
|
+
import { z as z11 } from "zod";
|
|
24615
24735
|
|
|
24616
24736
|
// src/review/types.ts
|
|
24617
24737
|
import { z as z10 } from "zod";
|
|
@@ -24636,21 +24756,6 @@ function yymmdd(date = new Date) {
|
|
|
24636
24756
|
function reviewRecordName(target, date = new Date) {
|
|
24637
24757
|
return `${yymmdd(date)}-${reviewSlug(target) || "uncommitted"}`;
|
|
24638
24758
|
}
|
|
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
|
-
}
|
|
24654
24759
|
function dedupeFindings(findings) {
|
|
24655
24760
|
const rank = { BLOCKING: 3, CONSIDER: 2, NOTE: 1 };
|
|
24656
24761
|
const byKey = new Map;
|
|
@@ -24696,18 +24801,6 @@ function renderCommentBody(finding) {
|
|
|
24696
24801
|
return `${prefix}${finding.body}${reference}`;
|
|
24697
24802
|
}
|
|
24698
24803
|
// 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
|
-
});
|
|
24711
24804
|
function renderReviewDoc(input) {
|
|
24712
24805
|
const anchored = dedupeFindings(input.findings).filter((finding) => finding.line > 0);
|
|
24713
24806
|
const lines = [`# Review: ${input.title}`, "", "## Metadata"];
|
|
@@ -24749,88 +24842,6 @@ function renderReviewDoc(input) {
|
|
|
24749
24842
|
`).trimEnd()}
|
|
24750
24843
|
`;
|
|
24751
24844
|
}
|
|
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
|
-
}
|
|
24834
24845
|
// src/review/github-review-backend.ts
|
|
24835
24846
|
function GitHubReviewBackend(vcs, number) {
|
|
24836
24847
|
const pendingReview = async () => {
|
|
@@ -24892,9 +24903,6 @@ function GitHubReviewBackend(vcs, number) {
|
|
|
24892
24903
|
id: thread.id,
|
|
24893
24904
|
file: thread.path ?? undefined,
|
|
24894
24905
|
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] : []),
|
|
24898
24906
|
body,
|
|
24899
24907
|
author: comment?.author?.login,
|
|
24900
24908
|
resolved: thread.isResolved,
|
|
@@ -24904,7 +24912,6 @@ function GitHubReviewBackend(vcs, number) {
|
|
|
24904
24912
|
});
|
|
24905
24913
|
};
|
|
24906
24914
|
return {
|
|
24907
|
-
kind: "remote",
|
|
24908
24915
|
async stage(draft) {
|
|
24909
24916
|
const pending = await pendingReview();
|
|
24910
24917
|
if (!pending) {
|
|
@@ -24962,33 +24969,6 @@ function GitHubReviewBackend(vcs, number) {
|
|
|
24962
24969
|
await ghChecked(args);
|
|
24963
24970
|
}
|
|
24964
24971
|
},
|
|
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
|
-
},
|
|
24992
24972
|
listThreads,
|
|
24993
24973
|
async reply(input) {
|
|
24994
24974
|
const threads = await listThreads();
|
|
@@ -25005,32 +24985,14 @@ function GitHubReviewBackend(vcs, number) {
|
|
|
25005
24985
|
`body=${input.body}`
|
|
25006
24986
|
]);
|
|
25007
24987
|
}
|
|
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) {
|
|
24988
|
+
if (input.resolve) {
|
|
25027
24989
|
await ghChecked([
|
|
25028
24990
|
"api",
|
|
25029
24991
|
"graphql",
|
|
25030
24992
|
"-f",
|
|
25031
|
-
`query=${
|
|
24993
|
+
`query=${GITHUB_RESOLVE_MUTATION}`,
|
|
25032
24994
|
"-f",
|
|
25033
|
-
`
|
|
24995
|
+
`threadId=${input.threadId}`
|
|
25034
24996
|
]);
|
|
25035
24997
|
}
|
|
25036
24998
|
},
|
|
@@ -25049,13 +25011,11 @@ function GitHubReviewBackend(vcs, number) {
|
|
|
25049
25011
|
function githubReviewSubmissionEndpoint(owner, repo, id, pendingReviewId) {
|
|
25050
25012
|
return pendingReviewId ? `/repos/${owner}/${repo}/pulls/${id}/reviews/${pendingReviewId}/events` : `/repos/${owner}/${repo}/pulls/${id}/reviews`;
|
|
25051
25013
|
}
|
|
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{
|
|
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}}}}}`;
|
|
25053
25015
|
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}}}`;
|
|
25054
25016
|
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}}}`;
|
|
25055
25017
|
var GITHUB_REPLY_MUTATION = `mutation($threadId:ID!,$body:String!){addPullRequestReviewThreadReply(input:{pullRequestReviewThreadId:$threadId,body:$body}){comment{id}}}`;
|
|
25056
25018
|
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}}`;
|
|
25059
25019
|
|
|
25060
25020
|
// src/review/gitlab-review-backend.ts
|
|
25061
25021
|
function GitLabReviewBackend(vcs, number) {
|
|
@@ -25092,7 +25052,6 @@ function GitLabReviewBackend(vcs, number) {
|
|
|
25092
25052
|
});
|
|
25093
25053
|
};
|
|
25094
25054
|
return {
|
|
25095
|
-
kind: "remote",
|
|
25096
25055
|
async stage(draft) {
|
|
25097
25056
|
const endpoint = `${mergeRequestEndpoint()}/draft_notes`;
|
|
25098
25057
|
if (draft.body.trim()) {
|
|
@@ -25121,36 +25080,6 @@ function GitLabReviewBackend(vcs, number) {
|
|
|
25121
25080
|
});
|
|
25122
25081
|
}
|
|
25123
25082
|
},
|
|
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
|
-
},
|
|
25154
25083
|
listThreads,
|
|
25155
25084
|
async reply(input) {
|
|
25156
25085
|
const endpoint = `${mergeRequestEndpoint()}/discussions/${encodeURIComponent(input.threadId)}`;
|
|
@@ -25162,15 +25091,7 @@ function GitLabReviewBackend(vcs, number) {
|
|
|
25162
25091
|
});
|
|
25163
25092
|
}
|
|
25164
25093
|
if (input.resolve)
|
|
25165
|
-
await
|
|
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]);
|
|
25094
|
+
await glabChecked(["api", "--method", "PUT", `${endpoint}?resolved=true`]);
|
|
25174
25095
|
},
|
|
25175
25096
|
async publish(event) {
|
|
25176
25097
|
assertReviewEventSupported(vcs.provider, event);
|
|
@@ -25210,8 +25131,17 @@ function hasGitlabDraftNotes(input) {
|
|
|
25210
25131
|
}
|
|
25211
25132
|
}
|
|
25212
25133
|
|
|
25213
|
-
// src/review/
|
|
25214
|
-
|
|
25134
|
+
// src/review/review-backend.ts
|
|
25135
|
+
function createRemoteReviewBackend(vcs, number) {
|
|
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";
|
|
25215
25145
|
|
|
25216
25146
|
// src/extensions/tuicrx.ts
|
|
25217
25147
|
import { readFile as readFile11, realpath as realpath4 } from "node:fs/promises";
|
|
@@ -25282,11 +25212,6 @@ async function resolveReviewSession(cwd, target) {
|
|
|
25282
25212
|
}
|
|
25283
25213
|
return resolveSession(cwd, target.branch);
|
|
25284
25214
|
}
|
|
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
|
-
}
|
|
25290
25215
|
async function resolvePrSession(cwd, owner, repo, number) {
|
|
25291
25216
|
const coordinate = `${owner}/${repo}`.toLowerCase();
|
|
25292
25217
|
const sessions = await listSessions(cwd);
|
|
@@ -25321,8 +25246,6 @@ async function readSession(path) {
|
|
|
25321
25246
|
function toLocalReviewThreads(session) {
|
|
25322
25247
|
const threads = [];
|
|
25323
25248
|
const add = (comment, location = {}) => {
|
|
25324
|
-
if (isLocalResponse(comment.content))
|
|
25325
|
-
return;
|
|
25326
25249
|
const author = commentAuthor(comment);
|
|
25327
25250
|
threads.push({
|
|
25328
25251
|
id: comment.id ?? `local-${threads.length + 1}`,
|
|
@@ -25348,54 +25271,6 @@ function toLocalReviewThreads(session) {
|
|
|
25348
25271
|
}
|
|
25349
25272
|
return threads;
|
|
25350
25273
|
}
|
|
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
|
-
}
|
|
25399
25274
|
async function requireTuicr() {
|
|
25400
25275
|
if (!await tuicrAvailable())
|
|
25401
25276
|
throw new Error("tuicr is not installed or is not available on PATH.");
|
|
@@ -25411,167 +25286,83 @@ async function canonicalPath3(path) {
|
|
|
25411
25286
|
}
|
|
25412
25287
|
}
|
|
25413
25288
|
|
|
25414
|
-
// src/review/local-
|
|
25415
|
-
function
|
|
25416
|
-
|
|
25417
|
-
|
|
25418
|
-
|
|
25419
|
-
|
|
25420
|
-
|
|
25421
|
-
|
|
25422
|
-
|
|
25423
|
-
|
|
25424
|
-
|
|
25425
|
-
|
|
25426
|
-
|
|
25427
|
-
|
|
25428
|
-
|
|
25429
|
-
|
|
25430
|
-
|
|
25431
|
-
|
|
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
|
-
}
|
|
25289
|
+
// src/review/local-reviews.ts
|
|
25290
|
+
async function captureLocalReview(input) {
|
|
25291
|
+
const id = reviewSlug(input.branch) || "local-review";
|
|
25292
|
+
const dir = join20(await reviewsDir(input.cwd, input.homeDir), id);
|
|
25293
|
+
await mkdir7(dir, { recursive: true });
|
|
25294
|
+
const revision = await nextRevision(dir);
|
|
25295
|
+
const output = await readFile12(input.session.path, "utf8");
|
|
25296
|
+
const comments = toLocalReviewThreads(await readSession(input.session.path));
|
|
25297
|
+
const dump = {
|
|
25298
|
+
schemaVersion: 1,
|
|
25299
|
+
id,
|
|
25300
|
+
revision,
|
|
25301
|
+
branch: input.branch,
|
|
25302
|
+
base: input.base,
|
|
25303
|
+
createdAt: (input.now ?? (() => new Date))().toISOString(),
|
|
25304
|
+
diff: input.diff,
|
|
25305
|
+
comments,
|
|
25306
|
+
tuicr: { slug: input.session.slug, output }
|
|
25449
25307
|
};
|
|
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 };
|
|
25450
25313
|
}
|
|
25451
|
-
|
|
25452
|
-
|
|
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}`);
|
|
25484
|
-
}
|
|
25485
|
-
function unpublishedReviewComments(comments, knownFingerprints) {
|
|
25486
|
-
return comments.filter((comment) => !knownFingerprints.has(reviewCommentFingerprint(comment)));
|
|
25314
|
+
async function nextRevision(dir) {
|
|
25315
|
+
return await latestRevision(dir) + 1;
|
|
25487
25316
|
}
|
|
25488
|
-
async function
|
|
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`);
|
|
25317
|
+
async function latestRevision(dir) {
|
|
25491
25318
|
try {
|
|
25492
|
-
|
|
25493
|
-
|
|
25319
|
+
return (await readdir3(dir)).reduce((latest, name) => {
|
|
25320
|
+
const match = name.match(/^(\d+)\.json$/);
|
|
25321
|
+
return match ? Math.max(latest, Number(match[1])) : latest;
|
|
25322
|
+
}, 0);
|
|
25494
25323
|
} catch (error) {
|
|
25495
|
-
if (error.code === "ENOENT")
|
|
25496
|
-
return
|
|
25497
|
-
}
|
|
25498
|
-
if (error instanceof SyntaxError || error instanceof z12.ZodError) {
|
|
25499
|
-
throw new Error(`Cannot parse review publication state: ${path}`);
|
|
25500
|
-
}
|
|
25324
|
+
if (error.code === "ENOENT")
|
|
25325
|
+
return 0;
|
|
25501
25326
|
throw error;
|
|
25502
25327
|
}
|
|
25503
25328
|
}
|
|
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
|
-
}
|
|
25513
25329
|
// src/tools/review.ts
|
|
25514
|
-
var contextSchema =
|
|
25515
|
-
cwd:
|
|
25516
|
-
target:
|
|
25517
|
-
workingTree:
|
|
25330
|
+
var contextSchema = z11.object({
|
|
25331
|
+
cwd: z11.string().optional(),
|
|
25332
|
+
target: z11.string().optional(),
|
|
25333
|
+
workingTree: z11.boolean().optional()
|
|
25518
25334
|
});
|
|
25519
|
-
var localSchema = contextSchema.extend({ local:
|
|
25335
|
+
var localSchema = contextSchema.extend({ local: z11.boolean().optional() });
|
|
25520
25336
|
var openSchema = localSchema.extend({
|
|
25521
|
-
title:
|
|
25522
|
-
intent:
|
|
25523
|
-
issueUrl:
|
|
25524
|
-
base:
|
|
25337
|
+
title: z11.string().optional(),
|
|
25338
|
+
intent: z11.string().optional(),
|
|
25339
|
+
issueUrl: z11.string().url().optional(),
|
|
25340
|
+
base: z11.string().optional()
|
|
25525
25341
|
});
|
|
25526
25342
|
var submitSchema = localSchema.extend({
|
|
25527
25343
|
findings: findingsSchema,
|
|
25528
|
-
overallIssues:
|
|
25529
|
-
notVerified:
|
|
25530
|
-
title:
|
|
25344
|
+
overallIssues: z11.array(z11.string()).optional(),
|
|
25345
|
+
notVerified: z11.array(z11.string()).optional(),
|
|
25346
|
+
title: z11.string().optional()
|
|
25531
25347
|
});
|
|
25532
25348
|
var addCommentSchema = localSchema.extend({
|
|
25533
|
-
body:
|
|
25534
|
-
file:
|
|
25535
|
-
line:
|
|
25536
|
-
side:
|
|
25349
|
+
body: z11.string().min(1),
|
|
25350
|
+
file: z11.string().min(1),
|
|
25351
|
+
line: z11.number().int().positive(),
|
|
25352
|
+
side: z11.enum(["LEFT", "RIGHT"]).optional()
|
|
25537
25353
|
});
|
|
25538
25354
|
var respondSchema = localSchema.extend({
|
|
25539
|
-
threadId:
|
|
25540
|
-
body:
|
|
25541
|
-
question:
|
|
25542
|
-
resolve:
|
|
25355
|
+
threadId: z11.string().min(1),
|
|
25356
|
+
body: z11.string().min(1),
|
|
25357
|
+
question: z11.boolean().optional(),
|
|
25358
|
+
resolve: z11.boolean().optional()
|
|
25543
25359
|
});
|
|
25544
25360
|
var publishSchema = localSchema.extend({
|
|
25545
|
-
status:
|
|
25361
|
+
status: z11.enum(["COMMENT", "APPROVE", "REQUEST_CHANGES", "CLOSE"]).optional()
|
|
25546
25362
|
});
|
|
25547
25363
|
var completeSchema = localSchema.extend({
|
|
25548
|
-
action:
|
|
25364
|
+
action: z11.enum(["approve", "reject", "abandon"]).optional()
|
|
25549
25365
|
});
|
|
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
|
-
}
|
|
25575
25366
|
async function workingTreeDiff(cwd) {
|
|
25576
25367
|
const tracked = await run2("git", ["-C", cwd, "diff", "HEAD"], { capture: "unbounded" });
|
|
25577
25368
|
if (tracked.code !== 0)
|
|
@@ -25767,7 +25558,7 @@ ${openMessage}`, {
|
|
|
25767
25558
|
const gates = await runMiseGates(review.cwd);
|
|
25768
25559
|
const baseRef = review.pr?.baseRef ?? (review.forge ? await review.forge.defaultBranch() : "local");
|
|
25769
25560
|
const artifact = await newReviewArtifactPath(review.cwd, params.local || params.workingTree || !review.pr ? "uncommitted" : await reviewTargetId(review));
|
|
25770
|
-
await
|
|
25561
|
+
await writeFile6(artifact, renderReviewDoc({
|
|
25771
25562
|
title: params.title ?? review.pr?.title ?? review.vcs.branch,
|
|
25772
25563
|
number: useLocalBackend ? undefined : review.pr?.number,
|
|
25773
25564
|
url: useLocalBackend ? undefined : review.pr?.url,
|
|
@@ -25790,17 +25581,21 @@ ${openMessage}`, {
|
|
|
25790
25581
|
count: findings.length
|
|
25791
25582
|
});
|
|
25792
25583
|
}
|
|
25793
|
-
const
|
|
25794
|
-
|
|
25795
|
-
|
|
25796
|
-
|
|
25797
|
-
|
|
25798
|
-
|
|
25584
|
+
for (const comment of comments) {
|
|
25585
|
+
await addComment(session.path, comment.body, {
|
|
25586
|
+
targetFile: comment.file,
|
|
25587
|
+
line: comment.line,
|
|
25588
|
+
side: comment.side === "LEFT" ? "old" : "new",
|
|
25589
|
+
username: localReviewAuthor(model)
|
|
25590
|
+
});
|
|
25591
|
+
}
|
|
25592
|
+
if (body.trim())
|
|
25593
|
+
await addComment(session.path, body, { username: localReviewAuthor(model) });
|
|
25799
25594
|
return result3(`Local review staged in tuicr: ${artifact}`, { artifact, count: findings.length, session });
|
|
25800
25595
|
}
|
|
25801
25596
|
if (!review.pr)
|
|
25802
25597
|
return result3(`Review written: ${artifact}. No PR/MR matches this remote target.`, { artifact });
|
|
25803
|
-
const hasDraft =
|
|
25598
|
+
const hasDraft = comments.length > 0 || body.trim().length > 0;
|
|
25804
25599
|
if (hasDraft) {
|
|
25805
25600
|
await createRemoteReviewBackend(review.vcs, review.pr.number).stage({ comments, body });
|
|
25806
25601
|
}
|
|
@@ -25836,12 +25631,12 @@ ${openMessage}`, {
|
|
|
25836
25631
|
const session = await resolveTuicrSession(review, Boolean(params.local || params.workingTree));
|
|
25837
25632
|
if (!session)
|
|
25838
25633
|
return result3("No matching tuicr session. Open review_new or review_launch_ui first.");
|
|
25839
|
-
|
|
25840
|
-
|
|
25841
|
-
|
|
25842
|
-
|
|
25634
|
+
await addComment(session.path, comment.body, {
|
|
25635
|
+
targetFile: comment.file,
|
|
25636
|
+
line: comment.line,
|
|
25637
|
+
side: comment.side === "LEFT" ? "old" : "new",
|
|
25638
|
+
username: localReviewAuthor(model)
|
|
25843
25639
|
});
|
|
25844
|
-
await backend.stage({ comments: [comment], body: "" });
|
|
25845
25640
|
return result3(`Comment added to tuicr session ${session.slug}.`, { session, comment });
|
|
25846
25641
|
}
|
|
25847
25642
|
if (!review.pr)
|
|
@@ -25853,88 +25648,70 @@ ${openMessage}`, {
|
|
|
25853
25648
|
defineTool6({
|
|
25854
25649
|
name: "review_comments",
|
|
25855
25650
|
label: "review comments",
|
|
25856
|
-
description: "
|
|
25857
|
-
promptSnippet: "Call review_comments before addressing findings",
|
|
25858
|
-
promptGuidelines: ["
|
|
25651
|
+
description: "Read remote GitHub or GitLab review threads.",
|
|
25652
|
+
promptSnippet: "Call review_comments before addressing remote findings",
|
|
25653
|
+
promptGuidelines: ["Use review_dump for a local tuicr review."],
|
|
25859
25654
|
parameters: parameters6(localSchema),
|
|
25860
|
-
executionMode: "
|
|
25655
|
+
executionMode: "parallel",
|
|
25861
25656
|
async execute(_id, input) {
|
|
25862
25657
|
const params = localSchema.parse(input);
|
|
25658
|
+
if (params.local || params.workingTree)
|
|
25659
|
+
return result3("Use review_dump for a local tuicr review.");
|
|
25863
25660
|
const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
|
|
25864
|
-
|
|
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 {
|
|
25661
|
+
if (!review.pr || !review.forge)
|
|
25878
25662
|
return result3(review.forge ? "No PR/MR matches this remote review target." : unsupportedForgeMessage());
|
|
25879
|
-
|
|
25663
|
+
const threads = await createRemoteReviewBackend(review.vcs, review.pr.number).listThreads();
|
|
25880
25664
|
return result3(threads.map((thread) => `${thread.id} ${thread.file ?? "review"}:${thread.line ?? "-"} — ${thread.body}`).join(`
|
|
25881
|
-
`) || "No comments.", {
|
|
25665
|
+
`) || "No comments.", { pr: review.pr, threads });
|
|
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 });
|
|
25882
25692
|
}
|
|
25883
25693
|
}),
|
|
25884
25694
|
defineTool6({
|
|
25885
25695
|
name: "review_respond",
|
|
25886
25696
|
label: "review respond",
|
|
25887
|
-
description: "
|
|
25888
|
-
promptSnippet: "Call review_respond after addressing a comment",
|
|
25889
|
-
promptGuidelines: ["
|
|
25697
|
+
description: "Reply directly to a remote GitHub or GitLab review thread.",
|
|
25698
|
+
promptSnippet: "Call review_respond after addressing a remote comment",
|
|
25699
|
+
promptGuidelines: ["Leave threads open unless the user explicitly requests resolution."],
|
|
25890
25700
|
parameters: parameters6(respondSchema),
|
|
25891
25701
|
executionMode: "sequential",
|
|
25892
25702
|
async execute(_id, input, _signal, _onUpdate, ctx) {
|
|
25893
25703
|
const params = respondSchema.parse(input);
|
|
25704
|
+
if (params.local)
|
|
25705
|
+
return result3("Local reviews use immutable revision dumps, not thread replies.");
|
|
25894
25706
|
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
|
-
}
|
|
25926
25707
|
if (!review.pr || !review.forge)
|
|
25927
25708
|
return result3("No remote PR/MR for this reply.");
|
|
25928
|
-
const
|
|
25929
|
-
|
|
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({
|
|
25709
|
+
const resolve = params.question ? false : params.resolve ?? false;
|
|
25710
|
+
await createRemoteReviewBackend(review.vcs, review.pr.number).reply({
|
|
25934
25711
|
threadId: params.threadId,
|
|
25935
|
-
body: withRemoteProvenance(params.body,
|
|
25712
|
+
body: withRemoteProvenance(params.body, modelRoute(ctx)),
|
|
25936
25713
|
resolve,
|
|
25937
|
-
question
|
|
25714
|
+
question: params.question
|
|
25938
25715
|
});
|
|
25939
25716
|
return result3(`Replied to ${params.threadId}${resolve ? " and resolved it" : " and left it open"}.`, {
|
|
25940
25717
|
pr: review.pr,
|
|
@@ -25945,51 +25722,40 @@ ${params.body}`, {
|
|
|
25945
25722
|
defineTool6({
|
|
25946
25723
|
name: "review_publish",
|
|
25947
25724
|
label: "review publish",
|
|
25948
|
-
description: "Publish pending remote
|
|
25725
|
+
description: "Publish pending remote review comments and status.",
|
|
25949
25726
|
promptSnippet: "Call review_publish to make remote review work public",
|
|
25950
|
-
promptGuidelines: ["
|
|
25727
|
+
promptGuidelines: ["Local reviews use revision dumps and are never published remotely."],
|
|
25951
25728
|
parameters: parameters6(publishSchema),
|
|
25952
25729
|
executionMode: "sequential",
|
|
25953
|
-
async execute(_id, input
|
|
25730
|
+
async execute(_id, input) {
|
|
25954
25731
|
const params = publishSchema.parse(input);
|
|
25732
|
+
if (params.local)
|
|
25733
|
+
return result3("Local reviews use immutable revision dumps and have no publish step.");
|
|
25955
25734
|
const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
|
|
25956
25735
|
if (!review.pr || !review.forge)
|
|
25957
25736
|
return result3("No remote PR/MR to publish.");
|
|
25958
|
-
return publishResolvedReview(review, params
|
|
25737
|
+
return publishResolvedReview(review, params);
|
|
25959
25738
|
}
|
|
25960
25739
|
}),
|
|
25961
25740
|
defineTool6({
|
|
25962
25741
|
name: "review_complete",
|
|
25963
25742
|
label: "review complete",
|
|
25964
|
-
description: "Approve, reject, or abandon a remote review
|
|
25965
|
-
promptSnippet: "Call review_complete to finish a review without merging it.",
|
|
25966
|
-
promptGuidelines: ["
|
|
25743
|
+
description: "Approve, reject, or abandon a remote review.",
|
|
25744
|
+
promptSnippet: "Call review_complete to finish a remote review without merging it.",
|
|
25745
|
+
promptGuidelines: ["Use review_dump to finish a local review revision."],
|
|
25967
25746
|
parameters: parameters6(completeSchema),
|
|
25968
25747
|
executionMode: "sequential",
|
|
25969
|
-
async execute(_id, input
|
|
25748
|
+
async execute(_id, input) {
|
|
25970
25749
|
const params = completeSchema.parse(input);
|
|
25750
|
+
if (params.local)
|
|
25751
|
+
return result3("Use review_dump to finish a local review revision.");
|
|
25971
25752
|
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
|
-
}
|
|
25987
25753
|
if (!review.pr || !review.forge)
|
|
25988
25754
|
return result3("No remote PR/MR to complete.");
|
|
25989
25755
|
if (!params.action)
|
|
25990
25756
|
return result3("Choose a complete action: approve, reject, or abandon.");
|
|
25991
25757
|
const status = { approve: "APPROVE", reject: "REQUEST_CHANGES", abandon: "CLOSE" }[params.action];
|
|
25992
|
-
return publishResolvedReview(review, { ...params, status }
|
|
25758
|
+
return publishResolvedReview(review, { ...params, status });
|
|
25993
25759
|
}
|
|
25994
25760
|
}),
|
|
25995
25761
|
defineTool6({
|
|
@@ -25998,10 +25764,10 @@ ${params.body}`, {
|
|
|
25998
25764
|
description: "Squash-merge an open, ready GitHub PR after checking its conventional subject.",
|
|
25999
25765
|
promptSnippet: "Call review_merge after confirming the PR is open, ready, and CI is settled; approval is optional",
|
|
26000
25766
|
promptGuidelines: ["This is intentionally GitHub-only until GitLab merge support is added."],
|
|
26001
|
-
parameters: parameters6(contextSchema.extend({ subject:
|
|
25767
|
+
parameters: parameters6(contextSchema.extend({ subject: z11.string().optional() })),
|
|
26002
25768
|
executionMode: "sequential",
|
|
26003
25769
|
async execute(_id, input) {
|
|
26004
|
-
const params = contextSchema.extend({ subject:
|
|
25770
|
+
const params = contextSchema.extend({ subject: z11.string().optional() }).parse(input);
|
|
26005
25771
|
const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
|
|
26006
25772
|
if (review.vcs.provider !== "github" || !review.forge)
|
|
26007
25773
|
return result3("review_merge currently supports GitHub only.");
|
|
@@ -26034,32 +25800,8 @@ ${params.body}`, {
|
|
|
26034
25800
|
})
|
|
26035
25801
|
];
|
|
26036
25802
|
}
|
|
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
|
-
}
|
|
26061
25803
|
function parameters6(schema) {
|
|
26062
|
-
return
|
|
25804
|
+
return z11.toJSONSchema(schema, { io: "input" });
|
|
26063
25805
|
}
|
|
26064
25806
|
function resolveWorkingDirectory(params) {
|
|
26065
25807
|
return params.cwd ?? process.cwd();
|
|
@@ -26075,159 +25817,17 @@ function modelRoute(ctx) {
|
|
|
26075
25817
|
throw new Error("Cannot record review provenance because Pi did not provide an active model route.");
|
|
26076
25818
|
return `${ctx.model.provider}/${ctx.model.id}`;
|
|
26077
25819
|
}
|
|
26078
|
-
async function publishResolvedReview(review, params
|
|
25820
|
+
async function publishResolvedReview(review, params) {
|
|
26079
25821
|
const status = params.status ?? "COMMENT";
|
|
26080
25822
|
const event = status === "CLOSE" ? "COMMENT" : status;
|
|
26081
25823
|
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;
|
|
26084
25824
|
if (status !== "CLOSE" && review.pr.isDraft)
|
|
26085
25825
|
await review.forge.markReady(review.pr.number);
|
|
26086
|
-
await
|
|
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
|
-
}
|
|
25826
|
+
await createRemoteReviewBackend(review.vcs, review.pr.number).publish(event);
|
|
26098
25827
|
if (status === "CLOSE")
|
|
26099
25828
|
await review.forge.closePr(review.pr.number);
|
|
26100
25829
|
const finalPr = await review.forge.viewPr(String(review.pr.number));
|
|
26101
|
-
|
|
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;
|
|
25830
|
+
return result3(`Published #${review.pr.number} (${status}).`, { pr: finalPr ?? review.pr, status });
|
|
26231
25831
|
}
|
|
26232
25832
|
async function resolveReviewContext(cwd, target) {
|
|
26233
25833
|
const vcs = await detectVcs(cwd);
|
|
@@ -26268,10 +25868,6 @@ async function assertRemoteBranchReady(cwd) {
|
|
|
26268
25868
|
if (head.stdout.trim() !== pushed.stdout.trim())
|
|
26269
25869
|
throw new Error("Push the current branch before opening a remote review.");
|
|
26270
25870
|
}
|
|
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
|
-
}
|
|
26275
25871
|
async function reviewTargetId(review) {
|
|
26276
25872
|
if (!review.pr)
|
|
26277
25873
|
return "uncommitted";
|
|
@@ -26288,60 +25884,9 @@ async function headSha(cwd) {
|
|
|
26288
25884
|
}
|
|
26289
25885
|
async function newReviewArtifactPath(cwd, target) {
|
|
26290
25886
|
const dir = await reviewsDir(cwd);
|
|
26291
|
-
await
|
|
25887
|
+
await mkdir8(dir, { recursive: true });
|
|
26292
25888
|
return uniqueRecordPath(dir, reviewRecordName(target));
|
|
26293
25889
|
}
|
|
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
|
-
}
|
|
26345
25890
|
function deriveTitle(branch) {
|
|
26346
25891
|
return branch.replace(/^(feature|feat|fix|bug|chore)\//, "").replace(/^eng-\d+-/i, "").replace(/[-_]+/g, " ").replace(/^\w/, (char) => char.toUpperCase());
|
|
26347
25892
|
}
|
|
@@ -26352,30 +25897,19 @@ function uniqueRecordPath(dir, base) {
|
|
|
26352
25897
|
path = join21(dir, `${base}-${count++}.md`);
|
|
26353
25898
|
return path;
|
|
26354
25899
|
}
|
|
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
|
-
}
|
|
26366
25900
|
|
|
26367
25901
|
// src/tools/setup.ts
|
|
26368
25902
|
import { defineTool as defineTool7 } from "@earendil-works/pi-coding-agent";
|
|
26369
|
-
import { z as
|
|
25903
|
+
import { z as z12 } from "zod";
|
|
26370
25904
|
|
|
26371
25905
|
// src/setup.ts
|
|
26372
25906
|
import { parseFrontmatter as parseFrontmatter3 } from "@earendil-works/pi-coding-agent";
|
|
26373
|
-
import { readdir as readdir4, readFile as
|
|
25907
|
+
import { readdir as readdir4, readFile as readFile14 } from "node:fs/promises";
|
|
26374
25908
|
import { homedir as homedir10 } from "node:os";
|
|
26375
25909
|
import { basename as basename7, join as join24 } from "node:path";
|
|
26376
25910
|
|
|
26377
25911
|
// src/mcp.ts
|
|
26378
|
-
import { mkdir as
|
|
25912
|
+
import { mkdir as mkdir9, readFile as readFile13, writeFile as writeFile7 } from "node:fs/promises";
|
|
26379
25913
|
import { homedir as homedir8 } from "node:os";
|
|
26380
25914
|
import { dirname as dirname9, join as join22 } from "node:path";
|
|
26381
25915
|
var mcp = {
|
|
@@ -26393,8 +25927,8 @@ var mcp = {
|
|
|
26393
25927
|
const next = { ...current, mcpServers: nextServers };
|
|
26394
25928
|
const changed = JSON.stringify(current) !== JSON.stringify(next);
|
|
26395
25929
|
if (changed && !options.dryRun) {
|
|
26396
|
-
await
|
|
26397
|
-
await
|
|
25930
|
+
await mkdir9(dirname9(path), { recursive: true });
|
|
25931
|
+
await writeFile7(path, `${JSON.stringify(next, null, 2)}
|
|
26398
25932
|
`, "utf8");
|
|
26399
25933
|
}
|
|
26400
25934
|
return { path, changed, existed: currentText !== undefined, planned: changed && options.dryRun === true };
|
|
@@ -26423,7 +25957,7 @@ function getParsedConfig(content, path) {
|
|
|
26423
25957
|
}
|
|
26424
25958
|
async function getOptionalFile2(path) {
|
|
26425
25959
|
try {
|
|
26426
|
-
return await
|
|
25960
|
+
return await readFile13(path, "utf8");
|
|
26427
25961
|
} catch (error) {
|
|
26428
25962
|
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
26429
25963
|
return;
|
|
@@ -26435,7 +25969,7 @@ function isRecord(value) {
|
|
|
26435
25969
|
}
|
|
26436
25970
|
|
|
26437
25971
|
// src/pi.ts
|
|
26438
|
-
import { mkdir as
|
|
25972
|
+
import { mkdir as mkdir10, writeFile as writeFile8 } from "node:fs/promises";
|
|
26439
25973
|
import { homedir as homedir9 } from "node:os";
|
|
26440
25974
|
import { dirname as dirname10, join as join23 } from "node:path";
|
|
26441
25975
|
var pi = {
|
|
@@ -26454,8 +25988,8 @@ async function ensurePiAgent(filename, content, agentDir = resolvePiAgentDir(),
|
|
|
26454
25988
|
const currentText = await readTextIfExists(path);
|
|
26455
25989
|
const changed = currentText !== content;
|
|
26456
25990
|
if (changed && !dryRun) {
|
|
26457
|
-
await
|
|
26458
|
-
await
|
|
25991
|
+
await mkdir10(dirname10(path), { recursive: true });
|
|
25992
|
+
await writeFile8(path, content, "utf8");
|
|
26459
25993
|
}
|
|
26460
25994
|
return { path, changed, existed: currentText !== undefined, planned: changed && dryRun };
|
|
26461
25995
|
}
|
|
@@ -26491,8 +26025,8 @@ async function ensurePiConfig(path, update, dryRun = false) {
|
|
|
26491
26025
|
const next = update(current);
|
|
26492
26026
|
const changed = JSON.stringify(current) !== JSON.stringify(next);
|
|
26493
26027
|
if (changed && !dryRun) {
|
|
26494
|
-
await
|
|
26495
|
-
await
|
|
26028
|
+
await mkdir10(dirname10(path), { recursive: true });
|
|
26029
|
+
await writeFile8(path, `${JSON.stringify(next, null, 2)}
|
|
26496
26030
|
`, "utf8");
|
|
26497
26031
|
}
|
|
26498
26032
|
return { path, changed, existed: currentText !== undefined, planned: changed && dryRun };
|
|
@@ -26630,7 +26164,7 @@ async function ensurePiAgents(options = {}) {
|
|
|
26630
26164
|
const actions = [];
|
|
26631
26165
|
for (const entry of entries) {
|
|
26632
26166
|
const id = basename7(entry.name, ".md").replace(/^diffpi-/, "");
|
|
26633
|
-
const source = await
|
|
26167
|
+
const source = await readFile14(join24(bundledAgentsDir, entry.name), "utf8");
|
|
26634
26168
|
const content = materializeAgentModels(source, id, userConfig.config, options.availableModels);
|
|
26635
26169
|
const result = await pi.agentEnsure(entry.name, content, agentDir, options.dryRun);
|
|
26636
26170
|
actions.push(getConfigSetupAction(`pi agent ${id}`, result));
|
|
@@ -26835,7 +26369,7 @@ function reportProgress(options, message) {
|
|
|
26835
26369
|
options.onProgress?.(message);
|
|
26836
26370
|
}
|
|
26837
26371
|
async function packageVersion2() {
|
|
26838
|
-
const source = await
|
|
26372
|
+
const source = await readFile14(join24(resolveBundledAgentsDir(), "..", "package.json"), "utf8");
|
|
26839
26373
|
let value;
|
|
26840
26374
|
try {
|
|
26841
26375
|
value = JSON.parse(source);
|
|
@@ -26851,13 +26385,13 @@ function getRecord(value) {
|
|
|
26851
26385
|
}
|
|
26852
26386
|
|
|
26853
26387
|
// src/tools/setup.ts
|
|
26854
|
-
var setupParametersSchema =
|
|
26855
|
-
issueTracker:
|
|
26856
|
-
forges:
|
|
26857
|
-
forge:
|
|
26858
|
-
bindZedKey:
|
|
26388
|
+
var setupParametersSchema = z12.object({
|
|
26389
|
+
issueTracker: z12.enum(["none", "linear", "jira"]).default("none").describe("Issue tracker MCP server to configure. Use none unless the user explicitly selects Linear or Jira."),
|
|
26390
|
+
forges: z12.array(z12.enum(["github", "gitlab"])).default([]).describe("Hosted VCS MCP servers to configure. Installs matching CLIs and registers each MCP server."),
|
|
26391
|
+
forge: z12.enum(["none", "github", "gitlab"]).optional().describe("Deprecated single-host alias. Prefer forges for one or more VCS MCP servers."),
|
|
26392
|
+
bindZedKey: z12.boolean().default(false).describe("Opt in to a Zed keybinding for the tuicr review task.")
|
|
26859
26393
|
});
|
|
26860
|
-
var setupParameters =
|
|
26394
|
+
var setupParameters = z12.toJSONSchema(setupParametersSchema, { io: "input" });
|
|
26861
26395
|
var diffpiSetupTool = defineTool7({
|
|
26862
26396
|
name: "diffpi_setup",
|
|
26863
26397
|
label: "diffpi setup",
|
|
@@ -26930,11 +26464,11 @@ ${lines.join(`
|
|
|
26930
26464
|
|
|
26931
26465
|
// src/tools/templates.ts
|
|
26932
26466
|
import { defineTool as defineTool8 } from "@earendil-works/pi-coding-agent";
|
|
26933
|
-
import { z as
|
|
26934
|
-
var templateSchema =
|
|
26935
|
-
name:
|
|
26936
|
-
variables:
|
|
26937
|
-
homeDir:
|
|
26467
|
+
import { z as z13 } from "zod";
|
|
26468
|
+
var templateSchema = z13.object({
|
|
26469
|
+
name: z13.string().min(1),
|
|
26470
|
+
variables: z13.record(z13.string(), z13.string()).optional(),
|
|
26471
|
+
homeDir: z13.string().optional()
|
|
26938
26472
|
});
|
|
26939
26473
|
var diffpiTemplateTool = defineTool8({
|
|
26940
26474
|
name: "diffpi_template",
|
|
@@ -26942,7 +26476,7 @@ var diffpiTemplateTool = defineTool8({
|
|
|
26942
26476
|
description: "Load a bundled Diffpi template or a user override and render named variables.",
|
|
26943
26477
|
promptSnippet: "Use diffpi_template for package workflow templates",
|
|
26944
26478
|
promptGuidelines: ["User overrides live under ~/.difflab/diffpi/templates."],
|
|
26945
|
-
parameters:
|
|
26479
|
+
parameters: z13.toJSONSchema(templateSchema, { io: "input" }),
|
|
26946
26480
|
executionMode: "parallel",
|
|
26947
26481
|
async execute(_id, input) {
|
|
26948
26482
|
const params = templateSchema.parse(input);
|