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