@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.
Files changed (61) hide show
  1. package/README.md +2 -22
  2. package/agents/diffpi-orchestrator.md +0 -10
  3. package/agents/diffpi-worker.md +1 -9
  4. package/dist/commands/index.d.ts +0 -1
  5. package/dist/commands/index.d.ts.map +1 -1
  6. package/dist/commands/review.d.ts.map +1 -1
  7. package/dist/environment.d.ts.map +1 -1
  8. package/dist/extensions/index.js +844 -2773
  9. package/dist/extensions/zedx.d.ts +0 -2
  10. package/dist/extensions/zedx.d.ts.map +1 -1
  11. package/dist/index.d.ts +2 -5
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +128 -1140
  14. package/dist/setup.d.ts.map +1 -1
  15. package/dist/store.d.ts +0 -1
  16. package/dist/store.d.ts.map +1 -1
  17. package/dist/tools/index.d.ts +0 -1
  18. package/dist/tools/index.d.ts.map +1 -1
  19. package/dist/tools/index.js +678 -2400
  20. package/package.json +1 -4
  21. package/agents/diffpi-planner.md +0 -30
  22. package/dist/cli/plan.d.ts +0 -7
  23. package/dist/cli/plan.d.ts.map +0 -1
  24. package/dist/cli.d.ts +0 -3
  25. package/dist/cli.d.ts.map +0 -1
  26. package/dist/cli.js +0 -729
  27. package/dist/commands/background.d.ts +0 -39
  28. package/dist/commands/background.d.ts.map +0 -1
  29. package/dist/commands/plan.d.ts +0 -21
  30. package/dist/commands/plan.d.ts.map +0 -1
  31. package/dist/plan/annotations.d.ts +0 -25
  32. package/dist/plan/annotations.d.ts.map +0 -1
  33. package/dist/plan/escalation.d.ts +0 -4
  34. package/dist/plan/escalation.d.ts.map +0 -1
  35. package/dist/plan/execution.d.ts +0 -19
  36. package/dist/plan/execution.d.ts.map +0 -1
  37. package/dist/plan/index.d.ts +0 -15
  38. package/dist/plan/index.d.ts.map +0 -1
  39. package/dist/plan/lock.d.ts +0 -7
  40. package/dist/plan/lock.d.ts.map +0 -1
  41. package/dist/plan/log.d.ts +0 -5
  42. package/dist/plan/log.d.ts.map +0 -1
  43. package/dist/plan/markdown.d.ts +0 -6
  44. package/dist/plan/markdown.d.ts.map +0 -1
  45. package/dist/plan/store.d.ts +0 -34
  46. package/dist/plan/store.d.ts.map +0 -1
  47. package/dist/plan/transitions.d.ts +0 -9
  48. package/dist/plan/transitions.d.ts.map +0 -1
  49. package/dist/plan/types.d.ts +0 -146
  50. package/dist/plan/types.d.ts.map +0 -1
  51. package/dist/tools/plan.d.ts +0 -6
  52. package/dist/tools/plan.d.ts.map +0 -1
  53. package/skills/plan/SKILL.md +0 -13
  54. package/skills/plan/references/workflows/annotate.md +0 -6
  55. package/skills/plan/references/workflows/finalize.md +0 -7
  56. package/skills/plan/references/workflows/go.md +0 -7
  57. package/skills/plan/references/workflows/help.md +0 -13
  58. package/skills/plan/references/workflows/init.md +0 -6
  59. package/skills/plan/references/workflows/new.md +0 -7
  60. package/skills/plan/references/workflows/update.md +0 -7
  61. package/templates/plan/PLAN.md +0 -38
@@ -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 join11 } from "node:path";
13745
+ import { join as join6 } from "node:path";
14213
13746
  var REVIEWER_VERBS = new Set(["address", "auto", "launch"]);
14214
- var ORCHESTRATOR_AGENT_PATH = join11(resolveBundledAgentsDir(), "diffpi-orchestrator.md");
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
- await launchBackgroundPi(pi, {
14227
- name: `Review ${verb}`,
14228
- agentPath: ORCHESTRATOR_AGENT_PATH,
14229
- model: "openai-codex/gpt-5.6-luna",
14230
- thinking: "medium",
14231
- cwd: ctx.cwd,
14232
- prompt: reviewPrompt(invocation, "orchestrator")
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/plan.ts
14393
- import { join as join19 } from "node:path";
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 z5 } from "zod";
13950
+ import { z as z7 } from "zod";
14396
13951
 
14397
13952
  // src/environment.ts
14398
- import { readFile as readFile4 } from "node:fs/promises";
14399
- import { basename as basename3, join as join13 } from "node:path";
13953
+ import { basename as basename2 } from "node:path";
14400
13954
 
14401
- // src/extensions/zedx.ts
14402
- import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
14403
- import { homedir as homedir5 } from "node:os";
14404
- import { dirname as dirname4, join as join12 } from "node:path";
14405
- var ZED_LOCAL_REVIEW_TASK_NAME = "diffpi: tuicr local review";
14406
- var ZED_PR_REVIEW_TASK_NAME = "diffpi: tuicr PR review";
14407
- var ZED_PLAN_ANNOTATE_TASK_NAME = "diffpi: annotate plan";
14408
- var LEGACY_ZED_REVIEW_TASK_NAME = "diffpi: tuicr review";
14409
- var REVIEW_KEYBINDING = "cmd-alt-r";
14410
- function zedTasksPath(homeDir = homedir5()) {
14411
- return join12(homeDir, ".config", "zed", "tasks.json");
14412
- }
14413
- function zedKeymapPath(homeDir = homedir5()) {
14414
- return join12(homeDir, ".config", "zed", "keymap.json");
14415
- }
14416
- async function ensureZedReviewTask(homeDir = homedir5(), _command = ["tuicr", "-w", "-r", "main..HEAD"]) {
14417
- const path = zedTasksPath(homeDir);
14418
- const currentText = await readOptional(path);
14419
- const tasks = parseJsonArray(currentText, path);
14420
- const migrated = tasks.filter((task) => task.label !== LEGACY_ZED_REVIEW_TASK_NAME);
14421
- const next = [...migrated];
14422
- for (const task of [localReviewTask(), prReviewTask()]) {
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 task = {
14442
- label: ZED_PLAN_ANNOTATE_TASK_NAME,
14443
- command: "npx",
14444
- args: ["--yes", `@difflab/pi@${packageVersion}`, "plan", "annotate", "--cwd", "$ZED_WORKTREE_ROOT"],
14445
- cwd: "$ZED_WORKTREE_ROOT",
14446
- use_new_terminal: true,
14447
- reveal: "always",
14448
- reveal_target: "center"
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 = homedir5()) {
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 mkdir3(dirname4(path), { recursive: true });
14565
- await writeFile2(path, `${JSON.stringify(value, null, 2)}
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
- function assertAuthoringRevision(plan, expected) {
16486
- if (plan.revision !== expected)
16487
- throw new Error(`Stale plan revision: expected ${expected}, found ${plan.revision}.`);
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 assertAuthorable(plan, blockedPhaseId) {
16490
- if (plan.status === "completed")
16491
- throw new Error("Completed plans cannot be structurally changed.");
16492
- if (!plan.execution?.active)
16493
- return;
16494
- if (!blockedPhaseId)
16495
- throw new Error("An active execution owns this plan.");
16496
- const phase = getPhase(plan, blockedPhaseId);
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 escalationFor(plan, phaseId, taskId, params) {
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 heartbeat(plan) {
16558
- return plan.execution?.active ? { ...plan, execution: { ...plan.execution, heartbeatAt: new Date().toISOString() } } : plan;
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
- async function verifyCommit(cwd, requestedSha) {
16561
- await runChecked("git", ["-C", cwd, "cat-file", "-e", `${requestedSha}^{commit}`]);
16562
- const [sha, subject] = await Promise.all([
16563
- runChecked("git", ["-C", cwd, "rev-parse", "HEAD"]),
16564
- runChecked("git", ["-C", cwd, "log", "-1", "--format=%s"])
16565
- ]);
16566
- return { sha: sha.stdout.trim(), subject: subject.stdout.trim() };
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 currentBranch2(cwd) {
16569
- const result = await run("git", ["-C", cwd, "branch", "--show-current"]);
16570
- if (result.code !== 0 || !result.stdout.trim())
16571
- throw new Error(`Cannot determine current branch in ${cwd}.`);
16572
- return result.stdout.trim();
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 parameters5(schema) {
16575
- return z5.toJSONSchema(schema, { io: "input" });
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 result(text, details = {}) {
16578
- return { content: [{ type: "text", text }], details };
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
- var createVcsBackend = createForgeBackend;
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 z6 } from "zod";
16836
- var severitySchema = z6.enum(["BLOCKING", "CONSIDER", "NOTE"]);
16837
- var findingSchema = z6.object({
16838
- file: z6.string().min(1),
16839
- line: z6.number().int().nonnegative(),
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: z6.string().min(1),
16842
- reference: z6.string().optional().default("")
14761
+ body: z4.string().min(1),
14762
+ reference: z4.string().optional().default("")
16843
14763
  });
16844
- var findingsSchema = z6.array(findingSchema);
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 z7 } from "zod";
16909
- var reviewThreadRecordSchema = z7.object({
16910
- id: z7.string().min(1),
16911
- file: z7.string().optional(),
16912
- line: z7.number().int().positive().optional(),
16913
- body: z7.string(),
16914
- author: z7.string().optional(),
16915
- resolved: z7.boolean(),
16916
- addressed: z7.boolean().optional(),
16917
- question: z7.boolean(),
16918
- replies: z7.array(z7.string()).optional()
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 = z7.array(reviewThreadRecordSchema).parse(decoded);
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 readFile12, writeFile as writeFile6 } from "node:fs/promises";
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 canonicalPath3(await gitToplevel(cwd));
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 canonicalPath3(data.repo_path) === repository)
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 readFile11(path, "utf8");
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 canonicalPath3(path) {
15513
+ async function canonicalPath2(path) {
17553
15514
  try {
17554
- return await realpath4(path);
15515
+ return await realpath2(path);
17555
15516
  } catch {
17556
- return resolve5(path);
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 readFile12(options.artifactPath, "utf8"));
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 readFile12(options.artifactPath, "utf8");
17590
- await writeFile6(options.artifactPath, upsertThreadReply(content, input.threadId, input.body, input.question, input.resolve), "utf8");
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 readFile13, rename as rename3, writeFile as writeFile7 } from "node:fs/promises";
17612
- import { join as join20 } from "node:path";
17613
- import { z as z8 } from "zod";
17614
- var reviewPublicationStateSchema = z8.object({
17615
- target: z8.string().optional(),
17616
- bodies: z8.array(z8.string()).default([]),
17617
- comments: z8.array(z8.string()).default([]),
17618
- replies: z8.array(z8.string()).default([]),
17619
- overlayPath: z8.string().optional()
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 = join20(await sessionsDir(cwd, homeDir), `review-publish-${digest(target).slice(0, 16)}.json`);
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 readFile13(path, "utf8")));
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 z8.ZodError) {
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 writeFile7(temp, `${JSON.stringify(state, null, 2)}
15697
+ await writeFile4(temp, `${JSON.stringify(state, null, 2)}
17652
15698
  `, "utf8");
17653
- await rename3(temp, path);
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 = z9.object({
17660
- cwd: z9.string().optional(),
17661
- target: z9.string().optional(),
17662
- workingTree: z9.boolean().optional()
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: z9.boolean().optional() });
15746
+ var localSchema = contextSchema.extend({ local: z7.boolean().optional() });
17665
15747
  var openSchema = localSchema.extend({
17666
- title: z9.string().optional(),
17667
- intent: z9.string().optional(),
17668
- issueUrl: z9.string().url().optional(),
17669
- base: z9.string().optional()
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: z9.array(z9.string()).optional(),
17674
- notVerified: z9.array(z9.string()).optional(),
17675
- title: z9.string().optional()
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: z9.string().min(1),
17679
- file: z9.string().min(1),
17680
- line: z9.number().int().positive(),
17681
- side: z9.enum(["LEFT", "RIGHT"]).optional()
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: z9.string().min(1),
17685
- body: z9.string().min(1),
17686
- question: z9.boolean().optional(),
17687
- resolve: z9.boolean().optional()
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: z9.enum(["COMMENT", "APPROVE", "REQUEST_CHANGES", "CLOSE"]).optional()
15772
+ status: z7.enum(["COMMENT", "APPROVE", "REQUEST_CHANGES", "CLOSE"]).optional()
17691
15773
  });
17692
15774
  var completeSchema = localSchema.extend({
17693
- action: z9.enum(["approve", "reject", "abandon"]).optional()
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
- defineTool4({
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: parameters6(localSchema),
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 result2([
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
- defineTool4({
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: parameters6(openSchema),
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 result2(unsupportedForgeMessage());
15842
+ return result(unsupportedForgeMessage());
17761
15843
  if (review.pr)
17762
- return result2(`A remote review already exists for this branch: ${review.pr.url}. Use review_edit to open it.`);
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 result2(`Draft PR/MR created from ${template.source} template: ${pr.url}
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
- defineTool4({
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: parameters6(localSchema),
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 result2("No local tuicr review exists. Use review_new with local=true to create one.");
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 result2(unsupportedForgeMessage());
15888
+ return result(unsupportedForgeMessage());
17807
15889
  if (!review.pr)
17808
- return result2("No remote PR/MR exists. Use review_new to create a draft review first.");
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
- defineTool4({
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: parameters6(localSchema),
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 result2(diff || "No working-tree changes.", { diff, target: "local" });
15907
+ return result(diff || "No working-tree changes.", { diff, target: "local" });
17826
15908
  }
17827
15909
  if (!review.forge)
17828
- return result2(unsupportedForgeMessage());
15910
+ return result(unsupportedForgeMessage());
17829
15911
  if (!review.pr)
17830
- return result2("No PR/MR matches this remote review target.");
15912
+ return result("No PR/MR matches this remote review target.");
17831
15913
  const diff = await review.forge.prDiff(review.pr.number);
17832
- return result2(diff || "Empty diff.", { diff, pr: review.pr });
15914
+ return result(diff || "Empty diff.", { diff, pr: review.pr });
17833
15915
  }
17834
15916
  }),
17835
- defineTool4({
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: parameters6(localSchema),
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 result2(gates.map((gate) => `- ${gate.name}: ${gate.status} — ${gate.detail}`).join(`
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
- defineTool4({
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: parameters6(submitSchema),
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 result2(unsupportedForgeMessage());
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 writeFile8(artifact, renderReviewDoc({
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 result2(`Review written: ${artifact}. Open tuicr, then call review_submit again to seed comments.`, {
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 result2(`Local review staged in tuicr: ${artifact}`, { artifact, count: findings.length, session });
15989
+ return result(`Local review staged in tuicr: ${artifact}`, { artifact, count: findings.length, session });
17908
15990
  }
17909
15991
  if (!review.pr)
17910
- return result2(`Review written: ${artifact}. No PR/MR matches this remote target.`, { artifact });
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 result2(`${hasDraft ? "Pending review staged" : "Clean review recorded"} on #${review.pr.number}. Artifact: ${artifact}`, {
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
- defineTool4({
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: parameters6(addCommentSchema),
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 result2(unsupportedForgeMessage());
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 result2("No matching tuicr session. Open review_new or review_launch_ui first.");
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 result2(`Comment added to tuicr session ${session.slug}.`, { session, comment });
16035
+ return result(`Comment added to tuicr session ${session.slug}.`, { session, comment });
17954
16036
  }
17955
16037
  if (!review.pr)
17956
- return result2("No PR/MR matches this remote review target.");
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 result2(`Draft comment added to #${review.pr.number}.`, { pr: review.pr, comment });
16040
+ return result(`Draft comment added to #${review.pr.number}.`, { pr: review.pr, comment });
17959
16041
  }
17960
16042
  }),
17961
- defineTool4({
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: parameters6(localSchema),
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 result2("No matching tuicr session found.");
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 writeFile8(artifact, renderThreadArtifact(review.pr.title, target, threads, { number: review.pr.number, url: review.pr.url }), "utf8");
16066
+ await writeFile5(artifact, renderThreadArtifact(review.pr.title, target, threads, { number: review.pr.number, url: review.pr.url }), "utf8");
17985
16067
  } else {
17986
- return result2(review.forge ? "No PR/MR matches this remote review target." : unsupportedForgeMessage());
16068
+ return result(review.forge ? "No PR/MR matches this remote review target." : unsupportedForgeMessage());
17987
16069
  }
17988
- return result2(threads.map((thread) => `${thread.id} ${thread.file ?? "review"}:${thread.line ?? "-"} — ${thread.body}`).join(`
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
- defineTool4({
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: parameters6(respondSchema),
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 result2("No matching tuicr session. Open review_new or review_launch_ui first.");
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 result2("No local review artifact. Run review_comments with local=true first.");
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 result2(`Local ${question ? "answer" : "response"} posted to tuicr and recorded in ${artifact}${awaitUserDeletion ? "; delete the source comment in tuicr to resolve it" : ""}.`, { artifact, question, awaitUserDeletion, session: tuicrSession });
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 result2("No remote PR/MR for this reply.");
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 result2(`Replied to ${params.threadId}${resolve ? " and resolved it" : " and left it open"}.`, {
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
- defineTool4({
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: parameters6(publishSchema),
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 result2("No remote PR/MR to publish.");
16147
+ return result("No remote PR/MR to publish.");
18066
16148
  return publishResolvedReview(review, params, modelRoute(ctx));
18067
16149
  }
18068
16150
  }),
18069
- defineTool4({
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: parameters6(completeSchema),
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 result2("No matching local tuicr session to close.");
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 result2("No local review artifact. Run review_comments with local=true first.");
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 rename4(artifact, archived);
16170
+ await rename2(artifact, archived);
18089
16171
  await unlink2(session.path);
18090
- return result2(`Archived ${archived} and deleted the tuicr session ${session.slug}.`, {
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 result2("No remote PR/MR to complete.");
16178
+ return result("No remote PR/MR to complete.");
18097
16179
  if (!params.action)
18098
- return result2("Choose a complete action: approve, reject, or abandon.");
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
- defineTool4({
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: parameters6(contextSchema.extend({ subject: z9.string().optional() })),
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: z9.string().optional() }).parse(input);
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 result2("review_merge currently supports GitHub only.");
16197
+ return result("review_merge currently supports GitHub only.");
18116
16198
  if (!review.pr)
18117
- return result2("No open PR/MR for this branch.");
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 result2(`Merge blocked: ${guard.detail}`, { pr: review.pr, guard });
16203
+ return result(`Merge blocked: ${guard.detail}`, { pr: review.pr, guard });
18122
16204
  await review.forge.mergePr(review.pr.number, subject);
18123
- return result2(`Merged #${review.pr.number} with subject: ${subject}.`, { pr: review.pr, guard });
16205
+ return result(`Merged #${review.pr.number} with subject: ${subject}.`, { pr: review.pr, guard });
18124
16206
  }
18125
16207
  }),
18126
- defineTool4({
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: parameters6(localSchema),
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 result2(unsupportedForgeMessage());
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 readFile14(path, "utf8"));
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 writeFile8(path, renderThreadArtifact(title, sessionSlug, merged), "utf8");
16248
+ await writeFile5(path, renderThreadArtifact(title, sessionSlug, merged), "utf8");
18167
16249
  return merged;
18168
16250
  }
18169
- function parameters6(schema) {
18170
- return z9.toJSONSchema(schema, { io: "input" });
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 result2(text, details = {}) {
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 result2(`Published #${review.pr.number} (${status}); promoted ${promotedBodies} review bodies, ${promotedComments} comments, and ${promotedReplies} replies.`, { pr: finalPr ?? review.pr, status, promotedBodies, promotedComments, promotedReplies });
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 result2(launched.launched ? `Opened ${commandTarget} in tuicr (${launched.via}).` : launched.instruction ?? `Run: ${launched.command}`, { launched, pr: review.pr, target: commandTarget });
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 mkdir7(dir, { recursive: true });
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 join21(dir, `${reviewSlug(sessionSlug) || "local-review"}.md`);
16418
+ return join14(dir, `${reviewSlug(sessionSlug) || "local-review"}.md`);
18337
16419
  }
18338
16420
  async function readThreadArtifact(path) {
18339
16421
  try {
18340
- return parseThreadArtifact(await readFile14(path, "utf8"));
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 = join21(dir, name);
18365
- const info = await stat2(path);
18366
- return { path, name, content: await readFile14(path, "utf8"), modified: info.mtimeMs };
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 readdir4(dir);
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 = join21(dir, `${base}.md`);
16471
+ let path = join14(dir, `${base}.md`);
18390
16472
  let count = 2;
18391
16473
  while (existsSync3(path))
18392
- path = join21(dir, `${base}-${count++}.md`);
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 defineTool5 } from "@earendil-works/pi-coding-agent";
18409
- import { z as z10 } from "zod";
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 readdir5, readFile as readFile16 } from "node:fs/promises";
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 basename7, join as join24 } from "node:path";
16497
+ import { basename as basename5, join as join17 } from "node:path";
18416
16498
 
18417
16499
  // src/mcp.ts
18418
- import { mkdir as mkdir8, readFile as readFile15, writeFile as writeFile9 } from "node:fs/promises";
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 dirname8, join as join22 } from "node:path";
16502
+ import { dirname as dirname6, join as join15 } from "node:path";
18421
16503
  var mcp = {
18422
16504
  globalConfigPath(homeDir = homedir8()) {
18423
- return join22(homeDir, ".config", "mcp", "mcp.json");
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 mkdir8(dirname8(path), { recursive: true });
18437
- await writeFile9(path, `${JSON.stringify(next, null, 2)}
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 readFile15(path, "utf8");
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 mkdir9, writeFile as writeFile10 } from "node:fs/promises";
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 dirname9, join as join23 } from "node:path";
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 = join23(agentDir, "agents", filename);
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 mkdir9(dirname9(path), { recursive: true });
18498
- await writeFile10(path, content, "utf8");
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 = join23(homedir9(), ".agents", "skills")) {
18503
- const roots = [join23(agentDir, "skills"), sharedSkillsDir];
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(join23(root, name, "SKILL.md")) !== undefined)
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 mkdir9(dirname9(path), { recursive: true });
18535
- await writeFile10(path, `${JSON.stringify(next, null, 2)}
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 ? join23(process.env.XDG_CONFIG_HOME, "pi") : join23(homeDir, ".pi", "agent"));
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(join24(homeDir, ".local", "bin", "mise"));
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(join24(agentDir, "web-search.json"), (config) => ({ ...config, workflow: "auto-summary" }), options.dryRun);
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(join24(agentDir, "pi-lsp.json"), (config) => ({
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 readdir5(bundledAgentsDir, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.startsWith("diffpi-") && entry.name.endsWith(".md")).sort((left, right) => left.name.localeCompare(right.name));
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 = basename7(entry.name, ".md").replace(/^diffpi-/, "");
18673
- const source = await readFile16(join24(bundledAgentsDir, entry.name), "utf8");
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 = join24(options.homeDir ?? homedir10(), ".agents", "skills");
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 = z10.object({
18890
- issueTracker: z10.enum(["none", "linear", "jira"]).default("none").describe("Issue tracker MCP server to configure. Use none unless the user explicitly selects Linear or Jira."),
18891
- forges: z10.array(z10.enum(["github", "gitlab"])).default([]).describe("Hosted VCS MCP servers to configure. Installs matching CLIs and registers each MCP server."),
18892
- forge: z10.enum(["none", "github", "gitlab"]).optional().describe("Deprecated single-host alias. Prefer forges for one or more VCS MCP servers."),
18893
- bindZedKey: z10.boolean().default(false).describe("Opt in to a Zed keybinding for the tuicr review task.")
18894
- });
18895
- var setupParameters = z10.toJSONSchema(setupParametersSchema, { io: "input" });
18896
- var diffpiSetupTool = defineTool5({
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 = defineTool5({
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 defineTool6 } from "@earendil-works/pi-coding-agent";
18968
- import { z as z11 } from "zod";
18969
- var templateSchema = z11.object({
18970
- name: z11.string().min(1),
18971
- variables: z11.record(z11.string(), z11.string()).optional(),
18972
- homeDir: z11.string().optional()
18973
- });
18974
- var diffpiTemplateTool = defineTool6({
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: z11.toJSONSchema(templateSchema, { io: "input" }),
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