@difflab/pi 0.3.0-rc.202609200047.ac3661a → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -22
- package/agents/diffpi-orchestrator.md +0 -10
- package/agents/diffpi-worker.md +1 -9
- package/dist/commands/index.d.ts +0 -1
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/environment.d.ts.map +1 -1
- package/dist/extensions/index.js +844 -2773
- package/dist/extensions/zedx.d.ts +0 -2
- package/dist/extensions/zedx.d.ts.map +1 -1
- package/dist/index.d.ts +2 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +128 -1140
- package/dist/setup.d.ts.map +1 -1
- package/dist/store.d.ts +0 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/tools/index.d.ts +0 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +678 -2400
- package/package.json +1 -4
- package/agents/diffpi-planner.md +0 -30
- package/dist/cli/plan.d.ts +0 -7
- package/dist/cli/plan.d.ts.map +0 -1
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -729
- package/dist/commands/background.d.ts +0 -39
- package/dist/commands/background.d.ts.map +0 -1
- package/dist/commands/plan.d.ts +0 -21
- package/dist/commands/plan.d.ts.map +0 -1
- package/dist/plan/annotations.d.ts +0 -25
- package/dist/plan/annotations.d.ts.map +0 -1
- package/dist/plan/escalation.d.ts +0 -4
- package/dist/plan/escalation.d.ts.map +0 -1
- package/dist/plan/execution.d.ts +0 -19
- package/dist/plan/execution.d.ts.map +0 -1
- package/dist/plan/index.d.ts +0 -15
- package/dist/plan/index.d.ts.map +0 -1
- package/dist/plan/lock.d.ts +0 -7
- package/dist/plan/lock.d.ts.map +0 -1
- package/dist/plan/log.d.ts +0 -5
- package/dist/plan/log.d.ts.map +0 -1
- package/dist/plan/markdown.d.ts +0 -6
- package/dist/plan/markdown.d.ts.map +0 -1
- package/dist/plan/store.d.ts +0 -34
- package/dist/plan/store.d.ts.map +0 -1
- package/dist/plan/transitions.d.ts +0 -9
- package/dist/plan/transitions.d.ts.map +0 -1
- package/dist/plan/types.d.ts +0 -146
- package/dist/plan/types.d.ts.map +0 -1
- package/dist/tools/plan.d.ts +0 -6
- package/dist/tools/plan.d.ts.map +0 -1
- package/skills/plan/SKILL.md +0 -13
- package/skills/plan/references/workflows/annotate.md +0 -6
- package/skills/plan/references/workflows/finalize.md +0 -7
- package/skills/plan/references/workflows/go.md +0 -7
- package/skills/plan/references/workflows/help.md +0 -13
- package/skills/plan/references/workflows/init.md +0 -6
- package/skills/plan/references/workflows/new.md +0 -7
- package/skills/plan/references/workflows/update.md +0 -7
- package/templates/plan/PLAN.md +0 -38
package/dist/index.js
CHANGED
|
@@ -95,30 +95,13 @@ function normalizeModelReference(value) {
|
|
|
95
95
|
return value.toLowerCase().replace(/^~/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
96
96
|
}
|
|
97
97
|
// src/environment.ts
|
|
98
|
-
import {
|
|
99
|
-
import { basename, join as join5 } from "node:path";
|
|
100
|
-
|
|
101
|
-
// src/assets.ts
|
|
102
|
-
import { existsSync } from "node:fs";
|
|
103
|
-
import { dirname, join as join2 } from "node:path";
|
|
104
|
-
import { fileURLToPath } from "node:url";
|
|
105
|
-
function resolveBundledAssetDir(name, moduleUrl = import.meta.url) {
|
|
106
|
-
const moduleDir = dirname(fileURLToPath(moduleUrl));
|
|
107
|
-
const candidates = [join2(moduleDir, name), join2(moduleDir, "..", name), join2(moduleDir, "..", "..", name)];
|
|
108
|
-
return candidates.find((path) => existsSync(path)) ?? candidates[1];
|
|
109
|
-
}
|
|
110
|
-
function resolveBundledAgentsDir(moduleUrl = import.meta.url) {
|
|
111
|
-
return resolveBundledAssetDir("agents", moduleUrl);
|
|
112
|
-
}
|
|
113
|
-
function resolveBundledTemplatesDir(moduleUrl = import.meta.url) {
|
|
114
|
-
return resolveBundledAssetDir("templates", moduleUrl);
|
|
115
|
-
}
|
|
98
|
+
import { basename } from "node:path";
|
|
116
99
|
|
|
117
100
|
// src/extensions/processx.ts
|
|
118
101
|
import { spawn } from "node:child_process";
|
|
119
102
|
import { constants } from "node:fs";
|
|
120
103
|
import { access } from "node:fs/promises";
|
|
121
|
-
import { delimiter, join as
|
|
104
|
+
import { delimiter, join as join2 } from "node:path";
|
|
122
105
|
async function findExecutable(name) {
|
|
123
106
|
if (name.includes("/")) {
|
|
124
107
|
try {
|
|
@@ -131,7 +114,7 @@ async function findExecutable(name) {
|
|
|
131
114
|
for (const directory of (process.env.PATH ?? "").split(delimiter)) {
|
|
132
115
|
if (!directory)
|
|
133
116
|
continue;
|
|
134
|
-
const candidate =
|
|
117
|
+
const candidate = join2(directory, name);
|
|
135
118
|
try {
|
|
136
119
|
await access(candidate, constants.X_OK);
|
|
137
120
|
return candidate;
|
|
@@ -191,18 +174,17 @@ function appendBounded(current, next) {
|
|
|
191
174
|
// src/extensions/zedx.ts
|
|
192
175
|
import { mkdir, readFile as readFile2, writeFile } from "node:fs/promises";
|
|
193
176
|
import { homedir as homedir2 } from "node:os";
|
|
194
|
-
import { dirname
|
|
177
|
+
import { dirname, join as join3 } from "node:path";
|
|
195
178
|
var ZED_LOCAL_REVIEW_TASK_NAME = "diffpi: tuicr local review";
|
|
196
179
|
var ZED_REVIEW_TASK_NAME = ZED_LOCAL_REVIEW_TASK_NAME;
|
|
197
180
|
var ZED_PR_REVIEW_TASK_NAME = "diffpi: tuicr PR review";
|
|
198
|
-
var ZED_PLAN_ANNOTATE_TASK_NAME = "diffpi: annotate plan";
|
|
199
181
|
var LEGACY_ZED_REVIEW_TASK_NAME = "diffpi: tuicr review";
|
|
200
182
|
var REVIEW_KEYBINDING = "cmd-alt-r";
|
|
201
183
|
function zedTasksPath(homeDir = homedir2()) {
|
|
202
|
-
return
|
|
184
|
+
return join3(homeDir, ".config", "zed", "tasks.json");
|
|
203
185
|
}
|
|
204
186
|
function zedKeymapPath(homeDir = homedir2()) {
|
|
205
|
-
return
|
|
187
|
+
return join3(homeDir, ".config", "zed", "keymap.json");
|
|
206
188
|
}
|
|
207
189
|
async function ensureZedReviewTask(homeDir = homedir2(), _command = ["tuicr", "-w", "-r", "main..HEAD"]) {
|
|
208
190
|
const path = zedTasksPath(homeDir);
|
|
@@ -222,33 +204,6 @@ async function ensureZedReviewTask(homeDir = homedir2(), _command = ["tuicr", "-
|
|
|
222
204
|
await writeJson(path, next);
|
|
223
205
|
return { path, changed, existed: currentText !== undefined };
|
|
224
206
|
}
|
|
225
|
-
async function ensureZedPlanTask(packageVersion, homeDir = homedir2()) {
|
|
226
|
-
if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(packageVersion)) {
|
|
227
|
-
throw new Error(`Invalid @difflab/pi package version: ${packageVersion}.`);
|
|
228
|
-
}
|
|
229
|
-
const path = zedTasksPath(homeDir);
|
|
230
|
-
const currentText = await readOptional(path);
|
|
231
|
-
const tasks = parseJsonArray(currentText, path);
|
|
232
|
-
const task = {
|
|
233
|
-
label: ZED_PLAN_ANNOTATE_TASK_NAME,
|
|
234
|
-
command: "npx",
|
|
235
|
-
args: ["--yes", `@difflab/pi@${packageVersion}`, "plan", "annotate", "--cwd", "$ZED_WORKTREE_ROOT"],
|
|
236
|
-
cwd: "$ZED_WORKTREE_ROOT",
|
|
237
|
-
use_new_terminal: true,
|
|
238
|
-
reveal: "always",
|
|
239
|
-
reveal_target: "center"
|
|
240
|
-
};
|
|
241
|
-
const next = [...tasks];
|
|
242
|
-
const index = next.findIndex((candidate) => candidate.label === task.label);
|
|
243
|
-
if (index >= 0)
|
|
244
|
-
next[index] = { ...next[index], ...task };
|
|
245
|
-
else
|
|
246
|
-
next.push(task);
|
|
247
|
-
const changed = JSON.stringify(tasks) !== JSON.stringify(next);
|
|
248
|
-
if (changed)
|
|
249
|
-
await writeJson(path, next);
|
|
250
|
-
return { path, changed, existed: currentText !== undefined };
|
|
251
|
-
}
|
|
252
207
|
async function ensureZedReviewKeybinding(homeDir = homedir2()) {
|
|
253
208
|
const path = zedKeymapPath(homeDir);
|
|
254
209
|
const currentText = await readOptional(path);
|
|
@@ -268,9 +223,6 @@ async function ensureZedReviewKeybinding(homeDir = homedir2()) {
|
|
|
268
223
|
return { path, changed, existed: currentText !== undefined };
|
|
269
224
|
}
|
|
270
225
|
function zedReviewTaskName(command) {
|
|
271
|
-
if (command.some((value, index) => value === "plan" && command[index + 1] === "annotate")) {
|
|
272
|
-
return ZED_PLAN_ANNOTATE_TASK_NAME;
|
|
273
|
-
}
|
|
274
226
|
return command[0] === "tuicr" && command[1] === "pr" ? ZED_PR_REVIEW_TASK_NAME : ZED_LOCAL_REVIEW_TASK_NAME;
|
|
275
227
|
}
|
|
276
228
|
var LOCAL_REVIEW_SCRIPT = `set -eu
|
|
@@ -352,7 +304,7 @@ function parseJsonArray(content, path) {
|
|
|
352
304
|
return value;
|
|
353
305
|
}
|
|
354
306
|
async function writeJson(path, value) {
|
|
355
|
-
await mkdir(
|
|
307
|
+
await mkdir(dirname(path), { recursive: true });
|
|
356
308
|
await writeFile(path, `${JSON.stringify(value, null, 2)}
|
|
357
309
|
`, "utf8");
|
|
358
310
|
}
|
|
@@ -421,10 +373,7 @@ async function openInNewTab(command, opts) {
|
|
|
421
373
|
if (detectIde(env) === "zed") {
|
|
422
374
|
try {
|
|
423
375
|
const taskName = zedReviewTaskName(command);
|
|
424
|
-
|
|
425
|
-
await ensureZedPlanTask(await packageVersion(), opts.homeDir);
|
|
426
|
-
else
|
|
427
|
-
await ensureZedReviewTask(opts.homeDir, command);
|
|
376
|
+
await ensureZedReviewTask(opts.homeDir, command);
|
|
428
377
|
return {
|
|
429
378
|
launched: false,
|
|
430
379
|
configured: true,
|
|
@@ -440,12 +389,6 @@ async function openInNewTab(command, opts) {
|
|
|
440
389
|
function screenWindowArgs(command, cwd, name) {
|
|
441
390
|
return ["-X", "screen", "-t", name, "sh", "-lc", 'cd -- "$1" && shift && exec "$@"', "sh", cwd, ...command];
|
|
442
391
|
}
|
|
443
|
-
async function packageVersion() {
|
|
444
|
-
const value = JSON.parse(await readFile3(join5(resolveBundledAgentsDir(), "..", "package.json"), "utf8"));
|
|
445
|
-
if (typeof value.version !== "string")
|
|
446
|
-
throw new Error("Cannot resolve the installed @difflab/pi version.");
|
|
447
|
-
return value.version;
|
|
448
|
-
}
|
|
449
392
|
async function openMuxTab(mux, command, cwd, name, printable) {
|
|
450
393
|
if (mux === "zellij" && await findExecutable("zellij")) {
|
|
451
394
|
const result = await run("zellij", ["action", "new-tab", "--cwd", cwd, "--name", name, "--", ...command]);
|
|
@@ -469,7 +412,7 @@ async function openMuxTab(mux, command, cwd, name, printable) {
|
|
|
469
412
|
}
|
|
470
413
|
// src/extensions/gitx.ts
|
|
471
414
|
import { realpath } from "node:fs/promises";
|
|
472
|
-
import { basename as basename2, isAbsolute, join as
|
|
415
|
+
import { basename as basename2, isAbsolute, join as join4, resolve } from "node:path";
|
|
473
416
|
async function gitToplevel(cwd) {
|
|
474
417
|
const result = await run("git", ["-C", cwd, "rev-parse", "--show-toplevel"]);
|
|
475
418
|
const top = result.stdout.trim();
|
|
@@ -481,7 +424,7 @@ async function inspectGitRepository(cwd) {
|
|
|
481
424
|
const remote = remoteResult.code === 0 ? remoteResult.stdout.trim() : "";
|
|
482
425
|
const commonResult = await run("git", ["-C", root, "rev-parse", "--git-common-dir"]);
|
|
483
426
|
const common = commonResult.stdout.trim();
|
|
484
|
-
const commonPath = commonResult.code === 0 && common ? resolve(isAbsolute(common) ? common :
|
|
427
|
+
const commonPath = commonResult.code === 0 && common ? resolve(isAbsolute(common) ? common : join4(root, common)) : root;
|
|
485
428
|
const commonDir = await canonicalPath(commonPath);
|
|
486
429
|
return {
|
|
487
430
|
root,
|
|
@@ -753,9 +696,9 @@ function createForgeBackend(vcs) {
|
|
|
753
696
|
var createVcsBackend = createForgeBackend;
|
|
754
697
|
var createForge = createForgeBackend;
|
|
755
698
|
// src/extensions/misex.ts
|
|
756
|
-
import { mkdir as mkdir2, readFile as
|
|
699
|
+
import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
|
|
757
700
|
import { homedir as homedir3 } from "node:os";
|
|
758
|
-
import { basename as basename3, dirname as
|
|
701
|
+
import { basename as basename3, dirname as dirname2, join as join5 } from "node:path";
|
|
759
702
|
var MISE_HOOK_START = "# >>> @difflab/pi mise >>>";
|
|
760
703
|
var MISE_HOOK_END = "# <<< @difflab/pi mise <<<";
|
|
761
704
|
var mise = {
|
|
@@ -770,7 +713,7 @@ var mise = {
|
|
|
770
713
|
const platform = options.platform ?? process.platform;
|
|
771
714
|
if (platform === "win32")
|
|
772
715
|
throw new Error("Automatic mise installation supports macOS and Linux only.");
|
|
773
|
-
const installedPath =
|
|
716
|
+
const installedPath = join5(homeDir, ".local", "bin", "mise");
|
|
774
717
|
if (options.dryRun)
|
|
775
718
|
return installedPath;
|
|
776
719
|
await runChecked("sh", ["-c", "curl -fsSL https://mise.run | sh"]);
|
|
@@ -790,7 +733,7 @@ var mise = {
|
|
|
790
733
|
const separator = current.length === 0 || current.endsWith(`
|
|
791
734
|
`) ? "" : `
|
|
792
735
|
`;
|
|
793
|
-
await mkdir2(
|
|
736
|
+
await mkdir2(dirname2(hook.path), { recursive: true });
|
|
794
737
|
await writeFile2(hook.path, `${current}${separator}${hook.content}`, "utf8");
|
|
795
738
|
return { path: hook.path, changed: true, planned: false };
|
|
796
739
|
},
|
|
@@ -817,7 +760,7 @@ function getShellHook(shell, executable, homeDir) {
|
|
|
817
760
|
switch (shell.toLowerCase()) {
|
|
818
761
|
case "zsh":
|
|
819
762
|
return {
|
|
820
|
-
path:
|
|
763
|
+
path: join5(homeDir, ".zshrc"),
|
|
821
764
|
content: `${MISE_HOOK_START}
|
|
822
765
|
eval "$(${command} activate zsh)"
|
|
823
766
|
${MISE_HOOK_END}
|
|
@@ -825,7 +768,7 @@ ${MISE_HOOK_END}
|
|
|
825
768
|
};
|
|
826
769
|
case "fish":
|
|
827
770
|
return {
|
|
828
|
-
path:
|
|
771
|
+
path: join5(homeDir, ".config", "fish", "config.fish"),
|
|
829
772
|
content: `${MISE_HOOK_START}
|
|
830
773
|
${command} activate fish | source
|
|
831
774
|
${MISE_HOOK_END}
|
|
@@ -834,7 +777,7 @@ ${MISE_HOOK_END}
|
|
|
834
777
|
case "nu":
|
|
835
778
|
case "nushell":
|
|
836
779
|
return {
|
|
837
|
-
path:
|
|
780
|
+
path: join5(homeDir, ".config", "nushell", "config.nu"),
|
|
838
781
|
content: `${MISE_HOOK_START}
|
|
839
782
|
let mise_bin = ${command}
|
|
840
783
|
let mise_path = $nu.default-config-dir | path join mise.nu
|
|
@@ -845,7 +788,7 @@ ${MISE_HOOK_END}
|
|
|
845
788
|
};
|
|
846
789
|
case "xonsh":
|
|
847
790
|
return {
|
|
848
|
-
path:
|
|
791
|
+
path: join5(homeDir, ".xonshrc"),
|
|
849
792
|
content: `${MISE_HOOK_START}
|
|
850
793
|
execx($(${command} activate xonsh))
|
|
851
794
|
${MISE_HOOK_END}
|
|
@@ -853,7 +796,7 @@ ${MISE_HOOK_END}
|
|
|
853
796
|
};
|
|
854
797
|
case "elvish":
|
|
855
798
|
return {
|
|
856
|
-
path:
|
|
799
|
+
path: join5(homeDir, ".config", "elvish", "rc.elv"),
|
|
857
800
|
content: `${MISE_HOOK_START}
|
|
858
801
|
var mise: = (ns [&])
|
|
859
802
|
eval (${command} activate elvish | slurp) &ns=$mise: &on-end={|ns| set mise: = $ns }
|
|
@@ -864,7 +807,7 @@ ${MISE_HOOK_END}
|
|
|
864
807
|
case "pwsh":
|
|
865
808
|
case "powershell":
|
|
866
809
|
return {
|
|
867
|
-
path:
|
|
810
|
+
path: join5(homeDir, ".config", "powershell", "Microsoft.PowerShell_profile.ps1"),
|
|
868
811
|
content: `${MISE_HOOK_START}
|
|
869
812
|
(& ${command} activate pwsh) | Out-String | Invoke-Expression
|
|
870
813
|
${MISE_HOOK_END}
|
|
@@ -873,7 +816,7 @@ ${MISE_HOOK_END}
|
|
|
873
816
|
case "bash":
|
|
874
817
|
default:
|
|
875
818
|
return {
|
|
876
|
-
path:
|
|
819
|
+
path: join5(homeDir, ".bashrc"),
|
|
877
820
|
content: `${MISE_HOOK_START}
|
|
878
821
|
eval "$(${command} activate bash)"
|
|
879
822
|
${MISE_HOOK_END}
|
|
@@ -912,7 +855,7 @@ function isVersionAtLeast(version, minimumVersion) {
|
|
|
912
855
|
}
|
|
913
856
|
async function getOptionalFile(path) {
|
|
914
857
|
try {
|
|
915
|
-
return await
|
|
858
|
+
return await readFile3(path, "utf8");
|
|
916
859
|
} catch (error) {
|
|
917
860
|
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
918
861
|
return "";
|
|
@@ -1532,10 +1475,10 @@ function hasGitlabDraftNotes(input) {
|
|
|
1532
1475
|
}
|
|
1533
1476
|
|
|
1534
1477
|
// src/review/local-review-backend.ts
|
|
1535
|
-
import { readFile as
|
|
1478
|
+
import { readFile as readFile5, writeFile as writeFile3 } from "node:fs/promises";
|
|
1536
1479
|
|
|
1537
1480
|
// src/extensions/tuicrx.ts
|
|
1538
|
-
import { readFile as
|
|
1481
|
+
import { readFile as readFile4, realpath as realpath2 } from "node:fs/promises";
|
|
1539
1482
|
import { resolve as resolve2 } from "node:path";
|
|
1540
1483
|
async function tuicrAvailable() {
|
|
1541
1484
|
return Boolean(await findExecutable("tuicr"));
|
|
@@ -1626,7 +1569,7 @@ async function findMatchingSession(sessions, cwd, branch) {
|
|
|
1626
1569
|
return;
|
|
1627
1570
|
}
|
|
1628
1571
|
async function readSession(path) {
|
|
1629
|
-
const content = await
|
|
1572
|
+
const content = await readFile4(path, "utf8");
|
|
1630
1573
|
try {
|
|
1631
1574
|
return JSON.parse(content);
|
|
1632
1575
|
} catch {
|
|
@@ -1705,7 +1648,7 @@ function LocalReviewBackend(options) {
|
|
|
1705
1648
|
},
|
|
1706
1649
|
async listThreads() {
|
|
1707
1650
|
try {
|
|
1708
|
-
return parseThreadArtifact(await
|
|
1651
|
+
return parseThreadArtifact(await readFile5(options.artifactPath, "utf8"));
|
|
1709
1652
|
} catch (error) {
|
|
1710
1653
|
if (error.code === "ENOENT")
|
|
1711
1654
|
return [];
|
|
@@ -1713,7 +1656,7 @@ function LocalReviewBackend(options) {
|
|
|
1713
1656
|
}
|
|
1714
1657
|
},
|
|
1715
1658
|
async reply(input) {
|
|
1716
|
-
const content = await
|
|
1659
|
+
const content = await readFile5(options.artifactPath, "utf8");
|
|
1717
1660
|
await writeFile3(options.artifactPath, upsertThreadReply(content, input.threadId, input.body, input.question, input.resolve), "utf8");
|
|
1718
1661
|
},
|
|
1719
1662
|
async publish() {
|
|
@@ -1735,29 +1678,29 @@ function createLocalReviewBackend(options) {
|
|
|
1735
1678
|
}
|
|
1736
1679
|
// src/review/review-state.ts
|
|
1737
1680
|
import { createHash as createHash2 } from "node:crypto";
|
|
1738
|
-
import { readFile as
|
|
1739
|
-
import { join as
|
|
1681
|
+
import { readFile as readFile6, rename, writeFile as writeFile4 } from "node:fs/promises";
|
|
1682
|
+
import { join as join7 } from "node:path";
|
|
1740
1683
|
import { z as z4 } from "zod";
|
|
1741
1684
|
|
|
1742
1685
|
// src/store.ts
|
|
1743
1686
|
import { createHash } from "node:crypto";
|
|
1744
1687
|
import { lstat, mkdir as mkdir3, readlink, realpath as realpath3, symlink, unlink } from "node:fs/promises";
|
|
1745
1688
|
import { homedir as homedir4 } from "node:os";
|
|
1746
|
-
import { dirname as
|
|
1689
|
+
import { dirname as dirname3, isAbsolute as isAbsolute2, join as join6, resolve as resolve3 } from "node:path";
|
|
1747
1690
|
var STORE_LINK = ".diffpi";
|
|
1748
|
-
var LEGACY_STORE_LINK =
|
|
1691
|
+
var LEGACY_STORE_LINK = join6(".pi", "diffpi");
|
|
1749
1692
|
async function computeProjectSlug(cwd) {
|
|
1750
1693
|
return projectSlug(await inspectGitRepository(cwd));
|
|
1751
1694
|
}
|
|
1752
1695
|
function storeGlobalRoot(homeDir = homedir4()) {
|
|
1753
|
-
return
|
|
1696
|
+
return join6(homeDir, ".difflab", "diffpi", "projects");
|
|
1754
1697
|
}
|
|
1755
1698
|
async function ensureStore(cwd, homeDir = homedir4()) {
|
|
1756
1699
|
const repository = await inspectGitRepository(cwd);
|
|
1757
1700
|
const root = repository.root;
|
|
1758
1701
|
const slug = projectSlug(repository);
|
|
1759
|
-
const dest =
|
|
1760
|
-
const link =
|
|
1702
|
+
const dest = join6(storeGlobalRoot(homeDir), slug);
|
|
1703
|
+
const link = join6(root, STORE_LINK);
|
|
1761
1704
|
await mkdir3(dest, { recursive: true });
|
|
1762
1705
|
try {
|
|
1763
1706
|
await assertStoreLink(link, dest);
|
|
@@ -1766,28 +1709,22 @@ async function ensureStore(cwd, homeDir = homedir4()) {
|
|
|
1766
1709
|
throw error;
|
|
1767
1710
|
await symlink(dest, link);
|
|
1768
1711
|
}
|
|
1769
|
-
await removeLegacyStoreLink(
|
|
1712
|
+
await removeLegacyStoreLink(join6(root, LEGACY_STORE_LINK), dest);
|
|
1770
1713
|
return { slug, root, dest, link, linked: true };
|
|
1771
1714
|
}
|
|
1772
1715
|
async function storeDir(cwd, homeDir = homedir4()) {
|
|
1773
1716
|
const store = await ensureStore(cwd, homeDir);
|
|
1774
1717
|
return store.dest;
|
|
1775
1718
|
}
|
|
1776
|
-
async function plansDir(cwd, homeDir = homedir4()) {
|
|
1777
|
-
const store = await ensureStore(cwd, homeDir);
|
|
1778
|
-
const dir = join8(store.link, "plan");
|
|
1779
|
-
await mkdir3(dir, { recursive: true });
|
|
1780
|
-
return dir;
|
|
1781
|
-
}
|
|
1782
1719
|
async function reviewsDir(cwd, homeDir = homedir4()) {
|
|
1783
1720
|
const store = await ensureStore(cwd, homeDir);
|
|
1784
|
-
const dir =
|
|
1721
|
+
const dir = join6(store.link, "review");
|
|
1785
1722
|
await mkdir3(dir, { recursive: true });
|
|
1786
1723
|
return dir;
|
|
1787
1724
|
}
|
|
1788
1725
|
async function sessionsDir(cwd, homeDir = homedir4()) {
|
|
1789
1726
|
const store = await ensureStore(cwd, homeDir);
|
|
1790
|
-
const dir =
|
|
1727
|
+
const dir = join6(store.link, "sessions");
|
|
1791
1728
|
await mkdir3(dir, { recursive: true });
|
|
1792
1729
|
return dir;
|
|
1793
1730
|
}
|
|
@@ -1814,7 +1751,7 @@ async function removeLegacyStoreLink(path, dest) {
|
|
|
1814
1751
|
}
|
|
1815
1752
|
async function symlinkTarget(path) {
|
|
1816
1753
|
const target = await readlink(path);
|
|
1817
|
-
return canonicalPath3(isAbsolute2(target) ? target : resolve3(
|
|
1754
|
+
return canonicalPath3(isAbsolute2(target) ? target : resolve3(dirname3(path), target));
|
|
1818
1755
|
}
|
|
1819
1756
|
async function canonicalPath3(path) {
|
|
1820
1757
|
try {
|
|
@@ -1851,9 +1788,9 @@ function unpublishedReviewComments(comments, knownFingerprints) {
|
|
|
1851
1788
|
}
|
|
1852
1789
|
async function loadReviewPublicationState(cwd, vcs, number, homeDir) {
|
|
1853
1790
|
const target = `${vcs.provider}:${vcs.owner}/${vcs.repo}#${number}`;
|
|
1854
|
-
const path =
|
|
1791
|
+
const path = join7(await sessionsDir(cwd, homeDir), `review-publish-${digest(target).slice(0, 16)}.json`);
|
|
1855
1792
|
try {
|
|
1856
|
-
const parsed = reviewPublicationStateSchema.parse(JSON.parse(await
|
|
1793
|
+
const parsed = reviewPublicationStateSchema.parse(JSON.parse(await readFile6(path, "utf8")));
|
|
1857
1794
|
return { path, state: { ...parsed, target } };
|
|
1858
1795
|
} catch (error) {
|
|
1859
1796
|
if (error.code === "ENOENT") {
|
|
@@ -1875,12 +1812,12 @@ function digest(value) {
|
|
|
1875
1812
|
return createHash2("sha256").update(value).digest("hex");
|
|
1876
1813
|
}
|
|
1877
1814
|
// src/mcp.ts
|
|
1878
|
-
import { mkdir as mkdir4, readFile as
|
|
1815
|
+
import { mkdir as mkdir4, readFile as readFile7, writeFile as writeFile5 } from "node:fs/promises";
|
|
1879
1816
|
import { homedir as homedir5 } from "node:os";
|
|
1880
|
-
import { dirname as
|
|
1817
|
+
import { dirname as dirname4, join as join8 } from "node:path";
|
|
1881
1818
|
var mcp = {
|
|
1882
1819
|
globalConfigPath(homeDir = homedir5()) {
|
|
1883
|
-
return
|
|
1820
|
+
return join8(homeDir, ".config", "mcp", "mcp.json");
|
|
1884
1821
|
},
|
|
1885
1822
|
async serversEnsure(servers, options = {}) {
|
|
1886
1823
|
const path = options.path ?? mcp.globalConfigPath();
|
|
@@ -1893,7 +1830,7 @@ var mcp = {
|
|
|
1893
1830
|
const next = { ...current, mcpServers: nextServers };
|
|
1894
1831
|
const changed = JSON.stringify(current) !== JSON.stringify(next);
|
|
1895
1832
|
if (changed && !options.dryRun) {
|
|
1896
|
-
await mkdir4(
|
|
1833
|
+
await mkdir4(dirname4(path), { recursive: true });
|
|
1897
1834
|
await writeFile5(path, `${JSON.stringify(next, null, 2)}
|
|
1898
1835
|
`, "utf8");
|
|
1899
1836
|
}
|
|
@@ -1923,7 +1860,7 @@ function getParsedConfig(content, path) {
|
|
|
1923
1860
|
}
|
|
1924
1861
|
async function getOptionalFile2(path) {
|
|
1925
1862
|
try {
|
|
1926
|
-
return await
|
|
1863
|
+
return await readFile7(path, "utf8");
|
|
1927
1864
|
} catch (error) {
|
|
1928
1865
|
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
1929
1866
|
return;
|
|
@@ -1938,27 +1875,45 @@ import {
|
|
|
1938
1875
|
getAgentDir,
|
|
1939
1876
|
parseFrontmatter as parseFrontmatter2
|
|
1940
1877
|
} from "@earendil-works/pi-coding-agent";
|
|
1941
|
-
import { readFile as
|
|
1878
|
+
import { readFile as readFile8 } from "node:fs/promises";
|
|
1942
1879
|
import { homedir as homedir6 } from "node:os";
|
|
1943
|
-
import { basename as basename4, extname, join as
|
|
1880
|
+
import { basename as basename4, extname, join as join10 } from "node:path";
|
|
1881
|
+
|
|
1882
|
+
// src/assets.ts
|
|
1883
|
+
import { existsSync } from "node:fs";
|
|
1884
|
+
import { dirname as dirname5, join as join9 } from "node:path";
|
|
1885
|
+
import { fileURLToPath } from "node:url";
|
|
1886
|
+
function resolveBundledAssetDir(name, moduleUrl = import.meta.url) {
|
|
1887
|
+
const moduleDir = dirname5(fileURLToPath(moduleUrl));
|
|
1888
|
+
const candidates = [join9(moduleDir, name), join9(moduleDir, "..", name), join9(moduleDir, "..", "..", name)];
|
|
1889
|
+
return candidates.find((path) => existsSync(path)) ?? candidates[1];
|
|
1890
|
+
}
|
|
1891
|
+
function resolveBundledAgentsDir(moduleUrl = import.meta.url) {
|
|
1892
|
+
return resolveBundledAssetDir("agents", moduleUrl);
|
|
1893
|
+
}
|
|
1894
|
+
function resolveBundledTemplatesDir(moduleUrl = import.meta.url) {
|
|
1895
|
+
return resolveBundledAssetDir("templates", moduleUrl);
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
// src/modes.ts
|
|
1944
1899
|
async function discoverAgentModes(options) {
|
|
1945
1900
|
const agentDir = options.agentDir ?? getAgentDir();
|
|
1946
1901
|
const homeDir = options.homeDir ?? homedir6();
|
|
1947
1902
|
const modes = new Map;
|
|
1948
1903
|
const diagnostics = [];
|
|
1949
1904
|
await loadAgentModes(options.bundledAgentsDir ?? BUNDLED_AGENTS_DIR, "diffpi agent", modes, diagnostics);
|
|
1950
|
-
await loadAgentModes(
|
|
1905
|
+
await loadAgentModes(join10(agentDir, "agents"), "user agent", modes, diagnostics);
|
|
1951
1906
|
if (options.includeSkills) {
|
|
1952
|
-
await loadSkillModes(
|
|
1953
|
-
await loadSkillModes(
|
|
1907
|
+
await loadSkillModes(join10(homeDir, ".agents", "skills"), "user skill", modes, diagnostics);
|
|
1908
|
+
await loadSkillModes(join10(agentDir, "skills"), "pi user skill", modes, diagnostics);
|
|
1954
1909
|
}
|
|
1955
1910
|
if (options.projectTrusted === true) {
|
|
1956
1911
|
if (options.includeSkills) {
|
|
1957
|
-
await loadSkillModes(
|
|
1958
|
-
await loadSkillModes(
|
|
1912
|
+
await loadSkillModes(join10(options.cwd, ".agents", "skills"), "project skill", modes, diagnostics);
|
|
1913
|
+
await loadSkillModes(join10(options.cwd, ".pi", "skills"), "pi project skill", modes, diagnostics);
|
|
1959
1914
|
}
|
|
1960
|
-
await loadAgentModes(
|
|
1961
|
-
await loadAgentModes(
|
|
1915
|
+
await loadAgentModes(join10(options.cwd, ".agents", "agents"), "project agent", modes, diagnostics);
|
|
1916
|
+
await loadAgentModes(join10(options.cwd, ".pi", "agents"), "pi project agent", modes, diagnostics);
|
|
1962
1917
|
}
|
|
1963
1918
|
const userConfig = await loadDiffpiConfig({ homeDir });
|
|
1964
1919
|
const configuredModes = [...modes.values()].map((mode) => ({
|
|
@@ -2129,15 +2084,15 @@ function captureRuntime(pi, ctx) {
|
|
|
2129
2084
|
async function loadSkillModes(skillsDir, source, modes, diagnostics) {
|
|
2130
2085
|
const entries = await readDirectoryIfExists(skillsDir);
|
|
2131
2086
|
for (const entry of entries.filter((item) => item.isDirectory()).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
2132
|
-
await loadAgentModes(
|
|
2087
|
+
await loadAgentModes(join10(skillsDir, entry.name, "agents"), `${source} ${entry.name}`, modes, diagnostics, entry.name);
|
|
2133
2088
|
}
|
|
2134
2089
|
}
|
|
2135
2090
|
async function loadAgentModes(directory, source, modes, diagnostics, skillName) {
|
|
2136
2091
|
const entries = await readDirectoryIfExists(directory);
|
|
2137
2092
|
for (const entry of entries.filter((item) => item.isFile() && item.name.endsWith(".md")).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
2138
|
-
const path =
|
|
2093
|
+
const path = join10(directory, entry.name);
|
|
2139
2094
|
try {
|
|
2140
|
-
const content = await
|
|
2095
|
+
const content = await readFile8(path, "utf8");
|
|
2141
2096
|
const { frontmatter, body } = parseFrontmatter2(content.startsWith("\uFEFF") ? content.slice(1) : content);
|
|
2142
2097
|
if (frontmatter.enabled === false || frontmatter.inline === false)
|
|
2143
2098
|
continue;
|
|
@@ -2195,7 +2150,7 @@ function isModeBaseline(value) {
|
|
|
2195
2150
|
// src/pi.ts
|
|
2196
2151
|
import { mkdir as mkdir5, writeFile as writeFile6 } from "node:fs/promises";
|
|
2197
2152
|
import { homedir as homedir7 } from "node:os";
|
|
2198
|
-
import { dirname as dirname6, join as
|
|
2153
|
+
import { dirname as dirname6, join as join11 } from "node:path";
|
|
2199
2154
|
var pi = {
|
|
2200
2155
|
executableCheck: findPiExecutable,
|
|
2201
2156
|
packageList: listPiPackages,
|
|
@@ -2208,7 +2163,7 @@ var pi = {
|
|
|
2208
2163
|
configEnsure: ensurePiConfig
|
|
2209
2164
|
};
|
|
2210
2165
|
async function ensurePiAgent(filename, content, agentDir = resolvePiAgentDir(), dryRun = false) {
|
|
2211
|
-
const path =
|
|
2166
|
+
const path = join11(agentDir, "agents", filename);
|
|
2212
2167
|
const currentText = await readTextIfExists(path);
|
|
2213
2168
|
const changed = currentText !== content;
|
|
2214
2169
|
if (changed && !dryRun) {
|
|
@@ -2217,10 +2172,10 @@ async function ensurePiAgent(filename, content, agentDir = resolvePiAgentDir(),
|
|
|
2217
2172
|
}
|
|
2218
2173
|
return { path, changed, existed: currentText !== undefined, planned: changed && dryRun };
|
|
2219
2174
|
}
|
|
2220
|
-
async function checkGlobalPiSkill(name, agentDir = resolvePiAgentDir(), sharedSkillsDir =
|
|
2221
|
-
const roots = [
|
|
2175
|
+
async function checkGlobalPiSkill(name, agentDir = resolvePiAgentDir(), sharedSkillsDir = join11(homedir7(), ".agents", "skills")) {
|
|
2176
|
+
const roots = [join11(agentDir, "skills"), sharedSkillsDir];
|
|
2222
2177
|
for (const root of roots) {
|
|
2223
|
-
if (await readTextIfExists(
|
|
2178
|
+
if (await readTextIfExists(join11(root, name, "SKILL.md")) !== undefined)
|
|
2224
2179
|
return true;
|
|
2225
2180
|
}
|
|
2226
2181
|
return false;
|
|
@@ -2270,7 +2225,7 @@ async function installPiPackage(executable, source) {
|
|
|
2270
2225
|
await runChecked(executable, ["install", source]);
|
|
2271
2226
|
}
|
|
2272
2227
|
function resolvePiAgentDir(homeDir = homedir7()) {
|
|
2273
|
-
return process.env.PI_CODING_AGENT_DIR ?? (process.env.XDG_CONFIG_HOME ?
|
|
2228
|
+
return process.env.PI_CODING_AGENT_DIR ?? (process.env.XDG_CONFIG_HOME ? join11(process.env.XDG_CONFIG_HOME, "pi") : join11(homeDir, ".pi", "agent"));
|
|
2274
2229
|
}
|
|
2275
2230
|
function parseJsonObject(content, path) {
|
|
2276
2231
|
if (!content?.trim())
|
|
@@ -2282,975 +2237,11 @@ function parseJsonObject(content, path) {
|
|
|
2282
2237
|
} catch {}
|
|
2283
2238
|
throw new Error(`Expected valid JSON object in ${path}.`);
|
|
2284
2239
|
}
|
|
2285
|
-
// src/plan/annotations.ts
|
|
2286
|
-
import { spawn as spawn2 } from "node:child_process";
|
|
2287
|
-
import { readFile as readFile12, rename as rename2, writeFile as writeFile8 } from "node:fs/promises";
|
|
2288
|
-
import { basename as basename5, dirname as dirname7, join as join14 } from "node:path";
|
|
2289
|
-
|
|
2290
|
-
// src/plan/log.ts
|
|
2291
|
-
import { randomUUID } from "node:crypto";
|
|
2292
|
-
import { appendFile, readFile as readFile10 } from "node:fs/promises";
|
|
2293
|
-
async function appendPlanLog(logPath, event) {
|
|
2294
|
-
const entry = {
|
|
2295
|
-
...event,
|
|
2296
|
-
version: 1,
|
|
2297
|
-
eventId: event.eventId ?? randomUUID(),
|
|
2298
|
-
timestamp: event.timestamp ?? new Date().toISOString()
|
|
2299
|
-
};
|
|
2300
|
-
await appendFile(logPath, `${JSON.stringify(entry)}
|
|
2301
|
-
`, { encoding: "utf8", mode: 384 });
|
|
2302
|
-
return entry;
|
|
2303
|
-
}
|
|
2304
|
-
async function readPlanLog(logPath) {
|
|
2305
|
-
let source;
|
|
2306
|
-
try {
|
|
2307
|
-
source = await readFile10(logPath, "utf8");
|
|
2308
|
-
} catch (error) {
|
|
2309
|
-
if (error.code === "ENOENT")
|
|
2310
|
-
return [];
|
|
2311
|
-
throw error;
|
|
2312
|
-
}
|
|
2313
|
-
return source.split(`
|
|
2314
|
-
`).filter(Boolean).map((line, index) => {
|
|
2315
|
-
try {
|
|
2316
|
-
return JSON.parse(line);
|
|
2317
|
-
} catch {
|
|
2318
|
-
throw new Error(`Malformed plan log entry at line ${index + 1}.`);
|
|
2319
|
-
}
|
|
2320
|
-
});
|
|
2321
|
-
}
|
|
2322
|
-
|
|
2323
|
-
// src/plan/lock.ts
|
|
2324
|
-
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2325
|
-
import { mkdir as mkdir6, readFile as readFile11, rm, writeFile as writeFile7 } from "node:fs/promises";
|
|
2326
|
-
import { hostname } from "node:os";
|
|
2327
|
-
import { join as join13 } from "node:path";
|
|
2328
|
-
async function withPlanLock(planDir, operation, options = {}) {
|
|
2329
|
-
const lockDir = join13(planDir, ".lock");
|
|
2330
|
-
const waitMs = options.waitMs ?? 5000;
|
|
2331
|
-
const pollMs = options.pollMs ?? 25;
|
|
2332
|
-
const owner = {
|
|
2333
|
-
token: randomUUID2(),
|
|
2334
|
-
pid: process.pid,
|
|
2335
|
-
hostname: hostname(),
|
|
2336
|
-
operation: options.operation ?? "plan mutation",
|
|
2337
|
-
acquiredAt: new Date().toISOString()
|
|
2338
|
-
};
|
|
2339
|
-
const started = Date.now();
|
|
2340
|
-
while (true) {
|
|
2341
|
-
try {
|
|
2342
|
-
await mkdir6(lockDir);
|
|
2343
|
-
await writeFile7(join13(lockDir, "owner.json"), `${JSON.stringify(owner)}
|
|
2344
|
-
`, { mode: 384 });
|
|
2345
|
-
break;
|
|
2346
|
-
} catch (error) {
|
|
2347
|
-
if (error.code !== "EEXIST")
|
|
2348
|
-
throw error;
|
|
2349
|
-
if (Date.now() - started >= waitMs) {
|
|
2350
|
-
let existing = "unknown owner";
|
|
2351
|
-
try {
|
|
2352
|
-
existing = (await readFile11(join13(lockDir, "owner.json"), "utf8")).trim();
|
|
2353
|
-
} catch {}
|
|
2354
|
-
throw new Error(`Timed out waiting for plan lock ${lockDir}; owner: ${existing}. Remove it only after confirming the owner is stale.`);
|
|
2355
|
-
}
|
|
2356
|
-
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
2357
|
-
}
|
|
2358
|
-
}
|
|
2359
|
-
try {
|
|
2360
|
-
return await operation();
|
|
2361
|
-
} finally {
|
|
2362
|
-
let current;
|
|
2363
|
-
try {
|
|
2364
|
-
current = JSON.parse(await readFile11(join13(lockDir, "owner.json"), "utf8"));
|
|
2365
|
-
} catch {}
|
|
2366
|
-
if (current?.token === owner.token)
|
|
2367
|
-
await rm(lockDir, { recursive: true, force: true });
|
|
2368
|
-
}
|
|
2369
|
-
}
|
|
2370
|
-
|
|
2371
|
-
// src/plan/annotations.ts
|
|
2372
|
-
async function annotatePlan(record, runtime = {}) {
|
|
2373
|
-
const execute = runtime.execute ?? executeProcess;
|
|
2374
|
-
const result = await execute("tuicr", ["--file", record.planPath], dirname7(record.planPath), true);
|
|
2375
|
-
const sessionSlug = [...result.stderr.matchAll(/^tuicr-session:\s*(\S+)\s*$/gm)].at(-1)?.[1];
|
|
2376
|
-
if (result.code !== 0)
|
|
2377
|
-
throw new Error(result.stderr.trim() || `tuicr exited with status ${result.code}.`);
|
|
2378
|
-
if (!sessionSlug)
|
|
2379
|
-
throw new Error("tuicr did not report a tuicr-session marker.");
|
|
2380
|
-
const state = await withPlanLock(record.dir, async () => {
|
|
2381
|
-
const previous = await readAnnotationState(record).catch(() => {
|
|
2382
|
-
return;
|
|
2383
|
-
});
|
|
2384
|
-
const next = {
|
|
2385
|
-
schemaVersion: 1,
|
|
2386
|
-
sessionSlug,
|
|
2387
|
-
updatedAt: new Date().toISOString(),
|
|
2388
|
-
appliedCommentIds: previous?.appliedCommentIds ?? []
|
|
2389
|
-
};
|
|
2390
|
-
await atomicJson(annotationStatePath(record), next);
|
|
2391
|
-
return next;
|
|
2392
|
-
}, { operation: "save annotation session" });
|
|
2393
|
-
return { sessionSlug, code: result.code, state };
|
|
2394
|
-
}
|
|
2395
|
-
async function readPlanAnnotations(record, options = {}) {
|
|
2396
|
-
const state = await readAnnotationState(record).catch((error) => {
|
|
2397
|
-
if (error.code === "ENOENT")
|
|
2398
|
-
return;
|
|
2399
|
-
throw error;
|
|
2400
|
-
});
|
|
2401
|
-
if (!state)
|
|
2402
|
-
return { comments: [], pending: [] };
|
|
2403
|
-
const execute = options.runtime?.execute ?? executeProcess;
|
|
2404
|
-
const response = await execute("tuicr", ["review", "comments", "--session", state.sessionSlug], record.dir, false);
|
|
2405
|
-
if (response.code !== 0) {
|
|
2406
|
-
if (/not found|no session|deleted/i.test(response.stderr))
|
|
2407
|
-
return { state, comments: [], pending: [] };
|
|
2408
|
-
throw new Error(response.stderr.trim() || "Cannot read tuicr comments.");
|
|
2409
|
-
}
|
|
2410
|
-
let raw;
|
|
2411
|
-
try {
|
|
2412
|
-
raw = JSON.parse(response.stdout);
|
|
2413
|
-
} catch {
|
|
2414
|
-
throw new Error("tuicr returned malformed comment JSON.");
|
|
2415
|
-
}
|
|
2416
|
-
if (!Array.isArray(raw))
|
|
2417
|
-
throw new Error("tuicr comment output must be a JSON array.");
|
|
2418
|
-
const lines = record.source.split(`
|
|
2419
|
-
`);
|
|
2420
|
-
const applied = new Set(state.appliedCommentIds);
|
|
2421
|
-
const comments = raw.map((value, index) => normalizeComment(value, index, lines, applied));
|
|
2422
|
-
const pending = comments.filter((comment) => !comment.applied);
|
|
2423
|
-
return { state, comments: options.includeApplied ? comments : pending, pending };
|
|
2424
|
-
}
|
|
2425
|
-
async function acknowledgePlanAnnotations(record, commentIds, summary) {
|
|
2426
|
-
if (!summary.trim())
|
|
2427
|
-
throw new Error("Annotation acknowledgement summary is required.");
|
|
2428
|
-
return withPlanLock(record.dir, async () => {
|
|
2429
|
-
const state = await readAnnotationState(record);
|
|
2430
|
-
const next = {
|
|
2431
|
-
...state,
|
|
2432
|
-
updatedAt: new Date().toISOString(),
|
|
2433
|
-
appliedCommentIds: [...new Set([...state.appliedCommentIds, ...commentIds])]
|
|
2434
|
-
};
|
|
2435
|
-
await atomicJson(annotationStatePath(record), next);
|
|
2436
|
-
await appendPlanLog(record.logPath, {
|
|
2437
|
-
planRevision: record.document.revision,
|
|
2438
|
-
kind: "annotation",
|
|
2439
|
-
actor: "planner",
|
|
2440
|
-
message: summary,
|
|
2441
|
-
data: { commentIds: [...commentIds], sessionSlug: state.sessionSlug }
|
|
2442
|
-
});
|
|
2443
|
-
return next;
|
|
2444
|
-
}, { operation: "acknowledge annotations" });
|
|
2445
|
-
}
|
|
2446
|
-
function annotationStatePath(record) {
|
|
2447
|
-
return join14(record.dir, "annotations.json");
|
|
2448
|
-
}
|
|
2449
|
-
async function readAnnotationState(record) {
|
|
2450
|
-
const value = JSON.parse(await readFile12(annotationStatePath(record), "utf8"));
|
|
2451
|
-
if (value.schemaVersion !== 1 || typeof value.sessionSlug !== "string" || !Array.isArray(value.appliedCommentIds)) {
|
|
2452
|
-
throw new Error(`Malformed annotation state: ${annotationStatePath(record)}.`);
|
|
2453
|
-
}
|
|
2454
|
-
return value;
|
|
2455
|
-
}
|
|
2456
|
-
function normalizeComment(value, index, lines, applied) {
|
|
2457
|
-
if (!value || typeof value !== "object")
|
|
2458
|
-
throw new Error(`Malformed tuicr comment at index ${index}.`);
|
|
2459
|
-
const raw = value;
|
|
2460
|
-
if (typeof raw.id !== "string" || typeof raw.content !== "string")
|
|
2461
|
-
throw new Error(`Malformed tuicr comment at index ${index}.`);
|
|
2462
|
-
const line = integer(raw.start_line) ?? integer(raw.line);
|
|
2463
|
-
const endLine = integer(raw.end_line) ?? line;
|
|
2464
|
-
const targetPath = typeof raw.path === "string" ? raw.path : undefined;
|
|
2465
|
-
const appliesToPlan = !targetPath || basename5(targetPath) === basename5("PLAN.md");
|
|
2466
|
-
const validAnchor = appliesToPlan && line !== undefined && line > 0 && line <= lines.length;
|
|
2467
|
-
return {
|
|
2468
|
-
id: raw.id,
|
|
2469
|
-
body: raw.content,
|
|
2470
|
-
file: targetPath,
|
|
2471
|
-
line,
|
|
2472
|
-
endLine,
|
|
2473
|
-
context: validAnchor ? lines.slice(line - 1, Math.min(endLine ?? line, lines.length)).join(`
|
|
2474
|
-
`) : undefined,
|
|
2475
|
-
stale: line !== undefined && !validAnchor,
|
|
2476
|
-
applied: applied.has(raw.id)
|
|
2477
|
-
};
|
|
2478
|
-
}
|
|
2479
|
-
async function executeProcess(command, args, cwd, interactive) {
|
|
2480
|
-
if (!interactive) {
|
|
2481
|
-
const result = await run(command, args, { cwd, capture: "unbounded" });
|
|
2482
|
-
return { code: result.code, stdout: result.stdout, stderr: result.stderr };
|
|
2483
|
-
}
|
|
2484
|
-
return new Promise((resolve, reject) => {
|
|
2485
|
-
const child = spawn2(command, args, { cwd, stdio: ["inherit", "inherit", "pipe"] });
|
|
2486
|
-
let stderr = "";
|
|
2487
|
-
child.stderr.on("data", (chunk) => {
|
|
2488
|
-
const text = chunk.toString();
|
|
2489
|
-
stderr += text;
|
|
2490
|
-
process.stderr.write(text);
|
|
2491
|
-
});
|
|
2492
|
-
child.on("error", reject);
|
|
2493
|
-
child.on("close", (code) => resolve({ code: code ?? 1, stdout: "", stderr }));
|
|
2494
|
-
});
|
|
2495
|
-
}
|
|
2496
|
-
async function atomicJson(path, value) {
|
|
2497
|
-
const temp = join14(dirname7(path), `.${basename5(path)}.${crypto.randomUUID()}.tmp`);
|
|
2498
|
-
await writeFile8(temp, `${JSON.stringify(value, null, 2)}
|
|
2499
|
-
`, { mode: 384, flag: "wx" });
|
|
2500
|
-
await rename2(temp, path);
|
|
2501
|
-
}
|
|
2502
|
-
function integer(value) {
|
|
2503
|
-
return typeof value === "number" && Number.isInteger(value) ? value : undefined;
|
|
2504
|
-
}
|
|
2505
|
-
// src/plan/escalation.ts
|
|
2506
|
-
import { z as z5 } from "zod";
|
|
2507
|
-
var escalationSchema = z5.object({
|
|
2508
|
-
planId: z5.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
|
|
2509
|
-
executionId: z5.string().min(1),
|
|
2510
|
-
phaseId: z5.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
|
|
2511
|
-
taskId: z5.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/).optional(),
|
|
2512
|
-
blocker: z5.string().min(1),
|
|
2513
|
-
attempts: z5.array(z5.string()),
|
|
2514
|
-
evidence: z5.array(z5.string()),
|
|
2515
|
-
needsUserDecision: z5.boolean()
|
|
2516
|
-
}).strict();
|
|
2517
|
-
var OPEN = "<diffpi-planner-escalation>";
|
|
2518
|
-
var CLOSE = "</diffpi-planner-escalation>";
|
|
2519
|
-
function renderPlannerEscalation(value) {
|
|
2520
|
-
return `${OPEN}${JSON.stringify(escalationSchema.parse(value))}${CLOSE}`;
|
|
2521
|
-
}
|
|
2522
|
-
function parsePlannerEscalation(output) {
|
|
2523
|
-
const start = output.indexOf(OPEN);
|
|
2524
|
-
const end = output.indexOf(CLOSE, start + OPEN.length);
|
|
2525
|
-
if (start < 0 || end < 0 || output.indexOf(OPEN, start + OPEN.length) >= 0)
|
|
2526
|
-
throw new Error("Missing or ambiguous planner escalation payload.");
|
|
2527
|
-
const payload = output.slice(start + OPEN.length, end);
|
|
2528
|
-
try {
|
|
2529
|
-
return escalationSchema.parse(JSON.parse(payload));
|
|
2530
|
-
} catch (error) {
|
|
2531
|
-
throw new Error(`Malformed planner escalation payload: ${error instanceof Error ? error.message : String(error)}`);
|
|
2532
|
-
}
|
|
2533
|
-
}
|
|
2534
|
-
// src/plan/execution.ts
|
|
2535
|
-
function createExecutionPacket(plan, coordinator) {
|
|
2536
|
-
if (!plan.execution?.active)
|
|
2537
|
-
throw new Error(`Plan ${plan.id} has no active execution.`);
|
|
2538
|
-
return {
|
|
2539
|
-
version: 1,
|
|
2540
|
-
planId: plan.id,
|
|
2541
|
-
executionId: plan.execution.id,
|
|
2542
|
-
policy: plan.execution.policy,
|
|
2543
|
-
cwd: plan.execution.cwd,
|
|
2544
|
-
branch: plan.execution.branch,
|
|
2545
|
-
coordinator
|
|
2546
|
-
};
|
|
2547
|
-
}
|
|
2548
|
-
function renderExecutionPrompt(packet) {
|
|
2549
|
-
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.`;
|
|
2550
|
-
}
|
|
2551
|
-
function eligiblePlanTasks(plan) {
|
|
2552
|
-
const completedPhases = new Set(plan.phases.filter((phase) => phase.status === "completed" || phase.status === "skipped").map((phase) => phase.id));
|
|
2553
|
-
const completedTasks = new Set(plan.phases.flatMap((phase) => phase.tasks).filter((task) => task.status === "completed" || task.status === "skipped").map((task) => task.id));
|
|
2554
|
-
for (const phase of plan.phases) {
|
|
2555
|
-
if (phase.status === "completed" || phase.status === "skipped")
|
|
2556
|
-
continue;
|
|
2557
|
-
if (!phase.dependencies.every((dependency) => completedPhases.has(dependency)))
|
|
2558
|
-
continue;
|
|
2559
|
-
return phase.tasks.filter((task) => task.status === "pending" && task.dependencies.every((dependency) => completedTasks.has(dependency))).map((task) => ({ phase, task }));
|
|
2560
|
-
}
|
|
2561
|
-
return [];
|
|
2562
|
-
}
|
|
2563
|
-
function tasksMayRunInParallel(left, right) {
|
|
2564
|
-
if (!left.fileScopes.length || !right.fileScopes.length)
|
|
2565
|
-
return false;
|
|
2566
|
-
if (left.dependencies.includes(right.id) || right.dependencies.includes(left.id))
|
|
2567
|
-
return false;
|
|
2568
|
-
return left.fileScopes.every((scope) => right.fileScopes.every((other) => !scopesOverlap(scope, other)));
|
|
2569
|
-
}
|
|
2570
|
-
function phaseCommitCommand(policy) {
|
|
2571
|
-
return policy === "commit-per-phase" ? "/git commit --yes --no-push" : undefined;
|
|
2572
|
-
}
|
|
2573
|
-
function scopesOverlap(left, right) {
|
|
2574
|
-
const normalize = (value) => value.replace(/^\.\//, "").replace(/\*.*$/, "").replace(/\/$/, "");
|
|
2575
|
-
const a = normalize(left);
|
|
2576
|
-
const b = normalize(right);
|
|
2577
|
-
return !a || !b || a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`);
|
|
2578
|
-
}
|
|
2579
|
-
// src/plan/transitions.ts
|
|
2580
|
-
var STABLE_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
2581
|
-
var PLAN_TRANSITIONS = {
|
|
2582
|
-
draft: ["ready"],
|
|
2583
|
-
ready: ["draft", "in_progress"],
|
|
2584
|
-
in_progress: ["blocked", "completed"],
|
|
2585
|
-
blocked: ["draft", "ready", "in_progress"],
|
|
2586
|
-
completed: []
|
|
2587
|
-
};
|
|
2588
|
-
var WORK_TRANSITIONS = {
|
|
2589
|
-
pending: ["in_progress", "skipped"],
|
|
2590
|
-
in_progress: ["blocked", "completed"],
|
|
2591
|
-
blocked: ["pending", "in_progress", "skipped"],
|
|
2592
|
-
completed: [],
|
|
2593
|
-
skipped: []
|
|
2594
|
-
};
|
|
2595
|
-
function isStableId(value) {
|
|
2596
|
-
return STABLE_ID.test(value) && value.length <= 80;
|
|
2597
|
-
}
|
|
2598
|
-
function assertStableId(value, label = "identifier") {
|
|
2599
|
-
if (!isStableId(value))
|
|
2600
|
-
throw new Error(`${label} must be a lowercase stable slug, not a path: ${value}`);
|
|
2601
|
-
}
|
|
2602
|
-
function assertPlanTransition(from, to) {
|
|
2603
|
-
if (from === to)
|
|
2604
|
-
return;
|
|
2605
|
-
if (!PLAN_TRANSITIONS[from].includes(to))
|
|
2606
|
-
throw new Error(`Invalid plan status transition: ${from} -> ${to}.`);
|
|
2607
|
-
}
|
|
2608
|
-
function assertPhaseTransition(from, to) {
|
|
2609
|
-
if (from === to)
|
|
2610
|
-
return;
|
|
2611
|
-
if (!WORK_TRANSITIONS[from].includes(to))
|
|
2612
|
-
throw new Error(`Invalid phase status transition: ${from} -> ${to}.`);
|
|
2613
|
-
}
|
|
2614
|
-
function assertTaskTransition(task, to, executionId, actor) {
|
|
2615
|
-
if (task.status !== to && !WORK_TRANSITIONS[task.status].includes(to)) {
|
|
2616
|
-
throw new Error(`Invalid task status transition: ${task.status} -> ${to}.`);
|
|
2617
|
-
}
|
|
2618
|
-
if (task.status === "in_progress" && task.executionId && executionId !== task.executionId) {
|
|
2619
|
-
throw new Error(`Task ${task.id} is owned by execution ${task.executionId}.`);
|
|
2620
|
-
}
|
|
2621
|
-
if (task.status === "in_progress" && task.owner && actor && actor !== task.owner) {
|
|
2622
|
-
throw new Error(`Task ${task.id} is owned by ${task.owner}.`);
|
|
2623
|
-
}
|
|
2624
|
-
}
|
|
2625
|
-
function assertUniqueIds(ids, label) {
|
|
2626
|
-
const seen = new Set;
|
|
2627
|
-
for (const id of ids) {
|
|
2628
|
-
assertStableId(id, label);
|
|
2629
|
-
if (seen.has(id))
|
|
2630
|
-
throw new Error(`Duplicate ${label}: ${id}.`);
|
|
2631
|
-
seen.add(id);
|
|
2632
|
-
}
|
|
2633
|
-
}
|
|
2634
|
-
|
|
2635
|
-
// src/plan/markdown.ts
|
|
2636
|
-
var PLAN_MARKER = /<!-- diffpi-plan: (\{[^\n]+\}) -->/;
|
|
2637
|
-
var PHASE_MARKER = /<!-- diffpi-phase: (\{[^\n]+\}) -->/g;
|
|
2638
|
-
var TASK_MARKER = /<!-- diffpi-task: (\{[^\n]+\}) -->/g;
|
|
2639
|
-
var SUPPORTED_SCHEMA = 1;
|
|
2640
|
-
function countDesignWords(plan) {
|
|
2641
|
-
return [plan.design.bigIdeas, plan.design.keyApiUpdates, plan.design.consequences].join(" ").trim().split(/\s+/).filter(Boolean).length;
|
|
2642
|
-
}
|
|
2643
|
-
function validatePlanDocument(plan, options = {}) {
|
|
2644
|
-
const issues = [];
|
|
2645
|
-
const add = (code, severity, message, path) => issues.push({ code, severity, message, path });
|
|
2646
|
-
try {
|
|
2647
|
-
assertStableId(plan.id, "plan id");
|
|
2648
|
-
assertUniqueIds(plan.phases.map((phase) => phase.id), "phase id");
|
|
2649
|
-
assertUniqueIds(plan.phases.flatMap((phase) => phase.tasks.map((task) => task.id)), "task id");
|
|
2650
|
-
} catch (error) {
|
|
2651
|
-
add("invalid-id", "error", error.message);
|
|
2652
|
-
}
|
|
2653
|
-
if (plan.schemaVersion !== SUPPORTED_SCHEMA)
|
|
2654
|
-
add("schema", "error", `Unsupported plan schema: ${plan.schemaVersion}.`);
|
|
2655
|
-
const phaseIds = new Set(plan.phases.map((phase) => phase.id));
|
|
2656
|
-
const taskIds = new Set(plan.phases.flatMap((phase) => phase.tasks.map((task) => task.id)));
|
|
2657
|
-
for (const phase of plan.phases) {
|
|
2658
|
-
for (const dependency of phase.dependencies) {
|
|
2659
|
-
if (!phaseIds.has(dependency))
|
|
2660
|
-
add("dangling-dependency", "error", `Phase ${phase.id} depends on missing ${dependency}.`);
|
|
2661
|
-
}
|
|
2662
|
-
for (const task of phase.tasks) {
|
|
2663
|
-
for (const dependency of task.dependencies) {
|
|
2664
|
-
if (!taskIds.has(dependency))
|
|
2665
|
-
add("dangling-dependency", "error", `Task ${task.id} depends on missing ${dependency}.`);
|
|
2666
|
-
}
|
|
2667
|
-
if (options.strict && task.acceptanceCriteria.length === 0)
|
|
2668
|
-
add("acceptance", "error", `Task ${task.id} has no acceptance criteria.`, task.id);
|
|
2669
|
-
}
|
|
2670
|
-
if (options.strict && phase.tasks.length === 0)
|
|
2671
|
-
add("empty-phase", "error", `Phase ${phase.id} has no tasks.`, phase.id);
|
|
2672
|
-
}
|
|
2673
|
-
for (const cycle of dependencyCycles(plan))
|
|
2674
|
-
add("dependency-cycle", "error", `Dependency cycle: ${cycle.join(" -> ")}.`);
|
|
2675
|
-
const designWords = countDesignWords(plan);
|
|
2676
|
-
if (designWords > 300)
|
|
2677
|
-
add("design-length", "warning", `Design is ${designWords} words; prefer 300 or fewer.`);
|
|
2678
|
-
if (options.strict && designWords > 800)
|
|
2679
|
-
add("design-too-long", "error", `Design is ${designWords} words; finalization allows at most 800.`);
|
|
2680
|
-
if (options.strict && plan.phases.length === 0)
|
|
2681
|
-
add("no-phases", "error", "Finalization requires at least one phase.");
|
|
2682
|
-
if (options.strict && (options.pendingAnnotations ?? 0) > 0)
|
|
2683
|
-
add("pending-annotations", "error", `${options.pendingAnnotations} annotations remain pending.`);
|
|
2684
|
-
return issues;
|
|
2685
|
-
}
|
|
2686
|
-
function renderPlanDocument(plan, previousSource) {
|
|
2687
|
-
if (previousSource && canPatch(previousSource, plan))
|
|
2688
|
-
return patchMarkers(previousSource, plan);
|
|
2689
|
-
const marker = {
|
|
2690
|
-
schemaVersion: plan.schemaVersion,
|
|
2691
|
-
id: plan.id,
|
|
2692
|
-
revision: plan.revision,
|
|
2693
|
-
branch: plan.branch,
|
|
2694
|
-
status: plan.status,
|
|
2695
|
-
execution: plan.execution,
|
|
2696
|
-
createdAt: plan.createdAt,
|
|
2697
|
-
updatedAt: plan.updatedAt
|
|
2698
|
-
};
|
|
2699
|
-
const requirements = plan.requirements.length ? plan.requirements.map((item) => `- ${item}`).join(`
|
|
2700
|
-
`) : "<!-- Add requirements. -->";
|
|
2701
|
-
const phases = plan.phases.map(renderPhase).join(`
|
|
2702
|
-
|
|
2703
|
-
`);
|
|
2704
|
-
const references = plan.references.length ? plan.references.map((reference) => `- <!-- diffpi-reference: ${json(reference)} --> ${reference.value}`).join(`
|
|
2705
|
-
`) : "<!-- Add references. -->";
|
|
2706
|
-
return `<!-- diffpi-plan: ${json(marker)} -->
|
|
2707
|
-
# ${plan.title}
|
|
2708
|
-
|
|
2709
|
-
- **Plan ID:** ${plan.id}
|
|
2710
|
-
- **Branch:** ${plan.branch}
|
|
2711
|
-
- **Status:** ${plan.status}
|
|
2712
|
-
- **Revision:** ${plan.revision}
|
|
2713
|
-
|
|
2714
|
-
## Intent
|
|
2715
|
-
|
|
2716
|
-
${plan.intent || "<!-- Describe the intended outcome. -->"}
|
|
2717
|
-
|
|
2718
|
-
## Requirements
|
|
2719
|
-
|
|
2720
|
-
${requirements}
|
|
2721
|
-
|
|
2722
|
-
## Design
|
|
2723
|
-
|
|
2724
|
-
### Big Ideas
|
|
2725
|
-
|
|
2726
|
-
${plan.design.bigIdeas || "<!-- Describe the main approach. -->"}
|
|
2727
|
-
|
|
2728
|
-
### Key API Addition/Updates
|
|
2729
|
-
|
|
2730
|
-
${plan.design.keyApiUpdates || "<!-- Describe public API changes. -->"}
|
|
2731
|
-
|
|
2732
|
-
### Consequences
|
|
2733
|
-
|
|
2734
|
-
${plan.design.consequences || "<!-- Describe trade-offs and limitations. -->"}
|
|
2735
|
-
|
|
2736
|
-
## Implementation
|
|
2737
|
-
|
|
2738
|
-
${phases || "<!-- Add phases with plan_add_phase. -->"}
|
|
2739
|
-
|
|
2740
|
-
## References
|
|
2741
|
-
|
|
2742
|
-
${references}
|
|
2743
|
-
`;
|
|
2744
|
-
}
|
|
2745
|
-
function parsePlanDocument(source, ref = "PLAN.md") {
|
|
2746
|
-
const planMatch = source.match(PLAN_MARKER);
|
|
2747
|
-
if (!planMatch)
|
|
2748
|
-
throw new Error(`${ref}: missing or malformed diffpi-plan marker.`);
|
|
2749
|
-
if ((source.match(new RegExp(PLAN_MARKER.source, "g")) ?? []).length !== 1)
|
|
2750
|
-
throw new Error(`${ref}: duplicate diffpi-plan marker.`);
|
|
2751
|
-
const marker = parseMarker(planMatch[1], `${ref} plan`);
|
|
2752
|
-
if (marker.schemaVersion !== SUPPORTED_SCHEMA)
|
|
2753
|
-
throw new Error(`${ref}: unsupported plan schema ${String(marker.schemaVersion)}.`);
|
|
2754
|
-
assertStableId(marker.id, "plan id");
|
|
2755
|
-
const title = source.match(/^# (.+)$/m)?.[1]?.trim();
|
|
2756
|
-
if (!title)
|
|
2757
|
-
throw new Error(`${ref}: missing plan title.`);
|
|
2758
|
-
const phases = parsePhases(section(source, "Implementation"), ref);
|
|
2759
|
-
const document = {
|
|
2760
|
-
...marker,
|
|
2761
|
-
schemaVersion: 1,
|
|
2762
|
-
title,
|
|
2763
|
-
intent: cleanPlaceholder(section(source, "Intent")),
|
|
2764
|
-
requirements: parseBullets(section(source, "Requirements")),
|
|
2765
|
-
design: {
|
|
2766
|
-
bigIdeas: cleanPlaceholder(subsection(source, "Design", "Big Ideas")),
|
|
2767
|
-
keyApiUpdates: cleanPlaceholder(subsection(source, "Design", "Key API Addition/Updates")),
|
|
2768
|
-
consequences: cleanPlaceholder(subsection(source, "Design", "Consequences"))
|
|
2769
|
-
},
|
|
2770
|
-
phases,
|
|
2771
|
-
references: parseReferences(section(source, "References"))
|
|
2772
|
-
};
|
|
2773
|
-
const errors = validatePlanDocument(document).filter((issue) => issue.severity === "error");
|
|
2774
|
-
if (errors.length)
|
|
2775
|
-
throw new Error(`${ref}: ${errors.map((issue) => issue.message).join(" ")}`);
|
|
2776
|
-
return document;
|
|
2777
|
-
}
|
|
2778
|
-
function renderPhase(phase) {
|
|
2779
|
-
const marker = {
|
|
2780
|
-
id: phase.id,
|
|
2781
|
-
revision: phase.revision,
|
|
2782
|
-
status: phase.status,
|
|
2783
|
-
gate: phase.gate,
|
|
2784
|
-
commit: phase.commit,
|
|
2785
|
-
blocker: phase.blocker
|
|
2786
|
-
};
|
|
2787
|
-
const dependencies = phase.dependencies.length ? phase.dependencies.join(", ") : "none";
|
|
2788
|
-
return `<!-- diffpi-phase: ${json(marker)} -->
|
|
2789
|
-
### Phase: ${phase.title}
|
|
2790
|
-
|
|
2791
|
-
**Objective:** ${phase.objective}
|
|
2792
|
-
|
|
2793
|
-
**Dependencies:** ${dependencies}
|
|
2794
|
-
|
|
2795
|
-
${phase.tasks.map(renderTask).join(`
|
|
2796
|
-
|
|
2797
|
-
`)}
|
|
2798
|
-
|
|
2799
|
-
<!-- /diffpi-phase -->`;
|
|
2800
|
-
}
|
|
2801
|
-
function renderTask(task) {
|
|
2802
|
-
const marker = {
|
|
2803
|
-
id: task.id,
|
|
2804
|
-
revision: task.revision,
|
|
2805
|
-
status: task.status,
|
|
2806
|
-
owner: task.owner,
|
|
2807
|
-
executionId: task.executionId,
|
|
2808
|
-
blocker: task.blocker
|
|
2809
|
-
};
|
|
2810
|
-
const checked = task.status === "completed" || task.status === "skipped" ? "x" : " ";
|
|
2811
|
-
return `<!-- diffpi-task: ${json(marker)} -->
|
|
2812
|
-
- [${checked}] **${task.title}**
|
|
2813
|
-
- Steps: ${list(task.steps ?? [])}
|
|
2814
|
-
- Dependencies: ${list(task.dependencies)}
|
|
2815
|
-
- File scopes: ${list(task.fileScopes)}
|
|
2816
|
-
- Acceptance criteria: ${list(task.acceptanceCriteria)}
|
|
2817
|
-
<!-- /diffpi-task -->`;
|
|
2818
|
-
}
|
|
2819
|
-
function parsePhases(input, ref) {
|
|
2820
|
-
const starts = [...input.matchAll(PHASE_MARKER)];
|
|
2821
|
-
const phases = starts.map((match, index) => {
|
|
2822
|
-
const start = match.index;
|
|
2823
|
-
const end = input.indexOf("<!-- /diffpi-phase -->", start);
|
|
2824
|
-
if (end < 0)
|
|
2825
|
-
throw new Error(`${ref}: phase marker has no closing marker.`);
|
|
2826
|
-
const next = starts[index + 1]?.index;
|
|
2827
|
-
if (next !== undefined && next < end)
|
|
2828
|
-
throw new Error(`${ref}: nested or unclosed phase marker.`);
|
|
2829
|
-
const body = input.slice(start + match[0].length, end);
|
|
2830
|
-
const marker = parseMarker(match[1], `${ref} phase`);
|
|
2831
|
-
assertStableId(marker.id, "phase id");
|
|
2832
|
-
const title = body.match(/^### Phase: (.+)$/m)?.[1]?.trim();
|
|
2833
|
-
const objective = body.match(/^\*\*Objective:\*\*\s*(.*)$/m)?.[1]?.trim();
|
|
2834
|
-
if (!title || !objective)
|
|
2835
|
-
throw new Error(`${ref}: phase ${marker.id} is missing title or objective.`);
|
|
2836
|
-
return {
|
|
2837
|
-
...marker,
|
|
2838
|
-
title,
|
|
2839
|
-
objective,
|
|
2840
|
-
dependencies: parseCsv(body.match(/^\*\*Dependencies:\*\*\s*(.*)$/m)?.[1]),
|
|
2841
|
-
tasks: parseTasks(body, ref)
|
|
2842
|
-
};
|
|
2843
|
-
});
|
|
2844
|
-
assertUniqueIds(phases.map((phase) => phase.id), "phase id");
|
|
2845
|
-
return phases;
|
|
2846
|
-
}
|
|
2847
|
-
function parseTasks(input, ref) {
|
|
2848
|
-
const starts = [...input.matchAll(TASK_MARKER)];
|
|
2849
|
-
const tasks = starts.map((match, index) => {
|
|
2850
|
-
const start = match.index;
|
|
2851
|
-
const end = input.indexOf("<!-- /diffpi-task -->", start);
|
|
2852
|
-
if (end < 0)
|
|
2853
|
-
throw new Error(`${ref}: task marker has no closing marker.`);
|
|
2854
|
-
const next = starts[index + 1]?.index;
|
|
2855
|
-
if (next !== undefined && next < end)
|
|
2856
|
-
throw new Error(`${ref}: nested or unclosed task marker.`);
|
|
2857
|
-
const body = input.slice(start + match[0].length, end);
|
|
2858
|
-
const marker = parseMarker(match[1], `${ref} task`);
|
|
2859
|
-
assertStableId(marker.id, "task id");
|
|
2860
|
-
const title = body.match(/^- \[[ xX]\] \*\*(.+)\*\*$/m)?.[1]?.trim();
|
|
2861
|
-
if (!title)
|
|
2862
|
-
throw new Error(`${ref}: task ${marker.id} is missing its checkbox title.`);
|
|
2863
|
-
return {
|
|
2864
|
-
...marker,
|
|
2865
|
-
title,
|
|
2866
|
-
steps: parseListValue(body, "Steps"),
|
|
2867
|
-
dependencies: parseListValue(body, "Dependencies"),
|
|
2868
|
-
fileScopes: parseListValue(body, "File scopes"),
|
|
2869
|
-
acceptanceCriteria: parseListValue(body, "Acceptance criteria")
|
|
2870
|
-
};
|
|
2871
|
-
});
|
|
2872
|
-
assertUniqueIds(tasks.map((task) => task.id), "task id");
|
|
2873
|
-
return tasks;
|
|
2874
|
-
}
|
|
2875
|
-
function parseReferences(input) {
|
|
2876
|
-
return [...input.matchAll(/^- <!-- diffpi-reference: (\{[^\n]+\}) -->\s*(.*)$/gm)].map((match) => {
|
|
2877
|
-
const marker = parseMarker(match[1], "reference");
|
|
2878
|
-
assertStableId(marker.id, "reference id");
|
|
2879
|
-
return { id: marker.id, value: match[2].trim() || marker.value };
|
|
2880
|
-
});
|
|
2881
|
-
}
|
|
2882
|
-
function canPatch(source, next) {
|
|
2883
|
-
try {
|
|
2884
|
-
const old = parsePlanDocument(source);
|
|
2885
|
-
return JSON.stringify(contentShape(old)) === JSON.stringify(contentShape(next));
|
|
2886
|
-
} catch {
|
|
2887
|
-
return false;
|
|
2888
|
-
}
|
|
2889
|
-
}
|
|
2890
|
-
function patchMarkers(source, plan) {
|
|
2891
|
-
const planMarker = {
|
|
2892
|
-
schemaVersion: plan.schemaVersion,
|
|
2893
|
-
id: plan.id,
|
|
2894
|
-
revision: plan.revision,
|
|
2895
|
-
branch: plan.branch,
|
|
2896
|
-
status: plan.status,
|
|
2897
|
-
execution: plan.execution,
|
|
2898
|
-
createdAt: plan.createdAt,
|
|
2899
|
-
updatedAt: plan.updatedAt
|
|
2900
|
-
};
|
|
2901
|
-
let output = source.replace(PLAN_MARKER, `<!-- diffpi-plan: ${json(planMarker)} -->`);
|
|
2902
|
-
output = output.replace(/^- \*\*Status:\*\* .*$/m, `- **Status:** ${plan.status}`);
|
|
2903
|
-
output = output.replace(/^- \*\*Revision:\*\* .*$/m, `- **Revision:** ${plan.revision}`);
|
|
2904
|
-
for (const phase of plan.phases) {
|
|
2905
|
-
const marker = {
|
|
2906
|
-
id: phase.id,
|
|
2907
|
-
revision: phase.revision,
|
|
2908
|
-
status: phase.status,
|
|
2909
|
-
gate: phase.gate,
|
|
2910
|
-
commit: phase.commit,
|
|
2911
|
-
blocker: phase.blocker
|
|
2912
|
-
};
|
|
2913
|
-
output = replaceMarkerById(output, "phase", phase.id, marker);
|
|
2914
|
-
for (const task of phase.tasks) {
|
|
2915
|
-
const taskMarker = {
|
|
2916
|
-
id: task.id,
|
|
2917
|
-
revision: task.revision,
|
|
2918
|
-
status: task.status,
|
|
2919
|
-
owner: task.owner,
|
|
2920
|
-
executionId: task.executionId,
|
|
2921
|
-
blocker: task.blocker
|
|
2922
|
-
};
|
|
2923
|
-
output = replaceMarkerById(output, "task", task.id, taskMarker);
|
|
2924
|
-
const checked = task.status === "completed" || task.status === "skipped" ? "x" : " ";
|
|
2925
|
-
const escaped = escapeRegExp(task.title);
|
|
2926
|
-
output = output.replace(new RegExp(`^- \\[[ xX]\\] \\*\\*${escaped}\\*\\*$`, "m"), `- [${checked}] **${task.title}**`);
|
|
2927
|
-
}
|
|
2928
|
-
}
|
|
2929
|
-
return output;
|
|
2930
|
-
}
|
|
2931
|
-
function replaceMarkerById(source, kind, id, marker) {
|
|
2932
|
-
const pattern = new RegExp(`<!-- diffpi-${kind}: \\{[^\\n]*"id":"${escapeRegExp(id)}"[^\\n]*\\} -->`);
|
|
2933
|
-
if (!pattern.test(source))
|
|
2934
|
-
throw new Error(`Cannot update missing ${kind} marker ${id}.`);
|
|
2935
|
-
return source.replace(pattern, `<!-- diffpi-${kind}: ${json(marker)} -->`);
|
|
2936
|
-
}
|
|
2937
|
-
function contentShape(plan) {
|
|
2938
|
-
return {
|
|
2939
|
-
title: plan.title,
|
|
2940
|
-
branch: plan.branch,
|
|
2941
|
-
intent: plan.intent,
|
|
2942
|
-
requirements: plan.requirements,
|
|
2943
|
-
design: plan.design,
|
|
2944
|
-
references: plan.references,
|
|
2945
|
-
phases: plan.phases.map(({ id, title, objective, dependencies, tasks }) => ({
|
|
2946
|
-
id,
|
|
2947
|
-
title,
|
|
2948
|
-
objective,
|
|
2949
|
-
dependencies,
|
|
2950
|
-
tasks: tasks.map(({ id: taskId, title: taskTitle, steps, dependencies: taskDependencies, fileScopes, acceptanceCriteria }) => ({
|
|
2951
|
-
id: taskId,
|
|
2952
|
-
title: taskTitle,
|
|
2953
|
-
steps,
|
|
2954
|
-
dependencies: taskDependencies,
|
|
2955
|
-
fileScopes,
|
|
2956
|
-
acceptanceCriteria
|
|
2957
|
-
}))
|
|
2958
|
-
}))
|
|
2959
|
-
};
|
|
2960
|
-
}
|
|
2961
|
-
function dependencyCycles(plan) {
|
|
2962
|
-
const graph = new Map;
|
|
2963
|
-
for (const phase of plan.phases)
|
|
2964
|
-
graph.set(phase.id, phase.dependencies);
|
|
2965
|
-
for (const task of plan.phases.flatMap((phase) => phase.tasks))
|
|
2966
|
-
graph.set(task.id, task.dependencies);
|
|
2967
|
-
const cycles = [];
|
|
2968
|
-
const visiting = new Set;
|
|
2969
|
-
const visited = new Set;
|
|
2970
|
-
const walk = (id, path) => {
|
|
2971
|
-
if (visiting.has(id)) {
|
|
2972
|
-
cycles.push([...path.slice(path.indexOf(id)), id]);
|
|
2973
|
-
return;
|
|
2974
|
-
}
|
|
2975
|
-
if (visited.has(id))
|
|
2976
|
-
return;
|
|
2977
|
-
visiting.add(id);
|
|
2978
|
-
for (const dependency of graph.get(id) ?? [])
|
|
2979
|
-
walk(dependency, [...path, id]);
|
|
2980
|
-
visiting.delete(id);
|
|
2981
|
-
visited.add(id);
|
|
2982
|
-
};
|
|
2983
|
-
for (const id of graph.keys())
|
|
2984
|
-
walk(id, []);
|
|
2985
|
-
return cycles;
|
|
2986
|
-
}
|
|
2987
|
-
function section(source, heading) {
|
|
2988
|
-
const match = source.match(new RegExp(`^## ${escapeRegExp(heading)}\\s*$`, "m"));
|
|
2989
|
-
if (!match?.index) {
|
|
2990
|
-
if (match?.index === 0)
|
|
2991
|
-
return "";
|
|
2992
|
-
throw new Error(`Missing required heading: ${heading}.`);
|
|
2993
|
-
}
|
|
2994
|
-
const start = match.index + match[0].length;
|
|
2995
|
-
const rest = source.slice(start);
|
|
2996
|
-
const end = rest.search(/^## /m);
|
|
2997
|
-
return (end < 0 ? rest : rest.slice(0, end)).trim();
|
|
2998
|
-
}
|
|
2999
|
-
function subsection(source, parent, heading) {
|
|
3000
|
-
const body = section(source, parent);
|
|
3001
|
-
const match = body.match(new RegExp(`^### ${escapeRegExp(heading)}\\s*$`, "m"));
|
|
3002
|
-
if (match?.index === undefined)
|
|
3003
|
-
throw new Error(`Missing required heading: ${parent}/${heading}.`);
|
|
3004
|
-
const rest = body.slice(match.index + match[0].length);
|
|
3005
|
-
const end = rest.search(/^### /m);
|
|
3006
|
-
return (end < 0 ? rest : rest.slice(0, end)).trim();
|
|
3007
|
-
}
|
|
3008
|
-
function parseBullets(input) {
|
|
3009
|
-
return [...input.matchAll(/^- (?!<!--)(.+)$/gm)].map((match) => match[1].trim());
|
|
3010
|
-
}
|
|
3011
|
-
function parseListValue(body, label) {
|
|
3012
|
-
const value = body.match(new RegExp(`^ - ${escapeRegExp(label)}:\\s*(.*)$`, "m"))?.[1];
|
|
3013
|
-
return parseCsv(value);
|
|
3014
|
-
}
|
|
3015
|
-
function parseCsv(value) {
|
|
3016
|
-
if (!value || value.trim().toLowerCase() === "none")
|
|
3017
|
-
return [];
|
|
3018
|
-
return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
3019
|
-
}
|
|
3020
|
-
function cleanPlaceholder(value) {
|
|
3021
|
-
return value.replace(/<!--[^]*?-->/g, "").trim();
|
|
3022
|
-
}
|
|
3023
|
-
function parseMarker(value, label) {
|
|
3024
|
-
try {
|
|
3025
|
-
return JSON.parse(value);
|
|
3026
|
-
} catch {
|
|
3027
|
-
throw new Error(`Malformed ${label} marker JSON.`);
|
|
3028
|
-
}
|
|
3029
|
-
}
|
|
3030
|
-
function list(values) {
|
|
3031
|
-
return values.length ? values.join(", ") : "none";
|
|
3032
|
-
}
|
|
3033
|
-
function json(value) {
|
|
3034
|
-
return JSON.stringify(value, (_key, entry) => entry === undefined ? undefined : entry);
|
|
3035
|
-
}
|
|
3036
|
-
function escapeRegExp(value) {
|
|
3037
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3038
|
-
}
|
|
3039
|
-
// src/plan/store.ts
|
|
3040
|
-
import { open, mkdir as mkdir7, readdir as readdir2, readFile as readFile14, realpath as realpath4, rename as rename3, rm as rm2 } from "node:fs/promises";
|
|
3041
|
-
import { basename as basename6, dirname as dirname8, join as join16, resolve as resolve4, sep } from "node:path";
|
|
3042
|
-
|
|
3043
|
-
// src/templates.ts
|
|
3044
|
-
import { readFile as readFile13 } from "node:fs/promises";
|
|
3045
|
-
import { homedir as homedir8 } from "node:os";
|
|
3046
|
-
import { join as join15, normalize } from "node:path";
|
|
3047
|
-
async function loadTemplate(name, options = {}) {
|
|
3048
|
-
const relative = templateRelativePath(name);
|
|
3049
|
-
const userPath = join15(options.homeDir ?? homedir8(), ".difflab", "diffpi", "templates", relative);
|
|
3050
|
-
const bundledPath = join15(options.bundledDir ?? resolveBundledTemplatesDir(), relative);
|
|
3051
|
-
const user = await readOptionalFile(userPath);
|
|
3052
|
-
if (user !== undefined)
|
|
3053
|
-
return { name, path: userPath, source: "user", content: user };
|
|
3054
|
-
const bundled = await readOptionalFile(bundledPath);
|
|
3055
|
-
if (bundled !== undefined)
|
|
3056
|
-
return { name, path: bundledPath, source: "bundled", content: bundled };
|
|
3057
|
-
throw new Error(`Template "${name}" was not found at ${userPath} or ${bundledPath}.`);
|
|
3058
|
-
}
|
|
3059
|
-
function renderTemplate(content, variables) {
|
|
3060
|
-
return content.replace(/\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g, (match, key) => variables[key] ?? match);
|
|
3061
|
-
}
|
|
3062
|
-
function templateRelativePath(name) {
|
|
3063
|
-
const normalized = normalize(name.replaceAll("\\", "/")).replace(/^\.\//, "");
|
|
3064
|
-
if (!normalized || normalized.startsWith("../") || normalized.includes("/../") || normalized.startsWith("/")) {
|
|
3065
|
-
throw new Error(`Invalid template name: ${name}`);
|
|
3066
|
-
}
|
|
3067
|
-
return normalized.endsWith(".md") ? normalized : `${normalized}.md`;
|
|
3068
|
-
}
|
|
3069
|
-
async function readOptionalFile(path) {
|
|
3070
|
-
try {
|
|
3071
|
-
return await readFile13(path, "utf8");
|
|
3072
|
-
} catch (error) {
|
|
3073
|
-
if (error.code === "ENOENT")
|
|
3074
|
-
return;
|
|
3075
|
-
throw error;
|
|
3076
|
-
}
|
|
3077
|
-
}
|
|
3078
|
-
|
|
3079
|
-
// src/plan/store.ts
|
|
3080
|
-
function planRecordName(shortSlug, date = new Date) {
|
|
3081
|
-
const slug = normalizeSlug(shortSlug);
|
|
3082
|
-
return `${date.toISOString().slice(2, 10).replaceAll("-", "")}-${slug}`;
|
|
3083
|
-
}
|
|
3084
|
-
async function resolvePlan(cwd, query, filters = {}, options = {}) {
|
|
3085
|
-
const root = await plansDir(cwd, options.homeDir);
|
|
3086
|
-
const entries = await readdir2(root, { withFileTypes: true });
|
|
3087
|
-
const normalizedQuery = query ? normalizeQuery(query) : undefined;
|
|
3088
|
-
const records = [];
|
|
3089
|
-
for (const entry of entries) {
|
|
3090
|
-
if (!entry.isDirectory() || entry.name === ".tmp" || entry.name === ".lock")
|
|
3091
|
-
continue;
|
|
3092
|
-
if (normalizedQuery && entry.name !== normalizedQuery && stripDate(entry.name) !== normalizedQuery)
|
|
3093
|
-
continue;
|
|
3094
|
-
const dir = join16(root, entry.name);
|
|
3095
|
-
await assertContained(root, dir);
|
|
3096
|
-
try {
|
|
3097
|
-
const record = await readRecord(dir);
|
|
3098
|
-
if (filters.branch && record.document.branch !== filters.branch)
|
|
3099
|
-
continue;
|
|
3100
|
-
if (filters.statuses && !filters.statuses.includes(record.document.status))
|
|
3101
|
-
continue;
|
|
3102
|
-
records.push(record);
|
|
3103
|
-
} catch (error) {
|
|
3104
|
-
if (error.code !== "ENOENT")
|
|
3105
|
-
throw error;
|
|
3106
|
-
}
|
|
3107
|
-
}
|
|
3108
|
-
records.sort((left, right) => left.id.localeCompare(right.id));
|
|
3109
|
-
return { candidates: records, record: records.length === 1 ? records[0] : undefined, ambiguous: records.length > 1 };
|
|
3110
|
-
}
|
|
3111
|
-
function createPlanStore(options = {}) {
|
|
3112
|
-
const now = options.now ?? (() => new Date);
|
|
3113
|
-
return {
|
|
3114
|
-
context(cwd, query, filters) {
|
|
3115
|
-
return resolvePlan(cwd, query, filters, options);
|
|
3116
|
-
},
|
|
3117
|
-
async init(input) {
|
|
3118
|
-
const root = await plansDir(input.cwd, options.homeDir);
|
|
3119
|
-
const id = planRecordName(input.shortSlug, now());
|
|
3120
|
-
const dir = join16(root, id);
|
|
3121
|
-
await assertContained(root, dirname8(dir));
|
|
3122
|
-
try {
|
|
3123
|
-
await mkdir7(dir);
|
|
3124
|
-
} catch (error) {
|
|
3125
|
-
if (error.code === "EEXIST")
|
|
3126
|
-
throw new Error(`Plan ${id} already exists.`);
|
|
3127
|
-
throw error;
|
|
3128
|
-
}
|
|
3129
|
-
try {
|
|
3130
|
-
const timestamp = now().toISOString();
|
|
3131
|
-
const template = await loadTemplate("plan/PLAN", {
|
|
3132
|
-
homeDir: options.homeDir,
|
|
3133
|
-
bundledDir: options.bundledTemplatesDir
|
|
3134
|
-
});
|
|
3135
|
-
const source = renderTemplate(template.content, {
|
|
3136
|
-
id,
|
|
3137
|
-
branch: input.branch,
|
|
3138
|
-
title: input.title?.trim() || titleFromSlug(input.shortSlug),
|
|
3139
|
-
intent: input.intent?.trim() || "<!-- Describe the intended outcome. -->",
|
|
3140
|
-
created_at: timestamp,
|
|
3141
|
-
updated_at: timestamp
|
|
3142
|
-
});
|
|
3143
|
-
const document = parsePlanDocument(source, join16(dir, "PLAN.md"));
|
|
3144
|
-
await atomicWrite(join16(dir, "PLAN.md"), source);
|
|
3145
|
-
await atomicWrite(join16(dir, "logs.txt"), "");
|
|
3146
|
-
await appendPlanLog(join16(dir, "logs.txt"), {
|
|
3147
|
-
planRevision: document.revision,
|
|
3148
|
-
kind: "created",
|
|
3149
|
-
actor: "diffpi",
|
|
3150
|
-
message: `Created plan ${id}.`
|
|
3151
|
-
});
|
|
3152
|
-
return { id, dir, planPath: join16(dir, "PLAN.md"), logPath: join16(dir, "logs.txt"), document, source };
|
|
3153
|
-
} catch (error) {
|
|
3154
|
-
await rm2(dir, { recursive: true, force: true });
|
|
3155
|
-
throw error;
|
|
3156
|
-
}
|
|
3157
|
-
},
|
|
3158
|
-
async read(cwd, query, filters) {
|
|
3159
|
-
const result = await resolvePlan(cwd, query, filters, options);
|
|
3160
|
-
if (!result.record) {
|
|
3161
|
-
if (result.ambiguous)
|
|
3162
|
-
throw new Error(`Plan query "${query}" is ambiguous: ${result.candidates.map((item) => item.id).join(", ")}.`);
|
|
3163
|
-
throw new Error(`Plan "${query}" was not found.`);
|
|
3164
|
-
}
|
|
3165
|
-
return result.record;
|
|
3166
|
-
},
|
|
3167
|
-
async mutate(cwd, query, operation, update) {
|
|
3168
|
-
const initial = await this.read(cwd, query);
|
|
3169
|
-
return withPlanLock(initial.dir, async () => {
|
|
3170
|
-
const current = await readRecord(initial.dir);
|
|
3171
|
-
const next = await update(structuredClone(current.document));
|
|
3172
|
-
if (next.id !== current.id)
|
|
3173
|
-
throw new Error("A plan mutation cannot change the plan ID.");
|
|
3174
|
-
const document = {
|
|
3175
|
-
...next,
|
|
3176
|
-
revision: current.document.revision + 1,
|
|
3177
|
-
updatedAt: now().toISOString()
|
|
3178
|
-
};
|
|
3179
|
-
const source = renderPlanDocument(document, current.source);
|
|
3180
|
-
parsePlanDocument(source, current.planPath);
|
|
3181
|
-
await atomicWrite(current.planPath, source);
|
|
3182
|
-
return { ...current, document, source };
|
|
3183
|
-
}, { operation });
|
|
3184
|
-
},
|
|
3185
|
-
async log(cwd, query, event) {
|
|
3186
|
-
const record = await this.read(cwd, query);
|
|
3187
|
-
return withPlanLock(record.dir, () => appendPlanLog(record.logPath, event), { operation: "append log" });
|
|
3188
|
-
}
|
|
3189
|
-
};
|
|
3190
|
-
}
|
|
3191
|
-
async function readRecord(dir) {
|
|
3192
|
-
const planPath = join16(dir, "PLAN.md");
|
|
3193
|
-
const logPath = join16(dir, "logs.txt");
|
|
3194
|
-
const source = await readFile14(planPath, "utf8");
|
|
3195
|
-
const document = parsePlanDocument(source, planPath);
|
|
3196
|
-
return { id: basename6(dir), dir, planPath, logPath, document, source };
|
|
3197
|
-
}
|
|
3198
|
-
async function atomicWrite(path, content) {
|
|
3199
|
-
const temp = join16(dirname8(path), `.${basename6(path)}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
3200
|
-
const file = await open(temp, "wx", 384);
|
|
3201
|
-
try {
|
|
3202
|
-
await file.writeFile(content, "utf8");
|
|
3203
|
-
await file.sync();
|
|
3204
|
-
} finally {
|
|
3205
|
-
await file.close();
|
|
3206
|
-
}
|
|
3207
|
-
await rename3(temp, path);
|
|
3208
|
-
try {
|
|
3209
|
-
const directory = await open(dirname8(path), "r");
|
|
3210
|
-
try {
|
|
3211
|
-
await directory.sync();
|
|
3212
|
-
} finally {
|
|
3213
|
-
await directory.close();
|
|
3214
|
-
}
|
|
3215
|
-
} catch {}
|
|
3216
|
-
}
|
|
3217
|
-
async function assertContained(root, candidate) {
|
|
3218
|
-
const canonicalRoot = await realpath4(root);
|
|
3219
|
-
let canonicalCandidate;
|
|
3220
|
-
try {
|
|
3221
|
-
canonicalCandidate = await realpath4(candidate);
|
|
3222
|
-
} catch {
|
|
3223
|
-
canonicalCandidate = resolve4(candidate);
|
|
3224
|
-
}
|
|
3225
|
-
if (canonicalCandidate !== canonicalRoot && !canonicalCandidate.startsWith(`${canonicalRoot}${sep}`)) {
|
|
3226
|
-
throw new Error(`Plan path escapes the shared store: ${candidate}.`);
|
|
3227
|
-
}
|
|
3228
|
-
}
|
|
3229
|
-
function normalizeSlug(value) {
|
|
3230
|
-
if (!value || value.includes("\x00") || value.includes("/") || value.includes("\\") || value.includes("..")) {
|
|
3231
|
-
throw new Error(`Invalid plan slug: ${value}.`);
|
|
3232
|
-
}
|
|
3233
|
-
const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3234
|
-
assertStableId(slug, "plan slug");
|
|
3235
|
-
return slug;
|
|
3236
|
-
}
|
|
3237
|
-
function normalizeQuery(value) {
|
|
3238
|
-
if (!value || value.includes("\x00") || value.includes("/") || value.includes("\\") || value.includes("..")) {
|
|
3239
|
-
throw new Error(`Invalid plan query: ${value}.`);
|
|
3240
|
-
}
|
|
3241
|
-
return value.toLowerCase();
|
|
3242
|
-
}
|
|
3243
|
-
function stripDate(value) {
|
|
3244
|
-
return value.replace(/^\d{6}-/, "");
|
|
3245
|
-
}
|
|
3246
|
-
function titleFromSlug(value) {
|
|
3247
|
-
return normalizeSlug(value).replace(/^(?:[a-z]+-\d+-)/, "").split("-").map((part) => part[0].toUpperCase() + part.slice(1)).join(" ");
|
|
3248
|
-
}
|
|
3249
2240
|
// src/setup.ts
|
|
3250
2241
|
import { parseFrontmatter as parseFrontmatter3 } from "@earendil-works/pi-coding-agent";
|
|
3251
|
-
import { readdir as
|
|
3252
|
-
import { homedir as
|
|
3253
|
-
import { basename as
|
|
2242
|
+
import { readdir as readdir2, readFile as readFile9 } from "node:fs/promises";
|
|
2243
|
+
import { homedir as homedir8 } from "node:os";
|
|
2244
|
+
import { basename as basename5, join as join12 } from "node:path";
|
|
3254
2245
|
var MISE_DEPENDENCIES = [
|
|
3255
2246
|
{ name: "node", tool: "node", spec: "node@22", minimumVersion: "22.19.0" },
|
|
3256
2247
|
{ name: "zellij", tool: "zellij", spec: "zellij@latest", minimumVersion: undefined },
|
|
@@ -3288,8 +2279,8 @@ var FORGE_DEPENDENCIES = {
|
|
|
3288
2279
|
gitlab: { name: "glab", tool: "glab", spec: "glab@latest", minimumVersion: undefined }
|
|
3289
2280
|
};
|
|
3290
2281
|
async function ensureMise(options = {}) {
|
|
3291
|
-
const homeDir = options.homeDir ??
|
|
3292
|
-
const current = await mise.executableCheck() ?? await mise.executableCheck(
|
|
2282
|
+
const homeDir = options.homeDir ?? homedir8();
|
|
2283
|
+
const current = await mise.executableCheck() ?? await mise.executableCheck(join12(homeDir, ".local", "bin", "mise"));
|
|
3293
2284
|
if (current)
|
|
3294
2285
|
return { executable: current, action: createSetupAction("mise", "ready", current) };
|
|
3295
2286
|
reportProgress(options, "Installing mise");
|
|
@@ -3338,9 +2329,9 @@ async function ensureMiseDeps(miseExecutable, options = {}) {
|
|
|
3338
2329
|
async function ensurePiPlugins(options = {}) {
|
|
3339
2330
|
const actions = await ensurePiPackages(PI_PACKAGES, options);
|
|
3340
2331
|
const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
|
|
3341
|
-
const webSearch = await pi.configEnsure(
|
|
2332
|
+
const webSearch = await pi.configEnsure(join12(agentDir, "web-search.json"), (config) => ({ ...config, workflow: "auto-summary" }), options.dryRun);
|
|
3342
2333
|
actions.push(getConfigSetupAction("web search settings", webSearch));
|
|
3343
|
-
const lsp = await pi.configEnsure(
|
|
2334
|
+
const lsp = await pi.configEnsure(join12(agentDir, "pi-lsp.json"), (config) => ({
|
|
3344
2335
|
...config,
|
|
3345
2336
|
progressive: { ...getRecord(config.progressive), enabled: true, inject: "none" }
|
|
3346
2337
|
}), options.dryRun);
|
|
@@ -3351,11 +2342,11 @@ async function ensurePiAgents(options = {}) {
|
|
|
3351
2342
|
const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
|
|
3352
2343
|
const bundledAgentsDir = options.bundledAgentsDir ?? BUNDLED_AGENTS_DIR2;
|
|
3353
2344
|
const userConfig = await loadDiffpiConfig({ homeDir: options.homeDir });
|
|
3354
|
-
const entries = (await
|
|
2345
|
+
const entries = (await readdir2(bundledAgentsDir, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.startsWith("diffpi-") && entry.name.endsWith(".md")).sort((left, right) => left.name.localeCompare(right.name));
|
|
3355
2346
|
const actions = [];
|
|
3356
2347
|
for (const entry of entries) {
|
|
3357
|
-
const id =
|
|
3358
|
-
const source = await
|
|
2348
|
+
const id = basename5(entry.name, ".md").replace(/^diffpi-/, "");
|
|
2349
|
+
const source = await readFile9(join12(bundledAgentsDir, entry.name), "utf8");
|
|
3359
2350
|
const content = materializeAgentModels(source, id, userConfig.config, options.availableModels);
|
|
3360
2351
|
const result = await pi.agentEnsure(entry.name, content, agentDir, options.dryRun);
|
|
3361
2352
|
actions.push(getConfigSetupAction(`pi agent ${id}`, result));
|
|
@@ -3364,7 +2355,7 @@ async function ensurePiAgents(options = {}) {
|
|
|
3364
2355
|
}
|
|
3365
2356
|
async function ensurePiSkills(miseExecutable, options = {}) {
|
|
3366
2357
|
const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
|
|
3367
|
-
const sharedSkillsDir =
|
|
2358
|
+
const sharedSkillsDir = join12(options.homeDir ?? homedir8(), ".agents", "skills");
|
|
3368
2359
|
const actions = [];
|
|
3369
2360
|
for (const source of PI_SKILL_SOURCES) {
|
|
3370
2361
|
const missing = [];
|
|
@@ -3445,8 +2436,7 @@ function setupRequiresRestart(actions) {
|
|
|
3445
2436
|
async function ensureZedIntegration(options = {}) {
|
|
3446
2437
|
if (options.dryRun) {
|
|
3447
2438
|
const actions = [
|
|
3448
|
-
createSetupAction("Zed review tasks", "planned", "global static runtime-resolver tasks in tasks.json")
|
|
3449
|
-
createSetupAction("Zed plan task", "planned", "pinned package CLI task in tasks.json")
|
|
2439
|
+
createSetupAction("Zed review tasks", "planned", "global static runtime-resolver tasks in tasks.json")
|
|
3450
2440
|
];
|
|
3451
2441
|
if (options.bindZedKey)
|
|
3452
2442
|
actions.push(createSetupAction("Zed review keybinding", "planned", "keymap.json"));
|
|
@@ -3456,8 +2446,6 @@ async function ensureZedIntegration(options = {}) {
|
|
|
3456
2446
|
try {
|
|
3457
2447
|
const tasks = await ensureZedReviewTask(options.homeDir);
|
|
3458
2448
|
actions.push(createSetupAction("Zed review tasks", tasks.changed ? "installed" : "ready", tasks.path));
|
|
3459
|
-
const planTask = await ensureZedPlanTask(await packageVersion2(), options.homeDir);
|
|
3460
|
-
actions.push(createSetupAction("Zed plan task", planTask.changed ? "installed" : "ready", planTask.path));
|
|
3461
2449
|
} catch (error) {
|
|
3462
2450
|
actions.push(createSetupAction("Zed review tasks", "skipped", error instanceof Error ? error.message : String(error)));
|
|
3463
2451
|
}
|
|
@@ -3559,45 +2547,61 @@ function createSetupAction(name, status, detail) {
|
|
|
3559
2547
|
function reportProgress(options, message) {
|
|
3560
2548
|
options.onProgress?.(message);
|
|
3561
2549
|
}
|
|
3562
|
-
async function packageVersion2() {
|
|
3563
|
-
const source = await readFile15(join17(resolveBundledAgentsDir(), "..", "package.json"), "utf8");
|
|
3564
|
-
const value = JSON.parse(source);
|
|
3565
|
-
if (typeof value.version !== "string")
|
|
3566
|
-
throw new Error("Cannot resolve the installed @difflab/pi version.");
|
|
3567
|
-
return value.version;
|
|
3568
|
-
}
|
|
3569
2550
|
function getRecord(value) {
|
|
3570
2551
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
3571
2552
|
}
|
|
2553
|
+
// src/templates.ts
|
|
2554
|
+
import { readFile as readFile10 } from "node:fs/promises";
|
|
2555
|
+
import { homedir as homedir9 } from "node:os";
|
|
2556
|
+
import { join as join13, normalize } from "node:path";
|
|
2557
|
+
async function loadTemplate(name, options = {}) {
|
|
2558
|
+
const relative = templateRelativePath(name);
|
|
2559
|
+
const userPath = join13(options.homeDir ?? homedir9(), ".difflab", "diffpi", "templates", relative);
|
|
2560
|
+
const bundledPath = join13(options.bundledDir ?? resolveBundledTemplatesDir(), relative);
|
|
2561
|
+
const user = await readOptionalFile(userPath);
|
|
2562
|
+
if (user !== undefined)
|
|
2563
|
+
return { name, path: userPath, source: "user", content: user };
|
|
2564
|
+
const bundled = await readOptionalFile(bundledPath);
|
|
2565
|
+
if (bundled !== undefined)
|
|
2566
|
+
return { name, path: bundledPath, source: "bundled", content: bundled };
|
|
2567
|
+
throw new Error(`Template "${name}" was not found at ${userPath} or ${bundledPath}.`);
|
|
2568
|
+
}
|
|
2569
|
+
function renderTemplate(content, variables) {
|
|
2570
|
+
return content.replace(/\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g, (match, key) => variables[key] ?? match);
|
|
2571
|
+
}
|
|
2572
|
+
function templateRelativePath(name) {
|
|
2573
|
+
const normalized = normalize(name.replaceAll("\\", "/")).replace(/^\.\//, "");
|
|
2574
|
+
if (!normalized || normalized.startsWith("../") || normalized.includes("/../") || normalized.startsWith("/")) {
|
|
2575
|
+
throw new Error(`Invalid template name: ${name}`);
|
|
2576
|
+
}
|
|
2577
|
+
return normalized.endsWith(".md") ? normalized : `${normalized}.md`;
|
|
2578
|
+
}
|
|
2579
|
+
async function readOptionalFile(path) {
|
|
2580
|
+
try {
|
|
2581
|
+
return await readFile10(path, "utf8");
|
|
2582
|
+
} catch (error) {
|
|
2583
|
+
if (error.code === "ENOENT")
|
|
2584
|
+
return;
|
|
2585
|
+
throw error;
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
3572
2588
|
export {
|
|
3573
2589
|
CONVENTIONAL_COMMIT,
|
|
3574
2590
|
GitHubVcsBackend,
|
|
3575
2591
|
GitLabVcsBackend,
|
|
3576
2592
|
ZED_LOCAL_REVIEW_TASK_NAME,
|
|
3577
|
-
ZED_PLAN_ANNOTATE_TASK_NAME,
|
|
3578
2593
|
ZED_PR_REVIEW_TASK_NAME,
|
|
3579
2594
|
ZED_REVIEW_TASK_NAME,
|
|
3580
|
-
acknowledgePlanAnnotations,
|
|
3581
2595
|
addComment,
|
|
3582
|
-
annotatePlan,
|
|
3583
|
-
annotationStatePath,
|
|
3584
|
-
appendPlanLog,
|
|
3585
2596
|
assertGitHubMergeReady,
|
|
3586
|
-
assertPhaseTransition,
|
|
3587
|
-
assertPlanTransition,
|
|
3588
2597
|
assertReviewEventSupported,
|
|
3589
|
-
assertStableId,
|
|
3590
|
-
assertTaskTransition,
|
|
3591
2598
|
checkConventionalSubject,
|
|
3592
2599
|
ciGate,
|
|
3593
2600
|
computeProjectSlug,
|
|
3594
|
-
countDesignWords,
|
|
3595
|
-
createExecutionPacket,
|
|
3596
2601
|
createForge,
|
|
3597
2602
|
createForgeBackend,
|
|
3598
2603
|
createLocalReviewBackend,
|
|
3599
2604
|
createModeController,
|
|
3600
|
-
createPlanStore,
|
|
3601
2605
|
createRemoteReviewBackend,
|
|
3602
2606
|
createVcsBackend,
|
|
3603
2607
|
dedupeFindings,
|
|
@@ -3607,7 +2611,6 @@ export {
|
|
|
3607
2611
|
detectVcs,
|
|
3608
2612
|
diffpiConfigPaths,
|
|
3609
2613
|
discoverAgentModes,
|
|
3610
|
-
eligiblePlanTasks,
|
|
3611
2614
|
ensureMcpAdapters,
|
|
3612
2615
|
ensureMise,
|
|
3613
2616
|
ensureMiseDeps,
|
|
@@ -3616,7 +2619,6 @@ export {
|
|
|
3616
2619
|
ensurePiPlugins,
|
|
3617
2620
|
ensurePiSkills,
|
|
3618
2621
|
ensureStore,
|
|
3619
|
-
ensureZedPlanTask,
|
|
3620
2622
|
ensureZedReviewKeybinding,
|
|
3621
2623
|
ensureZedReviewTask,
|
|
3622
2624
|
findPreferredModel,
|
|
@@ -3635,26 +2637,15 @@ export {
|
|
|
3635
2637
|
mise,
|
|
3636
2638
|
openInNewTab,
|
|
3637
2639
|
parseGitlabDiffRefs,
|
|
3638
|
-
parsePlanDocument,
|
|
3639
|
-
parsePlannerEscalation,
|
|
3640
2640
|
parseRemote,
|
|
3641
2641
|
parseThreadArtifact,
|
|
3642
|
-
phaseCommitCommand,
|
|
3643
2642
|
pi,
|
|
3644
|
-
planRecordName,
|
|
3645
|
-
plansDir,
|
|
3646
|
-
readPlanAnnotations,
|
|
3647
|
-
readPlanLog,
|
|
3648
2643
|
readSession,
|
|
3649
|
-
renderExecutionPrompt,
|
|
3650
|
-
renderPlanDocument,
|
|
3651
|
-
renderPlannerEscalation,
|
|
3652
2644
|
renderReviewDoc,
|
|
3653
2645
|
renderTemplate,
|
|
3654
2646
|
renderThreadArtifact,
|
|
3655
2647
|
resolveAgentMode,
|
|
3656
2648
|
resolveAgentModelPreferences,
|
|
3657
|
-
resolvePlan,
|
|
3658
2649
|
resolvePrSession,
|
|
3659
2650
|
resolveReviewSession,
|
|
3660
2651
|
resolveSession,
|
|
@@ -3671,15 +2662,12 @@ export {
|
|
|
3671
2662
|
severitySchema,
|
|
3672
2663
|
storeDir,
|
|
3673
2664
|
storeGlobalRoot,
|
|
3674
|
-
tasksMayRunInParallel,
|
|
3675
2665
|
templateRelativePath,
|
|
3676
2666
|
toFindings,
|
|
3677
2667
|
toReviewComments,
|
|
3678
2668
|
tuicrAvailable,
|
|
3679
2669
|
unpublishedReviewComments,
|
|
3680
2670
|
upsertThreadReply,
|
|
3681
|
-
validatePlanDocument,
|
|
3682
|
-
withPlanLock,
|
|
3683
2671
|
withRemoteProvenance,
|
|
3684
2672
|
yymmdd,
|
|
3685
2673
|
zedKeymapPath,
|