@difflab/pi 0.3.0-rc.202609200047.ac3661a → 0.3.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 +2 -22
- package/agents/diffpi-orchestrator.md +0 -10
- package/agents/diffpi-worker.md +1 -9
- package/dist/commands/index.d.ts +0 -1
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/environment.d.ts.map +1 -1
- package/dist/extensions/index.js +844 -2773
- package/dist/extensions/zedx.d.ts +0 -2
- package/dist/extensions/zedx.d.ts.map +1 -1
- package/dist/index.d.ts +2 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +128 -1140
- package/dist/setup.d.ts.map +1 -1
- package/dist/store.d.ts +0 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/tools/index.d.ts +0 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +678 -2400
- package/package.json +1 -4
- package/agents/diffpi-planner.md +0 -30
- package/dist/cli/plan.d.ts +0 -7
- package/dist/cli/plan.d.ts.map +0 -1
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -729
- package/dist/commands/background.d.ts +0 -39
- package/dist/commands/background.d.ts.map +0 -1
- package/dist/commands/plan.d.ts +0 -21
- package/dist/commands/plan.d.ts.map +0 -1
- package/dist/plan/annotations.d.ts +0 -25
- package/dist/plan/annotations.d.ts.map +0 -1
- package/dist/plan/escalation.d.ts +0 -4
- package/dist/plan/escalation.d.ts.map +0 -1
- package/dist/plan/execution.d.ts +0 -19
- package/dist/plan/execution.d.ts.map +0 -1
- package/dist/plan/index.d.ts +0 -15
- package/dist/plan/index.d.ts.map +0 -1
- package/dist/plan/lock.d.ts +0 -7
- package/dist/plan/lock.d.ts.map +0 -1
- package/dist/plan/log.d.ts +0 -5
- package/dist/plan/log.d.ts.map +0 -1
- package/dist/plan/markdown.d.ts +0 -6
- package/dist/plan/markdown.d.ts.map +0 -1
- package/dist/plan/store.d.ts +0 -34
- package/dist/plan/store.d.ts.map +0 -1
- package/dist/plan/transitions.d.ts +0 -9
- package/dist/plan/transitions.d.ts.map +0 -1
- package/dist/plan/types.d.ts +0 -146
- package/dist/plan/types.d.ts.map +0 -1
- package/dist/tools/plan.d.ts +0 -6
- package/dist/tools/plan.d.ts.map +0 -1
- package/skills/plan/SKILL.md +0 -13
- package/skills/plan/references/workflows/annotate.md +0 -6
- package/skills/plan/references/workflows/finalize.md +0 -7
- package/skills/plan/references/workflows/go.md +0 -7
- package/skills/plan/references/workflows/help.md +0 -13
- package/skills/plan/references/workflows/init.md +0 -6
- package/skills/plan/references/workflows/new.md +0 -7
- package/skills/plan/references/workflows/update.md +0 -7
- package/templates/plan/PLAN.md +0 -38
package/dist/extensions/index.js
CHANGED
|
@@ -13731,473 +13731,6 @@ async function handleModeCommand(args, ctx, modes) {
|
|
|
13731
13731
|
ctx.ui.notify(`${result.message} Changes apply on the next turn.`, result.ok ? "info" : "error");
|
|
13732
13732
|
}
|
|
13733
13733
|
|
|
13734
|
-
// src/commands/plan.ts
|
|
13735
|
-
import { join as join10 } from "node:path";
|
|
13736
|
-
|
|
13737
|
-
// src/extensions/processx.ts
|
|
13738
|
-
import { spawn as spawn2 } from "node:child_process";
|
|
13739
|
-
import { constants } from "node:fs";
|
|
13740
|
-
import { access } from "node:fs/promises";
|
|
13741
|
-
import { delimiter, join as join6 } from "node:path";
|
|
13742
|
-
async function findExecutable(name) {
|
|
13743
|
-
if (name.includes("/")) {
|
|
13744
|
-
try {
|
|
13745
|
-
await access(name, constants.X_OK);
|
|
13746
|
-
return name;
|
|
13747
|
-
} catch {
|
|
13748
|
-
return;
|
|
13749
|
-
}
|
|
13750
|
-
}
|
|
13751
|
-
for (const directory of (process.env.PATH ?? "").split(delimiter)) {
|
|
13752
|
-
if (!directory)
|
|
13753
|
-
continue;
|
|
13754
|
-
const candidate = join6(directory, name);
|
|
13755
|
-
try {
|
|
13756
|
-
await access(candidate, constants.X_OK);
|
|
13757
|
-
return candidate;
|
|
13758
|
-
} catch {}
|
|
13759
|
-
}
|
|
13760
|
-
return;
|
|
13761
|
-
}
|
|
13762
|
-
function run(command, args, options = {}) {
|
|
13763
|
-
return new Promise((resolve, reject) => {
|
|
13764
|
-
const child = spawn2(command, args, {
|
|
13765
|
-
cwd: options.cwd,
|
|
13766
|
-
env: options.env ?? process.env,
|
|
13767
|
-
stdio: [options.input === undefined ? "ignore" : "pipe", "pipe", "pipe"]
|
|
13768
|
-
});
|
|
13769
|
-
let stdout = "";
|
|
13770
|
-
let stderr = "";
|
|
13771
|
-
const stdoutChunks = [];
|
|
13772
|
-
const stderrChunks = [];
|
|
13773
|
-
const unbounded = options.capture === "unbounded";
|
|
13774
|
-
child.stdout?.on("data", (chunk) => {
|
|
13775
|
-
const text = chunk.toString();
|
|
13776
|
-
if (unbounded)
|
|
13777
|
-
stdoutChunks.push(text);
|
|
13778
|
-
else
|
|
13779
|
-
stdout = appendBounded(stdout, text);
|
|
13780
|
-
});
|
|
13781
|
-
child.stderr?.on("data", (chunk) => {
|
|
13782
|
-
const text = chunk.toString();
|
|
13783
|
-
if (unbounded)
|
|
13784
|
-
stderrChunks.push(text);
|
|
13785
|
-
else
|
|
13786
|
-
stderr = appendBounded(stderr, text);
|
|
13787
|
-
});
|
|
13788
|
-
child.on("error", reject);
|
|
13789
|
-
child.on("close", (code) => resolve({
|
|
13790
|
-
code: code ?? 1,
|
|
13791
|
-
stdout: unbounded ? stdoutChunks.join("") : stdout,
|
|
13792
|
-
stderr: unbounded ? stderrChunks.join("") : stderr
|
|
13793
|
-
}));
|
|
13794
|
-
if (options.input !== undefined && child.stdin)
|
|
13795
|
-
child.stdin.end(options.input);
|
|
13796
|
-
});
|
|
13797
|
-
}
|
|
13798
|
-
async function runChecked(command, args, options = {}) {
|
|
13799
|
-
const result = await run(command, args, options);
|
|
13800
|
-
if (result.code === 0)
|
|
13801
|
-
return result;
|
|
13802
|
-
const detail = result.stderr.trim() || result.stdout.trim() || `exit code ${result.code}`;
|
|
13803
|
-
throw new Error(`${command} ${args.join(" ")} failed: ${detail}`);
|
|
13804
|
-
}
|
|
13805
|
-
var MAX_CAPTURED_OUTPUT_LENGTH = 65536;
|
|
13806
|
-
function appendBounded(current, next) {
|
|
13807
|
-
const combined = current + next;
|
|
13808
|
-
return combined.length <= MAX_CAPTURED_OUTPUT_LENGTH ? combined : combined.slice(-MAX_CAPTURED_OUTPUT_LENGTH);
|
|
13809
|
-
}
|
|
13810
|
-
|
|
13811
|
-
// src/commands/background.ts
|
|
13812
|
-
import { chmod, mkdir as mkdir2, readdir as readdir2, rm, stat, writeFile } from "node:fs/promises";
|
|
13813
|
-
import { join as join9 } from "node:path";
|
|
13814
|
-
|
|
13815
|
-
// src/store.ts
|
|
13816
|
-
import { createHash } from "node:crypto";
|
|
13817
|
-
import { lstat, mkdir, readlink, realpath as realpath2, symlink, unlink } from "node:fs/promises";
|
|
13818
|
-
import { homedir as homedir4 } from "node:os";
|
|
13819
|
-
import { dirname as dirname3, isAbsolute as isAbsolute3, join as join8, resolve as resolve3 } from "node:path";
|
|
13820
|
-
|
|
13821
|
-
// src/extensions/gitx.ts
|
|
13822
|
-
import { realpath } from "node:fs/promises";
|
|
13823
|
-
import { basename as basename2, isAbsolute as isAbsolute2, join as join7, resolve as resolve2 } from "node:path";
|
|
13824
|
-
async function gitToplevel(cwd) {
|
|
13825
|
-
const result = await run("git", ["-C", cwd, "rev-parse", "--show-toplevel"]);
|
|
13826
|
-
const top = result.stdout.trim();
|
|
13827
|
-
return result.code === 0 && top ? top : resolve2(cwd);
|
|
13828
|
-
}
|
|
13829
|
-
async function inspectGitRepository(cwd) {
|
|
13830
|
-
const root = await gitToplevel(cwd);
|
|
13831
|
-
const remoteResult = await run("git", ["-C", root, "remote", "get-url", "origin"]);
|
|
13832
|
-
const remote = remoteResult.code === 0 ? remoteResult.stdout.trim() : "";
|
|
13833
|
-
const commonResult = await run("git", ["-C", root, "rev-parse", "--git-common-dir"]);
|
|
13834
|
-
const common = commonResult.stdout.trim();
|
|
13835
|
-
const commonPath = commonResult.code === 0 && common ? resolve2(isAbsolute2(common) ? common : join7(root, common)) : root;
|
|
13836
|
-
const commonDir = await canonicalPath(commonPath);
|
|
13837
|
-
return {
|
|
13838
|
-
root,
|
|
13839
|
-
commonDir,
|
|
13840
|
-
...remote ? { remote } : {},
|
|
13841
|
-
name: remote ? repositoryName(remote) : basename2(resolve2(commonDir, "..")) || basename2(root),
|
|
13842
|
-
identity: remote ? `remote:${normalizeRemote(remote)}` : `git-common-dir:${commonDir}`
|
|
13843
|
-
};
|
|
13844
|
-
}
|
|
13845
|
-
function normalizeRemote(remote) {
|
|
13846
|
-
return remote.trim().replace(/\.git\/?$/i, "").replace(/\/+$/, "").toLowerCase();
|
|
13847
|
-
}
|
|
13848
|
-
function repositoryName(remote) {
|
|
13849
|
-
const normalized = remote.trim().replace(/\.git\/?$/i, "").replace(/\/+$/, "");
|
|
13850
|
-
return normalized.split(/[/\\:]/).filter(Boolean).at(-1) ?? "";
|
|
13851
|
-
}
|
|
13852
|
-
async function canonicalPath(path) {
|
|
13853
|
-
try {
|
|
13854
|
-
return await realpath(path);
|
|
13855
|
-
} catch {
|
|
13856
|
-
return resolve2(path);
|
|
13857
|
-
}
|
|
13858
|
-
}
|
|
13859
|
-
|
|
13860
|
-
// src/store.ts
|
|
13861
|
-
var STORE_LINK = ".diffpi";
|
|
13862
|
-
var LEGACY_STORE_LINK = join8(".pi", "diffpi");
|
|
13863
|
-
function storeGlobalRoot(homeDir = homedir4()) {
|
|
13864
|
-
return join8(homeDir, ".difflab", "diffpi", "projects");
|
|
13865
|
-
}
|
|
13866
|
-
async function ensureStore(cwd, homeDir = homedir4()) {
|
|
13867
|
-
const repository = await inspectGitRepository(cwd);
|
|
13868
|
-
const root = repository.root;
|
|
13869
|
-
const slug = projectSlug(repository);
|
|
13870
|
-
const dest = join8(storeGlobalRoot(homeDir), slug);
|
|
13871
|
-
const link = join8(root, STORE_LINK);
|
|
13872
|
-
await mkdir(dest, { recursive: true });
|
|
13873
|
-
try {
|
|
13874
|
-
await assertStoreLink(link, dest);
|
|
13875
|
-
} catch (error) {
|
|
13876
|
-
if (error.code !== "ENOENT")
|
|
13877
|
-
throw error;
|
|
13878
|
-
await symlink(dest, link);
|
|
13879
|
-
}
|
|
13880
|
-
await removeLegacyStoreLink(join8(root, LEGACY_STORE_LINK), dest);
|
|
13881
|
-
return { slug, root, dest, link, linked: true };
|
|
13882
|
-
}
|
|
13883
|
-
async function plansDir(cwd, homeDir = homedir4()) {
|
|
13884
|
-
const store = await ensureStore(cwd, homeDir);
|
|
13885
|
-
const dir = join8(store.link, "plan");
|
|
13886
|
-
await mkdir(dir, { recursive: true });
|
|
13887
|
-
return dir;
|
|
13888
|
-
}
|
|
13889
|
-
async function reviewsDir(cwd, homeDir = homedir4()) {
|
|
13890
|
-
const store = await ensureStore(cwd, homeDir);
|
|
13891
|
-
const dir = join8(store.link, "review");
|
|
13892
|
-
await mkdir(dir, { recursive: true });
|
|
13893
|
-
return dir;
|
|
13894
|
-
}
|
|
13895
|
-
async function completedReviewsDir(cwd, homeDir = homedir4()) {
|
|
13896
|
-
const store = await ensureStore(cwd, homeDir);
|
|
13897
|
-
const dir = join8(store.link, "reviews");
|
|
13898
|
-
await mkdir(dir, { recursive: true });
|
|
13899
|
-
return dir;
|
|
13900
|
-
}
|
|
13901
|
-
async function sessionsDir(cwd, homeDir = homedir4()) {
|
|
13902
|
-
const store = await ensureStore(cwd, homeDir);
|
|
13903
|
-
const dir = join8(store.link, "sessions");
|
|
13904
|
-
await mkdir(dir, { recursive: true });
|
|
13905
|
-
return dir;
|
|
13906
|
-
}
|
|
13907
|
-
async function assertStoreLink(path, dest) {
|
|
13908
|
-
const entry = await lstat(path);
|
|
13909
|
-
if (!entry.isSymbolicLink())
|
|
13910
|
-
throw new Error(`${path} exists and is not a symlink.`);
|
|
13911
|
-
const target = await symlinkTarget(path);
|
|
13912
|
-
if (target !== await canonicalPath2(dest))
|
|
13913
|
-
throw new Error(`${path} points to ${target}, not ${dest}.`);
|
|
13914
|
-
}
|
|
13915
|
-
async function removeLegacyStoreLink(path, dest) {
|
|
13916
|
-
try {
|
|
13917
|
-
const entry = await lstat(path);
|
|
13918
|
-
if (!entry.isSymbolicLink())
|
|
13919
|
-
return;
|
|
13920
|
-
const target = await symlinkTarget(path);
|
|
13921
|
-
if (target === await canonicalPath2(dest))
|
|
13922
|
-
await unlink(path);
|
|
13923
|
-
} catch (error) {
|
|
13924
|
-
if (error.code !== "ENOENT")
|
|
13925
|
-
throw error;
|
|
13926
|
-
}
|
|
13927
|
-
}
|
|
13928
|
-
async function symlinkTarget(path) {
|
|
13929
|
-
const target = await readlink(path);
|
|
13930
|
-
return canonicalPath2(isAbsolute3(target) ? target : resolve3(dirname3(path), target));
|
|
13931
|
-
}
|
|
13932
|
-
async function canonicalPath2(path) {
|
|
13933
|
-
try {
|
|
13934
|
-
return await realpath2(path);
|
|
13935
|
-
} catch {
|
|
13936
|
-
return resolve3(path);
|
|
13937
|
-
}
|
|
13938
|
-
}
|
|
13939
|
-
function projectSlug(repository) {
|
|
13940
|
-
const readable = repository.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "unnamed";
|
|
13941
|
-
const digest = createHash("sha256").update(repository.identity).digest("hex").slice(0, 12);
|
|
13942
|
-
return `${readable}-${digest}`;
|
|
13943
|
-
}
|
|
13944
|
-
|
|
13945
|
-
// src/commands/background.ts
|
|
13946
|
-
async function launchBackgroundPi(pi, options) {
|
|
13947
|
-
const child = shellCommand([
|
|
13948
|
-
"pi",
|
|
13949
|
-
"--mode",
|
|
13950
|
-
"json",
|
|
13951
|
-
"--print",
|
|
13952
|
-
"--no-session",
|
|
13953
|
-
"--offline",
|
|
13954
|
-
"--approve",
|
|
13955
|
-
"--model",
|
|
13956
|
-
options.model,
|
|
13957
|
-
"--thinking",
|
|
13958
|
-
options.thinking,
|
|
13959
|
-
"--append-system-prompt",
|
|
13960
|
-
options.agentPath,
|
|
13961
|
-
"--",
|
|
13962
|
-
options.prompt
|
|
13963
|
-
]);
|
|
13964
|
-
const command = options.packetPath ? `sh -c ${shellQuote(`trap 'rm -f -- "$1"' EXIT HUP INT TERM; cd -- "$2" || exit; ${child}`)} sh ${shellQuote(options.packetPath)} ${shellQuote(options.cwd)}` : `cd ${shellQuote(options.cwd)} && ${child}`;
|
|
13965
|
-
await pi.sendUserMessage(`/bg --agent --name ${shellQuote(options.name)} -- ${command}`, {
|
|
13966
|
-
deliverAs: "followUp",
|
|
13967
|
-
expandPromptTemplates: true
|
|
13968
|
-
});
|
|
13969
|
-
return { name: options.name, command, queued: true };
|
|
13970
|
-
}
|
|
13971
|
-
async function createBackgroundPacket(cwd, payload, limits = {}) {
|
|
13972
|
-
const root = await plansDir(cwd);
|
|
13973
|
-
const temp = join9(root, ".tmp");
|
|
13974
|
-
await mkdir2(temp, { recursive: true, mode: 448 });
|
|
13975
|
-
await cleanupBackgroundPackets(temp, limits.retentionMs);
|
|
13976
|
-
const packet = { ...payload, version: 1, createdAt: new Date().toISOString() };
|
|
13977
|
-
const content = `${JSON.stringify(packet)}
|
|
13978
|
-
`;
|
|
13979
|
-
const maxBytes = limits.maxBytes ?? 64 * 1024;
|
|
13980
|
-
if (Buffer.byteLength(content) > maxBytes)
|
|
13981
|
-
throw new Error(`Background packet exceeds ${maxBytes} bytes.`);
|
|
13982
|
-
const path = join9(temp, `context-${crypto.randomUUID()}.json`);
|
|
13983
|
-
await writeFile(path, content, { mode: 384, flag: "wx" });
|
|
13984
|
-
await chmod(path, 384);
|
|
13985
|
-
return path;
|
|
13986
|
-
}
|
|
13987
|
-
function normalizeConversation(entries, maxMessages = 12, maxBytes = 48 * 1024) {
|
|
13988
|
-
const messages = entries.flatMap((entry) => {
|
|
13989
|
-
if (!entry || typeof entry !== "object")
|
|
13990
|
-
return [];
|
|
13991
|
-
const candidate = entry;
|
|
13992
|
-
if (candidate.secret === true || candidate.type !== "message" || !candidate.message || typeof candidate.message !== "object")
|
|
13993
|
-
return [];
|
|
13994
|
-
const message = candidate.message;
|
|
13995
|
-
if (message.role !== "user" && message.role !== "assistant")
|
|
13996
|
-
return [];
|
|
13997
|
-
const text = messageText(message.content);
|
|
13998
|
-
return text ? [{ role: message.role, text }] : [];
|
|
13999
|
-
});
|
|
14000
|
-
const selected = messages.slice(-Math.max(0, maxMessages));
|
|
14001
|
-
while (Buffer.byteLength(JSON.stringify(selected)) > maxBytes && selected.length)
|
|
14002
|
-
selected.shift();
|
|
14003
|
-
return selected;
|
|
14004
|
-
}
|
|
14005
|
-
function shellQuote(value) {
|
|
14006
|
-
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
14007
|
-
}
|
|
14008
|
-
function shellCommand(args) {
|
|
14009
|
-
return args.map(shellQuote).join(" ");
|
|
14010
|
-
}
|
|
14011
|
-
async function cleanupBackgroundPackets(dir, retentionMs = 24 * 60 * 60 * 1000) {
|
|
14012
|
-
const cutoff = Date.now() - retentionMs;
|
|
14013
|
-
for (const name of await readdir2(dir)) {
|
|
14014
|
-
if (!/^context-[a-f0-9-]+\.json$/i.test(name))
|
|
14015
|
-
continue;
|
|
14016
|
-
const path = join9(dir, name);
|
|
14017
|
-
if ((await stat(path)).mtimeMs < cutoff)
|
|
14018
|
-
await rm(path, { force: true });
|
|
14019
|
-
}
|
|
14020
|
-
}
|
|
14021
|
-
function messageText(content) {
|
|
14022
|
-
if (typeof content === "string")
|
|
14023
|
-
return content.trim();
|
|
14024
|
-
if (!Array.isArray(content))
|
|
14025
|
-
return "";
|
|
14026
|
-
return content.flatMap((part) => {
|
|
14027
|
-
if (!part || typeof part !== "object")
|
|
14028
|
-
return [];
|
|
14029
|
-
const candidate = part;
|
|
14030
|
-
return candidate.type === "text" && typeof candidate.text === "string" && candidate.source !== "secret" ? [candidate.text] : [];
|
|
14031
|
-
}).join(`
|
|
14032
|
-
`).trim();
|
|
14033
|
-
}
|
|
14034
|
-
|
|
14035
|
-
// src/commands/plan.ts
|
|
14036
|
-
var PLANNER_PATH = join10(resolveBundledAgentsDir(), "diffpi-planner.md");
|
|
14037
|
-
var ORCHESTRATOR_PATH = join10(resolveBundledAgentsDir(), "diffpi-orchestrator.md");
|
|
14038
|
-
var HELP = "Usage: /plan init|new|update|annotate|finalize|go|help. Run /plan help for exact grammar.";
|
|
14039
|
-
function registerPlanCommand(pi, modes) {
|
|
14040
|
-
pi.registerCommand("plan", {
|
|
14041
|
-
description: "Durable planning: init, new, update, annotate, finalize, go, help",
|
|
14042
|
-
handler: async (args, ctx) => handlePlanCommand(args, ctx, pi, modes)
|
|
14043
|
-
});
|
|
14044
|
-
}
|
|
14045
|
-
function parsePlanArgs(raw) {
|
|
14046
|
-
const tokens = tokenizePlanArgs(raw);
|
|
14047
|
-
const verb = tokens.shift() ?? "help";
|
|
14048
|
-
if (!["init", "new", "update", "annotate", "finalize", "go", "help"].includes(verb))
|
|
14049
|
-
throw new Error(`Unknown plan verb: ${verb}. ${HELP}`);
|
|
14050
|
-
let branch;
|
|
14051
|
-
let background = false;
|
|
14052
|
-
let policy;
|
|
14053
|
-
const positional = [];
|
|
14054
|
-
while (tokens.length) {
|
|
14055
|
-
const token = tokens.shift();
|
|
14056
|
-
if (token === "--branch") {
|
|
14057
|
-
if (branch)
|
|
14058
|
-
throw new Error(`Duplicate --branch. ${HELP}`);
|
|
14059
|
-
branch = tokens.shift();
|
|
14060
|
-
if (!branch || branch.startsWith("--"))
|
|
14061
|
-
throw new Error(`--branch requires a value. ${HELP}`);
|
|
14062
|
-
} else if (token === "--bg") {
|
|
14063
|
-
if (background)
|
|
14064
|
-
throw new Error(`Duplicate --bg. ${HELP}`);
|
|
14065
|
-
background = true;
|
|
14066
|
-
} else if (token === "--commit" || token === "--no-commit") {
|
|
14067
|
-
const next = token === "--commit" ? "commit-per-phase" : "no-commit";
|
|
14068
|
-
if (policy)
|
|
14069
|
-
throw new Error(`Conflicting or duplicate commit policy. ${HELP}`);
|
|
14070
|
-
policy = next;
|
|
14071
|
-
} else if (token.startsWith("--"))
|
|
14072
|
-
throw new Error(`Unknown flag: ${token}. ${HELP}`);
|
|
14073
|
-
else
|
|
14074
|
-
positional.push(token);
|
|
14075
|
-
}
|
|
14076
|
-
if (verb === "go") {
|
|
14077
|
-
if (branch)
|
|
14078
|
-
throw new Error(`go does not accept --branch. ${HELP}`);
|
|
14079
|
-
if (positional.length !== 1)
|
|
14080
|
-
throw new Error(`go requires exactly one plan slug. ${HELP}`);
|
|
14081
|
-
return { verb, plan: positional[0], background, policy, instructions: "" };
|
|
14082
|
-
}
|
|
14083
|
-
if (policy)
|
|
14084
|
-
throw new Error(`${verb} does not accept a commit policy. ${HELP}`);
|
|
14085
|
-
if (verb === "help") {
|
|
14086
|
-
if (positional.length || branch || background)
|
|
14087
|
-
throw new Error(`help does not accept arguments. ${HELP}`);
|
|
14088
|
-
return { verb, background: false, instructions: "" };
|
|
14089
|
-
}
|
|
14090
|
-
if (verb === "init") {
|
|
14091
|
-
if (background || positional.length !== 1)
|
|
14092
|
-
throw new Error(`init requires one slug and does not accept --bg. ${HELP}`);
|
|
14093
|
-
return { verb, plan: positional[0], branch, background: false, instructions: "" };
|
|
14094
|
-
}
|
|
14095
|
-
if (verb === "annotate" || verb === "finalize") {
|
|
14096
|
-
if (background || branch || positional.length > 1)
|
|
14097
|
-
throw new Error(`${verb} accepts only an optional plan slug. ${HELP}`);
|
|
14098
|
-
return { verb, plan: positional[0], background: false, instructions: "" };
|
|
14099
|
-
}
|
|
14100
|
-
const plan = positional.shift();
|
|
14101
|
-
if (verb === "new" && !plan)
|
|
14102
|
-
throw new Error(`new requires a plan slug. ${HELP}`);
|
|
14103
|
-
return { verb, plan, branch, background, instructions: positional.join(" ") };
|
|
14104
|
-
}
|
|
14105
|
-
function tokenizePlanArgs(raw) {
|
|
14106
|
-
const tokens = [];
|
|
14107
|
-
let value = "";
|
|
14108
|
-
let quote;
|
|
14109
|
-
let escaped = false;
|
|
14110
|
-
const push = () => {
|
|
14111
|
-
if (value)
|
|
14112
|
-
tokens.push(value);
|
|
14113
|
-
value = "";
|
|
14114
|
-
};
|
|
14115
|
-
for (const char of raw.trim()) {
|
|
14116
|
-
if (escaped) {
|
|
14117
|
-
value += char;
|
|
14118
|
-
escaped = false;
|
|
14119
|
-
} else if (char === "\\" && quote !== "'")
|
|
14120
|
-
escaped = true;
|
|
14121
|
-
else if (quote) {
|
|
14122
|
-
if (char === quote)
|
|
14123
|
-
quote = undefined;
|
|
14124
|
-
else
|
|
14125
|
-
value += char;
|
|
14126
|
-
} else if (char === '"' || char === "'")
|
|
14127
|
-
quote = char;
|
|
14128
|
-
else if (/\s/.test(char))
|
|
14129
|
-
push();
|
|
14130
|
-
else
|
|
14131
|
-
value += char;
|
|
14132
|
-
}
|
|
14133
|
-
if (escaped || quote)
|
|
14134
|
-
throw new Error(`Unterminated quote or escape. ${HELP}`);
|
|
14135
|
-
push();
|
|
14136
|
-
return tokens;
|
|
14137
|
-
}
|
|
14138
|
-
async function handlePlanCommand(args, ctx, pi, modes) {
|
|
14139
|
-
let request;
|
|
14140
|
-
try {
|
|
14141
|
-
request = parsePlanArgs(args);
|
|
14142
|
-
} catch (error) {
|
|
14143
|
-
ctx.ui.notify(error.message, "error");
|
|
14144
|
-
return;
|
|
14145
|
-
}
|
|
14146
|
-
if (request.background && (request.verb === "new" || request.verb === "update")) {
|
|
14147
|
-
const branch = request.branch ?? await currentBranch(ctx.cwd);
|
|
14148
|
-
const packet = await createBackgroundPacket(ctx.cwd, {
|
|
14149
|
-
cwd: ctx.cwd,
|
|
14150
|
-
branch,
|
|
14151
|
-
command: request,
|
|
14152
|
-
conversation: normalizeConversation(ctx.sessionManager.getBranch())
|
|
14153
|
-
});
|
|
14154
|
-
await launchBackgroundPi(pi, {
|
|
14155
|
-
name: `Plan ${request.verb}`,
|
|
14156
|
-
agentPath: PLANNER_PATH,
|
|
14157
|
-
model: "openai-codex/gpt-5.6-sol",
|
|
14158
|
-
thinking: "high",
|
|
14159
|
-
cwd: ctx.cwd,
|
|
14160
|
-
packetPath: packet,
|
|
14161
|
-
prompt: `Run the plan skill ${request.verb} workflow non-interactively using context packet ${packet}. Do not ask questions. Persist unresolved ambiguity as a blocker.`
|
|
14162
|
-
});
|
|
14163
|
-
return;
|
|
14164
|
-
}
|
|
14165
|
-
if (request.verb === "go" && request.background && request.policy) {
|
|
14166
|
-
await launchBackgroundPi(pi, {
|
|
14167
|
-
name: `Plan go ${request.plan}`,
|
|
14168
|
-
agentPath: ORCHESTRATOR_PATH,
|
|
14169
|
-
model: "openai-codex/gpt-5.6-luna",
|
|
14170
|
-
thinking: "medium",
|
|
14171
|
-
cwd: ctx.cwd,
|
|
14172
|
-
prompt: planPrompt(request, "orchestrator")
|
|
14173
|
-
});
|
|
14174
|
-
return;
|
|
14175
|
-
}
|
|
14176
|
-
const directInlineGo = request.verb === "go" && !request.background && request.policy;
|
|
14177
|
-
const agent = directInlineGo ? "worker" : "planner";
|
|
14178
|
-
const activation = await modes.set(agent, ctx);
|
|
14179
|
-
pi.sendMessage({
|
|
14180
|
-
customType: "diffpi-plan-command",
|
|
14181
|
-
display: false,
|
|
14182
|
-
content: `${planPrompt(request, agent)} ${activation.message}`
|
|
14183
|
-
}, { triggerTurn: true });
|
|
14184
|
-
}
|
|
14185
|
-
function planPrompt(request, agent) {
|
|
14186
|
-
const args = [request.verb];
|
|
14187
|
-
if (request.plan)
|
|
14188
|
-
args.push(request.plan);
|
|
14189
|
-
if ("branch" in request && request.branch)
|
|
14190
|
-
args.push("--branch", request.branch);
|
|
14191
|
-
if (request.verb === "go" && request.policy)
|
|
14192
|
-
args.push(request.policy === "commit-per-phase" ? "--commit" : "--no-commit");
|
|
14193
|
-
if (request.instructions)
|
|
14194
|
-
args.push(request.instructions);
|
|
14195
|
-
return `The user ran /plan ${args.join(" ")}. Active inline agent: ${agent}. Follow the plan skill dispatcher and use structured plan tools. Do not perform unrelated work.`;
|
|
14196
|
-
}
|
|
14197
|
-
async function currentBranch(cwd) {
|
|
14198
|
-
return (await runChecked("git", ["-C", cwd, "branch", "--show-current"])).stdout.trim();
|
|
14199
|
-
}
|
|
14200
|
-
|
|
14201
13734
|
// src/commands/reload.ts
|
|
14202
13735
|
function registerReloadCommand(pi) {
|
|
14203
13736
|
pi.registerCommand("diffpi-reload", {
|
|
@@ -14209,9 +13742,9 @@ function registerReloadCommand(pi) {
|
|
|
14209
13742
|
}
|
|
14210
13743
|
|
|
14211
13744
|
// src/commands/review.ts
|
|
14212
|
-
import { join as
|
|
13745
|
+
import { join as join6 } from "node:path";
|
|
14213
13746
|
var REVIEWER_VERBS = new Set(["address", "auto", "launch"]);
|
|
14214
|
-
var ORCHESTRATOR_AGENT_PATH =
|
|
13747
|
+
var ORCHESTRATOR_AGENT_PATH = join6(resolveBundledAgentsDir(), "diffpi-orchestrator.md");
|
|
14215
13748
|
function registerReviewCommand(pi, modes) {
|
|
14216
13749
|
pi.registerCommand("review", {
|
|
14217
13750
|
description: "Code review: auto, new, edit, address, publish, complete, merge (add --local or --bg)",
|
|
@@ -14223,13 +13756,28 @@ async function handleReviewCommand(args, ctx, pi, modes) {
|
|
|
14223
13756
|
const invocation = withoutFlag(args, "--bg") || "help";
|
|
14224
13757
|
const verb = reviewVerb(invocation);
|
|
14225
13758
|
if (background) {
|
|
14226
|
-
|
|
14227
|
-
|
|
14228
|
-
|
|
14229
|
-
|
|
14230
|
-
|
|
14231
|
-
|
|
14232
|
-
|
|
13759
|
+
const name = `Review ${verb}`;
|
|
13760
|
+
const prompt = reviewPrompt(invocation, "orchestrator");
|
|
13761
|
+
const command = shellCommand([
|
|
13762
|
+
"pi",
|
|
13763
|
+
"--mode",
|
|
13764
|
+
"json",
|
|
13765
|
+
"--print",
|
|
13766
|
+
"--no-session",
|
|
13767
|
+
"--offline",
|
|
13768
|
+
"--approve",
|
|
13769
|
+
"--model",
|
|
13770
|
+
"openai-codex/gpt-5.6-luna",
|
|
13771
|
+
"--thinking",
|
|
13772
|
+
"medium",
|
|
13773
|
+
"--append-system-prompt",
|
|
13774
|
+
ORCHESTRATOR_AGENT_PATH,
|
|
13775
|
+
"--",
|
|
13776
|
+
prompt
|
|
13777
|
+
]);
|
|
13778
|
+
await pi.sendUserMessage(`/bg --agent --name ${shellQuote(name)} -- ${command}`, {
|
|
13779
|
+
deliverAs: "followUp",
|
|
13780
|
+
expandPromptTemplates: true
|
|
14233
13781
|
});
|
|
14234
13782
|
return;
|
|
14235
13783
|
}
|
|
@@ -14254,13 +13802,18 @@ function hasFlag(input, flag) {
|
|
|
14254
13802
|
function withoutFlag(input, flag) {
|
|
14255
13803
|
return input.split(/\s+/).filter((token) => token && token !== flag).join(" ").trim();
|
|
14256
13804
|
}
|
|
13805
|
+
function shellCommand(args) {
|
|
13806
|
+
return args.map(shellQuote).join(" ");
|
|
13807
|
+
}
|
|
13808
|
+
function shellQuote(value) {
|
|
13809
|
+
return "'" + value.replaceAll("'", `'"'"'`) + "'";
|
|
13810
|
+
}
|
|
14257
13811
|
|
|
14258
13812
|
// src/commands/index.ts
|
|
14259
13813
|
function registerCommands(pi, modes) {
|
|
14260
13814
|
registerReloadCommand(pi);
|
|
14261
13815
|
registerModeCommand(pi, modes);
|
|
14262
13816
|
registerReviewCommand(pi, modes);
|
|
14263
|
-
registerPlanCommand(pi, modes);
|
|
14264
13817
|
}
|
|
14265
13818
|
|
|
14266
13819
|
// src/tools/reload.ts
|
|
@@ -14389,76 +13942,123 @@ function sanitize(value) {
|
|
|
14389
13942
|
return value.replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim();
|
|
14390
13943
|
}
|
|
14391
13944
|
|
|
14392
|
-
// src/tools/
|
|
14393
|
-
import {
|
|
13945
|
+
// src/tools/review.ts
|
|
13946
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
13947
|
+
import { mkdir as mkdir4, readdir as readdir2, readFile as readFile9, rename as rename2, stat, unlink as unlink2, writeFile as writeFile5 } from "node:fs/promises";
|
|
13948
|
+
import { join as join14 } from "node:path";
|
|
14394
13949
|
import { defineTool as defineTool3 } from "@earendil-works/pi-coding-agent";
|
|
14395
|
-
import { z as
|
|
13950
|
+
import { z as z7 } from "zod";
|
|
14396
13951
|
|
|
14397
13952
|
// src/environment.ts
|
|
14398
|
-
import {
|
|
14399
|
-
import { basename as basename3, join as join13 } from "node:path";
|
|
13953
|
+
import { basename as basename2 } from "node:path";
|
|
14400
13954
|
|
|
14401
|
-
// src/extensions/
|
|
14402
|
-
import {
|
|
14403
|
-
import {
|
|
14404
|
-
import {
|
|
14405
|
-
|
|
14406
|
-
|
|
14407
|
-
|
|
14408
|
-
|
|
14409
|
-
|
|
14410
|
-
|
|
14411
|
-
|
|
14412
|
-
|
|
14413
|
-
|
|
14414
|
-
|
|
14415
|
-
|
|
14416
|
-
|
|
14417
|
-
|
|
14418
|
-
|
|
14419
|
-
|
|
14420
|
-
|
|
14421
|
-
|
|
14422
|
-
|
|
14423
|
-
const index = next.findIndex((existing) => existing.label === task.label);
|
|
14424
|
-
if (index >= 0)
|
|
14425
|
-
next[index] = { ...next[index], ...task };
|
|
14426
|
-
else
|
|
14427
|
-
next.push(task);
|
|
14428
|
-
}
|
|
14429
|
-
const changed = JSON.stringify(tasks) !== JSON.stringify(next);
|
|
14430
|
-
if (changed)
|
|
14431
|
-
await writeJson(path, next);
|
|
14432
|
-
return { path, changed, existed: currentText !== undefined };
|
|
14433
|
-
}
|
|
14434
|
-
async function ensureZedPlanTask(packageVersion, homeDir = homedir5()) {
|
|
14435
|
-
if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(packageVersion)) {
|
|
14436
|
-
throw new Error(`Invalid @difflab/pi package version: ${packageVersion}.`);
|
|
13955
|
+
// src/extensions/processx.ts
|
|
13956
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
13957
|
+
import { constants } from "node:fs";
|
|
13958
|
+
import { access } from "node:fs/promises";
|
|
13959
|
+
import { delimiter, join as join7 } from "node:path";
|
|
13960
|
+
async function findExecutable(name) {
|
|
13961
|
+
if (name.includes("/")) {
|
|
13962
|
+
try {
|
|
13963
|
+
await access(name, constants.X_OK);
|
|
13964
|
+
return name;
|
|
13965
|
+
} catch {
|
|
13966
|
+
return;
|
|
13967
|
+
}
|
|
13968
|
+
}
|
|
13969
|
+
for (const directory of (process.env.PATH ?? "").split(delimiter)) {
|
|
13970
|
+
if (!directory)
|
|
13971
|
+
continue;
|
|
13972
|
+
const candidate = join7(directory, name);
|
|
13973
|
+
try {
|
|
13974
|
+
await access(candidate, constants.X_OK);
|
|
13975
|
+
return candidate;
|
|
13976
|
+
} catch {}
|
|
14437
13977
|
}
|
|
13978
|
+
return;
|
|
13979
|
+
}
|
|
13980
|
+
function run(command, args, options = {}) {
|
|
13981
|
+
return new Promise((resolve, reject) => {
|
|
13982
|
+
const child = spawn2(command, args, {
|
|
13983
|
+
cwd: options.cwd,
|
|
13984
|
+
env: options.env ?? process.env,
|
|
13985
|
+
stdio: [options.input === undefined ? "ignore" : "pipe", "pipe", "pipe"]
|
|
13986
|
+
});
|
|
13987
|
+
let stdout = "";
|
|
13988
|
+
let stderr = "";
|
|
13989
|
+
const stdoutChunks = [];
|
|
13990
|
+
const stderrChunks = [];
|
|
13991
|
+
const unbounded = options.capture === "unbounded";
|
|
13992
|
+
child.stdout?.on("data", (chunk) => {
|
|
13993
|
+
const text = chunk.toString();
|
|
13994
|
+
if (unbounded)
|
|
13995
|
+
stdoutChunks.push(text);
|
|
13996
|
+
else
|
|
13997
|
+
stdout = appendBounded(stdout, text);
|
|
13998
|
+
});
|
|
13999
|
+
child.stderr?.on("data", (chunk) => {
|
|
14000
|
+
const text = chunk.toString();
|
|
14001
|
+
if (unbounded)
|
|
14002
|
+
stderrChunks.push(text);
|
|
14003
|
+
else
|
|
14004
|
+
stderr = appendBounded(stderr, text);
|
|
14005
|
+
});
|
|
14006
|
+
child.on("error", reject);
|
|
14007
|
+
child.on("close", (code) => resolve({
|
|
14008
|
+
code: code ?? 1,
|
|
14009
|
+
stdout: unbounded ? stdoutChunks.join("") : stdout,
|
|
14010
|
+
stderr: unbounded ? stderrChunks.join("") : stderr
|
|
14011
|
+
}));
|
|
14012
|
+
if (options.input !== undefined && child.stdin)
|
|
14013
|
+
child.stdin.end(options.input);
|
|
14014
|
+
});
|
|
14015
|
+
}
|
|
14016
|
+
async function runChecked(command, args, options = {}) {
|
|
14017
|
+
const result = await run(command, args, options);
|
|
14018
|
+
if (result.code === 0)
|
|
14019
|
+
return result;
|
|
14020
|
+
const detail = result.stderr.trim() || result.stdout.trim() || `exit code ${result.code}`;
|
|
14021
|
+
throw new Error(`${command} ${args.join(" ")} failed: ${detail}`);
|
|
14022
|
+
}
|
|
14023
|
+
var MAX_CAPTURED_OUTPUT_LENGTH = 65536;
|
|
14024
|
+
function appendBounded(current, next) {
|
|
14025
|
+
const combined = current + next;
|
|
14026
|
+
return combined.length <= MAX_CAPTURED_OUTPUT_LENGTH ? combined : combined.slice(-MAX_CAPTURED_OUTPUT_LENGTH);
|
|
14027
|
+
}
|
|
14028
|
+
|
|
14029
|
+
// src/extensions/zedx.ts
|
|
14030
|
+
import { mkdir, readFile as readFile3, writeFile } from "node:fs/promises";
|
|
14031
|
+
import { homedir as homedir4 } from "node:os";
|
|
14032
|
+
import { dirname as dirname3, join as join8 } from "node:path";
|
|
14033
|
+
var ZED_LOCAL_REVIEW_TASK_NAME = "diffpi: tuicr local review";
|
|
14034
|
+
var ZED_PR_REVIEW_TASK_NAME = "diffpi: tuicr PR review";
|
|
14035
|
+
var LEGACY_ZED_REVIEW_TASK_NAME = "diffpi: tuicr review";
|
|
14036
|
+
var REVIEW_KEYBINDING = "cmd-alt-r";
|
|
14037
|
+
function zedTasksPath(homeDir = homedir4()) {
|
|
14038
|
+
return join8(homeDir, ".config", "zed", "tasks.json");
|
|
14039
|
+
}
|
|
14040
|
+
function zedKeymapPath(homeDir = homedir4()) {
|
|
14041
|
+
return join8(homeDir, ".config", "zed", "keymap.json");
|
|
14042
|
+
}
|
|
14043
|
+
async function ensureZedReviewTask(homeDir = homedir4(), _command = ["tuicr", "-w", "-r", "main..HEAD"]) {
|
|
14438
14044
|
const path = zedTasksPath(homeDir);
|
|
14439
14045
|
const currentText = await readOptional(path);
|
|
14440
14046
|
const tasks = parseJsonArray(currentText, path);
|
|
14441
|
-
const
|
|
14442
|
-
|
|
14443
|
-
|
|
14444
|
-
|
|
14445
|
-
|
|
14446
|
-
|
|
14447
|
-
|
|
14448
|
-
|
|
14449
|
-
}
|
|
14450
|
-
const next = [...tasks];
|
|
14451
|
-
const index = next.findIndex((candidate) => candidate.label === task.label);
|
|
14452
|
-
if (index >= 0)
|
|
14453
|
-
next[index] = { ...next[index], ...task };
|
|
14454
|
-
else
|
|
14455
|
-
next.push(task);
|
|
14047
|
+
const migrated = tasks.filter((task) => task.label !== LEGACY_ZED_REVIEW_TASK_NAME);
|
|
14048
|
+
const next = [...migrated];
|
|
14049
|
+
for (const task of [localReviewTask(), prReviewTask()]) {
|
|
14050
|
+
const index = next.findIndex((existing) => existing.label === task.label);
|
|
14051
|
+
if (index >= 0)
|
|
14052
|
+
next[index] = { ...next[index], ...task };
|
|
14053
|
+
else
|
|
14054
|
+
next.push(task);
|
|
14055
|
+
}
|
|
14456
14056
|
const changed = JSON.stringify(tasks) !== JSON.stringify(next);
|
|
14457
14057
|
if (changed)
|
|
14458
14058
|
await writeJson(path, next);
|
|
14459
14059
|
return { path, changed, existed: currentText !== undefined };
|
|
14460
14060
|
}
|
|
14461
|
-
async function ensureZedReviewKeybinding(homeDir =
|
|
14061
|
+
async function ensureZedReviewKeybinding(homeDir = homedir4()) {
|
|
14462
14062
|
const path = zedKeymapPath(homeDir);
|
|
14463
14063
|
const currentText = await readOptional(path);
|
|
14464
14064
|
const entries = parseJsonArray(currentText, path);
|
|
@@ -14477,9 +14077,6 @@ async function ensureZedReviewKeybinding(homeDir = homedir5()) {
|
|
|
14477
14077
|
return { path, changed, existed: currentText !== undefined };
|
|
14478
14078
|
}
|
|
14479
14079
|
function zedReviewTaskName(command) {
|
|
14480
|
-
if (command.some((value, index) => value === "plan" && command[index + 1] === "annotate")) {
|
|
14481
|
-
return ZED_PLAN_ANNOTATE_TASK_NAME;
|
|
14482
|
-
}
|
|
14483
14080
|
return command[0] === "tuicr" && command[1] === "pr" ? ZED_PR_REVIEW_TASK_NAME : ZED_LOCAL_REVIEW_TASK_NAME;
|
|
14484
14081
|
}
|
|
14485
14082
|
var LOCAL_REVIEW_SCRIPT = `set -eu
|
|
@@ -14561,2030 +14158,113 @@ function parseJsonArray(content, path) {
|
|
|
14561
14158
|
return value;
|
|
14562
14159
|
}
|
|
14563
14160
|
async function writeJson(path, value) {
|
|
14564
|
-
await
|
|
14565
|
-
await
|
|
14566
|
-
`, "utf8");
|
|
14567
|
-
}
|
|
14568
|
-
|
|
14569
|
-
// src/environment.ts
|
|
14570
|
-
function detectIde(env = process.env) {
|
|
14571
|
-
const program = (env.TERM_PROGRAM ?? "").toLowerCase();
|
|
14572
|
-
if (env.ZED_TERM === "true" || program === "zed")
|
|
14573
|
-
return "zed";
|
|
14574
|
-
if (env.CURSOR_TRACE_ID || program === "cursor")
|
|
14575
|
-
return "cursor";
|
|
14576
|
-
if (env.WINDSURF_ENV || program === "windsurf")
|
|
14577
|
-
return "windsurf";
|
|
14578
|
-
if (env.TERMINAL_EMULATOR?.toLowerCase().includes("jetbrains"))
|
|
14579
|
-
return "jetbrains";
|
|
14580
|
-
if (env.VSCODE_PID || env.VSCODE_GIT_IPC_HANDLE || program === "vscode")
|
|
14581
|
-
return "vscode";
|
|
14582
|
-
return "unknown";
|
|
14583
|
-
}
|
|
14584
|
-
function detectMux(env = process.env) {
|
|
14585
|
-
if (env.ZELLIJ || env.ZELLIJ_SESSION_NAME)
|
|
14586
|
-
return "zellij";
|
|
14587
|
-
if (env.TMUX)
|
|
14588
|
-
return "tmux";
|
|
14589
|
-
if (env.STY)
|
|
14590
|
-
return "screen";
|
|
14591
|
-
return "none";
|
|
14592
|
-
}
|
|
14593
|
-
function detectShell(env = process.env) {
|
|
14594
|
-
return env.SHELL ? basename3(env.SHELL) : "unknown";
|
|
14595
|
-
}
|
|
14596
|
-
async function detectVcs(cwd) {
|
|
14597
|
-
const root = (await run("git", ["-C", cwd, "rev-parse", "--show-toplevel"])).stdout.trim() || cwd;
|
|
14598
|
-
const branch = (await run("git", ["-C", root, "rev-parse", "--abbrev-ref", "HEAD"])).stdout.trim();
|
|
14599
|
-
const remote = (await run("git", ["-C", root, "remote", "get-url", "origin"])).stdout.trim();
|
|
14600
|
-
return { ...parseRemote(remote), branch, root };
|
|
14601
|
-
}
|
|
14602
|
-
function parseRemote(remote) {
|
|
14603
|
-
const empty = { provider: "none", host: "", owner: "", repo: "" };
|
|
14604
|
-
if (!remote)
|
|
14605
|
-
return empty;
|
|
14606
|
-
const scp = remote.match(/^[^@]+@([^:]+):(.+?)(?:\.git)?$/);
|
|
14607
|
-
const url = remote.match(/^[a-z]+:\/\/(?:[^@]+@)?([^/]+)\/(.+?)(?:\.git)?$/i);
|
|
14608
|
-
const match = scp ?? url;
|
|
14609
|
-
if (!match)
|
|
14610
|
-
return empty;
|
|
14611
|
-
const host = match[1];
|
|
14612
|
-
const segments = match[2].split("/").filter(Boolean);
|
|
14613
|
-
if (segments.length < 2)
|
|
14614
|
-
return { ...empty, host };
|
|
14615
|
-
const repo = segments.at(-1) ?? "";
|
|
14616
|
-
const owner = segments.slice(0, -1).join("/");
|
|
14617
|
-
const provider = /github/i.test(host) ? "github" : /gitlab/i.test(host) ? "gitlab" : "none";
|
|
14618
|
-
return { provider, host, owner, repo };
|
|
14619
|
-
}
|
|
14620
|
-
async function openInNewTab(command, opts) {
|
|
14621
|
-
const env = opts.env ?? process.env;
|
|
14622
|
-
const name = opts.name ?? "review";
|
|
14623
|
-
const printable = command.join(" ");
|
|
14624
|
-
const mux = detectMux(env);
|
|
14625
|
-
if (mux !== "none") {
|
|
14626
|
-
const opened = await openMuxTab(mux, command, opts.cwd, name, printable);
|
|
14627
|
-
if (opened)
|
|
14628
|
-
return opened;
|
|
14629
|
-
}
|
|
14630
|
-
if (detectIde(env) === "zed") {
|
|
14631
|
-
try {
|
|
14632
|
-
const taskName = zedReviewTaskName(command);
|
|
14633
|
-
if (taskName === ZED_PLAN_ANNOTATE_TASK_NAME)
|
|
14634
|
-
await ensureZedPlanTask(await packageVersion(), opts.homeDir);
|
|
14635
|
-
else
|
|
14636
|
-
await ensureZedReviewTask(opts.homeDir, command);
|
|
14637
|
-
return {
|
|
14638
|
-
launched: false,
|
|
14639
|
-
configured: true,
|
|
14640
|
-
via: "zed-task",
|
|
14641
|
-
command: printable,
|
|
14642
|
-
taskName,
|
|
14643
|
-
instruction: `Run the Zed task "${taskName}".`
|
|
14644
|
-
};
|
|
14645
|
-
} catch {}
|
|
14646
|
-
}
|
|
14647
|
-
return { launched: false, via: "print", command: printable };
|
|
14648
|
-
}
|
|
14649
|
-
function screenWindowArgs(command, cwd, name) {
|
|
14650
|
-
return ["-X", "screen", "-t", name, "sh", "-lc", 'cd -- "$1" && shift && exec "$@"', "sh", cwd, ...command];
|
|
14651
|
-
}
|
|
14652
|
-
async function packageVersion() {
|
|
14653
|
-
const value = JSON.parse(await readFile4(join13(resolveBundledAgentsDir(), "..", "package.json"), "utf8"));
|
|
14654
|
-
if (typeof value.version !== "string")
|
|
14655
|
-
throw new Error("Cannot resolve the installed @difflab/pi version.");
|
|
14656
|
-
return value.version;
|
|
14657
|
-
}
|
|
14658
|
-
async function openMuxTab(mux, command, cwd, name, printable) {
|
|
14659
|
-
if (mux === "zellij" && await findExecutable("zellij")) {
|
|
14660
|
-
const result = await run("zellij", ["action", "new-tab", "--cwd", cwd, "--name", name, "--", ...command]);
|
|
14661
|
-
if (result.code === 0)
|
|
14662
|
-
return { launched: true, via: "zellij", command: printable };
|
|
14663
|
-
const fallback = await run("zellij", ["run", "--cwd", cwd, "--name", name, "--", ...command]);
|
|
14664
|
-
if (fallback.code === 0)
|
|
14665
|
-
return { launched: true, via: "zellij-run", command: printable };
|
|
14666
|
-
}
|
|
14667
|
-
if (mux === "tmux" && await findExecutable("tmux")) {
|
|
14668
|
-
const result = await run("tmux", ["new-window", "-c", cwd, "-n", name, printable]);
|
|
14669
|
-
if (result.code === 0)
|
|
14670
|
-
return { launched: true, via: "tmux", command: printable };
|
|
14671
|
-
}
|
|
14672
|
-
if (mux === "screen" && await findExecutable("screen")) {
|
|
14673
|
-
const result = await run("screen", screenWindowArgs(command, cwd, name));
|
|
14674
|
-
if (result.code === 0)
|
|
14675
|
-
return { launched: true, via: "screen", command: printable };
|
|
14676
|
-
}
|
|
14677
|
-
return;
|
|
14678
|
-
}
|
|
14679
|
-
|
|
14680
|
-
// src/extensions/misex.ts
|
|
14681
|
-
import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile3 } from "node:fs/promises";
|
|
14682
|
-
import { homedir as homedir6 } from "node:os";
|
|
14683
|
-
import { basename as basename4, dirname as dirname5, join as join14 } from "node:path";
|
|
14684
|
-
var MISE_HOOK_START = "# >>> @difflab/pi mise >>>";
|
|
14685
|
-
var MISE_HOOK_END = "# <<< @difflab/pi mise <<<";
|
|
14686
|
-
var mise = {
|
|
14687
|
-
async executableCheck(name = "mise") {
|
|
14688
|
-
return findExecutable(name);
|
|
14689
|
-
},
|
|
14690
|
-
async run(args, options = {}) {
|
|
14691
|
-
return run("mise", args, options);
|
|
14692
|
-
},
|
|
14693
|
-
async install(options = {}) {
|
|
14694
|
-
const homeDir = options.homeDir ?? homedir6();
|
|
14695
|
-
const platform = options.platform ?? process.platform;
|
|
14696
|
-
if (platform === "win32")
|
|
14697
|
-
throw new Error("Automatic mise installation supports macOS and Linux only.");
|
|
14698
|
-
const installedPath = join14(homeDir, ".local", "bin", "mise");
|
|
14699
|
-
if (options.dryRun)
|
|
14700
|
-
return installedPath;
|
|
14701
|
-
await runChecked("sh", ["-c", "curl -fsSL https://mise.run | sh"]);
|
|
14702
|
-
const executable = await findExecutable(installedPath) ?? await findExecutable("mise");
|
|
14703
|
-
if (!executable)
|
|
14704
|
-
throw new Error(`mise installation completed, but ${installedPath} was not found.`);
|
|
14705
|
-
return executable;
|
|
14706
|
-
},
|
|
14707
|
-
async hookEnsure(executable, options = {}) {
|
|
14708
|
-
const homeDir = options.homeDir ?? homedir6();
|
|
14709
|
-
const hook = getShellHook(basename4(options.shell ?? process.env.SHELL ?? ""), executable, homeDir);
|
|
14710
|
-
const current = await getOptionalFile(hook.path);
|
|
14711
|
-
if (current.includes(MISE_HOOK_START))
|
|
14712
|
-
return { path: hook.path, changed: false, planned: false };
|
|
14713
|
-
if (options.dryRun)
|
|
14714
|
-
return { path: hook.path, changed: true, planned: true };
|
|
14715
|
-
const separator = current.length === 0 || current.endsWith(`
|
|
14716
|
-
`) ? "" : `
|
|
14717
|
-
`;
|
|
14718
|
-
await mkdir4(dirname5(hook.path), { recursive: true });
|
|
14719
|
-
await writeFile3(hook.path, `${current}${separator}${hook.content}`, "utf8");
|
|
14720
|
-
return { path: hook.path, changed: true, planned: false };
|
|
14721
|
-
},
|
|
14722
|
-
async toolCheckGlobal(executable, tool, minimumVersion) {
|
|
14723
|
-
const result = await run(executable, ["ls", "--global", "--installed", tool, "--json"]);
|
|
14724
|
-
return result.code === 0 && isToolInstalled(result.stdout, minimumVersion);
|
|
14725
|
-
},
|
|
14726
|
-
async toolInstallGlobal(executable, specification) {
|
|
14727
|
-
await runChecked(executable, ["use", "--global", specification]);
|
|
14728
|
-
},
|
|
14729
|
-
async toolCheckLocal(executable, tool, cwd = process.cwd()) {
|
|
14730
|
-
const result = await run(executable, ["ls", "--local", "--installed", tool, "--json"], { cwd });
|
|
14731
|
-
return result.code === 0 && isToolInstalled(result.stdout);
|
|
14732
|
-
},
|
|
14733
|
-
async toolInstallLocal(executable, specification, cwd = process.cwd()) {
|
|
14734
|
-
await runChecked(executable, ["use", "--path", cwd, specification], { cwd });
|
|
14735
|
-
},
|
|
14736
|
-
async toolUpdateAllGlobal(executable, homeDir = homedir6()) {
|
|
14737
|
-
await runChecked(executable, ["upgrade"], { cwd: homeDir });
|
|
14738
|
-
}
|
|
14739
|
-
};
|
|
14740
|
-
function getShellHook(shell, executable, homeDir) {
|
|
14741
|
-
const command = getShellQuoted(executable);
|
|
14742
|
-
switch (shell.toLowerCase()) {
|
|
14743
|
-
case "zsh":
|
|
14744
|
-
return {
|
|
14745
|
-
path: join14(homeDir, ".zshrc"),
|
|
14746
|
-
content: `${MISE_HOOK_START}
|
|
14747
|
-
eval "$(${command} activate zsh)"
|
|
14748
|
-
${MISE_HOOK_END}
|
|
14749
|
-
`
|
|
14750
|
-
};
|
|
14751
|
-
case "fish":
|
|
14752
|
-
return {
|
|
14753
|
-
path: join14(homeDir, ".config", "fish", "config.fish"),
|
|
14754
|
-
content: `${MISE_HOOK_START}
|
|
14755
|
-
${command} activate fish | source
|
|
14756
|
-
${MISE_HOOK_END}
|
|
14757
|
-
`
|
|
14758
|
-
};
|
|
14759
|
-
case "nu":
|
|
14760
|
-
case "nushell":
|
|
14761
|
-
return {
|
|
14762
|
-
path: join14(homeDir, ".config", "nushell", "config.nu"),
|
|
14763
|
-
content: `${MISE_HOOK_START}
|
|
14764
|
-
let mise_bin = ${command}
|
|
14765
|
-
let mise_path = $nu.default-config-dir | path join mise.nu
|
|
14766
|
-
^$mise_bin activate nu | save $mise_path --force
|
|
14767
|
-
use ($nu.default-config-dir | path join mise.nu)
|
|
14768
|
-
${MISE_HOOK_END}
|
|
14769
|
-
`
|
|
14770
|
-
};
|
|
14771
|
-
case "xonsh":
|
|
14772
|
-
return {
|
|
14773
|
-
path: join14(homeDir, ".xonshrc"),
|
|
14774
|
-
content: `${MISE_HOOK_START}
|
|
14775
|
-
execx($(${command} activate xonsh))
|
|
14776
|
-
${MISE_HOOK_END}
|
|
14777
|
-
`
|
|
14778
|
-
};
|
|
14779
|
-
case "elvish":
|
|
14780
|
-
return {
|
|
14781
|
-
path: join14(homeDir, ".config", "elvish", "rc.elv"),
|
|
14782
|
-
content: `${MISE_HOOK_START}
|
|
14783
|
-
var mise: = (ns [&])
|
|
14784
|
-
eval (${command} activate elvish | slurp) &ns=$mise: &on-end={|ns| set mise: = $ns }
|
|
14785
|
-
mise:activate
|
|
14786
|
-
${MISE_HOOK_END}
|
|
14787
|
-
`
|
|
14788
|
-
};
|
|
14789
|
-
case "pwsh":
|
|
14790
|
-
case "powershell":
|
|
14791
|
-
return {
|
|
14792
|
-
path: join14(homeDir, ".config", "powershell", "Microsoft.PowerShell_profile.ps1"),
|
|
14793
|
-
content: `${MISE_HOOK_START}
|
|
14794
|
-
(& ${command} activate pwsh) | Out-String | Invoke-Expression
|
|
14795
|
-
${MISE_HOOK_END}
|
|
14796
|
-
`
|
|
14797
|
-
};
|
|
14798
|
-
case "bash":
|
|
14799
|
-
default:
|
|
14800
|
-
return {
|
|
14801
|
-
path: join14(homeDir, ".bashrc"),
|
|
14802
|
-
content: `${MISE_HOOK_START}
|
|
14803
|
-
eval "$(${command} activate bash)"
|
|
14804
|
-
${MISE_HOOK_END}
|
|
14805
|
-
`
|
|
14806
|
-
};
|
|
14807
|
-
}
|
|
14808
|
-
}
|
|
14809
|
-
function isToolInstalled(output, minimumVersion) {
|
|
14810
|
-
try {
|
|
14811
|
-
const value = JSON.parse(output);
|
|
14812
|
-
if (!Array.isArray(value))
|
|
14813
|
-
return false;
|
|
14814
|
-
return value.some((entry) => {
|
|
14815
|
-
if (!entry || typeof entry !== "object" || !("installed" in entry) || entry.installed !== true)
|
|
14816
|
-
return false;
|
|
14817
|
-
if (!minimumVersion)
|
|
14818
|
-
return true;
|
|
14819
|
-
if (!("version" in entry) || typeof entry.version !== "string")
|
|
14820
|
-
return false;
|
|
14821
|
-
return isVersionAtLeast(entry.version, minimumVersion);
|
|
14822
|
-
});
|
|
14823
|
-
} catch {
|
|
14824
|
-
return false;
|
|
14825
|
-
}
|
|
14826
|
-
}
|
|
14827
|
-
function isVersionAtLeast(version, minimumVersion) {
|
|
14828
|
-
const current = version.match(/^v?(\d+)\.(\d+)\.(\d+)/)?.slice(1).map(Number);
|
|
14829
|
-
const minimum = minimumVersion.match(/^v?(\d+)\.(\d+)\.(\d+)/)?.slice(1).map(Number);
|
|
14830
|
-
if (!current || !minimum)
|
|
14831
|
-
return false;
|
|
14832
|
-
for (let index = 0;index < minimum.length; index += 1) {
|
|
14833
|
-
if (current[index] !== minimum[index])
|
|
14834
|
-
return current[index] > minimum[index];
|
|
14835
|
-
}
|
|
14836
|
-
return true;
|
|
14837
|
-
}
|
|
14838
|
-
async function getOptionalFile(path) {
|
|
14839
|
-
try {
|
|
14840
|
-
return await readFile5(path, "utf8");
|
|
14841
|
-
} catch (error) {
|
|
14842
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
14843
|
-
return "";
|
|
14844
|
-
throw error;
|
|
14845
|
-
}
|
|
14846
|
-
}
|
|
14847
|
-
function getShellQuoted(value) {
|
|
14848
|
-
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
14849
|
-
}
|
|
14850
|
-
|
|
14851
|
-
// src/gates.ts
|
|
14852
|
-
var CONVENTIONAL_COMMIT = /^(feat|fix|perf|refactor|docs|chore|test|build|ci|style|revert)(\([^)]+\))?!?: .+/;
|
|
14853
|
-
var MISE_GATES = ["format:check", "lint", "test"];
|
|
14854
|
-
function checkConventionalSubject(subject) {
|
|
14855
|
-
const trimmed = subject.trim();
|
|
14856
|
-
const ok = CONVENTIONAL_COMMIT.test(trimmed);
|
|
14857
|
-
return {
|
|
14858
|
-
name: "conventional-subject",
|
|
14859
|
-
status: ok ? "pass" : "warn",
|
|
14860
|
-
detail: ok ? trimmed : `not a conventional-commit subject: "${trimmed}"`
|
|
14861
|
-
};
|
|
14862
|
-
}
|
|
14863
|
-
async function runMiseGates(cwd) {
|
|
14864
|
-
const tasks = await discoverMiseTasks(cwd);
|
|
14865
|
-
const results = [];
|
|
14866
|
-
for (const gate of MISE_GATES) {
|
|
14867
|
-
const targets = tasks.get(gate) ?? [];
|
|
14868
|
-
if (targets.length === 0) {
|
|
14869
|
-
results.push({ name: gate, status: "skip", detail: "no mise recipe" });
|
|
14870
|
-
continue;
|
|
14871
|
-
}
|
|
14872
|
-
const invocations = targets.flatMap((target, index) => index === 0 ? [target] : [":::", target]);
|
|
14873
|
-
const result = await mise.run(["run", ...invocations], { cwd });
|
|
14874
|
-
results.push({
|
|
14875
|
-
name: gate,
|
|
14876
|
-
status: result.code === 0 ? "pass" : "fail",
|
|
14877
|
-
detail: result.code === 0 ? "clean" : (result.stderr.trim() || result.stdout.trim()).slice(-400)
|
|
14878
|
-
});
|
|
14879
|
-
}
|
|
14880
|
-
return results;
|
|
14881
|
-
}
|
|
14882
|
-
function ciGate(checksOutput) {
|
|
14883
|
-
const text = checksOutput.toLowerCase();
|
|
14884
|
-
if (!text.trim())
|
|
14885
|
-
return { name: "ci", status: "skip", detail: "no CI output" };
|
|
14886
|
-
if (/\bfail|error\b/.test(text))
|
|
14887
|
-
return { name: "ci", status: "warn", detail: "CI failing" };
|
|
14888
|
-
if (/\bpending|in progress|queued\b/.test(text))
|
|
14889
|
-
return { name: "ci", status: "warn", detail: "CI pending" };
|
|
14890
|
-
return { name: "ci", status: "pass", detail: "CI green" };
|
|
14891
|
-
}
|
|
14892
|
-
function parseMiseTasks(input) {
|
|
14893
|
-
let tasks;
|
|
14894
|
-
try {
|
|
14895
|
-
tasks = JSON.parse(input);
|
|
14896
|
-
} catch {
|
|
14897
|
-
return new Map;
|
|
14898
|
-
}
|
|
14899
|
-
if (!Array.isArray(tasks))
|
|
14900
|
-
return new Map;
|
|
14901
|
-
const found = new Map;
|
|
14902
|
-
for (const gate of MISE_GATES) {
|
|
14903
|
-
const targets = tasks.flatMap((task) => {
|
|
14904
|
-
if (typeof task.name !== "string")
|
|
14905
|
-
return [];
|
|
14906
|
-
return task.name === gate || task.name.endsWith(`:${gate}`) || task.aliases?.includes(gate) ? [task.name] : [];
|
|
14907
|
-
});
|
|
14908
|
-
if (targets.length > 0)
|
|
14909
|
-
found.set(gate, [...new Set(targets)]);
|
|
14910
|
-
}
|
|
14911
|
-
return found;
|
|
14912
|
-
}
|
|
14913
|
-
async function discoverMiseTasks(cwd) {
|
|
14914
|
-
const result = await mise.run(["tasks", "--json", "--all"], { cwd });
|
|
14915
|
-
if (result.code !== 0)
|
|
14916
|
-
return new Map;
|
|
14917
|
-
return parseMiseTasks(result.stdout);
|
|
14918
|
-
}
|
|
14919
|
-
|
|
14920
|
-
// src/plan/annotations.ts
|
|
14921
|
-
import { spawn as spawn3 } from "node:child_process";
|
|
14922
|
-
import { readFile as readFile8, rename, writeFile as writeFile5 } from "node:fs/promises";
|
|
14923
|
-
import { basename as basename5, dirname as dirname6, join as join16 } from "node:path";
|
|
14924
|
-
|
|
14925
|
-
// src/plan/log.ts
|
|
14926
|
-
import { randomUUID } from "node:crypto";
|
|
14927
|
-
import { appendFile, readFile as readFile6 } from "node:fs/promises";
|
|
14928
|
-
async function appendPlanLog(logPath, event) {
|
|
14929
|
-
const entry = {
|
|
14930
|
-
...event,
|
|
14931
|
-
version: 1,
|
|
14932
|
-
eventId: event.eventId ?? randomUUID(),
|
|
14933
|
-
timestamp: event.timestamp ?? new Date().toISOString()
|
|
14934
|
-
};
|
|
14935
|
-
await appendFile(logPath, `${JSON.stringify(entry)}
|
|
14936
|
-
`, { encoding: "utf8", mode: 384 });
|
|
14937
|
-
return entry;
|
|
14938
|
-
}
|
|
14939
|
-
|
|
14940
|
-
// src/plan/lock.ts
|
|
14941
|
-
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
14942
|
-
import { mkdir as mkdir5, readFile as readFile7, rm as rm2, writeFile as writeFile4 } from "node:fs/promises";
|
|
14943
|
-
import { hostname as hostname2 } from "node:os";
|
|
14944
|
-
import { join as join15 } from "node:path";
|
|
14945
|
-
async function withPlanLock(planDir, operation, options = {}) {
|
|
14946
|
-
const lockDir = join15(planDir, ".lock");
|
|
14947
|
-
const waitMs = options.waitMs ?? 5000;
|
|
14948
|
-
const pollMs = options.pollMs ?? 25;
|
|
14949
|
-
const owner = {
|
|
14950
|
-
token: randomUUID2(),
|
|
14951
|
-
pid: process.pid,
|
|
14952
|
-
hostname: hostname2(),
|
|
14953
|
-
operation: options.operation ?? "plan mutation",
|
|
14954
|
-
acquiredAt: new Date().toISOString()
|
|
14955
|
-
};
|
|
14956
|
-
const started = Date.now();
|
|
14957
|
-
while (true) {
|
|
14958
|
-
try {
|
|
14959
|
-
await mkdir5(lockDir);
|
|
14960
|
-
await writeFile4(join15(lockDir, "owner.json"), `${JSON.stringify(owner)}
|
|
14961
|
-
`, { mode: 384 });
|
|
14962
|
-
break;
|
|
14963
|
-
} catch (error) {
|
|
14964
|
-
if (error.code !== "EEXIST")
|
|
14965
|
-
throw error;
|
|
14966
|
-
if (Date.now() - started >= waitMs) {
|
|
14967
|
-
let existing = "unknown owner";
|
|
14968
|
-
try {
|
|
14969
|
-
existing = (await readFile7(join15(lockDir, "owner.json"), "utf8")).trim();
|
|
14970
|
-
} catch {}
|
|
14971
|
-
throw new Error(`Timed out waiting for plan lock ${lockDir}; owner: ${existing}. Remove it only after confirming the owner is stale.`);
|
|
14972
|
-
}
|
|
14973
|
-
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
14974
|
-
}
|
|
14975
|
-
}
|
|
14976
|
-
try {
|
|
14977
|
-
return await operation();
|
|
14978
|
-
} finally {
|
|
14979
|
-
let current;
|
|
14980
|
-
try {
|
|
14981
|
-
current = JSON.parse(await readFile7(join15(lockDir, "owner.json"), "utf8"));
|
|
14982
|
-
} catch {}
|
|
14983
|
-
if (current?.token === owner.token)
|
|
14984
|
-
await rm2(lockDir, { recursive: true, force: true });
|
|
14985
|
-
}
|
|
14986
|
-
}
|
|
14987
|
-
|
|
14988
|
-
// src/plan/annotations.ts
|
|
14989
|
-
async function readPlanAnnotations(record, options = {}) {
|
|
14990
|
-
const state = await readAnnotationState(record).catch((error) => {
|
|
14991
|
-
if (error.code === "ENOENT")
|
|
14992
|
-
return;
|
|
14993
|
-
throw error;
|
|
14994
|
-
});
|
|
14995
|
-
if (!state)
|
|
14996
|
-
return { comments: [], pending: [] };
|
|
14997
|
-
const execute = options.runtime?.execute ?? executeProcess;
|
|
14998
|
-
const response = await execute("tuicr", ["review", "comments", "--session", state.sessionSlug], record.dir, false);
|
|
14999
|
-
if (response.code !== 0) {
|
|
15000
|
-
if (/not found|no session|deleted/i.test(response.stderr))
|
|
15001
|
-
return { state, comments: [], pending: [] };
|
|
15002
|
-
throw new Error(response.stderr.trim() || "Cannot read tuicr comments.");
|
|
15003
|
-
}
|
|
15004
|
-
let raw;
|
|
15005
|
-
try {
|
|
15006
|
-
raw = JSON.parse(response.stdout);
|
|
15007
|
-
} catch {
|
|
15008
|
-
throw new Error("tuicr returned malformed comment JSON.");
|
|
15009
|
-
}
|
|
15010
|
-
if (!Array.isArray(raw))
|
|
15011
|
-
throw new Error("tuicr comment output must be a JSON array.");
|
|
15012
|
-
const lines = record.source.split(`
|
|
15013
|
-
`);
|
|
15014
|
-
const applied = new Set(state.appliedCommentIds);
|
|
15015
|
-
const comments = raw.map((value, index) => normalizeComment(value, index, lines, applied));
|
|
15016
|
-
const pending = comments.filter((comment) => !comment.applied);
|
|
15017
|
-
return { state, comments: options.includeApplied ? comments : pending, pending };
|
|
15018
|
-
}
|
|
15019
|
-
async function acknowledgePlanAnnotations(record, commentIds, summary) {
|
|
15020
|
-
if (!summary.trim())
|
|
15021
|
-
throw new Error("Annotation acknowledgement summary is required.");
|
|
15022
|
-
return withPlanLock(record.dir, async () => {
|
|
15023
|
-
const state = await readAnnotationState(record);
|
|
15024
|
-
const next = {
|
|
15025
|
-
...state,
|
|
15026
|
-
updatedAt: new Date().toISOString(),
|
|
15027
|
-
appliedCommentIds: [...new Set([...state.appliedCommentIds, ...commentIds])]
|
|
15028
|
-
};
|
|
15029
|
-
await atomicJson(annotationStatePath(record), next);
|
|
15030
|
-
await appendPlanLog(record.logPath, {
|
|
15031
|
-
planRevision: record.document.revision,
|
|
15032
|
-
kind: "annotation",
|
|
15033
|
-
actor: "planner",
|
|
15034
|
-
message: summary,
|
|
15035
|
-
data: { commentIds: [...commentIds], sessionSlug: state.sessionSlug }
|
|
15036
|
-
});
|
|
15037
|
-
return next;
|
|
15038
|
-
}, { operation: "acknowledge annotations" });
|
|
15039
|
-
}
|
|
15040
|
-
function annotationStatePath(record) {
|
|
15041
|
-
return join16(record.dir, "annotations.json");
|
|
15042
|
-
}
|
|
15043
|
-
async function readAnnotationState(record) {
|
|
15044
|
-
const value = JSON.parse(await readFile8(annotationStatePath(record), "utf8"));
|
|
15045
|
-
if (value.schemaVersion !== 1 || typeof value.sessionSlug !== "string" || !Array.isArray(value.appliedCommentIds)) {
|
|
15046
|
-
throw new Error(`Malformed annotation state: ${annotationStatePath(record)}.`);
|
|
15047
|
-
}
|
|
15048
|
-
return value;
|
|
15049
|
-
}
|
|
15050
|
-
function normalizeComment(value, index, lines, applied) {
|
|
15051
|
-
if (!value || typeof value !== "object")
|
|
15052
|
-
throw new Error(`Malformed tuicr comment at index ${index}.`);
|
|
15053
|
-
const raw = value;
|
|
15054
|
-
if (typeof raw.id !== "string" || typeof raw.content !== "string")
|
|
15055
|
-
throw new Error(`Malformed tuicr comment at index ${index}.`);
|
|
15056
|
-
const line = integer5(raw.start_line) ?? integer5(raw.line);
|
|
15057
|
-
const endLine = integer5(raw.end_line) ?? line;
|
|
15058
|
-
const targetPath = typeof raw.path === "string" ? raw.path : undefined;
|
|
15059
|
-
const appliesToPlan = !targetPath || basename5(targetPath) === basename5("PLAN.md");
|
|
15060
|
-
const validAnchor = appliesToPlan && line !== undefined && line > 0 && line <= lines.length;
|
|
15061
|
-
return {
|
|
15062
|
-
id: raw.id,
|
|
15063
|
-
body: raw.content,
|
|
15064
|
-
file: targetPath,
|
|
15065
|
-
line,
|
|
15066
|
-
endLine,
|
|
15067
|
-
context: validAnchor ? lines.slice(line - 1, Math.min(endLine ?? line, lines.length)).join(`
|
|
15068
|
-
`) : undefined,
|
|
15069
|
-
stale: line !== undefined && !validAnchor,
|
|
15070
|
-
applied: applied.has(raw.id)
|
|
15071
|
-
};
|
|
15072
|
-
}
|
|
15073
|
-
async function executeProcess(command, args, cwd, interactive) {
|
|
15074
|
-
if (!interactive) {
|
|
15075
|
-
const result = await run(command, args, { cwd, capture: "unbounded" });
|
|
15076
|
-
return { code: result.code, stdout: result.stdout, stderr: result.stderr };
|
|
15077
|
-
}
|
|
15078
|
-
return new Promise((resolve, reject) => {
|
|
15079
|
-
const child = spawn3(command, args, { cwd, stdio: ["inherit", "inherit", "pipe"] });
|
|
15080
|
-
let stderr = "";
|
|
15081
|
-
child.stderr.on("data", (chunk) => {
|
|
15082
|
-
const text = chunk.toString();
|
|
15083
|
-
stderr += text;
|
|
15084
|
-
process.stderr.write(text);
|
|
15085
|
-
});
|
|
15086
|
-
child.on("error", reject);
|
|
15087
|
-
child.on("close", (code) => resolve({ code: code ?? 1, stdout: "", stderr }));
|
|
15088
|
-
});
|
|
15089
|
-
}
|
|
15090
|
-
async function atomicJson(path, value) {
|
|
15091
|
-
const temp = join16(dirname6(path), `.${basename5(path)}.${crypto.randomUUID()}.tmp`);
|
|
15092
|
-
await writeFile5(temp, `${JSON.stringify(value, null, 2)}
|
|
15093
|
-
`, { mode: 384, flag: "wx" });
|
|
15094
|
-
await rename(temp, path);
|
|
15095
|
-
}
|
|
15096
|
-
function integer5(value) {
|
|
15097
|
-
return typeof value === "number" && Number.isInteger(value) ? value : undefined;
|
|
15098
|
-
}
|
|
15099
|
-
// src/plan/escalation.ts
|
|
15100
|
-
import { z as z4 } from "zod";
|
|
15101
|
-
var escalationSchema = z4.object({
|
|
15102
|
-
planId: z4.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
|
|
15103
|
-
executionId: z4.string().min(1),
|
|
15104
|
-
phaseId: z4.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
|
|
15105
|
-
taskId: z4.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/).optional(),
|
|
15106
|
-
blocker: z4.string().min(1),
|
|
15107
|
-
attempts: z4.array(z4.string()),
|
|
15108
|
-
evidence: z4.array(z4.string()),
|
|
15109
|
-
needsUserDecision: z4.boolean()
|
|
15110
|
-
}).strict();
|
|
15111
|
-
var OPEN = "<diffpi-planner-escalation>";
|
|
15112
|
-
var CLOSE = "</diffpi-planner-escalation>";
|
|
15113
|
-
function renderPlannerEscalation(value) {
|
|
15114
|
-
return `${OPEN}${JSON.stringify(escalationSchema.parse(value))}${CLOSE}`;
|
|
15115
|
-
}
|
|
15116
|
-
// src/plan/execution.ts
|
|
15117
|
-
function createExecutionPacket(plan, coordinator) {
|
|
15118
|
-
if (!plan.execution?.active)
|
|
15119
|
-
throw new Error(`Plan ${plan.id} has no active execution.`);
|
|
15120
|
-
return {
|
|
15121
|
-
version: 1,
|
|
15122
|
-
planId: plan.id,
|
|
15123
|
-
executionId: plan.execution.id,
|
|
15124
|
-
policy: plan.execution.policy,
|
|
15125
|
-
cwd: plan.execution.cwd,
|
|
15126
|
-
branch: plan.execution.branch,
|
|
15127
|
-
coordinator
|
|
15128
|
-
};
|
|
15129
|
-
}
|
|
15130
|
-
function renderExecutionPrompt(packet) {
|
|
15131
|
-
return `Execute the durable Diffpi plan using this packet: ${JSON.stringify(packet)}. Call plan_context first. Coordinate eligible work, persist every transition and progress event, run phase gates, and honor the commit policy. Do not edit PLAN.md directly. Delegated workers never commit or restructure the plan.`;
|
|
15132
|
-
}
|
|
15133
|
-
// src/plan/transitions.ts
|
|
15134
|
-
var STABLE_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
15135
|
-
var PLAN_TRANSITIONS = {
|
|
15136
|
-
draft: ["ready"],
|
|
15137
|
-
ready: ["draft", "in_progress"],
|
|
15138
|
-
in_progress: ["blocked", "completed"],
|
|
15139
|
-
blocked: ["draft", "ready", "in_progress"],
|
|
15140
|
-
completed: []
|
|
15141
|
-
};
|
|
15142
|
-
var WORK_TRANSITIONS = {
|
|
15143
|
-
pending: ["in_progress", "skipped"],
|
|
15144
|
-
in_progress: ["blocked", "completed"],
|
|
15145
|
-
blocked: ["pending", "in_progress", "skipped"],
|
|
15146
|
-
completed: [],
|
|
15147
|
-
skipped: []
|
|
15148
|
-
};
|
|
15149
|
-
function isStableId(value) {
|
|
15150
|
-
return STABLE_ID.test(value) && value.length <= 80;
|
|
15151
|
-
}
|
|
15152
|
-
function assertStableId(value, label = "identifier") {
|
|
15153
|
-
if (!isStableId(value))
|
|
15154
|
-
throw new Error(`${label} must be a lowercase stable slug, not a path: ${value}`);
|
|
15155
|
-
}
|
|
15156
|
-
function assertPlanTransition(from, to) {
|
|
15157
|
-
if (from === to)
|
|
15158
|
-
return;
|
|
15159
|
-
if (!PLAN_TRANSITIONS[from].includes(to))
|
|
15160
|
-
throw new Error(`Invalid plan status transition: ${from} -> ${to}.`);
|
|
15161
|
-
}
|
|
15162
|
-
function assertPhaseTransition(from, to) {
|
|
15163
|
-
if (from === to)
|
|
15164
|
-
return;
|
|
15165
|
-
if (!WORK_TRANSITIONS[from].includes(to))
|
|
15166
|
-
throw new Error(`Invalid phase status transition: ${from} -> ${to}.`);
|
|
15167
|
-
}
|
|
15168
|
-
function assertTaskTransition(task, to, executionId, actor) {
|
|
15169
|
-
if (task.status !== to && !WORK_TRANSITIONS[task.status].includes(to)) {
|
|
15170
|
-
throw new Error(`Invalid task status transition: ${task.status} -> ${to}.`);
|
|
15171
|
-
}
|
|
15172
|
-
if (task.status === "in_progress" && task.executionId && executionId !== task.executionId) {
|
|
15173
|
-
throw new Error(`Task ${task.id} is owned by execution ${task.executionId}.`);
|
|
15174
|
-
}
|
|
15175
|
-
if (task.status === "in_progress" && task.owner && actor && actor !== task.owner) {
|
|
15176
|
-
throw new Error(`Task ${task.id} is owned by ${task.owner}.`);
|
|
15177
|
-
}
|
|
15178
|
-
}
|
|
15179
|
-
function assertUniqueIds(ids, label) {
|
|
15180
|
-
const seen = new Set;
|
|
15181
|
-
for (const id of ids) {
|
|
15182
|
-
assertStableId(id, label);
|
|
15183
|
-
if (seen.has(id))
|
|
15184
|
-
throw new Error(`Duplicate ${label}: ${id}.`);
|
|
15185
|
-
seen.add(id);
|
|
15186
|
-
}
|
|
15187
|
-
}
|
|
15188
|
-
|
|
15189
|
-
// src/plan/markdown.ts
|
|
15190
|
-
var PLAN_MARKER = /<!-- diffpi-plan: (\{[^\n]+\}) -->/;
|
|
15191
|
-
var PHASE_MARKER = /<!-- diffpi-phase: (\{[^\n]+\}) -->/g;
|
|
15192
|
-
var TASK_MARKER = /<!-- diffpi-task: (\{[^\n]+\}) -->/g;
|
|
15193
|
-
var SUPPORTED_SCHEMA = 1;
|
|
15194
|
-
function countDesignWords(plan) {
|
|
15195
|
-
return [plan.design.bigIdeas, plan.design.keyApiUpdates, plan.design.consequences].join(" ").trim().split(/\s+/).filter(Boolean).length;
|
|
15196
|
-
}
|
|
15197
|
-
function validatePlanDocument(plan, options = {}) {
|
|
15198
|
-
const issues = [];
|
|
15199
|
-
const add = (code, severity, message, path) => issues.push({ code, severity, message, path });
|
|
15200
|
-
try {
|
|
15201
|
-
assertStableId(plan.id, "plan id");
|
|
15202
|
-
assertUniqueIds(plan.phases.map((phase) => phase.id), "phase id");
|
|
15203
|
-
assertUniqueIds(plan.phases.flatMap((phase) => phase.tasks.map((task) => task.id)), "task id");
|
|
15204
|
-
} catch (error) {
|
|
15205
|
-
add("invalid-id", "error", error.message);
|
|
15206
|
-
}
|
|
15207
|
-
if (plan.schemaVersion !== SUPPORTED_SCHEMA)
|
|
15208
|
-
add("schema", "error", `Unsupported plan schema: ${plan.schemaVersion}.`);
|
|
15209
|
-
const phaseIds = new Set(plan.phases.map((phase) => phase.id));
|
|
15210
|
-
const taskIds = new Set(plan.phases.flatMap((phase) => phase.tasks.map((task) => task.id)));
|
|
15211
|
-
for (const phase of plan.phases) {
|
|
15212
|
-
for (const dependency of phase.dependencies) {
|
|
15213
|
-
if (!phaseIds.has(dependency))
|
|
15214
|
-
add("dangling-dependency", "error", `Phase ${phase.id} depends on missing ${dependency}.`);
|
|
15215
|
-
}
|
|
15216
|
-
for (const task of phase.tasks) {
|
|
15217
|
-
for (const dependency of task.dependencies) {
|
|
15218
|
-
if (!taskIds.has(dependency))
|
|
15219
|
-
add("dangling-dependency", "error", `Task ${task.id} depends on missing ${dependency}.`);
|
|
15220
|
-
}
|
|
15221
|
-
if (options.strict && task.acceptanceCriteria.length === 0)
|
|
15222
|
-
add("acceptance", "error", `Task ${task.id} has no acceptance criteria.`, task.id);
|
|
15223
|
-
}
|
|
15224
|
-
if (options.strict && phase.tasks.length === 0)
|
|
15225
|
-
add("empty-phase", "error", `Phase ${phase.id} has no tasks.`, phase.id);
|
|
15226
|
-
}
|
|
15227
|
-
for (const cycle of dependencyCycles(plan))
|
|
15228
|
-
add("dependency-cycle", "error", `Dependency cycle: ${cycle.join(" -> ")}.`);
|
|
15229
|
-
const designWords = countDesignWords(plan);
|
|
15230
|
-
if (designWords > 300)
|
|
15231
|
-
add("design-length", "warning", `Design is ${designWords} words; prefer 300 or fewer.`);
|
|
15232
|
-
if (options.strict && designWords > 800)
|
|
15233
|
-
add("design-too-long", "error", `Design is ${designWords} words; finalization allows at most 800.`);
|
|
15234
|
-
if (options.strict && plan.phases.length === 0)
|
|
15235
|
-
add("no-phases", "error", "Finalization requires at least one phase.");
|
|
15236
|
-
if (options.strict && (options.pendingAnnotations ?? 0) > 0)
|
|
15237
|
-
add("pending-annotations", "error", `${options.pendingAnnotations} annotations remain pending.`);
|
|
15238
|
-
return issues;
|
|
15239
|
-
}
|
|
15240
|
-
function renderPlanDocument(plan, previousSource) {
|
|
15241
|
-
if (previousSource && canPatch(previousSource, plan))
|
|
15242
|
-
return patchMarkers(previousSource, plan);
|
|
15243
|
-
const marker = {
|
|
15244
|
-
schemaVersion: plan.schemaVersion,
|
|
15245
|
-
id: plan.id,
|
|
15246
|
-
revision: plan.revision,
|
|
15247
|
-
branch: plan.branch,
|
|
15248
|
-
status: plan.status,
|
|
15249
|
-
execution: plan.execution,
|
|
15250
|
-
createdAt: plan.createdAt,
|
|
15251
|
-
updatedAt: plan.updatedAt
|
|
15252
|
-
};
|
|
15253
|
-
const requirements = plan.requirements.length ? plan.requirements.map((item) => `- ${item}`).join(`
|
|
15254
|
-
`) : "<!-- Add requirements. -->";
|
|
15255
|
-
const phases = plan.phases.map(renderPhase).join(`
|
|
15256
|
-
|
|
15257
|
-
`);
|
|
15258
|
-
const references = plan.references.length ? plan.references.map((reference) => `- <!-- diffpi-reference: ${json(reference)} --> ${reference.value}`).join(`
|
|
15259
|
-
`) : "<!-- Add references. -->";
|
|
15260
|
-
return `<!-- diffpi-plan: ${json(marker)} -->
|
|
15261
|
-
# ${plan.title}
|
|
15262
|
-
|
|
15263
|
-
- **Plan ID:** ${plan.id}
|
|
15264
|
-
- **Branch:** ${plan.branch}
|
|
15265
|
-
- **Status:** ${plan.status}
|
|
15266
|
-
- **Revision:** ${plan.revision}
|
|
15267
|
-
|
|
15268
|
-
## Intent
|
|
15269
|
-
|
|
15270
|
-
${plan.intent || "<!-- Describe the intended outcome. -->"}
|
|
15271
|
-
|
|
15272
|
-
## Requirements
|
|
15273
|
-
|
|
15274
|
-
${requirements}
|
|
15275
|
-
|
|
15276
|
-
## Design
|
|
15277
|
-
|
|
15278
|
-
### Big Ideas
|
|
15279
|
-
|
|
15280
|
-
${plan.design.bigIdeas || "<!-- Describe the main approach. -->"}
|
|
15281
|
-
|
|
15282
|
-
### Key API Addition/Updates
|
|
15283
|
-
|
|
15284
|
-
${plan.design.keyApiUpdates || "<!-- Describe public API changes. -->"}
|
|
15285
|
-
|
|
15286
|
-
### Consequences
|
|
15287
|
-
|
|
15288
|
-
${plan.design.consequences || "<!-- Describe trade-offs and limitations. -->"}
|
|
15289
|
-
|
|
15290
|
-
## Implementation
|
|
15291
|
-
|
|
15292
|
-
${phases || "<!-- Add phases with plan_add_phase. -->"}
|
|
15293
|
-
|
|
15294
|
-
## References
|
|
15295
|
-
|
|
15296
|
-
${references}
|
|
15297
|
-
`;
|
|
15298
|
-
}
|
|
15299
|
-
function parsePlanDocument(source, ref = "PLAN.md") {
|
|
15300
|
-
const planMatch = source.match(PLAN_MARKER);
|
|
15301
|
-
if (!planMatch)
|
|
15302
|
-
throw new Error(`${ref}: missing or malformed diffpi-plan marker.`);
|
|
15303
|
-
if ((source.match(new RegExp(PLAN_MARKER.source, "g")) ?? []).length !== 1)
|
|
15304
|
-
throw new Error(`${ref}: duplicate diffpi-plan marker.`);
|
|
15305
|
-
const marker = parseMarker(planMatch[1], `${ref} plan`);
|
|
15306
|
-
if (marker.schemaVersion !== SUPPORTED_SCHEMA)
|
|
15307
|
-
throw new Error(`${ref}: unsupported plan schema ${String(marker.schemaVersion)}.`);
|
|
15308
|
-
assertStableId(marker.id, "plan id");
|
|
15309
|
-
const title = source.match(/^# (.+)$/m)?.[1]?.trim();
|
|
15310
|
-
if (!title)
|
|
15311
|
-
throw new Error(`${ref}: missing plan title.`);
|
|
15312
|
-
const phases = parsePhases(section(source, "Implementation"), ref);
|
|
15313
|
-
const document = {
|
|
15314
|
-
...marker,
|
|
15315
|
-
schemaVersion: 1,
|
|
15316
|
-
title,
|
|
15317
|
-
intent: cleanPlaceholder(section(source, "Intent")),
|
|
15318
|
-
requirements: parseBullets(section(source, "Requirements")),
|
|
15319
|
-
design: {
|
|
15320
|
-
bigIdeas: cleanPlaceholder(subsection(source, "Design", "Big Ideas")),
|
|
15321
|
-
keyApiUpdates: cleanPlaceholder(subsection(source, "Design", "Key API Addition/Updates")),
|
|
15322
|
-
consequences: cleanPlaceholder(subsection(source, "Design", "Consequences"))
|
|
15323
|
-
},
|
|
15324
|
-
phases,
|
|
15325
|
-
references: parseReferences(section(source, "References"))
|
|
15326
|
-
};
|
|
15327
|
-
const errors = validatePlanDocument(document).filter((issue) => issue.severity === "error");
|
|
15328
|
-
if (errors.length)
|
|
15329
|
-
throw new Error(`${ref}: ${errors.map((issue) => issue.message).join(" ")}`);
|
|
15330
|
-
return document;
|
|
15331
|
-
}
|
|
15332
|
-
function renderPhase(phase) {
|
|
15333
|
-
const marker = {
|
|
15334
|
-
id: phase.id,
|
|
15335
|
-
revision: phase.revision,
|
|
15336
|
-
status: phase.status,
|
|
15337
|
-
gate: phase.gate,
|
|
15338
|
-
commit: phase.commit,
|
|
15339
|
-
blocker: phase.blocker
|
|
15340
|
-
};
|
|
15341
|
-
const dependencies = phase.dependencies.length ? phase.dependencies.join(", ") : "none";
|
|
15342
|
-
return `<!-- diffpi-phase: ${json(marker)} -->
|
|
15343
|
-
### Phase: ${phase.title}
|
|
15344
|
-
|
|
15345
|
-
**Objective:** ${phase.objective}
|
|
15346
|
-
|
|
15347
|
-
**Dependencies:** ${dependencies}
|
|
15348
|
-
|
|
15349
|
-
${phase.tasks.map(renderTask).join(`
|
|
15350
|
-
|
|
15351
|
-
`)}
|
|
15352
|
-
|
|
15353
|
-
<!-- /diffpi-phase -->`;
|
|
15354
|
-
}
|
|
15355
|
-
function renderTask(task) {
|
|
15356
|
-
const marker = {
|
|
15357
|
-
id: task.id,
|
|
15358
|
-
revision: task.revision,
|
|
15359
|
-
status: task.status,
|
|
15360
|
-
owner: task.owner,
|
|
15361
|
-
executionId: task.executionId,
|
|
15362
|
-
blocker: task.blocker
|
|
15363
|
-
};
|
|
15364
|
-
const checked = task.status === "completed" || task.status === "skipped" ? "x" : " ";
|
|
15365
|
-
return `<!-- diffpi-task: ${json(marker)} -->
|
|
15366
|
-
- [${checked}] **${task.title}**
|
|
15367
|
-
- Steps: ${list(task.steps ?? [])}
|
|
15368
|
-
- Dependencies: ${list(task.dependencies)}
|
|
15369
|
-
- File scopes: ${list(task.fileScopes)}
|
|
15370
|
-
- Acceptance criteria: ${list(task.acceptanceCriteria)}
|
|
15371
|
-
<!-- /diffpi-task -->`;
|
|
15372
|
-
}
|
|
15373
|
-
function parsePhases(input, ref) {
|
|
15374
|
-
const starts = [...input.matchAll(PHASE_MARKER)];
|
|
15375
|
-
const phases = starts.map((match, index) => {
|
|
15376
|
-
const start = match.index;
|
|
15377
|
-
const end = input.indexOf("<!-- /diffpi-phase -->", start);
|
|
15378
|
-
if (end < 0)
|
|
15379
|
-
throw new Error(`${ref}: phase marker has no closing marker.`);
|
|
15380
|
-
const next = starts[index + 1]?.index;
|
|
15381
|
-
if (next !== undefined && next < end)
|
|
15382
|
-
throw new Error(`${ref}: nested or unclosed phase marker.`);
|
|
15383
|
-
const body = input.slice(start + match[0].length, end);
|
|
15384
|
-
const marker = parseMarker(match[1], `${ref} phase`);
|
|
15385
|
-
assertStableId(marker.id, "phase id");
|
|
15386
|
-
const title = body.match(/^### Phase: (.+)$/m)?.[1]?.trim();
|
|
15387
|
-
const objective = body.match(/^\*\*Objective:\*\*\s*(.*)$/m)?.[1]?.trim();
|
|
15388
|
-
if (!title || !objective)
|
|
15389
|
-
throw new Error(`${ref}: phase ${marker.id} is missing title or objective.`);
|
|
15390
|
-
return {
|
|
15391
|
-
...marker,
|
|
15392
|
-
title,
|
|
15393
|
-
objective,
|
|
15394
|
-
dependencies: parseCsv(body.match(/^\*\*Dependencies:\*\*\s*(.*)$/m)?.[1]),
|
|
15395
|
-
tasks: parseTasks(body, ref)
|
|
15396
|
-
};
|
|
15397
|
-
});
|
|
15398
|
-
assertUniqueIds(phases.map((phase) => phase.id), "phase id");
|
|
15399
|
-
return phases;
|
|
15400
|
-
}
|
|
15401
|
-
function parseTasks(input, ref) {
|
|
15402
|
-
const starts = [...input.matchAll(TASK_MARKER)];
|
|
15403
|
-
const tasks = starts.map((match, index) => {
|
|
15404
|
-
const start = match.index;
|
|
15405
|
-
const end = input.indexOf("<!-- /diffpi-task -->", start);
|
|
15406
|
-
if (end < 0)
|
|
15407
|
-
throw new Error(`${ref}: task marker has no closing marker.`);
|
|
15408
|
-
const next = starts[index + 1]?.index;
|
|
15409
|
-
if (next !== undefined && next < end)
|
|
15410
|
-
throw new Error(`${ref}: nested or unclosed task marker.`);
|
|
15411
|
-
const body = input.slice(start + match[0].length, end);
|
|
15412
|
-
const marker = parseMarker(match[1], `${ref} task`);
|
|
15413
|
-
assertStableId(marker.id, "task id");
|
|
15414
|
-
const title = body.match(/^- \[[ xX]\] \*\*(.+)\*\*$/m)?.[1]?.trim();
|
|
15415
|
-
if (!title)
|
|
15416
|
-
throw new Error(`${ref}: task ${marker.id} is missing its checkbox title.`);
|
|
15417
|
-
return {
|
|
15418
|
-
...marker,
|
|
15419
|
-
title,
|
|
15420
|
-
steps: parseListValue(body, "Steps"),
|
|
15421
|
-
dependencies: parseListValue(body, "Dependencies"),
|
|
15422
|
-
fileScopes: parseListValue(body, "File scopes"),
|
|
15423
|
-
acceptanceCriteria: parseListValue(body, "Acceptance criteria")
|
|
15424
|
-
};
|
|
15425
|
-
});
|
|
15426
|
-
assertUniqueIds(tasks.map((task) => task.id), "task id");
|
|
15427
|
-
return tasks;
|
|
15428
|
-
}
|
|
15429
|
-
function parseReferences(input) {
|
|
15430
|
-
return [...input.matchAll(/^- <!-- diffpi-reference: (\{[^\n]+\}) -->\s*(.*)$/gm)].map((match) => {
|
|
15431
|
-
const marker = parseMarker(match[1], "reference");
|
|
15432
|
-
assertStableId(marker.id, "reference id");
|
|
15433
|
-
return { id: marker.id, value: match[2].trim() || marker.value };
|
|
15434
|
-
});
|
|
15435
|
-
}
|
|
15436
|
-
function canPatch(source, next) {
|
|
15437
|
-
try {
|
|
15438
|
-
const old = parsePlanDocument(source);
|
|
15439
|
-
return JSON.stringify(contentShape(old)) === JSON.stringify(contentShape(next));
|
|
15440
|
-
} catch {
|
|
15441
|
-
return false;
|
|
15442
|
-
}
|
|
15443
|
-
}
|
|
15444
|
-
function patchMarkers(source, plan) {
|
|
15445
|
-
const planMarker = {
|
|
15446
|
-
schemaVersion: plan.schemaVersion,
|
|
15447
|
-
id: plan.id,
|
|
15448
|
-
revision: plan.revision,
|
|
15449
|
-
branch: plan.branch,
|
|
15450
|
-
status: plan.status,
|
|
15451
|
-
execution: plan.execution,
|
|
15452
|
-
createdAt: plan.createdAt,
|
|
15453
|
-
updatedAt: plan.updatedAt
|
|
15454
|
-
};
|
|
15455
|
-
let output = source.replace(PLAN_MARKER, `<!-- diffpi-plan: ${json(planMarker)} -->`);
|
|
15456
|
-
output = output.replace(/^- \*\*Status:\*\* .*$/m, `- **Status:** ${plan.status}`);
|
|
15457
|
-
output = output.replace(/^- \*\*Revision:\*\* .*$/m, `- **Revision:** ${plan.revision}`);
|
|
15458
|
-
for (const phase of plan.phases) {
|
|
15459
|
-
const marker = {
|
|
15460
|
-
id: phase.id,
|
|
15461
|
-
revision: phase.revision,
|
|
15462
|
-
status: phase.status,
|
|
15463
|
-
gate: phase.gate,
|
|
15464
|
-
commit: phase.commit,
|
|
15465
|
-
blocker: phase.blocker
|
|
15466
|
-
};
|
|
15467
|
-
output = replaceMarkerById(output, "phase", phase.id, marker);
|
|
15468
|
-
for (const task of phase.tasks) {
|
|
15469
|
-
const taskMarker = {
|
|
15470
|
-
id: task.id,
|
|
15471
|
-
revision: task.revision,
|
|
15472
|
-
status: task.status,
|
|
15473
|
-
owner: task.owner,
|
|
15474
|
-
executionId: task.executionId,
|
|
15475
|
-
blocker: task.blocker
|
|
15476
|
-
};
|
|
15477
|
-
output = replaceMarkerById(output, "task", task.id, taskMarker);
|
|
15478
|
-
const checked = task.status === "completed" || task.status === "skipped" ? "x" : " ";
|
|
15479
|
-
const escaped = escapeRegExp(task.title);
|
|
15480
|
-
output = output.replace(new RegExp(`^- \\[[ xX]\\] \\*\\*${escaped}\\*\\*$`, "m"), `- [${checked}] **${task.title}**`);
|
|
15481
|
-
}
|
|
15482
|
-
}
|
|
15483
|
-
return output;
|
|
15484
|
-
}
|
|
15485
|
-
function replaceMarkerById(source, kind, id, marker) {
|
|
15486
|
-
const pattern = new RegExp(`<!-- diffpi-${kind}: \\{[^\\n]*"id":"${escapeRegExp(id)}"[^\\n]*\\} -->`);
|
|
15487
|
-
if (!pattern.test(source))
|
|
15488
|
-
throw new Error(`Cannot update missing ${kind} marker ${id}.`);
|
|
15489
|
-
return source.replace(pattern, `<!-- diffpi-${kind}: ${json(marker)} -->`);
|
|
15490
|
-
}
|
|
15491
|
-
function contentShape(plan) {
|
|
15492
|
-
return {
|
|
15493
|
-
title: plan.title,
|
|
15494
|
-
branch: plan.branch,
|
|
15495
|
-
intent: plan.intent,
|
|
15496
|
-
requirements: plan.requirements,
|
|
15497
|
-
design: plan.design,
|
|
15498
|
-
references: plan.references,
|
|
15499
|
-
phases: plan.phases.map(({ id, title, objective, dependencies, tasks }) => ({
|
|
15500
|
-
id,
|
|
15501
|
-
title,
|
|
15502
|
-
objective,
|
|
15503
|
-
dependencies,
|
|
15504
|
-
tasks: tasks.map(({ id: taskId, title: taskTitle, steps, dependencies: taskDependencies, fileScopes, acceptanceCriteria }) => ({
|
|
15505
|
-
id: taskId,
|
|
15506
|
-
title: taskTitle,
|
|
15507
|
-
steps,
|
|
15508
|
-
dependencies: taskDependencies,
|
|
15509
|
-
fileScopes,
|
|
15510
|
-
acceptanceCriteria
|
|
15511
|
-
}))
|
|
15512
|
-
}))
|
|
15513
|
-
};
|
|
15514
|
-
}
|
|
15515
|
-
function dependencyCycles(plan) {
|
|
15516
|
-
const graph = new Map;
|
|
15517
|
-
for (const phase of plan.phases)
|
|
15518
|
-
graph.set(phase.id, phase.dependencies);
|
|
15519
|
-
for (const task of plan.phases.flatMap((phase) => phase.tasks))
|
|
15520
|
-
graph.set(task.id, task.dependencies);
|
|
15521
|
-
const cycles = [];
|
|
15522
|
-
const visiting = new Set;
|
|
15523
|
-
const visited = new Set;
|
|
15524
|
-
const walk = (id, path) => {
|
|
15525
|
-
if (visiting.has(id)) {
|
|
15526
|
-
cycles.push([...path.slice(path.indexOf(id)), id]);
|
|
15527
|
-
return;
|
|
15528
|
-
}
|
|
15529
|
-
if (visited.has(id))
|
|
15530
|
-
return;
|
|
15531
|
-
visiting.add(id);
|
|
15532
|
-
for (const dependency of graph.get(id) ?? [])
|
|
15533
|
-
walk(dependency, [...path, id]);
|
|
15534
|
-
visiting.delete(id);
|
|
15535
|
-
visited.add(id);
|
|
15536
|
-
};
|
|
15537
|
-
for (const id of graph.keys())
|
|
15538
|
-
walk(id, []);
|
|
15539
|
-
return cycles;
|
|
15540
|
-
}
|
|
15541
|
-
function section(source, heading) {
|
|
15542
|
-
const match = source.match(new RegExp(`^## ${escapeRegExp(heading)}\\s*$`, "m"));
|
|
15543
|
-
if (!match?.index) {
|
|
15544
|
-
if (match?.index === 0)
|
|
15545
|
-
return "";
|
|
15546
|
-
throw new Error(`Missing required heading: ${heading}.`);
|
|
15547
|
-
}
|
|
15548
|
-
const start = match.index + match[0].length;
|
|
15549
|
-
const rest = source.slice(start);
|
|
15550
|
-
const end = rest.search(/^## /m);
|
|
15551
|
-
return (end < 0 ? rest : rest.slice(0, end)).trim();
|
|
15552
|
-
}
|
|
15553
|
-
function subsection(source, parent, heading) {
|
|
15554
|
-
const body = section(source, parent);
|
|
15555
|
-
const match = body.match(new RegExp(`^### ${escapeRegExp(heading)}\\s*$`, "m"));
|
|
15556
|
-
if (match?.index === undefined)
|
|
15557
|
-
throw new Error(`Missing required heading: ${parent}/${heading}.`);
|
|
15558
|
-
const rest = body.slice(match.index + match[0].length);
|
|
15559
|
-
const end = rest.search(/^### /m);
|
|
15560
|
-
return (end < 0 ? rest : rest.slice(0, end)).trim();
|
|
15561
|
-
}
|
|
15562
|
-
function parseBullets(input) {
|
|
15563
|
-
return [...input.matchAll(/^- (?!<!--)(.+)$/gm)].map((match) => match[1].trim());
|
|
15564
|
-
}
|
|
15565
|
-
function parseListValue(body, label) {
|
|
15566
|
-
const value = body.match(new RegExp(`^ - ${escapeRegExp(label)}:\\s*(.*)$`, "m"))?.[1];
|
|
15567
|
-
return parseCsv(value);
|
|
15568
|
-
}
|
|
15569
|
-
function parseCsv(value) {
|
|
15570
|
-
if (!value || value.trim().toLowerCase() === "none")
|
|
15571
|
-
return [];
|
|
15572
|
-
return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
15573
|
-
}
|
|
15574
|
-
function cleanPlaceholder(value) {
|
|
15575
|
-
return value.replace(/<!--[^]*?-->/g, "").trim();
|
|
15576
|
-
}
|
|
15577
|
-
function parseMarker(value, label) {
|
|
15578
|
-
try {
|
|
15579
|
-
return JSON.parse(value);
|
|
15580
|
-
} catch {
|
|
15581
|
-
throw new Error(`Malformed ${label} marker JSON.`);
|
|
15582
|
-
}
|
|
15583
|
-
}
|
|
15584
|
-
function list(values) {
|
|
15585
|
-
return values.length ? values.join(", ") : "none";
|
|
15586
|
-
}
|
|
15587
|
-
function json(value) {
|
|
15588
|
-
return JSON.stringify(value, (_key, entry) => entry === undefined ? undefined : entry);
|
|
15589
|
-
}
|
|
15590
|
-
function escapeRegExp(value) {
|
|
15591
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
15592
|
-
}
|
|
15593
|
-
// src/plan/store.ts
|
|
15594
|
-
import { open, mkdir as mkdir6, readdir as readdir3, readFile as readFile10, realpath as realpath3, rename as rename2, rm as rm3 } from "node:fs/promises";
|
|
15595
|
-
import { basename as basename6, dirname as dirname7, join as join18, resolve as resolve4, sep } from "node:path";
|
|
15596
|
-
|
|
15597
|
-
// src/templates.ts
|
|
15598
|
-
import { readFile as readFile9 } from "node:fs/promises";
|
|
15599
|
-
import { homedir as homedir7 } from "node:os";
|
|
15600
|
-
import { join as join17, normalize } from "node:path";
|
|
15601
|
-
async function loadTemplate(name, options = {}) {
|
|
15602
|
-
const relative = templateRelativePath(name);
|
|
15603
|
-
const userPath = join17(options.homeDir ?? homedir7(), ".difflab", "diffpi", "templates", relative);
|
|
15604
|
-
const bundledPath = join17(options.bundledDir ?? resolveBundledTemplatesDir(), relative);
|
|
15605
|
-
const user = await readOptionalFile(userPath);
|
|
15606
|
-
if (user !== undefined)
|
|
15607
|
-
return { name, path: userPath, source: "user", content: user };
|
|
15608
|
-
const bundled = await readOptionalFile(bundledPath);
|
|
15609
|
-
if (bundled !== undefined)
|
|
15610
|
-
return { name, path: bundledPath, source: "bundled", content: bundled };
|
|
15611
|
-
throw new Error(`Template "${name}" was not found at ${userPath} or ${bundledPath}.`);
|
|
15612
|
-
}
|
|
15613
|
-
function renderTemplate(content, variables) {
|
|
15614
|
-
return content.replace(/\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g, (match, key) => variables[key] ?? match);
|
|
15615
|
-
}
|
|
15616
|
-
function templateRelativePath(name) {
|
|
15617
|
-
const normalized = normalize(name.replaceAll("\\", "/")).replace(/^\.\//, "");
|
|
15618
|
-
if (!normalized || normalized.startsWith("../") || normalized.includes("/../") || normalized.startsWith("/")) {
|
|
15619
|
-
throw new Error(`Invalid template name: ${name}`);
|
|
15620
|
-
}
|
|
15621
|
-
return normalized.endsWith(".md") ? normalized : `${normalized}.md`;
|
|
15622
|
-
}
|
|
15623
|
-
async function readOptionalFile(path) {
|
|
15624
|
-
try {
|
|
15625
|
-
return await readFile9(path, "utf8");
|
|
15626
|
-
} catch (error) {
|
|
15627
|
-
if (error.code === "ENOENT")
|
|
15628
|
-
return;
|
|
15629
|
-
throw error;
|
|
15630
|
-
}
|
|
15631
|
-
}
|
|
15632
|
-
|
|
15633
|
-
// src/plan/store.ts
|
|
15634
|
-
function planRecordName(shortSlug, date = new Date) {
|
|
15635
|
-
const slug = normalizeSlug(shortSlug);
|
|
15636
|
-
return `${date.toISOString().slice(2, 10).replaceAll("-", "")}-${slug}`;
|
|
15637
|
-
}
|
|
15638
|
-
async function resolvePlan(cwd, query, filters = {}, options = {}) {
|
|
15639
|
-
const root = await plansDir(cwd, options.homeDir);
|
|
15640
|
-
const entries = await readdir3(root, { withFileTypes: true });
|
|
15641
|
-
const normalizedQuery = query ? normalizeQuery(query) : undefined;
|
|
15642
|
-
const records = [];
|
|
15643
|
-
for (const entry of entries) {
|
|
15644
|
-
if (!entry.isDirectory() || entry.name === ".tmp" || entry.name === ".lock")
|
|
15645
|
-
continue;
|
|
15646
|
-
if (normalizedQuery && entry.name !== normalizedQuery && stripDate(entry.name) !== normalizedQuery)
|
|
15647
|
-
continue;
|
|
15648
|
-
const dir = join18(root, entry.name);
|
|
15649
|
-
await assertContained(root, dir);
|
|
15650
|
-
try {
|
|
15651
|
-
const record = await readRecord(dir);
|
|
15652
|
-
if (filters.branch && record.document.branch !== filters.branch)
|
|
15653
|
-
continue;
|
|
15654
|
-
if (filters.statuses && !filters.statuses.includes(record.document.status))
|
|
15655
|
-
continue;
|
|
15656
|
-
records.push(record);
|
|
15657
|
-
} catch (error) {
|
|
15658
|
-
if (error.code !== "ENOENT")
|
|
15659
|
-
throw error;
|
|
15660
|
-
}
|
|
15661
|
-
}
|
|
15662
|
-
records.sort((left, right) => left.id.localeCompare(right.id));
|
|
15663
|
-
return { candidates: records, record: records.length === 1 ? records[0] : undefined, ambiguous: records.length > 1 };
|
|
15664
|
-
}
|
|
15665
|
-
function createPlanStore(options = {}) {
|
|
15666
|
-
const now = options.now ?? (() => new Date);
|
|
15667
|
-
return {
|
|
15668
|
-
context(cwd, query, filters) {
|
|
15669
|
-
return resolvePlan(cwd, query, filters, options);
|
|
15670
|
-
},
|
|
15671
|
-
async init(input) {
|
|
15672
|
-
const root = await plansDir(input.cwd, options.homeDir);
|
|
15673
|
-
const id = planRecordName(input.shortSlug, now());
|
|
15674
|
-
const dir = join18(root, id);
|
|
15675
|
-
await assertContained(root, dirname7(dir));
|
|
15676
|
-
try {
|
|
15677
|
-
await mkdir6(dir);
|
|
15678
|
-
} catch (error) {
|
|
15679
|
-
if (error.code === "EEXIST")
|
|
15680
|
-
throw new Error(`Plan ${id} already exists.`);
|
|
15681
|
-
throw error;
|
|
15682
|
-
}
|
|
15683
|
-
try {
|
|
15684
|
-
const timestamp = now().toISOString();
|
|
15685
|
-
const template = await loadTemplate("plan/PLAN", {
|
|
15686
|
-
homeDir: options.homeDir,
|
|
15687
|
-
bundledDir: options.bundledTemplatesDir
|
|
15688
|
-
});
|
|
15689
|
-
const source = renderTemplate(template.content, {
|
|
15690
|
-
id,
|
|
15691
|
-
branch: input.branch,
|
|
15692
|
-
title: input.title?.trim() || titleFromSlug(input.shortSlug),
|
|
15693
|
-
intent: input.intent?.trim() || "<!-- Describe the intended outcome. -->",
|
|
15694
|
-
created_at: timestamp,
|
|
15695
|
-
updated_at: timestamp
|
|
15696
|
-
});
|
|
15697
|
-
const document = parsePlanDocument(source, join18(dir, "PLAN.md"));
|
|
15698
|
-
await atomicWrite(join18(dir, "PLAN.md"), source);
|
|
15699
|
-
await atomicWrite(join18(dir, "logs.txt"), "");
|
|
15700
|
-
await appendPlanLog(join18(dir, "logs.txt"), {
|
|
15701
|
-
planRevision: document.revision,
|
|
15702
|
-
kind: "created",
|
|
15703
|
-
actor: "diffpi",
|
|
15704
|
-
message: `Created plan ${id}.`
|
|
15705
|
-
});
|
|
15706
|
-
return { id, dir, planPath: join18(dir, "PLAN.md"), logPath: join18(dir, "logs.txt"), document, source };
|
|
15707
|
-
} catch (error) {
|
|
15708
|
-
await rm3(dir, { recursive: true, force: true });
|
|
15709
|
-
throw error;
|
|
15710
|
-
}
|
|
15711
|
-
},
|
|
15712
|
-
async read(cwd, query, filters) {
|
|
15713
|
-
const result = await resolvePlan(cwd, query, filters, options);
|
|
15714
|
-
if (!result.record) {
|
|
15715
|
-
if (result.ambiguous)
|
|
15716
|
-
throw new Error(`Plan query "${query}" is ambiguous: ${result.candidates.map((item) => item.id).join(", ")}.`);
|
|
15717
|
-
throw new Error(`Plan "${query}" was not found.`);
|
|
15718
|
-
}
|
|
15719
|
-
return result.record;
|
|
15720
|
-
},
|
|
15721
|
-
async mutate(cwd, query, operation, update) {
|
|
15722
|
-
const initial = await this.read(cwd, query);
|
|
15723
|
-
return withPlanLock(initial.dir, async () => {
|
|
15724
|
-
const current = await readRecord(initial.dir);
|
|
15725
|
-
const next = await update(structuredClone(current.document));
|
|
15726
|
-
if (next.id !== current.id)
|
|
15727
|
-
throw new Error("A plan mutation cannot change the plan ID.");
|
|
15728
|
-
const document = {
|
|
15729
|
-
...next,
|
|
15730
|
-
revision: current.document.revision + 1,
|
|
15731
|
-
updatedAt: now().toISOString()
|
|
15732
|
-
};
|
|
15733
|
-
const source = renderPlanDocument(document, current.source);
|
|
15734
|
-
parsePlanDocument(source, current.planPath);
|
|
15735
|
-
await atomicWrite(current.planPath, source);
|
|
15736
|
-
return { ...current, document, source };
|
|
15737
|
-
}, { operation });
|
|
15738
|
-
},
|
|
15739
|
-
async log(cwd, query, event) {
|
|
15740
|
-
const record = await this.read(cwd, query);
|
|
15741
|
-
return withPlanLock(record.dir, () => appendPlanLog(record.logPath, event), { operation: "append log" });
|
|
15742
|
-
}
|
|
15743
|
-
};
|
|
15744
|
-
}
|
|
15745
|
-
async function readRecord(dir) {
|
|
15746
|
-
const planPath = join18(dir, "PLAN.md");
|
|
15747
|
-
const logPath = join18(dir, "logs.txt");
|
|
15748
|
-
const source = await readFile10(planPath, "utf8");
|
|
15749
|
-
const document = parsePlanDocument(source, planPath);
|
|
15750
|
-
return { id: basename6(dir), dir, planPath, logPath, document, source };
|
|
15751
|
-
}
|
|
15752
|
-
async function atomicWrite(path, content) {
|
|
15753
|
-
const temp = join18(dirname7(path), `.${basename6(path)}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
15754
|
-
const file = await open(temp, "wx", 384);
|
|
15755
|
-
try {
|
|
15756
|
-
await file.writeFile(content, "utf8");
|
|
15757
|
-
await file.sync();
|
|
15758
|
-
} finally {
|
|
15759
|
-
await file.close();
|
|
15760
|
-
}
|
|
15761
|
-
await rename2(temp, path);
|
|
15762
|
-
try {
|
|
15763
|
-
const directory = await open(dirname7(path), "r");
|
|
15764
|
-
try {
|
|
15765
|
-
await directory.sync();
|
|
15766
|
-
} finally {
|
|
15767
|
-
await directory.close();
|
|
15768
|
-
}
|
|
15769
|
-
} catch {}
|
|
15770
|
-
}
|
|
15771
|
-
async function assertContained(root, candidate) {
|
|
15772
|
-
const canonicalRoot = await realpath3(root);
|
|
15773
|
-
let canonicalCandidate;
|
|
15774
|
-
try {
|
|
15775
|
-
canonicalCandidate = await realpath3(candidate);
|
|
15776
|
-
} catch {
|
|
15777
|
-
canonicalCandidate = resolve4(candidate);
|
|
15778
|
-
}
|
|
15779
|
-
if (canonicalCandidate !== canonicalRoot && !canonicalCandidate.startsWith(`${canonicalRoot}${sep}`)) {
|
|
15780
|
-
throw new Error(`Plan path escapes the shared store: ${candidate}.`);
|
|
15781
|
-
}
|
|
15782
|
-
}
|
|
15783
|
-
function normalizeSlug(value) {
|
|
15784
|
-
if (!value || value.includes("\x00") || value.includes("/") || value.includes("\\") || value.includes("..")) {
|
|
15785
|
-
throw new Error(`Invalid plan slug: ${value}.`);
|
|
15786
|
-
}
|
|
15787
|
-
const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
15788
|
-
assertStableId(slug, "plan slug");
|
|
15789
|
-
return slug;
|
|
15790
|
-
}
|
|
15791
|
-
function normalizeQuery(value) {
|
|
15792
|
-
if (!value || value.includes("\x00") || value.includes("/") || value.includes("\\") || value.includes("..")) {
|
|
15793
|
-
throw new Error(`Invalid plan query: ${value}.`);
|
|
15794
|
-
}
|
|
15795
|
-
return value.toLowerCase();
|
|
15796
|
-
}
|
|
15797
|
-
function stripDate(value) {
|
|
15798
|
-
return value.replace(/^\d{6}-/, "");
|
|
15799
|
-
}
|
|
15800
|
-
function titleFromSlug(value) {
|
|
15801
|
-
return normalizeSlug(value).replace(/^(?:[a-z]+-\d+-)/, "").split("-").map((part) => part[0].toUpperCase() + part.slice(1)).join(" ");
|
|
15802
|
-
}
|
|
15803
|
-
// src/tools/plan.ts
|
|
15804
|
-
var id2 = z5.string().min(1).max(80).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/);
|
|
15805
|
-
var text = z5.string().trim().min(1);
|
|
15806
|
-
var revision = z5.number().int().nonnegative();
|
|
15807
|
-
var cwd = z5.string().optional();
|
|
15808
|
-
var taskDraft = z5.object({
|
|
15809
|
-
id: id2,
|
|
15810
|
-
title: text,
|
|
15811
|
-
steps: z5.array(text).optional(),
|
|
15812
|
-
dependencies: z5.array(id2).optional(),
|
|
15813
|
-
fileScopes: z5.array(text).optional(),
|
|
15814
|
-
acceptanceCriteria: z5.array(text).optional()
|
|
15815
|
-
}).strict();
|
|
15816
|
-
var phaseDraft = z5.object({
|
|
15817
|
-
id: id2,
|
|
15818
|
-
title: text,
|
|
15819
|
-
objective: text,
|
|
15820
|
-
dependencies: z5.array(id2).optional(),
|
|
15821
|
-
tasks: z5.array(taskDraft).optional()
|
|
15822
|
-
}).strict();
|
|
15823
|
-
var reference = z5.object({ id: id2, value: text }).strict();
|
|
15824
|
-
var design = z5.object({ bigIdeas: z5.string(), keyApiUpdates: z5.string(), consequences: z5.string() }).strict();
|
|
15825
|
-
function createPlanTools(pi, modes, store = createPlanStore()) {
|
|
15826
|
-
return [
|
|
15827
|
-
defineTool3({
|
|
15828
|
-
name: "plan_context",
|
|
15829
|
-
label: "plan context",
|
|
15830
|
-
description: "Resolve one plan and summarize all matching plans without guessing among ambiguous matches.",
|
|
15831
|
-
parameters: parameters5(z5.object({
|
|
15832
|
-
cwd,
|
|
15833
|
-
plan: z5.string().optional(),
|
|
15834
|
-
branch: z5.string().optional(),
|
|
15835
|
-
statuses: z5.array(z5.enum(["draft", "ready", "in_progress", "blocked", "completed"])).optional()
|
|
15836
|
-
}).strict()),
|
|
15837
|
-
executionMode: "parallel",
|
|
15838
|
-
async execute(_id, input) {
|
|
15839
|
-
const params = z5.object({
|
|
15840
|
-
cwd,
|
|
15841
|
-
plan: z5.string().optional(),
|
|
15842
|
-
branch: z5.string().optional(),
|
|
15843
|
-
statuses: z5.array(z5.enum(["draft", "ready", "in_progress", "blocked", "completed"])).optional()
|
|
15844
|
-
}).strict().parse(input);
|
|
15845
|
-
const resolution = await store.context(params.cwd ?? process.cwd(), params.plan, {
|
|
15846
|
-
branch: params.branch,
|
|
15847
|
-
statuses: params.statuses
|
|
15848
|
-
});
|
|
15849
|
-
return result(resolution.candidates.length ? resolution.candidates.map((record) => `${record.id}: ${record.document.status} r${record.document.revision} (${record.document.branch})`).join(`
|
|
15850
|
-
`) : "No matching plans.", { selected: resolution.record, candidates: resolution.candidates, ambiguous: resolution.ambiguous });
|
|
15851
|
-
}
|
|
15852
|
-
}),
|
|
15853
|
-
defineTool3({
|
|
15854
|
-
name: "plan_init",
|
|
15855
|
-
label: "plan init",
|
|
15856
|
-
description: "Create a phase-less editable plan draft.",
|
|
15857
|
-
parameters: parameters5(z5.object({
|
|
15858
|
-
cwd,
|
|
15859
|
-
shortSlug: id2,
|
|
15860
|
-
branch: z5.string().min(1).optional(),
|
|
15861
|
-
title: text.optional(),
|
|
15862
|
-
intent: text.optional(),
|
|
15863
|
-
open: z5.boolean().optional()
|
|
15864
|
-
}).strict()),
|
|
15865
|
-
executionMode: "sequential",
|
|
15866
|
-
async execute(_id, input) {
|
|
15867
|
-
const params = z5.object({
|
|
15868
|
-
cwd,
|
|
15869
|
-
shortSlug: id2,
|
|
15870
|
-
branch: z5.string().min(1).optional(),
|
|
15871
|
-
title: text.optional(),
|
|
15872
|
-
intent: text.optional(),
|
|
15873
|
-
open: z5.boolean().optional()
|
|
15874
|
-
}).strict().parse(input);
|
|
15875
|
-
const workingDirectory = params.cwd ?? process.cwd();
|
|
15876
|
-
const branch = params.branch ?? await currentBranch2(workingDirectory);
|
|
15877
|
-
const record = await store.init({ ...params, cwd: workingDirectory, branch });
|
|
15878
|
-
return result(`Created ${record.id} at ${record.planPath}.`, { record, openRequested: params.open === true });
|
|
15879
|
-
}
|
|
15880
|
-
}),
|
|
15881
|
-
defineTool3({
|
|
15882
|
-
name: "plan_update_overview",
|
|
15883
|
-
label: "plan update overview",
|
|
15884
|
-
description: "Replace selected plan overview fields using an expected plan revision.",
|
|
15885
|
-
parameters: parameters5(z5.object({
|
|
15886
|
-
cwd,
|
|
15887
|
-
plan: text,
|
|
15888
|
-
expectedPlanRevision: revision,
|
|
15889
|
-
intent: text.optional(),
|
|
15890
|
-
requirements: z5.array(text).optional(),
|
|
15891
|
-
design: design.optional(),
|
|
15892
|
-
references: z5.array(reference).optional()
|
|
15893
|
-
}).strict().refine((value) => value.intent || value.requirements || value.design || value.references, "At least one overview field is required.")),
|
|
15894
|
-
executionMode: "sequential",
|
|
15895
|
-
async execute(_id, input) {
|
|
15896
|
-
const schema = z5.object({
|
|
15897
|
-
cwd,
|
|
15898
|
-
plan: text,
|
|
15899
|
-
expectedPlanRevision: revision,
|
|
15900
|
-
intent: text.optional(),
|
|
15901
|
-
requirements: z5.array(text).optional(),
|
|
15902
|
-
design: design.optional(),
|
|
15903
|
-
references: z5.array(reference).optional()
|
|
15904
|
-
}).strict().refine((value) => value.intent || value.requirements || value.design || value.references, "At least one overview field is required.");
|
|
15905
|
-
const params = schema.parse(input);
|
|
15906
|
-
const record = await store.mutate(params.cwd ?? process.cwd(), params.plan, "update overview", (plan) => {
|
|
15907
|
-
assertAuthoringRevision(plan, params.expectedPlanRevision);
|
|
15908
|
-
assertAuthorable(plan);
|
|
15909
|
-
return resetDraft({
|
|
15910
|
-
...plan,
|
|
15911
|
-
intent: params.intent ?? plan.intent,
|
|
15912
|
-
requirements: params.requirements ?? plan.requirements,
|
|
15913
|
-
design: params.design ?? plan.design,
|
|
15914
|
-
references: params.references ?? plan.references
|
|
15915
|
-
});
|
|
15916
|
-
});
|
|
15917
|
-
return result(`Updated ${record.id} to revision ${record.document.revision}.`, { record });
|
|
15918
|
-
}
|
|
15919
|
-
}),
|
|
15920
|
-
defineTool3({
|
|
15921
|
-
name: "plan_add_phase",
|
|
15922
|
-
label: "plan add phase",
|
|
15923
|
-
description: "Add an ordered phase with stable IDs.",
|
|
15924
|
-
parameters: parameters5(z5.object({ cwd, plan: text, expectedPlanRevision: revision, afterPhaseId: id2.optional(), phase: phaseDraft }).strict()),
|
|
15925
|
-
executionMode: "sequential",
|
|
15926
|
-
async execute(_id, input) {
|
|
15927
|
-
const params = z5.object({ cwd, plan: text, expectedPlanRevision: revision, afterPhaseId: id2.optional(), phase: phaseDraft }).strict().parse(input);
|
|
15928
|
-
const record = await store.mutate(params.cwd ?? process.cwd(), params.plan, "add phase", (plan) => {
|
|
15929
|
-
assertAuthoringRevision(plan, params.expectedPlanRevision);
|
|
15930
|
-
assertAuthorable(plan);
|
|
15931
|
-
if (allIds(plan).has(params.phase.id))
|
|
15932
|
-
throw new Error(`Duplicate stable ID: ${params.phase.id}.`);
|
|
15933
|
-
const phase = newPhase(params.phase);
|
|
15934
|
-
const phases = [...plan.phases];
|
|
15935
|
-
if (params.afterPhaseId) {
|
|
15936
|
-
const index = phases.findIndex((item) => item.id === params.afterPhaseId);
|
|
15937
|
-
if (index < 0)
|
|
15938
|
-
throw new Error(`Unknown phase: ${params.afterPhaseId}.`);
|
|
15939
|
-
phases.splice(index + 1, 0, phase);
|
|
15940
|
-
} else
|
|
15941
|
-
phases.push(phase);
|
|
15942
|
-
assertNewIds(plan, phase);
|
|
15943
|
-
return resetDraft({ ...plan, phases });
|
|
15944
|
-
});
|
|
15945
|
-
return result(`Added phase ${params.phase.id} to ${record.id}.`, { record });
|
|
15946
|
-
}
|
|
15947
|
-
}),
|
|
15948
|
-
defineTool3({
|
|
15949
|
-
name: "plan_remove_phase",
|
|
15950
|
-
label: "plan remove phase",
|
|
15951
|
-
description: "Remove an unfinished phase that has no dependents.",
|
|
15952
|
-
parameters: parameters5(z5.object({ cwd, plan: text, expectedPlanRevision: revision, phaseId: id2, reason: text }).strict()),
|
|
15953
|
-
executionMode: "sequential",
|
|
15954
|
-
async execute(_id, input) {
|
|
15955
|
-
const params = z5.object({ cwd, plan: text, expectedPlanRevision: revision, phaseId: id2, reason: text }).strict().parse(input);
|
|
15956
|
-
const record = await store.mutate(params.cwd ?? process.cwd(), params.plan, "remove phase", (plan) => {
|
|
15957
|
-
assertAuthoringRevision(plan, params.expectedPlanRevision);
|
|
15958
|
-
assertAuthorable(plan);
|
|
15959
|
-
const phase = getPhase(plan, params.phaseId);
|
|
15960
|
-
if (phase.status === "completed" || phase.tasks.some((task) => task.status === "completed"))
|
|
15961
|
-
throw new Error(`Completed phase ${phase.id} cannot be removed.`);
|
|
15962
|
-
if (plan.phases.some((item) => item.dependencies.includes(phase.id)))
|
|
15963
|
-
throw new Error(`Phase ${phase.id} has dependents and cannot be removed.`);
|
|
15964
|
-
return resetDraft({ ...plan, phases: plan.phases.filter((item) => item.id !== phase.id) });
|
|
15965
|
-
});
|
|
15966
|
-
await store.log(params.cwd ?? process.cwd(), record.id, {
|
|
15967
|
-
planRevision: record.document.revision,
|
|
15968
|
-
kind: "updated",
|
|
15969
|
-
actor: "planner",
|
|
15970
|
-
message: `Removed phase ${params.phaseId}: ${params.reason}`,
|
|
15971
|
-
phaseId: params.phaseId
|
|
15972
|
-
});
|
|
15973
|
-
return result(`Removed phase ${params.phaseId}.`, { record });
|
|
15974
|
-
}
|
|
15975
|
-
}),
|
|
15976
|
-
defineTool3({
|
|
15977
|
-
name: "plan_update_phase",
|
|
15978
|
-
label: "plan update phase",
|
|
15979
|
-
description: "Update an unfinished phase and its ordered tasks.",
|
|
15980
|
-
parameters: parameters5(z5.object({
|
|
15981
|
-
cwd,
|
|
15982
|
-
plan: text,
|
|
15983
|
-
expectedPlanRevision: revision,
|
|
15984
|
-
phaseId: id2,
|
|
15985
|
-
patch: z5.object({
|
|
15986
|
-
title: text.optional(),
|
|
15987
|
-
objective: text.optional(),
|
|
15988
|
-
dependencies: z5.array(id2).optional(),
|
|
15989
|
-
tasks: z5.array(taskDraft).optional()
|
|
15990
|
-
}).strict()
|
|
15991
|
-
}).strict()),
|
|
15992
|
-
executionMode: "sequential",
|
|
15993
|
-
async execute(_id, input) {
|
|
15994
|
-
const params = z5.object({
|
|
15995
|
-
cwd,
|
|
15996
|
-
plan: text,
|
|
15997
|
-
expectedPlanRevision: revision,
|
|
15998
|
-
phaseId: id2,
|
|
15999
|
-
patch: z5.object({
|
|
16000
|
-
title: text.optional(),
|
|
16001
|
-
objective: text.optional(),
|
|
16002
|
-
dependencies: z5.array(id2).optional(),
|
|
16003
|
-
tasks: z5.array(taskDraft).optional()
|
|
16004
|
-
}).strict()
|
|
16005
|
-
}).strict().parse(input);
|
|
16006
|
-
const record = await store.mutate(params.cwd ?? process.cwd(), params.plan, "update phase", (plan) => {
|
|
16007
|
-
assertAuthoringRevision(plan, params.expectedPlanRevision);
|
|
16008
|
-
assertAuthorable(plan, params.phaseId);
|
|
16009
|
-
const existing = getPhase(plan, params.phaseId);
|
|
16010
|
-
if (existing.status === "completed")
|
|
16011
|
-
throw new Error(`Completed phase ${existing.id} cannot be changed.`);
|
|
16012
|
-
const tasks = params.patch.tasks ? reconcileTasks(existing.tasks, params.patch.tasks) : existing.tasks;
|
|
16013
|
-
const updated = {
|
|
16014
|
-
...existing,
|
|
16015
|
-
title: params.patch.title ?? existing.title,
|
|
16016
|
-
objective: params.patch.objective ?? existing.objective,
|
|
16017
|
-
dependencies: params.patch.dependencies ?? existing.dependencies,
|
|
16018
|
-
tasks,
|
|
16019
|
-
revision: existing.revision + 1,
|
|
16020
|
-
status: existing.status === "blocked" ? "in_progress" : existing.status,
|
|
16021
|
-
blocker: existing.status === "blocked" ? undefined : existing.blocker
|
|
16022
|
-
};
|
|
16023
|
-
assertNewIds({ ...plan, phases: plan.phases.filter((phase) => phase.id !== existing.id) }, updated);
|
|
16024
|
-
return resetDraft({
|
|
16025
|
-
...plan,
|
|
16026
|
-
phases: plan.phases.map((phase) => phase.id === existing.id ? updated : phase)
|
|
16027
|
-
});
|
|
16028
|
-
});
|
|
16029
|
-
return result(`Updated phase ${params.phaseId}.`, { record });
|
|
16030
|
-
}
|
|
16031
|
-
}),
|
|
16032
|
-
defineTool3({
|
|
16033
|
-
name: "plan_validate",
|
|
16034
|
-
label: "plan validate",
|
|
16035
|
-
description: "Validate plan markers, dependencies, completeness, annotations, and Design length.",
|
|
16036
|
-
parameters: parameters5(z5.object({ cwd, plan: text, strict: z5.boolean().optional() }).strict()),
|
|
16037
|
-
executionMode: "parallel",
|
|
16038
|
-
async execute(_id, input) {
|
|
16039
|
-
const params = z5.object({ cwd, plan: text, strict: z5.boolean().optional() }).strict().parse(input);
|
|
16040
|
-
const record = await store.read(params.cwd ?? process.cwd(), params.plan);
|
|
16041
|
-
const annotations = params.strict ? await readPlanAnnotations(record) : { pending: [] };
|
|
16042
|
-
const issues = validatePlanDocument(record.document, {
|
|
16043
|
-
strict: params.strict,
|
|
16044
|
-
pendingAnnotations: annotations.pending.length
|
|
16045
|
-
});
|
|
16046
|
-
const words = countDesignWords(record.document);
|
|
16047
|
-
return result(issues.length ? issues.map((issue) => `- ${issue.severity}: ${issue.message}`).join(`
|
|
16048
|
-
`) : "Plan is valid.", { record, issues, designWordCount: words, ready: !issues.some((issue) => issue.severity === "error") });
|
|
16049
|
-
}
|
|
16050
|
-
}),
|
|
16051
|
-
defineTool3({
|
|
16052
|
-
name: "plan_log_progress",
|
|
16053
|
-
label: "plan log progress",
|
|
16054
|
-
description: "Append a progress event without changing plan revision.",
|
|
16055
|
-
parameters: parameters5(z5.object({
|
|
16056
|
-
cwd,
|
|
16057
|
-
plan: text,
|
|
16058
|
-
actor: text,
|
|
16059
|
-
message: text,
|
|
16060
|
-
executionId: id2.optional(),
|
|
16061
|
-
phaseId: id2.optional(),
|
|
16062
|
-
taskId: id2.optional(),
|
|
16063
|
-
evidence: z5.array(text).optional(),
|
|
16064
|
-
data: z5.record(z5.string(), z5.unknown()).optional()
|
|
16065
|
-
}).strict()),
|
|
16066
|
-
executionMode: "sequential",
|
|
16067
|
-
async execute(_id, input) {
|
|
16068
|
-
const params = z5.object({
|
|
16069
|
-
cwd,
|
|
16070
|
-
plan: text,
|
|
16071
|
-
actor: text,
|
|
16072
|
-
message: text,
|
|
16073
|
-
executionId: id2.optional(),
|
|
16074
|
-
phaseId: id2.optional(),
|
|
16075
|
-
taskId: id2.optional(),
|
|
16076
|
-
evidence: z5.array(text).optional(),
|
|
16077
|
-
data: z5.record(z5.string(), z5.unknown()).optional()
|
|
16078
|
-
}).strict().parse(input);
|
|
16079
|
-
const record = await store.read(params.cwd ?? process.cwd(), params.plan);
|
|
16080
|
-
const entry = await store.log(params.cwd ?? process.cwd(), params.plan, {
|
|
16081
|
-
planRevision: record.document.revision,
|
|
16082
|
-
kind: "progress",
|
|
16083
|
-
actor: params.actor,
|
|
16084
|
-
message: params.message,
|
|
16085
|
-
executionId: params.executionId,
|
|
16086
|
-
phaseId: params.phaseId,
|
|
16087
|
-
taskId: params.taskId,
|
|
16088
|
-
evidence: params.evidence,
|
|
16089
|
-
data: params.data
|
|
16090
|
-
});
|
|
16091
|
-
return result(`Logged ${entry.eventId}.`, { entry });
|
|
16092
|
-
}
|
|
16093
|
-
}),
|
|
16094
|
-
createStatusTool(pi, modes, store),
|
|
16095
|
-
defineTool3({
|
|
16096
|
-
name: "plan_run_gates",
|
|
16097
|
-
label: "plan run gates",
|
|
16098
|
-
description: "Run format check, lint, and tests without holding the plan lock, then persist non-stale results.",
|
|
16099
|
-
parameters: parameters5(z5.object({ cwd, plan: text, phaseId: id2, expectedPhaseRevision: revision, executionId: id2, actor: text }).strict()),
|
|
16100
|
-
executionMode: "sequential",
|
|
16101
|
-
async execute(_id, input) {
|
|
16102
|
-
const params = z5.object({ cwd, plan: text, phaseId: id2, expectedPhaseRevision: revision, executionId: id2, actor: text }).strict().parse(input);
|
|
16103
|
-
const workingDirectory = params.cwd ?? process.cwd();
|
|
16104
|
-
const before = await store.read(workingDirectory, params.plan);
|
|
16105
|
-
const phase = getPhase(before.document, params.phaseId);
|
|
16106
|
-
if (phase.revision !== params.expectedPhaseRevision)
|
|
16107
|
-
throw new Error(`Stale phase revision for ${phase.id}.`);
|
|
16108
|
-
if (!before.document.execution?.active || before.document.execution.id !== params.executionId)
|
|
16109
|
-
throw new Error(`Execution ${params.executionId} does not own this plan.`);
|
|
16110
|
-
if (phase.tasks.some((task) => task.status !== "completed" && task.status !== "skipped"))
|
|
16111
|
-
throw new Error(`Phase ${phase.id} still has incomplete tasks.`);
|
|
16112
|
-
const results = await runMiseGates(workingDirectory);
|
|
16113
|
-
const passed = results.every((gate) => gate.status === "pass" || gate.status === "skip");
|
|
16114
|
-
const record = await store.mutate(workingDirectory, params.plan, "persist gates", (plan) => {
|
|
16115
|
-
const current = getPhase(plan, params.phaseId);
|
|
16116
|
-
if (current.revision !== params.expectedPhaseRevision)
|
|
16117
|
-
throw new Error(`Gate results for ${current.id} are stale.`);
|
|
16118
|
-
if (current.tasks.some((task) => task.status !== "completed" && task.status !== "skipped"))
|
|
16119
|
-
throw new Error(`Gate results for ${current.id} are stale because task state changed.`);
|
|
16120
|
-
const updated = {
|
|
16121
|
-
...current,
|
|
16122
|
-
revision: current.revision + 1,
|
|
16123
|
-
gate: {
|
|
16124
|
-
phaseRevision: params.expectedPhaseRevision,
|
|
16125
|
-
status: passed ? "passed" : "failed",
|
|
16126
|
-
results,
|
|
16127
|
-
completedAt: new Date().toISOString()
|
|
16128
|
-
}
|
|
16129
|
-
};
|
|
16130
|
-
return {
|
|
16131
|
-
...heartbeat(plan),
|
|
16132
|
-
phases: plan.phases.map((item) => item.id === updated.id ? updated : item)
|
|
16133
|
-
};
|
|
16134
|
-
});
|
|
16135
|
-
await store.log(workingDirectory, record.id, {
|
|
16136
|
-
planRevision: record.document.revision,
|
|
16137
|
-
kind: "gate",
|
|
16138
|
-
actor: params.actor,
|
|
16139
|
-
message: `Phase ${phase.id} gates ${passed ? "passed" : "failed"}.`,
|
|
16140
|
-
executionId: params.executionId,
|
|
16141
|
-
phaseId: phase.id,
|
|
16142
|
-
data: { results }
|
|
16143
|
-
});
|
|
16144
|
-
return result(results.map((gate) => `${gate.name}: ${gate.status} — ${gate.detail}`).join(`
|
|
16145
|
-
`), {
|
|
16146
|
-
results,
|
|
16147
|
-
passed,
|
|
16148
|
-
record
|
|
16149
|
-
});
|
|
16150
|
-
}
|
|
16151
|
-
}),
|
|
16152
|
-
defineTool3({
|
|
16153
|
-
name: "plan_annotate",
|
|
16154
|
-
label: "plan annotate",
|
|
16155
|
-
description: "Launch the selected PLAN.md in tuicr standalone file annotation mode.",
|
|
16156
|
-
parameters: parameters5(z5.object({ cwd, plan: z5.string().optional() }).strict()),
|
|
16157
|
-
executionMode: "sequential",
|
|
16158
|
-
async execute(_id, input) {
|
|
16159
|
-
const params = z5.object({ cwd, plan: z5.string().optional() }).strict().parse(input);
|
|
16160
|
-
const workingDirectory = params.cwd ?? process.cwd();
|
|
16161
|
-
const resolution = await store.context(workingDirectory, params.plan);
|
|
16162
|
-
if (!resolution.record)
|
|
16163
|
-
throw new Error(resolution.ambiguous ? `Plan is ambiguous: ${resolution.candidates.map((item) => item.id).join(", ")}.` : "No matching plan.");
|
|
16164
|
-
const cliPath = join19(resolveBundledAgentsDir(), "..", "dist", "cli.js");
|
|
16165
|
-
const command = [
|
|
16166
|
-
process.execPath,
|
|
16167
|
-
cliPath,
|
|
16168
|
-
"plan",
|
|
16169
|
-
"annotate",
|
|
16170
|
-
resolution.record.id,
|
|
16171
|
-
"--cwd",
|
|
16172
|
-
workingDirectory
|
|
16173
|
-
];
|
|
16174
|
-
const launched = await openInNewTab(command, { cwd: workingDirectory, name: "plan-annotate" });
|
|
16175
|
-
const fallbackCommand = `npx --yes @difflab/pi plan annotate ${resolution.record.id} --cwd ${JSON.stringify(workingDirectory)}`;
|
|
16176
|
-
return result(launched.launched ? `Opened ${resolution.record.id} for annotation.` : launched.instruction ?? `Run: ${fallbackCommand}`, { record: resolution.record, launched, fallbackCommand, followUp: `/plan update ${resolution.record.id}` });
|
|
16177
|
-
}
|
|
16178
|
-
}),
|
|
16179
|
-
defineTool3({
|
|
16180
|
-
name: "plan_annotations",
|
|
16181
|
-
label: "plan annotations",
|
|
16182
|
-
description: "Read normalized pending comments from the selected plan annotation session.",
|
|
16183
|
-
parameters: parameters5(z5.object({ cwd, plan: z5.string().optional(), includeApplied: z5.boolean().optional() }).strict()),
|
|
16184
|
-
executionMode: "parallel",
|
|
16185
|
-
async execute(_id, input) {
|
|
16186
|
-
const params = z5.object({ cwd, plan: z5.string().optional(), includeApplied: z5.boolean().optional() }).strict().parse(input);
|
|
16187
|
-
const workingDirectory = params.cwd ?? process.cwd();
|
|
16188
|
-
const resolution = await store.context(workingDirectory, params.plan);
|
|
16189
|
-
if (!resolution.record)
|
|
16190
|
-
throw new Error(resolution.ambiguous ? `Plan is ambiguous: ${resolution.candidates.map((item) => item.id).join(", ")}.` : "No matching plan.");
|
|
16191
|
-
const annotations = await readPlanAnnotations(resolution.record, { includeApplied: params.includeApplied });
|
|
16192
|
-
return result(annotations.comments.length ? annotations.comments.map((comment) => `${comment.id}: ${comment.body}`).join(`
|
|
16193
|
-
`) : "No pending annotations.", { record: resolution.record, ...annotations });
|
|
16194
|
-
}
|
|
16195
|
-
}),
|
|
16196
|
-
defineTool3({
|
|
16197
|
-
name: "plan_ack_annotations",
|
|
16198
|
-
label: "plan acknowledge annotations",
|
|
16199
|
-
description: "Acknowledge only annotation comments successfully applied to the plan.",
|
|
16200
|
-
parameters: parameters5(z5.object({ cwd, plan: text, commentIds: z5.array(text).min(1), summary: text }).strict()),
|
|
16201
|
-
executionMode: "sequential",
|
|
16202
|
-
async execute(_id, input) {
|
|
16203
|
-
const params = z5.object({ cwd, plan: text, commentIds: z5.array(text).min(1), summary: text }).strict().parse(input);
|
|
16204
|
-
const record = await store.read(params.cwd ?? process.cwd(), params.plan);
|
|
16205
|
-
const state = await acknowledgePlanAnnotations(record, params.commentIds, params.summary);
|
|
16206
|
-
return result(`Acknowledged ${params.commentIds.length} annotations for ${record.id}.`, {
|
|
16207
|
-
record,
|
|
16208
|
-
state,
|
|
16209
|
-
acknowledged: params.commentIds
|
|
16210
|
-
});
|
|
16211
|
-
}
|
|
16212
|
-
}),
|
|
16213
|
-
defineTool3({
|
|
16214
|
-
name: "plan_start_execution",
|
|
16215
|
-
label: "plan start execution",
|
|
16216
|
-
description: "Start one inline or background plan execution.",
|
|
16217
|
-
parameters: parameters5(z5.object({
|
|
16218
|
-
cwd,
|
|
16219
|
-
plan: text,
|
|
16220
|
-
mode: z5.enum(["inline", "background"]),
|
|
16221
|
-
policy: z5.enum(["commit-per-phase", "no-commit"]),
|
|
16222
|
-
actor: text
|
|
16223
|
-
}).strict()),
|
|
16224
|
-
executionMode: "sequential",
|
|
16225
|
-
async execute(_id, input, _signal, _onUpdate, ctx) {
|
|
16226
|
-
const params = z5.object({
|
|
16227
|
-
cwd,
|
|
16228
|
-
plan: text,
|
|
16229
|
-
mode: z5.enum(["inline", "background"]),
|
|
16230
|
-
policy: z5.enum(["commit-per-phase", "no-commit"]),
|
|
16231
|
-
actor: text
|
|
16232
|
-
}).strict().parse(input);
|
|
16233
|
-
return startExecution(pi, modes, store, params, ctx);
|
|
16234
|
-
}
|
|
16235
|
-
})
|
|
16236
|
-
];
|
|
16237
|
-
}
|
|
16238
|
-
function createStatusTool(pi, modes, store) {
|
|
16239
|
-
const schema = z5.object({
|
|
16240
|
-
cwd,
|
|
16241
|
-
plan: text,
|
|
16242
|
-
target: z5.object({ type: z5.enum(["plan", "phase", "task"]), id: id2.optional() }).strict().refine((value) => value.type === "plan" || value.id, "Phase and task targets require an ID."),
|
|
16243
|
-
expectedStatus: z5.enum(["draft", "ready", "in_progress", "blocked", "completed", "pending", "skipped"]),
|
|
16244
|
-
status: z5.enum(["draft", "ready", "in_progress", "blocked", "completed", "pending", "skipped"]),
|
|
16245
|
-
actor: text,
|
|
16246
|
-
message: text,
|
|
16247
|
-
executionId: id2.optional(),
|
|
16248
|
-
evidence: z5.array(text).optional(),
|
|
16249
|
-
blockedReason: text.optional(),
|
|
16250
|
-
attempts: z5.array(text).optional(),
|
|
16251
|
-
needsUserDecision: z5.boolean().optional(),
|
|
16252
|
-
commit: z5.object({ sha: z5.string().regex(/^[a-f0-9]{7,64}$/i), subject: text, completedAt: z5.string().datetime() }).strict().optional()
|
|
16253
|
-
}).strict();
|
|
16254
|
-
return defineTool3({
|
|
16255
|
-
name: "plan_update_status",
|
|
16256
|
-
label: "plan update status",
|
|
16257
|
-
description: "Apply an ownership-checked plan, phase, or task status transition and append an audit event.",
|
|
16258
|
-
parameters: parameters5(schema),
|
|
16259
|
-
executionMode: "sequential",
|
|
16260
|
-
async execute(_id, input, _signal, _onUpdate, ctx) {
|
|
16261
|
-
const params = schema.parse(input);
|
|
16262
|
-
const workingDirectory = params.cwd ?? process.cwd();
|
|
16263
|
-
let escalation;
|
|
16264
|
-
if (params.commit) {
|
|
16265
|
-
if (params.target.type !== "phase" || params.status !== "completed")
|
|
16266
|
-
throw new Error("Commit metadata is accepted only when completing a phase.");
|
|
16267
|
-
const verified = await verifyCommit(workingDirectory, params.commit.sha);
|
|
16268
|
-
if (verified.sha !== params.commit.sha && !verified.sha.startsWith(params.commit.sha))
|
|
16269
|
-
throw new Error(`Commit ${params.commit.sha} is not the current HEAD ${verified.sha}.`);
|
|
16270
|
-
if (verified.subject !== params.commit.subject)
|
|
16271
|
-
throw new Error(`Commit subject does not match HEAD: ${verified.subject}.`);
|
|
16272
|
-
}
|
|
16273
|
-
const record = await store.mutate(workingDirectory, params.plan, "update status", (plan) => {
|
|
16274
|
-
if (params.target.type === "plan") {
|
|
16275
|
-
if (plan.status !== params.expectedStatus)
|
|
16276
|
-
throw new Error(`Expected plan status ${params.expectedStatus}, found ${plan.status}.`);
|
|
16277
|
-
assertPlanTransition(plan.status, params.status);
|
|
16278
|
-
if (params.status === "ready") {
|
|
16279
|
-
const errors = validatePlanDocument(plan, { strict: true }).filter((issue) => issue.severity === "error");
|
|
16280
|
-
if (errors.length)
|
|
16281
|
-
throw new Error(`Plan cannot become ready: ${errors.map((issue) => issue.message).join(" ")}`);
|
|
16282
|
-
}
|
|
16283
|
-
if ((params.status === "blocked" || params.status === "completed") && plan.status === "in_progress") {
|
|
16284
|
-
assertExecutionOwner(plan, params.executionId);
|
|
16285
|
-
}
|
|
16286
|
-
if (params.status === "completed" && plan.phases.some((phase) => phase.status !== "completed" && phase.status !== "skipped"))
|
|
16287
|
-
throw new Error("Every phase must be complete or skipped before the plan completes.");
|
|
16288
|
-
return {
|
|
16289
|
-
...heartbeat(plan),
|
|
16290
|
-
status: params.status,
|
|
16291
|
-
execution: params.status === "completed" || params.status === "blocked" ? plan.execution && { ...plan.execution, active: false, heartbeatAt: new Date().toISOString() } : plan.execution
|
|
16292
|
-
};
|
|
16293
|
-
}
|
|
16294
|
-
if (!params.target.id)
|
|
16295
|
-
throw new Error("Target ID is required.");
|
|
16296
|
-
const phase = params.target.type === "phase" ? getPhase(plan, params.target.id) : findTaskPhase(plan, params.target.id);
|
|
16297
|
-
if (params.target.type === "phase") {
|
|
16298
|
-
if (phase.status !== params.expectedStatus)
|
|
16299
|
-
throw new Error(`Expected phase status ${params.expectedStatus}, found ${phase.status}.`);
|
|
16300
|
-
assertExecutionOwner(plan, params.executionId);
|
|
16301
|
-
assertPhaseTransition(phase.status, params.status);
|
|
16302
|
-
if (params.status === "completed") {
|
|
16303
|
-
if (phase.tasks.some((task) => task.status !== "completed" && task.status !== "skipped"))
|
|
16304
|
-
throw new Error("All phase tasks must be complete or skipped.");
|
|
16305
|
-
if (phase.gate.status !== "passed")
|
|
16306
|
-
throw new Error("Phase gates must pass before completion.");
|
|
16307
|
-
if (plan.execution?.policy === "commit-per-phase" && !params.commit)
|
|
16308
|
-
throw new Error("Commit-per-phase execution requires commit metadata before phase completion.");
|
|
16309
|
-
if (plan.execution?.policy === "no-commit" && params.commit)
|
|
16310
|
-
throw new Error("No-commit execution cannot record a phase commit.");
|
|
16311
|
-
}
|
|
16312
|
-
const updated = {
|
|
16313
|
-
...phase,
|
|
16314
|
-
status: params.status,
|
|
16315
|
-
revision: phase.revision + 1,
|
|
16316
|
-
commit: params.commit ?? phase.commit,
|
|
16317
|
-
blocker: params.blockedReason ? blocker(params) : phase.blocker
|
|
16318
|
-
};
|
|
16319
|
-
if (params.status === "blocked" && params.blockedReason)
|
|
16320
|
-
escalation = escalationFor(plan, updated.id, undefined, params);
|
|
16321
|
-
return {
|
|
16322
|
-
...heartbeat(plan),
|
|
16323
|
-
phases: plan.phases.map((item) => item.id === updated.id ? updated : item)
|
|
16324
|
-
};
|
|
16325
|
-
}
|
|
16326
|
-
const task = getTask(phase, params.target.id);
|
|
16327
|
-
if (task.status !== params.expectedStatus)
|
|
16328
|
-
throw new Error(`Expected task status ${params.expectedStatus}, found ${task.status}.`);
|
|
16329
|
-
assertExecutionOwner(plan, params.executionId);
|
|
16330
|
-
assertTaskTransition(task, params.status, params.executionId, params.actor);
|
|
16331
|
-
const updatedTask = {
|
|
16332
|
-
...task,
|
|
16333
|
-
status: params.status,
|
|
16334
|
-
revision: task.revision + 1,
|
|
16335
|
-
owner: params.status === "in_progress" ? params.actor : task.owner,
|
|
16336
|
-
executionId: params.status === "in_progress" ? params.executionId : task.executionId,
|
|
16337
|
-
blocker: params.blockedReason ? blocker(params) : task.blocker
|
|
16338
|
-
};
|
|
16339
|
-
if (params.status === "blocked" && params.blockedReason)
|
|
16340
|
-
escalation = escalationFor(plan, phase.id, task.id, params);
|
|
16341
|
-
const updatedPhase = {
|
|
16342
|
-
...phase,
|
|
16343
|
-
revision: phase.revision + 1,
|
|
16344
|
-
tasks: phase.tasks.map((item) => item.id === updatedTask.id ? updatedTask : item)
|
|
16345
|
-
};
|
|
16346
|
-
return {
|
|
16347
|
-
...heartbeat(plan),
|
|
16348
|
-
phases: plan.phases.map((item) => item.id === updatedPhase.id ? updatedPhase : item)
|
|
16349
|
-
};
|
|
16350
|
-
});
|
|
16351
|
-
await store.log(workingDirectory, record.id, {
|
|
16352
|
-
planRevision: record.document.revision,
|
|
16353
|
-
kind: params.status === "blocked" ? "blocker" : "status",
|
|
16354
|
-
actor: params.actor,
|
|
16355
|
-
message: params.message,
|
|
16356
|
-
executionId: params.executionId,
|
|
16357
|
-
phaseId: params.target.type === "phase" ? params.target.id : escalation?.phaseId,
|
|
16358
|
-
taskId: params.target.type === "task" ? params.target.id : undefined,
|
|
16359
|
-
evidence: params.evidence
|
|
16360
|
-
});
|
|
16361
|
-
if (escalation && record.document.execution?.mode !== "background") {
|
|
16362
|
-
const selected = await modes.set("planner", ctx);
|
|
16363
|
-
if (selected.ok)
|
|
16364
|
-
pi.sendUserMessage(renderPlannerEscalation(escalation), {
|
|
16365
|
-
deliverAs: "followUp"
|
|
16366
|
-
});
|
|
16367
|
-
}
|
|
16368
|
-
return result(`Updated ${params.target.type} to ${params.status}.`, { record, escalation });
|
|
16369
|
-
}
|
|
16370
|
-
});
|
|
16371
|
-
}
|
|
16372
|
-
async function startExecution(pi, modes, store, params, ctx) {
|
|
16373
|
-
const workingDirectory = params.cwd ?? process.cwd();
|
|
16374
|
-
const before = await store.read(workingDirectory, params.plan);
|
|
16375
|
-
if (before.document.status !== "ready" && before.document.status !== "blocked")
|
|
16376
|
-
throw new Error(`Plan ${before.id} must be ready or blocked before execution.`);
|
|
16377
|
-
if (before.document.execution?.active)
|
|
16378
|
-
throw new Error(`Plan ${before.id} already has an active execution.`);
|
|
16379
|
-
const branch = await currentBranch2(workingDirectory);
|
|
16380
|
-
if (branch !== before.document.branch)
|
|
16381
|
-
throw new Error(`Plan branch is ${before.document.branch}; current branch is ${branch}.`);
|
|
16382
|
-
if (params.policy === "commit-per-phase") {
|
|
16383
|
-
const dirty = await runChecked("git", ["-C", workingDirectory, "status", "--porcelain"]);
|
|
16384
|
-
if (dirty.stdout.trim())
|
|
16385
|
-
throw new Error(`Commit-per-phase requires a clean worktree:
|
|
16386
|
-
${dirty.stdout.trim()}`);
|
|
16387
|
-
}
|
|
16388
|
-
const head = (await runChecked("git", ["-C", workingDirectory, "rev-parse", "HEAD"])).stdout.trim();
|
|
16389
|
-
const executionId = crypto.randomUUID();
|
|
16390
|
-
const timestamp = new Date().toISOString();
|
|
16391
|
-
const record = await store.mutate(workingDirectory, before.id, "start execution", (plan) => ({
|
|
16392
|
-
...plan,
|
|
16393
|
-
status: "in_progress",
|
|
16394
|
-
execution: {
|
|
16395
|
-
id: executionId,
|
|
16396
|
-
mode: params.mode,
|
|
16397
|
-
policy: params.policy,
|
|
16398
|
-
cwd: workingDirectory,
|
|
16399
|
-
branch: plan.branch,
|
|
16400
|
-
baseHead: head,
|
|
16401
|
-
actor: params.actor,
|
|
16402
|
-
startedAt: timestamp,
|
|
16403
|
-
heartbeatAt: timestamp,
|
|
16404
|
-
active: true
|
|
16405
|
-
}
|
|
16406
|
-
}));
|
|
16407
|
-
await store.log(workingDirectory, record.id, {
|
|
16408
|
-
planRevision: record.document.revision,
|
|
16409
|
-
kind: "execution",
|
|
16410
|
-
actor: params.actor,
|
|
16411
|
-
message: `Started ${params.mode} execution ${executionId}.`,
|
|
16412
|
-
executionId
|
|
16413
|
-
});
|
|
16414
|
-
const coordinator = params.mode === "inline" ? "worker" : "orchestrator";
|
|
16415
|
-
const prompt = renderExecutionPrompt(createExecutionPacket(record.document, coordinator));
|
|
16416
|
-
if (params.mode === "inline") {
|
|
16417
|
-
const selected = await modes.set("worker", ctx);
|
|
16418
|
-
if (!selected.ok)
|
|
16419
|
-
throw new Error(selected.message);
|
|
16420
|
-
if (!pi.sendMessage)
|
|
16421
|
-
throw new Error("Inline execution dispatch is unavailable.");
|
|
16422
|
-
pi.sendMessage({ customType: "diffpi-plan-execution", display: false, content: prompt }, { triggerTurn: true, deliverAs: "followUp" });
|
|
16423
|
-
} else {
|
|
16424
|
-
await launchBackgroundPi(pi, {
|
|
16425
|
-
name: `Plan go ${record.id}`,
|
|
16426
|
-
agentPath: join19(resolveBundledAgentsDir(), "diffpi-orchestrator.md"),
|
|
16427
|
-
model: "openai-codex/gpt-5.6-luna",
|
|
16428
|
-
thinking: "medium",
|
|
16429
|
-
cwd: workingDirectory,
|
|
16430
|
-
prompt
|
|
16431
|
-
});
|
|
16432
|
-
}
|
|
16433
|
-
return result(`Started ${params.mode} execution ${executionId}.`, {
|
|
16434
|
-
record,
|
|
16435
|
-
executionId,
|
|
16436
|
-
dispatched: true,
|
|
16437
|
-
queued: true,
|
|
16438
|
-
foregroundModeChanged: params.mode === "inline"
|
|
16439
|
-
});
|
|
16440
|
-
}
|
|
16441
|
-
function newPhase(input) {
|
|
16442
|
-
return {
|
|
16443
|
-
id: input.id,
|
|
16444
|
-
revision: 0,
|
|
16445
|
-
title: input.title,
|
|
16446
|
-
objective: input.objective,
|
|
16447
|
-
dependencies: input.dependencies ?? [],
|
|
16448
|
-
tasks: (input.tasks ?? []).map(newTask),
|
|
16449
|
-
status: "pending",
|
|
16450
|
-
gate: { phaseRevision: 0, status: "pending", results: [] }
|
|
16451
|
-
};
|
|
16452
|
-
}
|
|
16453
|
-
function newTask(input) {
|
|
16454
|
-
return {
|
|
16455
|
-
id: input.id,
|
|
16456
|
-
revision: 0,
|
|
16457
|
-
title: input.title,
|
|
16458
|
-
steps: input.steps,
|
|
16459
|
-
dependencies: input.dependencies ?? [],
|
|
16460
|
-
fileScopes: input.fileScopes ?? [],
|
|
16461
|
-
acceptanceCriteria: input.acceptanceCriteria ?? [],
|
|
16462
|
-
status: "pending"
|
|
16463
|
-
};
|
|
16464
|
-
}
|
|
16465
|
-
function reconcileTasks(existing, drafts) {
|
|
16466
|
-
const byId = new Map(existing.map((task) => [task.id, task]));
|
|
16467
|
-
const requested = new Set(drafts.map((task) => task.id));
|
|
16468
|
-
const removedCompleted = existing.find((task) => task.status === "completed" && !requested.has(task.id));
|
|
16469
|
-
if (removedCompleted)
|
|
16470
|
-
throw new Error(`Completed task ${removedCompleted.id} cannot be removed.`);
|
|
16471
|
-
return drafts.map((draft) => {
|
|
16472
|
-
const current = byId.get(draft.id);
|
|
16473
|
-
if (current?.status === "completed")
|
|
16474
|
-
return current;
|
|
16475
|
-
return current ? {
|
|
16476
|
-
...newTask(draft),
|
|
16477
|
-
revision: current.revision + 1,
|
|
16478
|
-
status: current.status === "blocked" ? "pending" : current.status,
|
|
16479
|
-
owner: current.status === "blocked" ? undefined : current.owner,
|
|
16480
|
-
executionId: current.status === "blocked" ? undefined : current.executionId,
|
|
16481
|
-
blocker: current.status === "blocked" ? undefined : current.blocker
|
|
16482
|
-
} : newTask(draft);
|
|
16483
|
-
});
|
|
14161
|
+
await mkdir(dirname3(path), { recursive: true });
|
|
14162
|
+
await writeFile(path, `${JSON.stringify(value, null, 2)}
|
|
14163
|
+
`, "utf8");
|
|
16484
14164
|
}
|
|
16485
|
-
|
|
16486
|
-
|
|
16487
|
-
|
|
14165
|
+
|
|
14166
|
+
// src/environment.ts
|
|
14167
|
+
function detectIde(env = process.env) {
|
|
14168
|
+
const program = (env.TERM_PROGRAM ?? "").toLowerCase();
|
|
14169
|
+
if (env.ZED_TERM === "true" || program === "zed")
|
|
14170
|
+
return "zed";
|
|
14171
|
+
if (env.CURSOR_TRACE_ID || program === "cursor")
|
|
14172
|
+
return "cursor";
|
|
14173
|
+
if (env.WINDSURF_ENV || program === "windsurf")
|
|
14174
|
+
return "windsurf";
|
|
14175
|
+
if (env.TERMINAL_EMULATOR?.toLowerCase().includes("jetbrains"))
|
|
14176
|
+
return "jetbrains";
|
|
14177
|
+
if (env.VSCODE_PID || env.VSCODE_GIT_IPC_HANDLE || program === "vscode")
|
|
14178
|
+
return "vscode";
|
|
14179
|
+
return "unknown";
|
|
16488
14180
|
}
|
|
16489
|
-
function
|
|
16490
|
-
if (
|
|
16491
|
-
|
|
16492
|
-
if (
|
|
16493
|
-
return;
|
|
16494
|
-
if (
|
|
16495
|
-
|
|
16496
|
-
|
|
16497
|
-
if (phase.status !== "blocked" && !phase.tasks.some((task) => task.status === "blocked"))
|
|
16498
|
-
throw new Error("Planner may amend only blocked work during an active execution.");
|
|
16499
|
-
}
|
|
16500
|
-
function resetDraft(plan) {
|
|
16501
|
-
return plan.status === "ready" || plan.status === "blocked" ? { ...plan, status: "draft" } : plan;
|
|
16502
|
-
}
|
|
16503
|
-
function allIds(plan) {
|
|
16504
|
-
return new Set([plan.id, ...plan.phases.flatMap((phase) => [phase.id, ...phase.tasks.map((task) => task.id)])]);
|
|
16505
|
-
}
|
|
16506
|
-
function assertNewIds(plan, phase) {
|
|
16507
|
-
const known = allIds(plan);
|
|
16508
|
-
for (const candidate of [phase.id, ...phase.tasks.map((task) => task.id)]) {
|
|
16509
|
-
assertStableId(candidate);
|
|
16510
|
-
if (known.has(candidate))
|
|
16511
|
-
throw new Error(`Duplicate stable ID: ${candidate}.`);
|
|
16512
|
-
known.add(candidate);
|
|
16513
|
-
}
|
|
16514
|
-
}
|
|
16515
|
-
function getPhase(plan, phaseId) {
|
|
16516
|
-
const phase = plan.phases.find((item) => item.id === phaseId);
|
|
16517
|
-
if (!phase)
|
|
16518
|
-
throw new Error(`Unknown phase: ${phaseId}.`);
|
|
16519
|
-
return phase;
|
|
16520
|
-
}
|
|
16521
|
-
function findTaskPhase(plan, taskId) {
|
|
16522
|
-
const phase = plan.phases.find((item) => item.tasks.some((task) => task.id === taskId));
|
|
16523
|
-
if (!phase)
|
|
16524
|
-
throw new Error(`Unknown task: ${taskId}.`);
|
|
16525
|
-
return phase;
|
|
16526
|
-
}
|
|
16527
|
-
function getTask(phase, taskId) {
|
|
16528
|
-
const task = phase.tasks.find((item) => item.id === taskId);
|
|
16529
|
-
if (!task)
|
|
16530
|
-
throw new Error(`Unknown task: ${taskId}.`);
|
|
16531
|
-
return task;
|
|
16532
|
-
}
|
|
16533
|
-
function assertExecutionOwner(plan, executionId) {
|
|
16534
|
-
if (!plan.execution?.active || !executionId || plan.execution.id !== executionId)
|
|
16535
|
-
throw new Error(`Execution ${executionId ?? "(missing)"} does not own this plan.`);
|
|
16536
|
-
}
|
|
16537
|
-
function blocker(params) {
|
|
16538
|
-
return {
|
|
16539
|
-
reason: params.blockedReason,
|
|
16540
|
-
attempts: params.attempts,
|
|
16541
|
-
evidence: params.evidence,
|
|
16542
|
-
needsUserDecision: params.needsUserDecision
|
|
16543
|
-
};
|
|
14181
|
+
function detectMux(env = process.env) {
|
|
14182
|
+
if (env.ZELLIJ || env.ZELLIJ_SESSION_NAME)
|
|
14183
|
+
return "zellij";
|
|
14184
|
+
if (env.TMUX)
|
|
14185
|
+
return "tmux";
|
|
14186
|
+
if (env.STY)
|
|
14187
|
+
return "screen";
|
|
14188
|
+
return "none";
|
|
16544
14189
|
}
|
|
16545
|
-
function
|
|
16546
|
-
return
|
|
16547
|
-
planId: plan.id,
|
|
16548
|
-
executionId: params.executionId,
|
|
16549
|
-
phaseId,
|
|
16550
|
-
taskId,
|
|
16551
|
-
blocker: params.blockedReason,
|
|
16552
|
-
attempts: params.attempts ?? [],
|
|
16553
|
-
evidence: params.evidence ?? [],
|
|
16554
|
-
needsUserDecision: params.needsUserDecision ?? false
|
|
16555
|
-
};
|
|
14190
|
+
function detectShell(env = process.env) {
|
|
14191
|
+
return env.SHELL ? basename2(env.SHELL) : "unknown";
|
|
16556
14192
|
}
|
|
16557
|
-
function
|
|
16558
|
-
|
|
14193
|
+
async function detectVcs(cwd) {
|
|
14194
|
+
const root = (await run("git", ["-C", cwd, "rev-parse", "--show-toplevel"])).stdout.trim() || cwd;
|
|
14195
|
+
const branch = (await run("git", ["-C", root, "rev-parse", "--abbrev-ref", "HEAD"])).stdout.trim();
|
|
14196
|
+
const remote = (await run("git", ["-C", root, "remote", "get-url", "origin"])).stdout.trim();
|
|
14197
|
+
return { ...parseRemote(remote), branch, root };
|
|
16559
14198
|
}
|
|
16560
|
-
|
|
16561
|
-
|
|
16562
|
-
|
|
16563
|
-
|
|
16564
|
-
|
|
16565
|
-
]);
|
|
16566
|
-
|
|
14199
|
+
function parseRemote(remote) {
|
|
14200
|
+
const empty = { provider: "none", host: "", owner: "", repo: "" };
|
|
14201
|
+
if (!remote)
|
|
14202
|
+
return empty;
|
|
14203
|
+
const scp = remote.match(/^[^@]+@([^:]+):(.+?)(?:\.git)?$/);
|
|
14204
|
+
const url = remote.match(/^[a-z]+:\/\/(?:[^@]+@)?([^/]+)\/(.+?)(?:\.git)?$/i);
|
|
14205
|
+
const match = scp ?? url;
|
|
14206
|
+
if (!match)
|
|
14207
|
+
return empty;
|
|
14208
|
+
const host = match[1];
|
|
14209
|
+
const segments = match[2].split("/").filter(Boolean);
|
|
14210
|
+
if (segments.length < 2)
|
|
14211
|
+
return { ...empty, host };
|
|
14212
|
+
const repo = segments.at(-1) ?? "";
|
|
14213
|
+
const owner = segments.slice(0, -1).join("/");
|
|
14214
|
+
const provider = /github/i.test(host) ? "github" : /gitlab/i.test(host) ? "gitlab" : "none";
|
|
14215
|
+
return { provider, host, owner, repo };
|
|
16567
14216
|
}
|
|
16568
|
-
async function
|
|
16569
|
-
const
|
|
16570
|
-
|
|
16571
|
-
|
|
16572
|
-
|
|
14217
|
+
async function openInNewTab(command, opts) {
|
|
14218
|
+
const env = opts.env ?? process.env;
|
|
14219
|
+
const name = opts.name ?? "review";
|
|
14220
|
+
const printable = command.join(" ");
|
|
14221
|
+
const mux = detectMux(env);
|
|
14222
|
+
if (mux !== "none") {
|
|
14223
|
+
const opened = await openMuxTab(mux, command, opts.cwd, name, printable);
|
|
14224
|
+
if (opened)
|
|
14225
|
+
return opened;
|
|
14226
|
+
}
|
|
14227
|
+
if (detectIde(env) === "zed") {
|
|
14228
|
+
try {
|
|
14229
|
+
const taskName = zedReviewTaskName(command);
|
|
14230
|
+
await ensureZedReviewTask(opts.homeDir, command);
|
|
14231
|
+
return {
|
|
14232
|
+
launched: false,
|
|
14233
|
+
configured: true,
|
|
14234
|
+
via: "zed-task",
|
|
14235
|
+
command: printable,
|
|
14236
|
+
taskName,
|
|
14237
|
+
instruction: `Run the Zed task "${taskName}".`
|
|
14238
|
+
};
|
|
14239
|
+
} catch {}
|
|
14240
|
+
}
|
|
14241
|
+
return { launched: false, via: "print", command: printable };
|
|
16573
14242
|
}
|
|
16574
|
-
function
|
|
16575
|
-
return
|
|
14243
|
+
function screenWindowArgs(command, cwd, name) {
|
|
14244
|
+
return ["-X", "screen", "-t", name, "sh", "-lc", 'cd -- "$1" && shift && exec "$@"', "sh", cwd, ...command];
|
|
16576
14245
|
}
|
|
16577
|
-
function
|
|
16578
|
-
|
|
14246
|
+
async function openMuxTab(mux, command, cwd, name, printable) {
|
|
14247
|
+
if (mux === "zellij" && await findExecutable("zellij")) {
|
|
14248
|
+
const result = await run("zellij", ["action", "new-tab", "--cwd", cwd, "--name", name, "--", ...command]);
|
|
14249
|
+
if (result.code === 0)
|
|
14250
|
+
return { launched: true, via: "zellij", command: printable };
|
|
14251
|
+
const fallback = await run("zellij", ["run", "--cwd", cwd, "--name", name, "--", ...command]);
|
|
14252
|
+
if (fallback.code === 0)
|
|
14253
|
+
return { launched: true, via: "zellij-run", command: printable };
|
|
14254
|
+
}
|
|
14255
|
+
if (mux === "tmux" && await findExecutable("tmux")) {
|
|
14256
|
+
const result = await run("tmux", ["new-window", "-c", cwd, "-n", name, printable]);
|
|
14257
|
+
if (result.code === 0)
|
|
14258
|
+
return { launched: true, via: "tmux", command: printable };
|
|
14259
|
+
}
|
|
14260
|
+
if (mux === "screen" && await findExecutable("screen")) {
|
|
14261
|
+
const result = await run("screen", screenWindowArgs(command, cwd, name));
|
|
14262
|
+
if (result.code === 0)
|
|
14263
|
+
return { launched: true, via: "screen", command: printable };
|
|
14264
|
+
}
|
|
14265
|
+
return;
|
|
16579
14266
|
}
|
|
16580
14267
|
|
|
16581
|
-
// src/tools/review.ts
|
|
16582
|
-
import { existsSync as existsSync3 } from "node:fs";
|
|
16583
|
-
import { mkdir as mkdir7, readdir as readdir4, readFile as readFile14, rename as rename4, stat as stat2, unlink as unlink2, writeFile as writeFile8 } from "node:fs/promises";
|
|
16584
|
-
import { join as join21 } from "node:path";
|
|
16585
|
-
import { defineTool as defineTool4 } from "@earendil-works/pi-coding-agent";
|
|
16586
|
-
import { z as z9 } from "zod";
|
|
16587
|
-
|
|
16588
14268
|
// src/extensions/ghx.ts
|
|
16589
14269
|
function gh(args, options) {
|
|
16590
14270
|
return run("gh", args, options);
|
|
@@ -16822,26 +14502,266 @@ function GitLabVcsBackend(vcs) {
|
|
|
16822
14502
|
};
|
|
16823
14503
|
}
|
|
16824
14504
|
|
|
16825
|
-
// src/vcs/forge-backend.ts
|
|
16826
|
-
function createForgeBackend(vcs) {
|
|
16827
|
-
if (vcs.provider === "github")
|
|
16828
|
-
return GitHubVcsBackend(vcs);
|
|
16829
|
-
if (vcs.provider === "gitlab")
|
|
16830
|
-
return GitLabVcsBackend(vcs);
|
|
16831
|
-
throw new Error("No supported forge detected from the git remote. Use --local for an offline review.");
|
|
14505
|
+
// src/vcs/forge-backend.ts
|
|
14506
|
+
function createForgeBackend(vcs) {
|
|
14507
|
+
if (vcs.provider === "github")
|
|
14508
|
+
return GitHubVcsBackend(vcs);
|
|
14509
|
+
if (vcs.provider === "gitlab")
|
|
14510
|
+
return GitLabVcsBackend(vcs);
|
|
14511
|
+
throw new Error("No supported forge detected from the git remote. Use --local for an offline review.");
|
|
14512
|
+
}
|
|
14513
|
+
var createVcsBackend = createForgeBackend;
|
|
14514
|
+
// src/extensions/misex.ts
|
|
14515
|
+
import { mkdir as mkdir2, readFile as readFile4, writeFile as writeFile2 } from "node:fs/promises";
|
|
14516
|
+
import { homedir as homedir5 } from "node:os";
|
|
14517
|
+
import { basename as basename3, dirname as dirname4, join as join9 } from "node:path";
|
|
14518
|
+
var MISE_HOOK_START = "# >>> @difflab/pi mise >>>";
|
|
14519
|
+
var MISE_HOOK_END = "# <<< @difflab/pi mise <<<";
|
|
14520
|
+
var mise = {
|
|
14521
|
+
async executableCheck(name = "mise") {
|
|
14522
|
+
return findExecutable(name);
|
|
14523
|
+
},
|
|
14524
|
+
async run(args, options = {}) {
|
|
14525
|
+
return run("mise", args, options);
|
|
14526
|
+
},
|
|
14527
|
+
async install(options = {}) {
|
|
14528
|
+
const homeDir = options.homeDir ?? homedir5();
|
|
14529
|
+
const platform = options.platform ?? process.platform;
|
|
14530
|
+
if (platform === "win32")
|
|
14531
|
+
throw new Error("Automatic mise installation supports macOS and Linux only.");
|
|
14532
|
+
const installedPath = join9(homeDir, ".local", "bin", "mise");
|
|
14533
|
+
if (options.dryRun)
|
|
14534
|
+
return installedPath;
|
|
14535
|
+
await runChecked("sh", ["-c", "curl -fsSL https://mise.run | sh"]);
|
|
14536
|
+
const executable = await findExecutable(installedPath) ?? await findExecutable("mise");
|
|
14537
|
+
if (!executable)
|
|
14538
|
+
throw new Error(`mise installation completed, but ${installedPath} was not found.`);
|
|
14539
|
+
return executable;
|
|
14540
|
+
},
|
|
14541
|
+
async hookEnsure(executable, options = {}) {
|
|
14542
|
+
const homeDir = options.homeDir ?? homedir5();
|
|
14543
|
+
const hook = getShellHook(basename3(options.shell ?? process.env.SHELL ?? ""), executable, homeDir);
|
|
14544
|
+
const current = await getOptionalFile(hook.path);
|
|
14545
|
+
if (current.includes(MISE_HOOK_START))
|
|
14546
|
+
return { path: hook.path, changed: false, planned: false };
|
|
14547
|
+
if (options.dryRun)
|
|
14548
|
+
return { path: hook.path, changed: true, planned: true };
|
|
14549
|
+
const separator = current.length === 0 || current.endsWith(`
|
|
14550
|
+
`) ? "" : `
|
|
14551
|
+
`;
|
|
14552
|
+
await mkdir2(dirname4(hook.path), { recursive: true });
|
|
14553
|
+
await writeFile2(hook.path, `${current}${separator}${hook.content}`, "utf8");
|
|
14554
|
+
return { path: hook.path, changed: true, planned: false };
|
|
14555
|
+
},
|
|
14556
|
+
async toolCheckGlobal(executable, tool, minimumVersion) {
|
|
14557
|
+
const result = await run(executable, ["ls", "--global", "--installed", tool, "--json"]);
|
|
14558
|
+
return result.code === 0 && isToolInstalled(result.stdout, minimumVersion);
|
|
14559
|
+
},
|
|
14560
|
+
async toolInstallGlobal(executable, specification) {
|
|
14561
|
+
await runChecked(executable, ["use", "--global", specification]);
|
|
14562
|
+
},
|
|
14563
|
+
async toolCheckLocal(executable, tool, cwd = process.cwd()) {
|
|
14564
|
+
const result = await run(executable, ["ls", "--local", "--installed", tool, "--json"], { cwd });
|
|
14565
|
+
return result.code === 0 && isToolInstalled(result.stdout);
|
|
14566
|
+
},
|
|
14567
|
+
async toolInstallLocal(executable, specification, cwd = process.cwd()) {
|
|
14568
|
+
await runChecked(executable, ["use", "--path", cwd, specification], { cwd });
|
|
14569
|
+
},
|
|
14570
|
+
async toolUpdateAllGlobal(executable, homeDir = homedir5()) {
|
|
14571
|
+
await runChecked(executable, ["upgrade"], { cwd: homeDir });
|
|
14572
|
+
}
|
|
14573
|
+
};
|
|
14574
|
+
function getShellHook(shell, executable, homeDir) {
|
|
14575
|
+
const command = getShellQuoted(executable);
|
|
14576
|
+
switch (shell.toLowerCase()) {
|
|
14577
|
+
case "zsh":
|
|
14578
|
+
return {
|
|
14579
|
+
path: join9(homeDir, ".zshrc"),
|
|
14580
|
+
content: `${MISE_HOOK_START}
|
|
14581
|
+
eval "$(${command} activate zsh)"
|
|
14582
|
+
${MISE_HOOK_END}
|
|
14583
|
+
`
|
|
14584
|
+
};
|
|
14585
|
+
case "fish":
|
|
14586
|
+
return {
|
|
14587
|
+
path: join9(homeDir, ".config", "fish", "config.fish"),
|
|
14588
|
+
content: `${MISE_HOOK_START}
|
|
14589
|
+
${command} activate fish | source
|
|
14590
|
+
${MISE_HOOK_END}
|
|
14591
|
+
`
|
|
14592
|
+
};
|
|
14593
|
+
case "nu":
|
|
14594
|
+
case "nushell":
|
|
14595
|
+
return {
|
|
14596
|
+
path: join9(homeDir, ".config", "nushell", "config.nu"),
|
|
14597
|
+
content: `${MISE_HOOK_START}
|
|
14598
|
+
let mise_bin = ${command}
|
|
14599
|
+
let mise_path = $nu.default-config-dir | path join mise.nu
|
|
14600
|
+
^$mise_bin activate nu | save $mise_path --force
|
|
14601
|
+
use ($nu.default-config-dir | path join mise.nu)
|
|
14602
|
+
${MISE_HOOK_END}
|
|
14603
|
+
`
|
|
14604
|
+
};
|
|
14605
|
+
case "xonsh":
|
|
14606
|
+
return {
|
|
14607
|
+
path: join9(homeDir, ".xonshrc"),
|
|
14608
|
+
content: `${MISE_HOOK_START}
|
|
14609
|
+
execx($(${command} activate xonsh))
|
|
14610
|
+
${MISE_HOOK_END}
|
|
14611
|
+
`
|
|
14612
|
+
};
|
|
14613
|
+
case "elvish":
|
|
14614
|
+
return {
|
|
14615
|
+
path: join9(homeDir, ".config", "elvish", "rc.elv"),
|
|
14616
|
+
content: `${MISE_HOOK_START}
|
|
14617
|
+
var mise: = (ns [&])
|
|
14618
|
+
eval (${command} activate elvish | slurp) &ns=$mise: &on-end={|ns| set mise: = $ns }
|
|
14619
|
+
mise:activate
|
|
14620
|
+
${MISE_HOOK_END}
|
|
14621
|
+
`
|
|
14622
|
+
};
|
|
14623
|
+
case "pwsh":
|
|
14624
|
+
case "powershell":
|
|
14625
|
+
return {
|
|
14626
|
+
path: join9(homeDir, ".config", "powershell", "Microsoft.PowerShell_profile.ps1"),
|
|
14627
|
+
content: `${MISE_HOOK_START}
|
|
14628
|
+
(& ${command} activate pwsh) | Out-String | Invoke-Expression
|
|
14629
|
+
${MISE_HOOK_END}
|
|
14630
|
+
`
|
|
14631
|
+
};
|
|
14632
|
+
case "bash":
|
|
14633
|
+
default:
|
|
14634
|
+
return {
|
|
14635
|
+
path: join9(homeDir, ".bashrc"),
|
|
14636
|
+
content: `${MISE_HOOK_START}
|
|
14637
|
+
eval "$(${command} activate bash)"
|
|
14638
|
+
${MISE_HOOK_END}
|
|
14639
|
+
`
|
|
14640
|
+
};
|
|
14641
|
+
}
|
|
14642
|
+
}
|
|
14643
|
+
function isToolInstalled(output, minimumVersion) {
|
|
14644
|
+
try {
|
|
14645
|
+
const value = JSON.parse(output);
|
|
14646
|
+
if (!Array.isArray(value))
|
|
14647
|
+
return false;
|
|
14648
|
+
return value.some((entry) => {
|
|
14649
|
+
if (!entry || typeof entry !== "object" || !("installed" in entry) || entry.installed !== true)
|
|
14650
|
+
return false;
|
|
14651
|
+
if (!minimumVersion)
|
|
14652
|
+
return true;
|
|
14653
|
+
if (!("version" in entry) || typeof entry.version !== "string")
|
|
14654
|
+
return false;
|
|
14655
|
+
return isVersionAtLeast(entry.version, minimumVersion);
|
|
14656
|
+
});
|
|
14657
|
+
} catch {
|
|
14658
|
+
return false;
|
|
14659
|
+
}
|
|
14660
|
+
}
|
|
14661
|
+
function isVersionAtLeast(version, minimumVersion) {
|
|
14662
|
+
const current = version.match(/^v?(\d+)\.(\d+)\.(\d+)/)?.slice(1).map(Number);
|
|
14663
|
+
const minimum = minimumVersion.match(/^v?(\d+)\.(\d+)\.(\d+)/)?.slice(1).map(Number);
|
|
14664
|
+
if (!current || !minimum)
|
|
14665
|
+
return false;
|
|
14666
|
+
for (let index = 0;index < minimum.length; index += 1) {
|
|
14667
|
+
if (current[index] !== minimum[index])
|
|
14668
|
+
return current[index] > minimum[index];
|
|
14669
|
+
}
|
|
14670
|
+
return true;
|
|
14671
|
+
}
|
|
14672
|
+
async function getOptionalFile(path) {
|
|
14673
|
+
try {
|
|
14674
|
+
return await readFile4(path, "utf8");
|
|
14675
|
+
} catch (error) {
|
|
14676
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
14677
|
+
return "";
|
|
14678
|
+
throw error;
|
|
14679
|
+
}
|
|
14680
|
+
}
|
|
14681
|
+
function getShellQuoted(value) {
|
|
14682
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
14683
|
+
}
|
|
14684
|
+
|
|
14685
|
+
// src/gates.ts
|
|
14686
|
+
var CONVENTIONAL_COMMIT = /^(feat|fix|perf|refactor|docs|chore|test|build|ci|style|revert)(\([^)]+\))?!?: .+/;
|
|
14687
|
+
var MISE_GATES = ["format:check", "lint", "test"];
|
|
14688
|
+
function checkConventionalSubject(subject) {
|
|
14689
|
+
const trimmed = subject.trim();
|
|
14690
|
+
const ok = CONVENTIONAL_COMMIT.test(trimmed);
|
|
14691
|
+
return {
|
|
14692
|
+
name: "conventional-subject",
|
|
14693
|
+
status: ok ? "pass" : "warn",
|
|
14694
|
+
detail: ok ? trimmed : `not a conventional-commit subject: "${trimmed}"`
|
|
14695
|
+
};
|
|
16832
14696
|
}
|
|
16833
|
-
|
|
14697
|
+
async function runMiseGates(cwd) {
|
|
14698
|
+
const tasks = await discoverMiseTasks(cwd);
|
|
14699
|
+
const results = [];
|
|
14700
|
+
for (const gate of MISE_GATES) {
|
|
14701
|
+
const targets = tasks.get(gate) ?? [];
|
|
14702
|
+
if (targets.length === 0) {
|
|
14703
|
+
results.push({ name: gate, status: "skip", detail: "no mise recipe" });
|
|
14704
|
+
continue;
|
|
14705
|
+
}
|
|
14706
|
+
const invocations = targets.flatMap((target, index) => index === 0 ? [target] : [":::", target]);
|
|
14707
|
+
const result = await mise.run(["run", ...invocations], { cwd });
|
|
14708
|
+
results.push({
|
|
14709
|
+
name: gate,
|
|
14710
|
+
status: result.code === 0 ? "pass" : "fail",
|
|
14711
|
+
detail: result.code === 0 ? "clean" : (result.stderr.trim() || result.stdout.trim()).slice(-400)
|
|
14712
|
+
});
|
|
14713
|
+
}
|
|
14714
|
+
return results;
|
|
14715
|
+
}
|
|
14716
|
+
function ciGate(checksOutput) {
|
|
14717
|
+
const text = checksOutput.toLowerCase();
|
|
14718
|
+
if (!text.trim())
|
|
14719
|
+
return { name: "ci", status: "skip", detail: "no CI output" };
|
|
14720
|
+
if (/\bfail|error\b/.test(text))
|
|
14721
|
+
return { name: "ci", status: "warn", detail: "CI failing" };
|
|
14722
|
+
if (/\bpending|in progress|queued\b/.test(text))
|
|
14723
|
+
return { name: "ci", status: "warn", detail: "CI pending" };
|
|
14724
|
+
return { name: "ci", status: "pass", detail: "CI green" };
|
|
14725
|
+
}
|
|
14726
|
+
function parseMiseTasks(input) {
|
|
14727
|
+
let tasks;
|
|
14728
|
+
try {
|
|
14729
|
+
tasks = JSON.parse(input);
|
|
14730
|
+
} catch {
|
|
14731
|
+
return new Map;
|
|
14732
|
+
}
|
|
14733
|
+
if (!Array.isArray(tasks))
|
|
14734
|
+
return new Map;
|
|
14735
|
+
const found = new Map;
|
|
14736
|
+
for (const gate of MISE_GATES) {
|
|
14737
|
+
const targets = tasks.flatMap((task) => {
|
|
14738
|
+
if (typeof task.name !== "string")
|
|
14739
|
+
return [];
|
|
14740
|
+
return task.name === gate || task.name.endsWith(`:${gate}`) || task.aliases?.includes(gate) ? [task.name] : [];
|
|
14741
|
+
});
|
|
14742
|
+
if (targets.length > 0)
|
|
14743
|
+
found.set(gate, [...new Set(targets)]);
|
|
14744
|
+
}
|
|
14745
|
+
return found;
|
|
14746
|
+
}
|
|
14747
|
+
async function discoverMiseTasks(cwd) {
|
|
14748
|
+
const result = await mise.run(["tasks", "--json", "--all"], { cwd });
|
|
14749
|
+
if (result.code !== 0)
|
|
14750
|
+
return new Map;
|
|
14751
|
+
return parseMiseTasks(result.stdout);
|
|
14752
|
+
}
|
|
14753
|
+
|
|
16834
14754
|
// src/review/types.ts
|
|
16835
|
-
import { z as
|
|
16836
|
-
var severitySchema =
|
|
16837
|
-
var findingSchema =
|
|
16838
|
-
file:
|
|
16839
|
-
line:
|
|
14755
|
+
import { z as z4 } from "zod";
|
|
14756
|
+
var severitySchema = z4.enum(["BLOCKING", "CONSIDER", "NOTE"]);
|
|
14757
|
+
var findingSchema = z4.object({
|
|
14758
|
+
file: z4.string().min(1),
|
|
14759
|
+
line: z4.number().int().nonnegative(),
|
|
16840
14760
|
severity: severitySchema,
|
|
16841
|
-
body:
|
|
16842
|
-
reference:
|
|
14761
|
+
body: z4.string().min(1),
|
|
14762
|
+
reference: z4.string().optional().default("")
|
|
16843
14763
|
});
|
|
16844
|
-
var findingsSchema =
|
|
14764
|
+
var findingsSchema = z4.array(findingSchema);
|
|
16845
14765
|
function reviewSlug(input) {
|
|
16846
14766
|
return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50);
|
|
16847
14767
|
}
|
|
@@ -16905,17 +14825,17 @@ function renderCommentBody(finding) {
|
|
|
16905
14825
|
return `${prefix}${finding.body}${reference}`;
|
|
16906
14826
|
}
|
|
16907
14827
|
// src/review/review-markdown.ts
|
|
16908
|
-
import { z as
|
|
16909
|
-
var reviewThreadRecordSchema =
|
|
16910
|
-
id:
|
|
16911
|
-
file:
|
|
16912
|
-
line:
|
|
16913
|
-
body:
|
|
16914
|
-
author:
|
|
16915
|
-
resolved:
|
|
16916
|
-
addressed:
|
|
16917
|
-
question:
|
|
16918
|
-
replies:
|
|
14828
|
+
import { z as z5 } from "zod";
|
|
14829
|
+
var reviewThreadRecordSchema = z5.object({
|
|
14830
|
+
id: z5.string().min(1),
|
|
14831
|
+
file: z5.string().optional(),
|
|
14832
|
+
line: z5.number().int().positive().optional(),
|
|
14833
|
+
body: z5.string(),
|
|
14834
|
+
author: z5.string().optional(),
|
|
14835
|
+
resolved: z5.boolean(),
|
|
14836
|
+
addressed: z5.boolean().optional(),
|
|
14837
|
+
question: z5.boolean(),
|
|
14838
|
+
replies: z5.array(z5.string()).optional()
|
|
16919
14839
|
});
|
|
16920
14840
|
function renderReviewDoc(input) {
|
|
16921
14841
|
const anchored = dedupeFindings(input.findings).filter((finding) => finding.line > 0);
|
|
@@ -17002,7 +14922,7 @@ function parseThreadArtifact(content) {
|
|
|
17002
14922
|
let threads;
|
|
17003
14923
|
try {
|
|
17004
14924
|
const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
17005
|
-
threads =
|
|
14925
|
+
threads = z5.array(reviewThreadRecordSchema).parse(decoded);
|
|
17006
14926
|
} catch {
|
|
17007
14927
|
throw new Error("Cannot parse the Diffpi thread artifact payload.");
|
|
17008
14928
|
}
|
|
@@ -17375,11 +15295,52 @@ function hasGitlabDraftNotes(input) {
|
|
|
17375
15295
|
}
|
|
17376
15296
|
|
|
17377
15297
|
// src/review/local-review-backend.ts
|
|
17378
|
-
import { readFile as
|
|
15298
|
+
import { readFile as readFile6, writeFile as writeFile3 } from "node:fs/promises";
|
|
15299
|
+
|
|
15300
|
+
// src/extensions/tuicrx.ts
|
|
15301
|
+
import { readFile as readFile5, realpath as realpath2 } from "node:fs/promises";
|
|
15302
|
+
import { resolve as resolve3 } from "node:path";
|
|
15303
|
+
|
|
15304
|
+
// src/extensions/gitx.ts
|
|
15305
|
+
import { realpath } from "node:fs/promises";
|
|
15306
|
+
import { basename as basename4, isAbsolute as isAbsolute2, join as join10, resolve as resolve2 } from "node:path";
|
|
15307
|
+
async function gitToplevel(cwd) {
|
|
15308
|
+
const result = await run("git", ["-C", cwd, "rev-parse", "--show-toplevel"]);
|
|
15309
|
+
const top = result.stdout.trim();
|
|
15310
|
+
return result.code === 0 && top ? top : resolve2(cwd);
|
|
15311
|
+
}
|
|
15312
|
+
async function inspectGitRepository(cwd) {
|
|
15313
|
+
const root = await gitToplevel(cwd);
|
|
15314
|
+
const remoteResult = await run("git", ["-C", root, "remote", "get-url", "origin"]);
|
|
15315
|
+
const remote = remoteResult.code === 0 ? remoteResult.stdout.trim() : "";
|
|
15316
|
+
const commonResult = await run("git", ["-C", root, "rev-parse", "--git-common-dir"]);
|
|
15317
|
+
const common = commonResult.stdout.trim();
|
|
15318
|
+
const commonPath = commonResult.code === 0 && common ? resolve2(isAbsolute2(common) ? common : join10(root, common)) : root;
|
|
15319
|
+
const commonDir = await canonicalPath(commonPath);
|
|
15320
|
+
return {
|
|
15321
|
+
root,
|
|
15322
|
+
commonDir,
|
|
15323
|
+
...remote ? { remote } : {},
|
|
15324
|
+
name: remote ? repositoryName(remote) : basename4(resolve2(commonDir, "..")) || basename4(root),
|
|
15325
|
+
identity: remote ? `remote:${normalizeRemote(remote)}` : `git-common-dir:${commonDir}`
|
|
15326
|
+
};
|
|
15327
|
+
}
|
|
15328
|
+
function normalizeRemote(remote) {
|
|
15329
|
+
return remote.trim().replace(/\.git\/?$/i, "").replace(/\/+$/, "").toLowerCase();
|
|
15330
|
+
}
|
|
15331
|
+
function repositoryName(remote) {
|
|
15332
|
+
const normalized = remote.trim().replace(/\.git\/?$/i, "").replace(/\/+$/, "");
|
|
15333
|
+
return normalized.split(/[/\\:]/).filter(Boolean).at(-1) ?? "";
|
|
15334
|
+
}
|
|
15335
|
+
async function canonicalPath(path) {
|
|
15336
|
+
try {
|
|
15337
|
+
return await realpath(path);
|
|
15338
|
+
} catch {
|
|
15339
|
+
return resolve2(path);
|
|
15340
|
+
}
|
|
15341
|
+
}
|
|
17379
15342
|
|
|
17380
15343
|
// src/extensions/tuicrx.ts
|
|
17381
|
-
import { readFile as readFile11, realpath as realpath4 } from "node:fs/promises";
|
|
17382
|
-
import { resolve as resolve5 } from "node:path";
|
|
17383
15344
|
async function tuicrAvailable() {
|
|
17384
15345
|
return Boolean(await findExecutable("tuicr"));
|
|
17385
15346
|
}
|
|
@@ -17454,7 +15415,7 @@ async function resolvePrSession(cwd, owner, repo, number) {
|
|
|
17454
15415
|
});
|
|
17455
15416
|
}
|
|
17456
15417
|
async function findMatchingSession(sessions, cwd, branch) {
|
|
17457
|
-
const repository = await
|
|
15418
|
+
const repository = await canonicalPath2(await gitToplevel(cwd));
|
|
17458
15419
|
for (const session of sessions) {
|
|
17459
15420
|
if (session.kind !== "local")
|
|
17460
15421
|
continue;
|
|
@@ -17462,14 +15423,14 @@ async function findMatchingSession(sessions, cwd, branch) {
|
|
|
17462
15423
|
const data = await readSession(session.path);
|
|
17463
15424
|
if (data.branch_name !== branch || !data.repo_path)
|
|
17464
15425
|
continue;
|
|
17465
|
-
if (await
|
|
15426
|
+
if (await canonicalPath2(data.repo_path) === repository)
|
|
17466
15427
|
return session;
|
|
17467
15428
|
} catch {}
|
|
17468
15429
|
}
|
|
17469
15430
|
return;
|
|
17470
15431
|
}
|
|
17471
15432
|
async function readSession(path) {
|
|
17472
|
-
const content = await
|
|
15433
|
+
const content = await readFile5(path, "utf8");
|
|
17473
15434
|
try {
|
|
17474
15435
|
return JSON.parse(content);
|
|
17475
15436
|
} catch {
|
|
@@ -17549,11 +15510,11 @@ async function requireTuicr() {
|
|
|
17549
15510
|
function commentAuthor(comment) {
|
|
17550
15511
|
return comment.username ?? comment.author;
|
|
17551
15512
|
}
|
|
17552
|
-
async function
|
|
15513
|
+
async function canonicalPath2(path) {
|
|
17553
15514
|
try {
|
|
17554
|
-
return await
|
|
15515
|
+
return await realpath2(path);
|
|
17555
15516
|
} catch {
|
|
17556
|
-
return
|
|
15517
|
+
return resolve3(path);
|
|
17557
15518
|
}
|
|
17558
15519
|
}
|
|
17559
15520
|
|
|
@@ -17578,7 +15539,7 @@ function LocalReviewBackend(options) {
|
|
|
17578
15539
|
},
|
|
17579
15540
|
async listThreads() {
|
|
17580
15541
|
try {
|
|
17581
|
-
return parseThreadArtifact(await
|
|
15542
|
+
return parseThreadArtifact(await readFile6(options.artifactPath, "utf8"));
|
|
17582
15543
|
} catch (error) {
|
|
17583
15544
|
if (error.code === "ENOENT")
|
|
17584
15545
|
return [];
|
|
@@ -17586,8 +15547,8 @@ function LocalReviewBackend(options) {
|
|
|
17586
15547
|
}
|
|
17587
15548
|
},
|
|
17588
15549
|
async reply(input) {
|
|
17589
|
-
const content = await
|
|
17590
|
-
await
|
|
15550
|
+
const content = await readFile6(options.artifactPath, "utf8");
|
|
15551
|
+
await writeFile3(options.artifactPath, upsertThreadReply(content, input.threadId, input.body, input.question, input.resolve), "utf8");
|
|
17591
15552
|
},
|
|
17592
15553
|
async publish() {
|
|
17593
15554
|
throw new Error("Promote a local draft through a remote review backend before publishing it.");
|
|
@@ -17608,15 +15569,100 @@ function createLocalReviewBackend(options) {
|
|
|
17608
15569
|
}
|
|
17609
15570
|
// src/review/review-state.ts
|
|
17610
15571
|
import { createHash as createHash2 } from "node:crypto";
|
|
17611
|
-
import { readFile as
|
|
17612
|
-
import { join as
|
|
17613
|
-
import { z as
|
|
17614
|
-
|
|
17615
|
-
|
|
17616
|
-
|
|
17617
|
-
|
|
17618
|
-
|
|
17619
|
-
|
|
15572
|
+
import { readFile as readFile7, rename, writeFile as writeFile4 } from "node:fs/promises";
|
|
15573
|
+
import { join as join12 } from "node:path";
|
|
15574
|
+
import { z as z6 } from "zod";
|
|
15575
|
+
|
|
15576
|
+
// src/store.ts
|
|
15577
|
+
import { createHash } from "node:crypto";
|
|
15578
|
+
import { lstat, mkdir as mkdir3, readlink, realpath as realpath3, symlink, unlink } from "node:fs/promises";
|
|
15579
|
+
import { homedir as homedir6 } from "node:os";
|
|
15580
|
+
import { dirname as dirname5, isAbsolute as isAbsolute3, join as join11, resolve as resolve4 } from "node:path";
|
|
15581
|
+
var STORE_LINK = ".diffpi";
|
|
15582
|
+
var LEGACY_STORE_LINK = join11(".pi", "diffpi");
|
|
15583
|
+
function storeGlobalRoot(homeDir = homedir6()) {
|
|
15584
|
+
return join11(homeDir, ".difflab", "diffpi", "projects");
|
|
15585
|
+
}
|
|
15586
|
+
async function ensureStore(cwd, homeDir = homedir6()) {
|
|
15587
|
+
const repository = await inspectGitRepository(cwd);
|
|
15588
|
+
const root = repository.root;
|
|
15589
|
+
const slug = projectSlug(repository);
|
|
15590
|
+
const dest = join11(storeGlobalRoot(homeDir), slug);
|
|
15591
|
+
const link = join11(root, STORE_LINK);
|
|
15592
|
+
await mkdir3(dest, { recursive: true });
|
|
15593
|
+
try {
|
|
15594
|
+
await assertStoreLink(link, dest);
|
|
15595
|
+
} catch (error) {
|
|
15596
|
+
if (error.code !== "ENOENT")
|
|
15597
|
+
throw error;
|
|
15598
|
+
await symlink(dest, link);
|
|
15599
|
+
}
|
|
15600
|
+
await removeLegacyStoreLink(join11(root, LEGACY_STORE_LINK), dest);
|
|
15601
|
+
return { slug, root, dest, link, linked: true };
|
|
15602
|
+
}
|
|
15603
|
+
async function reviewsDir(cwd, homeDir = homedir6()) {
|
|
15604
|
+
const store = await ensureStore(cwd, homeDir);
|
|
15605
|
+
const dir = join11(store.link, "review");
|
|
15606
|
+
await mkdir3(dir, { recursive: true });
|
|
15607
|
+
return dir;
|
|
15608
|
+
}
|
|
15609
|
+
async function completedReviewsDir(cwd, homeDir = homedir6()) {
|
|
15610
|
+
const store = await ensureStore(cwd, homeDir);
|
|
15611
|
+
const dir = join11(store.link, "reviews");
|
|
15612
|
+
await mkdir3(dir, { recursive: true });
|
|
15613
|
+
return dir;
|
|
15614
|
+
}
|
|
15615
|
+
async function sessionsDir(cwd, homeDir = homedir6()) {
|
|
15616
|
+
const store = await ensureStore(cwd, homeDir);
|
|
15617
|
+
const dir = join11(store.link, "sessions");
|
|
15618
|
+
await mkdir3(dir, { recursive: true });
|
|
15619
|
+
return dir;
|
|
15620
|
+
}
|
|
15621
|
+
async function assertStoreLink(path, dest) {
|
|
15622
|
+
const entry = await lstat(path);
|
|
15623
|
+
if (!entry.isSymbolicLink())
|
|
15624
|
+
throw new Error(`${path} exists and is not a symlink.`);
|
|
15625
|
+
const target = await symlinkTarget(path);
|
|
15626
|
+
if (target !== await canonicalPath3(dest))
|
|
15627
|
+
throw new Error(`${path} points to ${target}, not ${dest}.`);
|
|
15628
|
+
}
|
|
15629
|
+
async function removeLegacyStoreLink(path, dest) {
|
|
15630
|
+
try {
|
|
15631
|
+
const entry = await lstat(path);
|
|
15632
|
+
if (!entry.isSymbolicLink())
|
|
15633
|
+
return;
|
|
15634
|
+
const target = await symlinkTarget(path);
|
|
15635
|
+
if (target === await canonicalPath3(dest))
|
|
15636
|
+
await unlink(path);
|
|
15637
|
+
} catch (error) {
|
|
15638
|
+
if (error.code !== "ENOENT")
|
|
15639
|
+
throw error;
|
|
15640
|
+
}
|
|
15641
|
+
}
|
|
15642
|
+
async function symlinkTarget(path) {
|
|
15643
|
+
const target = await readlink(path);
|
|
15644
|
+
return canonicalPath3(isAbsolute3(target) ? target : resolve4(dirname5(path), target));
|
|
15645
|
+
}
|
|
15646
|
+
async function canonicalPath3(path) {
|
|
15647
|
+
try {
|
|
15648
|
+
return await realpath3(path);
|
|
15649
|
+
} catch {
|
|
15650
|
+
return resolve4(path);
|
|
15651
|
+
}
|
|
15652
|
+
}
|
|
15653
|
+
function projectSlug(repository) {
|
|
15654
|
+
const readable = repository.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "unnamed";
|
|
15655
|
+
const digest = createHash("sha256").update(repository.identity).digest("hex").slice(0, 12);
|
|
15656
|
+
return `${readable}-${digest}`;
|
|
15657
|
+
}
|
|
15658
|
+
|
|
15659
|
+
// src/review/review-state.ts
|
|
15660
|
+
var reviewPublicationStateSchema = z6.object({
|
|
15661
|
+
target: z6.string().optional(),
|
|
15662
|
+
bodies: z6.array(z6.string()).default([]),
|
|
15663
|
+
comments: z6.array(z6.string()).default([]),
|
|
15664
|
+
replies: z6.array(z6.string()).default([]),
|
|
15665
|
+
overlayPath: z6.string().optional()
|
|
17620
15666
|
});
|
|
17621
15667
|
function reviewBodyFingerprint(body) {
|
|
17622
15668
|
return digest(body);
|
|
@@ -17632,15 +15678,15 @@ function unpublishedReviewComments(comments, knownFingerprints) {
|
|
|
17632
15678
|
}
|
|
17633
15679
|
async function loadReviewPublicationState(cwd, vcs, number, homeDir) {
|
|
17634
15680
|
const target = `${vcs.provider}:${vcs.owner}/${vcs.repo}#${number}`;
|
|
17635
|
-
const path =
|
|
15681
|
+
const path = join12(await sessionsDir(cwd, homeDir), `review-publish-${digest(target).slice(0, 16)}.json`);
|
|
17636
15682
|
try {
|
|
17637
|
-
const parsed = reviewPublicationStateSchema.parse(JSON.parse(await
|
|
15683
|
+
const parsed = reviewPublicationStateSchema.parse(JSON.parse(await readFile7(path, "utf8")));
|
|
17638
15684
|
return { path, state: { ...parsed, target } };
|
|
17639
15685
|
} catch (error) {
|
|
17640
15686
|
if (error.code === "ENOENT") {
|
|
17641
15687
|
return { path, state: { target, bodies: [], comments: [], replies: [] } };
|
|
17642
15688
|
}
|
|
17643
|
-
if (error instanceof SyntaxError || error instanceof
|
|
15689
|
+
if (error instanceof SyntaxError || error instanceof z6.ZodError) {
|
|
17644
15690
|
throw new Error(`Cannot parse review publication state: ${path}`);
|
|
17645
15691
|
}
|
|
17646
15692
|
throw error;
|
|
@@ -17648,49 +15694,85 @@ async function loadReviewPublicationState(cwd, vcs, number, homeDir) {
|
|
|
17648
15694
|
}
|
|
17649
15695
|
async function saveReviewPublicationState(path, state) {
|
|
17650
15696
|
const temp = `${path}.${process.pid}.tmp`;
|
|
17651
|
-
await
|
|
15697
|
+
await writeFile4(temp, `${JSON.stringify(state, null, 2)}
|
|
17652
15698
|
`, "utf8");
|
|
17653
|
-
await
|
|
15699
|
+
await rename(temp, path);
|
|
17654
15700
|
}
|
|
17655
15701
|
function digest(value) {
|
|
17656
15702
|
return createHash2("sha256").update(value).digest("hex");
|
|
17657
15703
|
}
|
|
15704
|
+
// src/templates.ts
|
|
15705
|
+
import { readFile as readFile8 } from "node:fs/promises";
|
|
15706
|
+
import { homedir as homedir7 } from "node:os";
|
|
15707
|
+
import { join as join13, normalize } from "node:path";
|
|
15708
|
+
async function loadTemplate(name, options = {}) {
|
|
15709
|
+
const relative = templateRelativePath(name);
|
|
15710
|
+
const userPath = join13(options.homeDir ?? homedir7(), ".difflab", "diffpi", "templates", relative);
|
|
15711
|
+
const bundledPath = join13(options.bundledDir ?? resolveBundledTemplatesDir(), relative);
|
|
15712
|
+
const user = await readOptionalFile(userPath);
|
|
15713
|
+
if (user !== undefined)
|
|
15714
|
+
return { name, path: userPath, source: "user", content: user };
|
|
15715
|
+
const bundled = await readOptionalFile(bundledPath);
|
|
15716
|
+
if (bundled !== undefined)
|
|
15717
|
+
return { name, path: bundledPath, source: "bundled", content: bundled };
|
|
15718
|
+
throw new Error(`Template "${name}" was not found at ${userPath} or ${bundledPath}.`);
|
|
15719
|
+
}
|
|
15720
|
+
function renderTemplate(content, variables) {
|
|
15721
|
+
return content.replace(/\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g, (match, key) => variables[key] ?? match);
|
|
15722
|
+
}
|
|
15723
|
+
function templateRelativePath(name) {
|
|
15724
|
+
const normalized = normalize(name.replaceAll("\\", "/")).replace(/^\.\//, "");
|
|
15725
|
+
if (!normalized || normalized.startsWith("../") || normalized.includes("/../") || normalized.startsWith("/")) {
|
|
15726
|
+
throw new Error(`Invalid template name: ${name}`);
|
|
15727
|
+
}
|
|
15728
|
+
return normalized.endsWith(".md") ? normalized : `${normalized}.md`;
|
|
15729
|
+
}
|
|
15730
|
+
async function readOptionalFile(path) {
|
|
15731
|
+
try {
|
|
15732
|
+
return await readFile8(path, "utf8");
|
|
15733
|
+
} catch (error) {
|
|
15734
|
+
if (error.code === "ENOENT")
|
|
15735
|
+
return;
|
|
15736
|
+
throw error;
|
|
15737
|
+
}
|
|
15738
|
+
}
|
|
15739
|
+
|
|
17658
15740
|
// src/tools/review.ts
|
|
17659
|
-
var contextSchema =
|
|
17660
|
-
cwd:
|
|
17661
|
-
target:
|
|
17662
|
-
workingTree:
|
|
15741
|
+
var contextSchema = z7.object({
|
|
15742
|
+
cwd: z7.string().optional(),
|
|
15743
|
+
target: z7.string().optional(),
|
|
15744
|
+
workingTree: z7.boolean().optional()
|
|
17663
15745
|
});
|
|
17664
|
-
var localSchema = contextSchema.extend({ local:
|
|
15746
|
+
var localSchema = contextSchema.extend({ local: z7.boolean().optional() });
|
|
17665
15747
|
var openSchema = localSchema.extend({
|
|
17666
|
-
title:
|
|
17667
|
-
intent:
|
|
17668
|
-
issueUrl:
|
|
17669
|
-
base:
|
|
15748
|
+
title: z7.string().optional(),
|
|
15749
|
+
intent: z7.string().optional(),
|
|
15750
|
+
issueUrl: z7.string().url().optional(),
|
|
15751
|
+
base: z7.string().optional()
|
|
17670
15752
|
});
|
|
17671
15753
|
var submitSchema = localSchema.extend({
|
|
17672
15754
|
findings: findingsSchema,
|
|
17673
|
-
overallIssues:
|
|
17674
|
-
notVerified:
|
|
17675
|
-
title:
|
|
15755
|
+
overallIssues: z7.array(z7.string()).optional(),
|
|
15756
|
+
notVerified: z7.array(z7.string()).optional(),
|
|
15757
|
+
title: z7.string().optional()
|
|
17676
15758
|
});
|
|
17677
15759
|
var addCommentSchema = localSchema.extend({
|
|
17678
|
-
body:
|
|
17679
|
-
file:
|
|
17680
|
-
line:
|
|
17681
|
-
side:
|
|
15760
|
+
body: z7.string().min(1),
|
|
15761
|
+
file: z7.string().min(1),
|
|
15762
|
+
line: z7.number().int().positive(),
|
|
15763
|
+
side: z7.enum(["LEFT", "RIGHT"]).optional()
|
|
17682
15764
|
});
|
|
17683
15765
|
var respondSchema = localSchema.extend({
|
|
17684
|
-
threadId:
|
|
17685
|
-
body:
|
|
17686
|
-
question:
|
|
17687
|
-
resolve:
|
|
15766
|
+
threadId: z7.string().min(1),
|
|
15767
|
+
body: z7.string().min(1),
|
|
15768
|
+
question: z7.boolean().optional(),
|
|
15769
|
+
resolve: z7.boolean().optional()
|
|
17688
15770
|
});
|
|
17689
15771
|
var publishSchema = localSchema.extend({
|
|
17690
|
-
status:
|
|
15772
|
+
status: z7.enum(["COMMENT", "APPROVE", "REQUEST_CHANGES", "CLOSE"]).optional()
|
|
17691
15773
|
});
|
|
17692
15774
|
var completeSchema = localSchema.extend({
|
|
17693
|
-
action:
|
|
15775
|
+
action: z7.enum(["approve", "reject", "abandon"]).optional()
|
|
17694
15776
|
});
|
|
17695
15777
|
function hasReviewDraft(comments, body) {
|
|
17696
15778
|
return comments.length > 0 || body.trim().length > 0;
|
|
@@ -17714,13 +15796,13 @@ async function workingTreeDiff(cwd) {
|
|
|
17714
15796
|
}
|
|
17715
15797
|
function createReviewTools() {
|
|
17716
15798
|
return [
|
|
17717
|
-
|
|
15799
|
+
defineTool3({
|
|
17718
15800
|
name: "review_context",
|
|
17719
15801
|
label: "review context",
|
|
17720
15802
|
description: "Orient to the target, backend, forge, environment, shared store, PR/MR, and tuicr session.",
|
|
17721
15803
|
promptSnippet: "Call review_context first",
|
|
17722
15804
|
promptGuidelines: ["Call this before every review workflow."],
|
|
17723
|
-
parameters:
|
|
15805
|
+
parameters: parameters5(localSchema),
|
|
17724
15806
|
executionMode: "parallel",
|
|
17725
15807
|
async execute(_id, input) {
|
|
17726
15808
|
const params = localSchema.parse(input);
|
|
@@ -17730,7 +15812,7 @@ function createReviewTools() {
|
|
|
17730
15812
|
const session = await resolveTuicrSession(review, Boolean(params.local || params.workingTree));
|
|
17731
15813
|
const baseRef = review.pr?.baseRef ?? (review.forge ? await review.forge.defaultBranch() : "local");
|
|
17732
15814
|
const backend = params.local || params.workingTree ? "tuicr" : review.forge?.provider ?? "unsupported";
|
|
17733
|
-
return
|
|
15815
|
+
return result([
|
|
17734
15816
|
`Backend: ${backend}`,
|
|
17735
15817
|
`Forge: ${review.vcs.provider}${review.vcs.provider === "none" ? "" : ` (${review.vcs.owner}/${review.vcs.repo})`}`,
|
|
17736
15818
|
`Branch: ${review.vcs.branch} → ${baseRef}`,
|
|
@@ -17742,13 +15824,13 @@ function createReviewTools() {
|
|
|
17742
15824
|
`), { ...review, env, store, session, baseRef, backend });
|
|
17743
15825
|
}
|
|
17744
15826
|
}),
|
|
17745
|
-
|
|
15827
|
+
defineTool3({
|
|
17746
15828
|
name: "review_new",
|
|
17747
15829
|
label: "review new",
|
|
17748
15830
|
description: "Create a new local tuicr review or a remote draft PR/MR and open it in tuicr.",
|
|
17749
15831
|
promptSnippet: "Call review_new to start",
|
|
17750
15832
|
promptGuidelines: ["Use local to select tuicr as the review backend."],
|
|
17751
|
-
parameters:
|
|
15833
|
+
parameters: parameters5(openSchema),
|
|
17752
15834
|
executionMode: "sequential",
|
|
17753
15835
|
async execute(_id, input) {
|
|
17754
15836
|
const params = openSchema.parse(input);
|
|
@@ -17757,9 +15839,9 @@ function createReviewTools() {
|
|
|
17757
15839
|
if (params.local || params.workingTree)
|
|
17758
15840
|
return launchLocalReview(review, true, params.base);
|
|
17759
15841
|
if (!review.forge)
|
|
17760
|
-
return
|
|
15842
|
+
return result(unsupportedForgeMessage());
|
|
17761
15843
|
if (review.pr)
|
|
17762
|
-
return
|
|
15844
|
+
return result(`A remote review already exists for this branch: ${review.pr.url}. Use review_edit to open it.`);
|
|
17763
15845
|
await assertRemoteBranchReady(review.cwd);
|
|
17764
15846
|
const base = params.base ?? await review.forge.defaultBranch();
|
|
17765
15847
|
const template = await loadTemplate("review/draft-pr");
|
|
@@ -17777,7 +15859,7 @@ function createReviewTools() {
|
|
|
17777
15859
|
});
|
|
17778
15860
|
const launched = await launch(review.cwd, pr.number);
|
|
17779
15861
|
const openMessage = launched.launched ? `Opened PR/MR #${pr.number} in tuicr (${launched.via}).` : launched.instruction ?? `Run: ${launched.command}`;
|
|
17780
|
-
return
|
|
15862
|
+
return result(`Draft PR/MR created from ${template.source} template: ${pr.url}
|
|
17781
15863
|
${openMessage}`, {
|
|
17782
15864
|
pr,
|
|
17783
15865
|
template,
|
|
@@ -17785,13 +15867,13 @@ ${openMessage}`, {
|
|
|
17785
15867
|
});
|
|
17786
15868
|
}
|
|
17787
15869
|
}),
|
|
17788
|
-
|
|
15870
|
+
defineTool3({
|
|
17789
15871
|
name: "review_edit",
|
|
17790
15872
|
label: "review edit",
|
|
17791
15873
|
description: "Open an existing local tuicr session or remote PR/MR in tuicr without generating findings.",
|
|
17792
15874
|
promptSnippet: "Call review_edit to continue an existing review",
|
|
17793
15875
|
promptGuidelines: ["This tool never creates review findings or a PR/MR."],
|
|
17794
|
-
parameters:
|
|
15876
|
+
parameters: parameters5(localSchema),
|
|
17795
15877
|
executionMode: "sequential",
|
|
17796
15878
|
async execute(_id, input) {
|
|
17797
15879
|
const params = localSchema.parse(input);
|
|
@@ -17799,46 +15881,46 @@ ${openMessage}`, {
|
|
|
17799
15881
|
await ensureStore(review.cwd);
|
|
17800
15882
|
if (params.local || params.workingTree) {
|
|
17801
15883
|
if (!await resolveTuicrSession(review, true))
|
|
17802
|
-
return
|
|
15884
|
+
return result("No local tuicr review exists. Use review_new with local=true to create one.");
|
|
17803
15885
|
return launchLocalReview(review, true);
|
|
17804
15886
|
}
|
|
17805
15887
|
if (!review.forge)
|
|
17806
|
-
return
|
|
15888
|
+
return result(unsupportedForgeMessage());
|
|
17807
15889
|
if (!review.pr)
|
|
17808
|
-
return
|
|
15890
|
+
return result("No remote PR/MR exists. Use review_new to create a draft review first.");
|
|
17809
15891
|
return launchLocalReview(review, false);
|
|
17810
15892
|
}
|
|
17811
15893
|
}),
|
|
17812
|
-
|
|
15894
|
+
defineTool3({
|
|
17813
15895
|
name: "review_diff",
|
|
17814
15896
|
label: "review diff",
|
|
17815
15897
|
description: "Fetch the target PR/MR diff or auto-detected local working-tree diff.",
|
|
17816
15898
|
promptSnippet: "Call review_diff for the code under review",
|
|
17817
15899
|
promptGuidelines: ["Ground findings in this diff."],
|
|
17818
|
-
parameters:
|
|
15900
|
+
parameters: parameters5(localSchema),
|
|
17819
15901
|
executionMode: "parallel",
|
|
17820
15902
|
async execute(_id, input) {
|
|
17821
15903
|
const params = localSchema.parse(input);
|
|
17822
15904
|
const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
|
|
17823
15905
|
if (params.local || params.workingTree) {
|
|
17824
15906
|
const diff = await workingTreeDiff(review.cwd);
|
|
17825
|
-
return
|
|
15907
|
+
return result(diff || "No working-tree changes.", { diff, target: "local" });
|
|
17826
15908
|
}
|
|
17827
15909
|
if (!review.forge)
|
|
17828
|
-
return
|
|
15910
|
+
return result(unsupportedForgeMessage());
|
|
17829
15911
|
if (!review.pr)
|
|
17830
|
-
return
|
|
15912
|
+
return result("No PR/MR matches this remote review target.");
|
|
17831
15913
|
const diff = await review.forge.prDiff(review.pr.number);
|
|
17832
|
-
return
|
|
15914
|
+
return result(diff || "Empty diff.", { diff, pr: review.pr });
|
|
17833
15915
|
}
|
|
17834
15916
|
}),
|
|
17835
|
-
|
|
15917
|
+
defineTool3({
|
|
17836
15918
|
name: "review_gates",
|
|
17837
15919
|
label: "review gates",
|
|
17838
15920
|
description: "Run format, lint, test, conventional-subject, and available CI checks.",
|
|
17839
15921
|
promptSnippet: "Call review_gates before submitting findings",
|
|
17840
15922
|
promptGuidelines: ["Report skipped gates as skipped."],
|
|
17841
|
-
parameters:
|
|
15923
|
+
parameters: parameters5(localSchema),
|
|
17842
15924
|
executionMode: "parallel",
|
|
17843
15925
|
async execute(_id, input) {
|
|
17844
15926
|
const params = localSchema.parse(input);
|
|
@@ -17850,19 +15932,19 @@ ${openMessage}`, {
|
|
|
17850
15932
|
gates.push(checkConventionalSubject(subject));
|
|
17851
15933
|
if (!params.local && review.pr && review.forge)
|
|
17852
15934
|
gates.push(ciGate(await review.forge.prChecks(review.pr.number)));
|
|
17853
|
-
return
|
|
15935
|
+
return result(gates.map((gate) => `- ${gate.name}: ${gate.status} — ${gate.detail}`).join(`
|
|
17854
15936
|
`), {
|
|
17855
15937
|
results: gates
|
|
17856
15938
|
});
|
|
17857
15939
|
}
|
|
17858
15940
|
}),
|
|
17859
|
-
|
|
15941
|
+
defineTool3({
|
|
17860
15942
|
name: "review_submit",
|
|
17861
15943
|
label: "review submit",
|
|
17862
15944
|
description: "Write the review artifact and stage comments in the selected local or remote backend.",
|
|
17863
15945
|
promptSnippet: "Call review_submit with the findings JSON",
|
|
17864
15946
|
promptGuidelines: ["Remote comments remain pending until review_publish."],
|
|
17865
|
-
parameters:
|
|
15947
|
+
parameters: parameters5(submitSchema),
|
|
17866
15948
|
executionMode: "sequential",
|
|
17867
15949
|
async execute(_id, input, _signal, _onUpdate, ctx) {
|
|
17868
15950
|
const params = submitSchema.parse(input);
|
|
@@ -17870,12 +15952,12 @@ ${openMessage}`, {
|
|
|
17870
15952
|
const model = modelRoute(ctx);
|
|
17871
15953
|
const useLocalBackend = Boolean(params.local || params.workingTree);
|
|
17872
15954
|
if (!useLocalBackend && !review.forge)
|
|
17873
|
-
return
|
|
15955
|
+
return result(unsupportedForgeMessage());
|
|
17874
15956
|
const findings = dedupeFindings(params.findings);
|
|
17875
15957
|
const gates = await runMiseGates(review.cwd);
|
|
17876
15958
|
const baseRef = review.pr?.baseRef ?? (review.forge ? await review.forge.defaultBranch() : "local");
|
|
17877
15959
|
const artifact = await newReviewArtifactPath(review.cwd, params.local || params.workingTree || !review.pr ? "uncommitted" : await reviewTargetId(review));
|
|
17878
|
-
await
|
|
15960
|
+
await writeFile5(artifact, renderReviewDoc({
|
|
17879
15961
|
title: params.title ?? review.pr?.title ?? review.vcs.branch,
|
|
17880
15962
|
number: useLocalBackend ? undefined : review.pr?.number,
|
|
17881
15963
|
url: useLocalBackend ? undefined : review.pr?.url,
|
|
@@ -17893,7 +15975,7 @@ ${openMessage}`, {
|
|
|
17893
15975
|
if (useLocalBackend) {
|
|
17894
15976
|
const session = await resolveTuicrSession(review, Boolean(params.local || params.workingTree));
|
|
17895
15977
|
if (!session) {
|
|
17896
|
-
return
|
|
15978
|
+
return result(`Review written: ${artifact}. Open tuicr, then call review_submit again to seed comments.`, {
|
|
17897
15979
|
artifact,
|
|
17898
15980
|
count: findings.length
|
|
17899
15981
|
});
|
|
@@ -17904,28 +15986,28 @@ ${openMessage}`, {
|
|
|
17904
15986
|
author: localReviewAuthor(model)
|
|
17905
15987
|
});
|
|
17906
15988
|
await backend.stage({ comments, body });
|
|
17907
|
-
return
|
|
15989
|
+
return result(`Local review staged in tuicr: ${artifact}`, { artifact, count: findings.length, session });
|
|
17908
15990
|
}
|
|
17909
15991
|
if (!review.pr)
|
|
17910
|
-
return
|
|
15992
|
+
return result(`Review written: ${artifact}. No PR/MR matches this remote target.`, { artifact });
|
|
17911
15993
|
const hasDraft = hasReviewDraft(comments, body);
|
|
17912
15994
|
if (hasDraft) {
|
|
17913
15995
|
await createRemoteReviewBackend(review.vcs, review.pr.number).stage({ comments, body });
|
|
17914
15996
|
}
|
|
17915
|
-
return
|
|
15997
|
+
return result(`${hasDraft ? "Pending review staged" : "Clean review recorded"} on #${review.pr.number}. Artifact: ${artifact}`, {
|
|
17916
15998
|
artifact,
|
|
17917
15999
|
pr: review.pr,
|
|
17918
16000
|
count: findings.length
|
|
17919
16001
|
});
|
|
17920
16002
|
}
|
|
17921
16003
|
}),
|
|
17922
|
-
|
|
16004
|
+
defineTool3({
|
|
17923
16005
|
name: "review_add_comment",
|
|
17924
16006
|
label: "review add comment",
|
|
17925
16007
|
description: "Add one provenance-marked comment through tuicr or the remote pending-review backend.",
|
|
17926
16008
|
promptSnippet: "Call review_add_comment for incremental comments",
|
|
17927
16009
|
promptGuidelines: ["Pass local=true when tuicr owns the draft."],
|
|
17928
|
-
parameters:
|
|
16010
|
+
parameters: parameters5(addCommentSchema),
|
|
17929
16011
|
executionMode: "sequential",
|
|
17930
16012
|
async execute(_id, input, _signal, _onUpdate, ctx) {
|
|
17931
16013
|
const params = addCommentSchema.parse(input);
|
|
@@ -17933,7 +16015,7 @@ ${openMessage}`, {
|
|
|
17933
16015
|
const model = modelRoute(ctx);
|
|
17934
16016
|
const useLocalBackend = Boolean(params.local || params.workingTree);
|
|
17935
16017
|
if (!useLocalBackend && !review.forge)
|
|
17936
|
-
return
|
|
16018
|
+
return result(unsupportedForgeMessage());
|
|
17937
16019
|
const comment = {
|
|
17938
16020
|
file: params.file,
|
|
17939
16021
|
line: params.line,
|
|
@@ -17943,28 +16025,28 @@ ${openMessage}`, {
|
|
|
17943
16025
|
if (useLocalBackend) {
|
|
17944
16026
|
const session = await resolveTuicrSession(review, Boolean(params.local || params.workingTree));
|
|
17945
16027
|
if (!session)
|
|
17946
|
-
return
|
|
16028
|
+
return result("No matching tuicr session. Open review_new or review_launch_ui first.");
|
|
17947
16029
|
const backend = createLocalReviewBackend({
|
|
17948
16030
|
session: session.path,
|
|
17949
16031
|
artifactPath: "",
|
|
17950
16032
|
author: localReviewAuthor(model)
|
|
17951
16033
|
});
|
|
17952
16034
|
await backend.stage({ comments: [comment], body: "" });
|
|
17953
|
-
return
|
|
16035
|
+
return result(`Comment added to tuicr session ${session.slug}.`, { session, comment });
|
|
17954
16036
|
}
|
|
17955
16037
|
if (!review.pr)
|
|
17956
|
-
return
|
|
16038
|
+
return result("No PR/MR matches this remote review target.");
|
|
17957
16039
|
await createRemoteReviewBackend(review.vcs, review.pr.number).stage({ comments: [comment], body: "" });
|
|
17958
|
-
return
|
|
16040
|
+
return result(`Draft comment added to #${review.pr.number}.`, { pr: review.pr, comment });
|
|
17959
16041
|
}
|
|
17960
16042
|
}),
|
|
17961
|
-
|
|
16043
|
+
defineTool3({
|
|
17962
16044
|
name: "review_comments",
|
|
17963
16045
|
label: "review comments",
|
|
17964
16046
|
description: "Pull review threads or local tuicr comments and write a target-named artifact.",
|
|
17965
16047
|
promptSnippet: "Call review_comments before addressing findings",
|
|
17966
16048
|
promptGuidelines: ["Pass local=true to prepare the local reply overlay."],
|
|
17967
|
-
parameters:
|
|
16049
|
+
parameters: parameters5(localSchema),
|
|
17968
16050
|
executionMode: "sequential",
|
|
17969
16051
|
async execute(_id, input) {
|
|
17970
16052
|
const params = localSchema.parse(input);
|
|
@@ -17974,28 +16056,28 @@ ${openMessage}`, {
|
|
|
17974
16056
|
if (params.local || params.workingTree) {
|
|
17975
16057
|
const session = await resolveTuicrSession(review, true);
|
|
17976
16058
|
if (!session)
|
|
17977
|
-
return
|
|
16059
|
+
return result("No matching tuicr session found.");
|
|
17978
16060
|
artifact = await localThreadArtifactPath(review.cwd, session.slug);
|
|
17979
16061
|
threads = await syncLocalThreadArtifact(artifact, review.pr?.title ?? review.vcs.branch, session.slug, toLocalReviewThreads(await readSession(session.path)));
|
|
17980
16062
|
} else if (review.pr && review.forge) {
|
|
17981
16063
|
threads = await createRemoteReviewBackend(review.vcs, review.pr.number).listThreads();
|
|
17982
16064
|
const target = await reviewTargetId(review);
|
|
17983
16065
|
artifact = await newReviewArtifactPath(review.cwd, target);
|
|
17984
|
-
await
|
|
16066
|
+
await writeFile5(artifact, renderThreadArtifact(review.pr.title, target, threads, { number: review.pr.number, url: review.pr.url }), "utf8");
|
|
17985
16067
|
} else {
|
|
17986
|
-
return
|
|
16068
|
+
return result(review.forge ? "No PR/MR matches this remote review target." : unsupportedForgeMessage());
|
|
17987
16069
|
}
|
|
17988
|
-
return
|
|
16070
|
+
return result(threads.map((thread) => `${thread.id} ${thread.file ?? "review"}:${thread.line ?? "-"} — ${thread.body}`).join(`
|
|
17989
16071
|
`) || "No comments.", { artifact, threads });
|
|
17990
16072
|
}
|
|
17991
16073
|
}),
|
|
17992
|
-
|
|
16074
|
+
defineTool3({
|
|
17993
16075
|
name: "review_respond",
|
|
17994
16076
|
label: "review respond",
|
|
17995
16077
|
description: "Post local question replies to tuicr and record their publish overlay, or reply to remote threads.",
|
|
17996
16078
|
promptSnippet: "Call review_respond after addressing a comment",
|
|
17997
16079
|
promptGuidelines: ["Question replies remain unresolved."],
|
|
17998
|
-
parameters:
|
|
16080
|
+
parameters: parameters5(respondSchema),
|
|
17999
16081
|
executionMode: "sequential",
|
|
18000
16082
|
async execute(_id, input, _signal, _onUpdate, ctx) {
|
|
18001
16083
|
const params = respondSchema.parse(input);
|
|
@@ -18004,10 +16086,10 @@ ${openMessage}`, {
|
|
|
18004
16086
|
if (params.local) {
|
|
18005
16087
|
const tuicrSession = await resolveTuicrSession(review, true);
|
|
18006
16088
|
if (!tuicrSession)
|
|
18007
|
-
return
|
|
16089
|
+
return result("No matching tuicr session. Open review_new or review_launch_ui first.");
|
|
18008
16090
|
const artifact = await localThreadArtifactPath(review.cwd, tuicrSession.slug);
|
|
18009
16091
|
if (!existsSync3(artifact))
|
|
18010
|
-
return
|
|
16092
|
+
return result("No local review artifact. Run review_comments with local=true first.");
|
|
18011
16093
|
const threads = await readThreadArtifact(artifact);
|
|
18012
16094
|
const known = threads.find((thread) => thread.id === params.threadId);
|
|
18013
16095
|
const question = known?.question === true || params.question === true || !known && params.question === undefined;
|
|
@@ -18029,10 +16111,10 @@ ${params.body}`, {
|
|
|
18029
16111
|
resolve: false,
|
|
18030
16112
|
question
|
|
18031
16113
|
});
|
|
18032
|
-
return
|
|
16114
|
+
return result(`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 });
|
|
18033
16115
|
}
|
|
18034
16116
|
if (!review.pr || !review.forge)
|
|
18035
|
-
return
|
|
16117
|
+
return result("No remote PR/MR for this reply.");
|
|
18036
16118
|
const remote = createRemoteReviewBackend(review.vcs, review.pr.number);
|
|
18037
16119
|
const remoteThreads = await remote.listThreads();
|
|
18038
16120
|
const known = remoteThreads.find((thread) => thread.id === params.threadId);
|
|
@@ -18044,35 +16126,35 @@ ${params.body}`, {
|
|
|
18044
16126
|
resolve,
|
|
18045
16127
|
question
|
|
18046
16128
|
});
|
|
18047
|
-
return
|
|
16129
|
+
return result(`Replied to ${params.threadId}${resolve ? " and resolved it" : " and left it open"}.`, {
|
|
18048
16130
|
pr: review.pr,
|
|
18049
16131
|
resolve
|
|
18050
16132
|
});
|
|
18051
16133
|
}
|
|
18052
16134
|
}),
|
|
18053
|
-
|
|
16135
|
+
defineTool3({
|
|
18054
16136
|
name: "review_publish",
|
|
18055
16137
|
label: "review publish",
|
|
18056
16138
|
description: "Publish pending remote draft PR/MR review comments and status, or promote a local tuicr draft before publishing.",
|
|
18057
16139
|
promptSnippet: "Call review_publish to make remote review work public",
|
|
18058
16140
|
promptGuidelines: ["Statuses are COMMENT, APPROVE, REQUEST_CHANGES, or CLOSE."],
|
|
18059
|
-
parameters:
|
|
16141
|
+
parameters: parameters5(publishSchema),
|
|
18060
16142
|
executionMode: "sequential",
|
|
18061
16143
|
async execute(_id, input, _signal, _onUpdate, ctx) {
|
|
18062
16144
|
const params = publishSchema.parse(input);
|
|
18063
16145
|
const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
|
|
18064
16146
|
if (!review.pr || !review.forge)
|
|
18065
|
-
return
|
|
16147
|
+
return result("No remote PR/MR to publish.");
|
|
18066
16148
|
return publishResolvedReview(review, params, modelRoute(ctx));
|
|
18067
16149
|
}
|
|
18068
16150
|
}),
|
|
18069
|
-
|
|
16151
|
+
defineTool3({
|
|
18070
16152
|
name: "review_complete",
|
|
18071
16153
|
label: "review complete",
|
|
18072
16154
|
description: "Approve, reject, or abandon a remote review, or archive a local review artifact.",
|
|
18073
16155
|
promptSnippet: "Call review_complete to finish a review without merging it.",
|
|
18074
16156
|
promptGuidelines: ["A local completion archives the Diffpi artifact and deletes the matching tuicr session."],
|
|
18075
|
-
parameters:
|
|
16157
|
+
parameters: parameters5(completeSchema),
|
|
18076
16158
|
executionMode: "sequential",
|
|
18077
16159
|
async execute(_id, input, _signal, _onUpdate, ctx) {
|
|
18078
16160
|
const params = completeSchema.parse(input);
|
|
@@ -18080,63 +16162,63 @@ ${params.body}`, {
|
|
|
18080
16162
|
if (params.local) {
|
|
18081
16163
|
const session = await resolveTuicrSession(review, true);
|
|
18082
16164
|
if (!session)
|
|
18083
|
-
return
|
|
16165
|
+
return result("No matching local tuicr session to close.");
|
|
18084
16166
|
const artifact = await localThreadArtifactPath(review.cwd, session.slug);
|
|
18085
16167
|
if (!existsSync3(artifact))
|
|
18086
|
-
return
|
|
16168
|
+
return result("No local review artifact. Run review_comments with local=true first.");
|
|
18087
16169
|
const archived = await uniqueRecordPath(await completedReviewsDir(review.cwd), reviewSlug(session.slug));
|
|
18088
|
-
await
|
|
16170
|
+
await rename2(artifact, archived);
|
|
18089
16171
|
await unlink2(session.path);
|
|
18090
|
-
return
|
|
16172
|
+
return result(`Archived ${archived} and deleted the tuicr session ${session.slug}.`, {
|
|
18091
16173
|
artifact: archived,
|
|
18092
16174
|
session
|
|
18093
16175
|
});
|
|
18094
16176
|
}
|
|
18095
16177
|
if (!review.pr || !review.forge)
|
|
18096
|
-
return
|
|
16178
|
+
return result("No remote PR/MR to complete.");
|
|
18097
16179
|
if (!params.action)
|
|
18098
|
-
return
|
|
16180
|
+
return result("Choose a complete action: approve, reject, or abandon.");
|
|
18099
16181
|
const status = { approve: "APPROVE", reject: "REQUEST_CHANGES", abandon: "CLOSE" }[params.action];
|
|
18100
16182
|
return publishResolvedReview(review, { ...params, status }, modelRoute(ctx));
|
|
18101
16183
|
}
|
|
18102
16184
|
}),
|
|
18103
|
-
|
|
16185
|
+
defineTool3({
|
|
18104
16186
|
name: "review_merge",
|
|
18105
16187
|
label: "review merge",
|
|
18106
16188
|
description: "Squash-merge an approved GitHub PR after checking its conventional subject.",
|
|
18107
16189
|
promptSnippet: "Call review_merge only after review_publish APPROVE",
|
|
18108
16190
|
promptGuidelines: ["This is intentionally GitHub-only until GitLab merge support is added."],
|
|
18109
|
-
parameters:
|
|
16191
|
+
parameters: parameters5(contextSchema.extend({ subject: z7.string().optional() })),
|
|
18110
16192
|
executionMode: "sequential",
|
|
18111
16193
|
async execute(_id, input) {
|
|
18112
|
-
const params = contextSchema.extend({ subject:
|
|
16194
|
+
const params = contextSchema.extend({ subject: z7.string().optional() }).parse(input);
|
|
18113
16195
|
const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
|
|
18114
16196
|
if (review.vcs.provider !== "github" || !review.forge)
|
|
18115
|
-
return
|
|
16197
|
+
return result("review_merge currently supports GitHub only.");
|
|
18116
16198
|
if (!review.pr)
|
|
18117
|
-
return
|
|
16199
|
+
return result("No open PR/MR for this branch.");
|
|
18118
16200
|
const subject = params.subject ?? review.pr.title;
|
|
18119
16201
|
const guard = checkConventionalSubject(subject);
|
|
18120
16202
|
if (guard.status !== "pass")
|
|
18121
|
-
return
|
|
16203
|
+
return result(`Merge blocked: ${guard.detail}`, { pr: review.pr, guard });
|
|
18122
16204
|
await review.forge.mergePr(review.pr.number, subject);
|
|
18123
|
-
return
|
|
16205
|
+
return result(`Merged #${review.pr.number} with subject: ${subject}.`, { pr: review.pr, guard });
|
|
18124
16206
|
}
|
|
18125
16207
|
}),
|
|
18126
|
-
|
|
16208
|
+
defineTool3({
|
|
18127
16209
|
name: "review_launch_ui",
|
|
18128
16210
|
label: "review launch UI",
|
|
18129
16211
|
description: "Open a tuicr UI in a mux tab, configure Zed, or return the command to run.",
|
|
18130
16212
|
promptSnippet: "Call review_launch_ui for the interactive tuicr TUI",
|
|
18131
16213
|
promptGuidelines: ["Show the returned command when launch cannot open a tab."],
|
|
18132
|
-
parameters:
|
|
16214
|
+
parameters: parameters5(localSchema),
|
|
18133
16215
|
executionMode: "sequential",
|
|
18134
16216
|
async execute(_id, input) {
|
|
18135
16217
|
const params = localSchema.parse(input);
|
|
18136
16218
|
const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
|
|
18137
16219
|
const workingTree = Boolean(params.local || params.workingTree);
|
|
18138
16220
|
if (!workingTree && !review.forge)
|
|
18139
|
-
return
|
|
16221
|
+
return result(unsupportedForgeMessage());
|
|
18140
16222
|
return launchLocalReview(review, workingTree);
|
|
18141
16223
|
}
|
|
18142
16224
|
})
|
|
@@ -18145,7 +16227,7 @@ ${params.body}`, {
|
|
|
18145
16227
|
async function syncLocalThreadArtifact(path, title, sessionSlug, threads) {
|
|
18146
16228
|
let previous = [];
|
|
18147
16229
|
try {
|
|
18148
|
-
previous = parseThreadArtifact(await
|
|
16230
|
+
previous = parseThreadArtifact(await readFile9(path, "utf8"));
|
|
18149
16231
|
} catch (error) {
|
|
18150
16232
|
if (error.code !== "ENOENT")
|
|
18151
16233
|
throw error;
|
|
@@ -18163,16 +16245,16 @@ async function syncLocalThreadArtifact(path, title, sessionSlug, threads) {
|
|
|
18163
16245
|
});
|
|
18164
16246
|
const removed = previous.filter((thread) => !currentIds.has(thread.id)).map((thread) => ({ ...thread, resolved: true }));
|
|
18165
16247
|
const merged = [...current, ...removed];
|
|
18166
|
-
await
|
|
16248
|
+
await writeFile5(path, renderThreadArtifact(title, sessionSlug, merged), "utf8");
|
|
18167
16249
|
return merged;
|
|
18168
16250
|
}
|
|
18169
|
-
function
|
|
18170
|
-
return
|
|
16251
|
+
function parameters5(schema) {
|
|
16252
|
+
return z7.toJSONSchema(schema, { io: "input" });
|
|
18171
16253
|
}
|
|
18172
16254
|
function resolveWorkingDirectory(params) {
|
|
18173
16255
|
return params.cwd ?? process.cwd();
|
|
18174
16256
|
}
|
|
18175
|
-
function
|
|
16257
|
+
function result(text, details = {}) {
|
|
18176
16258
|
return { content: [{ type: "text", text }], details };
|
|
18177
16259
|
}
|
|
18178
16260
|
function unsupportedForgeMessage() {
|
|
@@ -18207,7 +16289,7 @@ async function publishResolvedReview(review, params, model) {
|
|
|
18207
16289
|
const promotedBodies = promotion?.promotedBodies ?? 0;
|
|
18208
16290
|
const promotedComments = promotion?.promotedComments ?? 0;
|
|
18209
16291
|
const promotedReplies = promotion?.promotedReplies ?? 0;
|
|
18210
|
-
return
|
|
16292
|
+
return result(`Published #${review.pr.number} (${status}); promoted ${promotedBodies} review bodies, ${promotedComments} comments, and ${promotedReplies} replies.`, { pr: finalPr ?? review.pr, status, promotedBodies, promotedComments, promotedReplies });
|
|
18211
16293
|
}
|
|
18212
16294
|
async function promoteLocalReview(review, remote, model, workingTree) {
|
|
18213
16295
|
const publication = await loadReviewPublicationState(review.cwd, review.vcs, review.pr.number);
|
|
@@ -18296,7 +16378,7 @@ async function launchLocalReview(review, workingTree, requestedBase) {
|
|
|
18296
16378
|
const base = workingTree ? requestedBase ?? review.pr?.baseRef ?? (review.forge ? await review.forge.defaultBranch() : undefined) : undefined;
|
|
18297
16379
|
const launched = await launch(review.cwd, workingTree ? undefined : review.pr?.number, base);
|
|
18298
16380
|
const commandTarget = workingTree || !review.pr ? "full branch" : `PR/MR #${review.pr.number}`;
|
|
18299
|
-
return
|
|
16381
|
+
return result(launched.launched ? `Opened ${commandTarget} in tuicr (${launched.via}).` : launched.instruction ?? `Run: ${launched.command}`, { launched, pr: review.pr, target: commandTarget });
|
|
18300
16382
|
}
|
|
18301
16383
|
async function assertRemoteBranchReady(cwd) {
|
|
18302
16384
|
const dirty = await runChecked("git", ["-C", cwd, "status", "--porcelain"]);
|
|
@@ -18328,16 +16410,16 @@ async function headSha(cwd) {
|
|
|
18328
16410
|
}
|
|
18329
16411
|
async function newReviewArtifactPath(cwd, target) {
|
|
18330
16412
|
const dir = await reviewsDir(cwd);
|
|
18331
|
-
await
|
|
16413
|
+
await mkdir4(dir, { recursive: true });
|
|
18332
16414
|
return uniqueRecordPath(dir, reviewRecordName(target));
|
|
18333
16415
|
}
|
|
18334
16416
|
async function localThreadArtifactPath(cwd, sessionSlug) {
|
|
18335
16417
|
const dir = await reviewsDir(cwd);
|
|
18336
|
-
return
|
|
16418
|
+
return join14(dir, `${reviewSlug(sessionSlug) || "local-review"}.md`);
|
|
18337
16419
|
}
|
|
18338
16420
|
async function readThreadArtifact(path) {
|
|
18339
16421
|
try {
|
|
18340
|
-
return parseThreadArtifact(await
|
|
16422
|
+
return parseThreadArtifact(await readFile9(path, "utf8"));
|
|
18341
16423
|
} catch (error) {
|
|
18342
16424
|
if (error.code === "ENOENT") {
|
|
18343
16425
|
throw new Error(`Expected local reply overlay is missing: ${path}`);
|
|
@@ -18361,9 +16443,9 @@ async function findLatestThreadArtifact(cwd, review, target) {
|
|
|
18361
16443
|
return [];
|
|
18362
16444
|
return [
|
|
18363
16445
|
(async () => {
|
|
18364
|
-
const path =
|
|
18365
|
-
const info = await
|
|
18366
|
-
return { path, name, content: await
|
|
16446
|
+
const path = join14(dir, name);
|
|
16447
|
+
const info = await stat(path);
|
|
16448
|
+
return { path, name, content: await readFile9(path, "utf8"), modified: info.mtimeMs };
|
|
18367
16449
|
})()
|
|
18368
16450
|
];
|
|
18369
16451
|
}));
|
|
@@ -18377,7 +16459,7 @@ async function findLatestThreadArtifact(cwd, review, target) {
|
|
|
18377
16459
|
}
|
|
18378
16460
|
async function listReviewArtifactNames(dir) {
|
|
18379
16461
|
try {
|
|
18380
|
-
return await
|
|
16462
|
+
return await readdir2(dir);
|
|
18381
16463
|
} catch (error) {
|
|
18382
16464
|
throw new Error(`Cannot read review artifacts in ${dir}.`, { cause: error });
|
|
18383
16465
|
}
|
|
@@ -18386,10 +16468,10 @@ function deriveTitle(branch) {
|
|
|
18386
16468
|
return branch.replace(/^(feature|feat|fix|bug|chore)\//, "").replace(/^eng-\d+-/i, "").replace(/[-_]+/g, " ").replace(/^\w/, (char) => char.toUpperCase());
|
|
18387
16469
|
}
|
|
18388
16470
|
function uniqueRecordPath(dir, base) {
|
|
18389
|
-
let path =
|
|
16471
|
+
let path = join14(dir, `${base}.md`);
|
|
18390
16472
|
let count = 2;
|
|
18391
16473
|
while (existsSync3(path))
|
|
18392
|
-
path =
|
|
16474
|
+
path = join14(dir, `${base}-${count++}.md`);
|
|
18393
16475
|
return path;
|
|
18394
16476
|
}
|
|
18395
16477
|
function artifactNameMatches(name, suffix) {
|
|
@@ -18405,22 +16487,22 @@ function artifactNameMatches(name, suffix) {
|
|
|
18405
16487
|
}
|
|
18406
16488
|
|
|
18407
16489
|
// src/tools/setup.ts
|
|
18408
|
-
import { defineTool as
|
|
18409
|
-
import { z as
|
|
16490
|
+
import { defineTool as defineTool4 } from "@earendil-works/pi-coding-agent";
|
|
16491
|
+
import { z as z8 } from "zod";
|
|
18410
16492
|
|
|
18411
16493
|
// src/setup.ts
|
|
18412
16494
|
import { parseFrontmatter as parseFrontmatter3 } from "@earendil-works/pi-coding-agent";
|
|
18413
|
-
import { readdir as
|
|
16495
|
+
import { readdir as readdir3, readFile as readFile11 } from "node:fs/promises";
|
|
18414
16496
|
import { homedir as homedir10 } from "node:os";
|
|
18415
|
-
import { basename as
|
|
16497
|
+
import { basename as basename5, join as join17 } from "node:path";
|
|
18416
16498
|
|
|
18417
16499
|
// src/mcp.ts
|
|
18418
|
-
import { mkdir as
|
|
16500
|
+
import { mkdir as mkdir5, readFile as readFile10, writeFile as writeFile6 } from "node:fs/promises";
|
|
18419
16501
|
import { homedir as homedir8 } from "node:os";
|
|
18420
|
-
import { dirname as
|
|
16502
|
+
import { dirname as dirname6, join as join15 } from "node:path";
|
|
18421
16503
|
var mcp = {
|
|
18422
16504
|
globalConfigPath(homeDir = homedir8()) {
|
|
18423
|
-
return
|
|
16505
|
+
return join15(homeDir, ".config", "mcp", "mcp.json");
|
|
18424
16506
|
},
|
|
18425
16507
|
async serversEnsure(servers, options = {}) {
|
|
18426
16508
|
const path = options.path ?? mcp.globalConfigPath();
|
|
@@ -18433,8 +16515,8 @@ var mcp = {
|
|
|
18433
16515
|
const next = { ...current, mcpServers: nextServers };
|
|
18434
16516
|
const changed = JSON.stringify(current) !== JSON.stringify(next);
|
|
18435
16517
|
if (changed && !options.dryRun) {
|
|
18436
|
-
await
|
|
18437
|
-
await
|
|
16518
|
+
await mkdir5(dirname6(path), { recursive: true });
|
|
16519
|
+
await writeFile6(path, `${JSON.stringify(next, null, 2)}
|
|
18438
16520
|
`, "utf8");
|
|
18439
16521
|
}
|
|
18440
16522
|
return { path, changed, existed: currentText !== undefined, planned: changed && options.dryRun === true };
|
|
@@ -18463,7 +16545,7 @@ function getParsedConfig(content, path) {
|
|
|
18463
16545
|
}
|
|
18464
16546
|
async function getOptionalFile2(path) {
|
|
18465
16547
|
try {
|
|
18466
|
-
return await
|
|
16548
|
+
return await readFile10(path, "utf8");
|
|
18467
16549
|
} catch (error) {
|
|
18468
16550
|
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
18469
16551
|
return;
|
|
@@ -18475,9 +16557,9 @@ function isRecord(value) {
|
|
|
18475
16557
|
}
|
|
18476
16558
|
|
|
18477
16559
|
// src/pi.ts
|
|
18478
|
-
import { mkdir as
|
|
16560
|
+
import { mkdir as mkdir6, writeFile as writeFile7 } from "node:fs/promises";
|
|
18479
16561
|
import { homedir as homedir9 } from "node:os";
|
|
18480
|
-
import { dirname as
|
|
16562
|
+
import { dirname as dirname7, join as join16 } from "node:path";
|
|
18481
16563
|
var pi = {
|
|
18482
16564
|
executableCheck: findPiExecutable,
|
|
18483
16565
|
packageList: listPiPackages,
|
|
@@ -18490,19 +16572,19 @@ var pi = {
|
|
|
18490
16572
|
configEnsure: ensurePiConfig
|
|
18491
16573
|
};
|
|
18492
16574
|
async function ensurePiAgent(filename, content, agentDir = resolvePiAgentDir(), dryRun = false) {
|
|
18493
|
-
const path =
|
|
16575
|
+
const path = join16(agentDir, "agents", filename);
|
|
18494
16576
|
const currentText = await readTextIfExists(path);
|
|
18495
16577
|
const changed = currentText !== content;
|
|
18496
16578
|
if (changed && !dryRun) {
|
|
18497
|
-
await
|
|
18498
|
-
await
|
|
16579
|
+
await mkdir6(dirname7(path), { recursive: true });
|
|
16580
|
+
await writeFile7(path, content, "utf8");
|
|
18499
16581
|
}
|
|
18500
16582
|
return { path, changed, existed: currentText !== undefined, planned: changed && dryRun };
|
|
18501
16583
|
}
|
|
18502
|
-
async function checkGlobalPiSkill(name, agentDir = resolvePiAgentDir(), sharedSkillsDir =
|
|
18503
|
-
const roots = [
|
|
16584
|
+
async function checkGlobalPiSkill(name, agentDir = resolvePiAgentDir(), sharedSkillsDir = join16(homedir9(), ".agents", "skills")) {
|
|
16585
|
+
const roots = [join16(agentDir, "skills"), sharedSkillsDir];
|
|
18504
16586
|
for (const root of roots) {
|
|
18505
|
-
if (await readTextIfExists(
|
|
16587
|
+
if (await readTextIfExists(join16(root, name, "SKILL.md")) !== undefined)
|
|
18506
16588
|
return true;
|
|
18507
16589
|
}
|
|
18508
16590
|
return false;
|
|
@@ -18531,8 +16613,8 @@ async function ensurePiConfig(path, update, dryRun = false) {
|
|
|
18531
16613
|
const next = update(current);
|
|
18532
16614
|
const changed = JSON.stringify(current) !== JSON.stringify(next);
|
|
18533
16615
|
if (changed && !dryRun) {
|
|
18534
|
-
await
|
|
18535
|
-
await
|
|
16616
|
+
await mkdir6(dirname7(path), { recursive: true });
|
|
16617
|
+
await writeFile7(path, `${JSON.stringify(next, null, 2)}
|
|
18536
16618
|
`, "utf8");
|
|
18537
16619
|
}
|
|
18538
16620
|
return { path, changed, existed: currentText !== undefined, planned: changed && dryRun };
|
|
@@ -18552,7 +16634,7 @@ async function installPiPackage(executable, source) {
|
|
|
18552
16634
|
await runChecked(executable, ["install", source]);
|
|
18553
16635
|
}
|
|
18554
16636
|
function resolvePiAgentDir(homeDir = homedir9()) {
|
|
18555
|
-
return process.env.PI_CODING_AGENT_DIR ?? (process.env.XDG_CONFIG_HOME ?
|
|
16637
|
+
return process.env.PI_CODING_AGENT_DIR ?? (process.env.XDG_CONFIG_HOME ? join16(process.env.XDG_CONFIG_HOME, "pi") : join16(homeDir, ".pi", "agent"));
|
|
18556
16638
|
}
|
|
18557
16639
|
function parseJsonObject(content, path) {
|
|
18558
16640
|
if (!content?.trim())
|
|
@@ -18604,7 +16686,7 @@ var FORGE_DEPENDENCIES = {
|
|
|
18604
16686
|
};
|
|
18605
16687
|
async function ensureMise(options = {}) {
|
|
18606
16688
|
const homeDir = options.homeDir ?? homedir10();
|
|
18607
|
-
const current = await mise.executableCheck() ?? await mise.executableCheck(
|
|
16689
|
+
const current = await mise.executableCheck() ?? await mise.executableCheck(join17(homeDir, ".local", "bin", "mise"));
|
|
18608
16690
|
if (current)
|
|
18609
16691
|
return { executable: current, action: createSetupAction("mise", "ready", current) };
|
|
18610
16692
|
reportProgress(options, "Installing mise");
|
|
@@ -18653,9 +16735,9 @@ async function ensureMiseDeps(miseExecutable, options = {}) {
|
|
|
18653
16735
|
async function ensurePiPlugins(options = {}) {
|
|
18654
16736
|
const actions = await ensurePiPackages(PI_PACKAGES, options);
|
|
18655
16737
|
const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
|
|
18656
|
-
const webSearch = await pi.configEnsure(
|
|
16738
|
+
const webSearch = await pi.configEnsure(join17(agentDir, "web-search.json"), (config) => ({ ...config, workflow: "auto-summary" }), options.dryRun);
|
|
18657
16739
|
actions.push(getConfigSetupAction("web search settings", webSearch));
|
|
18658
|
-
const lsp = await pi.configEnsure(
|
|
16740
|
+
const lsp = await pi.configEnsure(join17(agentDir, "pi-lsp.json"), (config) => ({
|
|
18659
16741
|
...config,
|
|
18660
16742
|
progressive: { ...getRecord(config.progressive), enabled: true, inject: "none" }
|
|
18661
16743
|
}), options.dryRun);
|
|
@@ -18666,11 +16748,11 @@ async function ensurePiAgents(options = {}) {
|
|
|
18666
16748
|
const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
|
|
18667
16749
|
const bundledAgentsDir = options.bundledAgentsDir ?? BUNDLED_AGENTS_DIR2;
|
|
18668
16750
|
const userConfig = await loadDiffpiConfig({ homeDir: options.homeDir });
|
|
18669
|
-
const entries = (await
|
|
16751
|
+
const entries = (await readdir3(bundledAgentsDir, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.startsWith("diffpi-") && entry.name.endsWith(".md")).sort((left, right) => left.name.localeCompare(right.name));
|
|
18670
16752
|
const actions = [];
|
|
18671
16753
|
for (const entry of entries) {
|
|
18672
|
-
const id =
|
|
18673
|
-
const source = await
|
|
16754
|
+
const id = basename5(entry.name, ".md").replace(/^diffpi-/, "");
|
|
16755
|
+
const source = await readFile11(join17(bundledAgentsDir, entry.name), "utf8");
|
|
18674
16756
|
const content = materializeAgentModels(source, id, userConfig.config, options.availableModels);
|
|
18675
16757
|
const result = await pi.agentEnsure(entry.name, content, agentDir, options.dryRun);
|
|
18676
16758
|
actions.push(getConfigSetupAction(`pi agent ${id}`, result));
|
|
@@ -18679,7 +16761,7 @@ async function ensurePiAgents(options = {}) {
|
|
|
18679
16761
|
}
|
|
18680
16762
|
async function ensurePiSkills(miseExecutable, options = {}) {
|
|
18681
16763
|
const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
|
|
18682
|
-
const sharedSkillsDir =
|
|
16764
|
+
const sharedSkillsDir = join17(options.homeDir ?? homedir10(), ".agents", "skills");
|
|
18683
16765
|
const actions = [];
|
|
18684
16766
|
for (const source of PI_SKILL_SOURCES) {
|
|
18685
16767
|
const missing = [];
|
|
@@ -18760,8 +16842,7 @@ function setupRequiresRestart(actions) {
|
|
|
18760
16842
|
async function ensureZedIntegration(options = {}) {
|
|
18761
16843
|
if (options.dryRun) {
|
|
18762
16844
|
const actions = [
|
|
18763
|
-
createSetupAction("Zed review tasks", "planned", "global static runtime-resolver tasks in tasks.json")
|
|
18764
|
-
createSetupAction("Zed plan task", "planned", "pinned package CLI task in tasks.json")
|
|
16845
|
+
createSetupAction("Zed review tasks", "planned", "global static runtime-resolver tasks in tasks.json")
|
|
18765
16846
|
];
|
|
18766
16847
|
if (options.bindZedKey)
|
|
18767
16848
|
actions.push(createSetupAction("Zed review keybinding", "planned", "keymap.json"));
|
|
@@ -18771,8 +16852,6 @@ async function ensureZedIntegration(options = {}) {
|
|
|
18771
16852
|
try {
|
|
18772
16853
|
const tasks = await ensureZedReviewTask(options.homeDir);
|
|
18773
16854
|
actions.push(createSetupAction("Zed review tasks", tasks.changed ? "installed" : "ready", tasks.path));
|
|
18774
|
-
const planTask = await ensureZedPlanTask(await packageVersion2(), options.homeDir);
|
|
18775
|
-
actions.push(createSetupAction("Zed plan task", planTask.changed ? "installed" : "ready", planTask.path));
|
|
18776
16855
|
} catch (error) {
|
|
18777
16856
|
actions.push(createSetupAction("Zed review tasks", "skipped", error instanceof Error ? error.message : String(error)));
|
|
18778
16857
|
}
|
|
@@ -18874,26 +16953,19 @@ function createSetupAction(name, status, detail) {
|
|
|
18874
16953
|
function reportProgress(options, message) {
|
|
18875
16954
|
options.onProgress?.(message);
|
|
18876
16955
|
}
|
|
18877
|
-
async function packageVersion2() {
|
|
18878
|
-
const source = await readFile16(join24(resolveBundledAgentsDir(), "..", "package.json"), "utf8");
|
|
18879
|
-
const value = JSON.parse(source);
|
|
18880
|
-
if (typeof value.version !== "string")
|
|
18881
|
-
throw new Error("Cannot resolve the installed @difflab/pi version.");
|
|
18882
|
-
return value.version;
|
|
18883
|
-
}
|
|
18884
16956
|
function getRecord(value) {
|
|
18885
16957
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
18886
16958
|
}
|
|
18887
16959
|
|
|
18888
16960
|
// src/tools/setup.ts
|
|
18889
|
-
var setupParametersSchema =
|
|
18890
|
-
issueTracker:
|
|
18891
|
-
forges:
|
|
18892
|
-
forge:
|
|
18893
|
-
bindZedKey:
|
|
18894
|
-
});
|
|
18895
|
-
var setupParameters =
|
|
18896
|
-
var diffpiSetupTool =
|
|
16961
|
+
var setupParametersSchema = z8.object({
|
|
16962
|
+
issueTracker: z8.enum(["none", "linear", "jira"]).default("none").describe("Issue tracker MCP server to configure. Use none unless the user explicitly selects Linear or Jira."),
|
|
16963
|
+
forges: z8.array(z8.enum(["github", "gitlab"])).default([]).describe("Hosted VCS MCP servers to configure. Installs matching CLIs and registers each MCP server."),
|
|
16964
|
+
forge: z8.enum(["none", "github", "gitlab"]).optional().describe("Deprecated single-host alias. Prefer forges for one or more VCS MCP servers."),
|
|
16965
|
+
bindZedKey: z8.boolean().default(false).describe("Opt in to a Zed keybinding for the tuicr review task.")
|
|
16966
|
+
});
|
|
16967
|
+
var setupParameters = z8.toJSONSchema(setupParametersSchema, { io: "input" });
|
|
16968
|
+
var diffpiSetupTool = defineTool4({
|
|
18897
16969
|
name: "diffpi_setup",
|
|
18898
16970
|
label: "diffpi setup",
|
|
18899
16971
|
description: "Install or repair the @difflab/pi environment. This mutates user-level tool installations and configuration files.",
|
|
@@ -18922,7 +16994,7 @@ var diffpiSetupTool = defineTool5({
|
|
|
18922
16994
|
return formatResult(result, "Setup complete.");
|
|
18923
16995
|
}
|
|
18924
16996
|
});
|
|
18925
|
-
var diffpiValidateTool =
|
|
16997
|
+
var diffpiValidateTool = defineTool4({
|
|
18926
16998
|
name: "diffpi_validate",
|
|
18927
16999
|
label: "diffpi validate",
|
|
18928
17000
|
description: "Inspect the @difflab/pi environment without installing software or changing configuration files.",
|
|
@@ -18964,20 +17036,20 @@ ${lines.join(`
|
|
|
18964
17036
|
}
|
|
18965
17037
|
|
|
18966
17038
|
// src/tools/templates.ts
|
|
18967
|
-
import { defineTool as
|
|
18968
|
-
import { z as
|
|
18969
|
-
var templateSchema =
|
|
18970
|
-
name:
|
|
18971
|
-
variables:
|
|
18972
|
-
homeDir:
|
|
18973
|
-
});
|
|
18974
|
-
var diffpiTemplateTool =
|
|
17039
|
+
import { defineTool as defineTool5 } from "@earendil-works/pi-coding-agent";
|
|
17040
|
+
import { z as z9 } from "zod";
|
|
17041
|
+
var templateSchema = z9.object({
|
|
17042
|
+
name: z9.string().min(1),
|
|
17043
|
+
variables: z9.record(z9.string(), z9.string()).optional(),
|
|
17044
|
+
homeDir: z9.string().optional()
|
|
17045
|
+
});
|
|
17046
|
+
var diffpiTemplateTool = defineTool5({
|
|
18975
17047
|
name: "diffpi_template",
|
|
18976
17048
|
label: "diffpi template",
|
|
18977
17049
|
description: "Load a bundled Diffpi template or a user override and render named variables.",
|
|
18978
17050
|
promptSnippet: "Use diffpi_template for package workflow templates",
|
|
18979
17051
|
promptGuidelines: ["User overrides live under ~/.difflab/diffpi/templates."],
|
|
18980
|
-
parameters:
|
|
17052
|
+
parameters: z9.toJSONSchema(templateSchema, { io: "input" }),
|
|
18981
17053
|
executionMode: "parallel",
|
|
18982
17054
|
async execute(_id, input) {
|
|
18983
17055
|
const params = templateSchema.parse(input);
|
|
@@ -18998,8 +17070,7 @@ function createPiTools(pi, modes) {
|
|
|
18998
17070
|
createDiffpiReloadTool(pi),
|
|
18999
17071
|
diffpiTemplateTool,
|
|
19000
17072
|
...createModeTools(modes),
|
|
19001
|
-
...createReviewTools()
|
|
19002
|
-
...createPlanTools(pi, modes)
|
|
17073
|
+
...createReviewTools()
|
|
19003
17074
|
];
|
|
19004
17075
|
}
|
|
19005
17076
|
|