@difflab/pi 0.4.0 → 0.5.0-rc.202609231313.eaf7fa0
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 +9 -9
- package/agents/diffpi-copilot.md +6 -3
- package/agents/diffpi-orchestrator.md +1 -1
- package/agents/diffpi-planner.md +3 -3
- package/agents/diffpi-reviewer.md +3 -3
- package/agents/diffpi-worker.md +2 -2
- package/dist/assets.d.ts +1 -0
- package/dist/assets.d.ts.map +1 -1
- package/dist/cli/plan.d.ts +7 -3
- package/dist/cli/plan.d.ts.map +1 -1
- package/dist/cli.js +306 -811
- package/dist/commands/plan.d.ts +0 -1
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/commands/review.d.ts +0 -1
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/commands.d.ts +22 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/environment.d.ts +2 -1
- package/dist/environment.d.ts.map +1 -1
- package/dist/extensions/cmdtsx.d.ts +2 -0
- package/dist/extensions/cmdtsx.d.ts.map +1 -0
- package/dist/extensions/fsx.d.ts +8 -0
- package/dist/extensions/fsx.d.ts.map +1 -0
- package/dist/extensions/index.js +5057 -3069
- package/dist/extensions/processx.d.ts +1 -0
- package/dist/extensions/processx.d.ts.map +1 -1
- package/dist/extensions/subagentx.d.ts +1 -1
- package/dist/extensions/subagentx.d.ts.map +1 -1
- package/dist/extensions/tuicrx.d.ts +1 -8
- package/dist/extensions/tuicrx.d.ts.map +1 -1
- package/dist/extensions/zodx.d.ts +8 -0
- package/dist/extensions/zodx.d.ts.map +1 -0
- package/dist/index.d.ts +10 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +352 -1149
- package/dist/plan/index.d.ts +2 -5
- package/dist/plan/index.d.ts.map +1 -1
- package/dist/plan/markdown.d.ts.map +1 -1
- package/dist/plan/operations.d.ts +43 -0
- package/dist/plan/operations.d.ts.map +1 -0
- package/dist/plan/reviews.d.ts +28 -0
- package/dist/plan/reviews.d.ts.map +1 -0
- package/dist/plan/store.d.ts +6 -3
- package/dist/plan/store.d.ts.map +1 -1
- package/dist/plan/types.d.ts +3 -22
- package/dist/plan/types.d.ts.map +1 -1
- package/dist/review/github-review-backend.d.ts.map +1 -1
- package/dist/review/gitlab-review-backend.d.ts.map +1 -1
- package/dist/review/index.d.ts +6 -6
- package/dist/review/index.d.ts.map +1 -1
- package/dist/review/local-reviews.d.ts +31 -0
- package/dist/review/local-reviews.d.ts.map +1 -0
- package/dist/review/review-backend.d.ts +2 -3
- package/dist/review/review-backend.d.ts.map +1 -1
- package/dist/review/review-markdown.d.ts +1 -4
- package/dist/review/review-markdown.d.ts.map +1 -1
- package/dist/review/types.d.ts +0 -28
- package/dist/review/types.d.ts.map +1 -1
- package/dist/store.d.ts +0 -2
- package/dist/store.d.ts.map +1 -1
- package/dist/tools/ci.d.ts +6 -0
- package/dist/tools/ci.d.ts.map +1 -0
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +1887 -2805
- package/dist/tools/log.d.ts.map +1 -1
- package/dist/tools/modes.d.ts.map +1 -1
- package/dist/tools/plan-schema.d.ts +211 -0
- package/dist/tools/plan-schema.d.ts.map +1 -0
- package/dist/tools/plan.d.ts +2 -2
- package/dist/tools/plan.d.ts.map +1 -1
- package/dist/tools/review.d.ts +0 -30
- package/dist/tools/review.d.ts.map +1 -1
- package/dist/workflow.d.ts +80 -0
- package/dist/workflow.d.ts.map +1 -0
- package/package.json +4 -2
- package/skills/plan/SKILL.md +15 -7
- package/skills/review/SKILL.md +6 -8
- package/templates/plan/PLAN.md +2 -0
- package/workflows/plan/annotate.md +13 -0
- package/workflows/plan/finalize.md +15 -0
- package/workflows/plan/go.md +20 -0
- package/{skills/plan/references/workflows → workflows/plan}/help.md +11 -4
- package/workflows/plan/init.md +13 -0
- package/workflows/plan/new.md +20 -0
- package/workflows/plan/update.md +18 -0
- package/workflows/review/address.md +8 -0
- package/workflows/review/auto.md +8 -0
- package/workflows/review/complete.md +7 -0
- package/workflows/review/edit.md +7 -0
- package/workflows/review/help.md +14 -0
- package/workflows/review/merge.md +6 -0
- package/workflows/review/new.md +7 -0
- package/workflows/review/publish.md +6 -0
- package/dist/plan/annotations.d.ts +0 -25
- package/dist/plan/annotations.d.ts.map +0 -1
- package/dist/plan/controller.d.ts +0 -65
- package/dist/plan/controller.d.ts.map +0 -1
- package/dist/plan/execution.d.ts +0 -13
- package/dist/plan/execution.d.ts.map +0 -1
- package/dist/plan/ids.d.ts +0 -5
- package/dist/plan/ids.d.ts.map +0 -1
- package/dist/plan/lock.d.ts +0 -11
- package/dist/plan/lock.d.ts.map +0 -1
- package/dist/plan/log.d.ts +0 -6
- package/dist/plan/log.d.ts.map +0 -1
- package/dist/plan/parser.d.ts +0 -18
- package/dist/plan/parser.d.ts.map +0 -1
- package/dist/review/local-review-backend.d.ts +0 -3
- package/dist/review/local-review-backend.d.ts.map +0 -1
- package/dist/review/review-state.d.ts +0 -20
- package/dist/review/review-state.d.ts.map +0 -1
- 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 -8
- package/skills/plan/references/workflows/init.md +0 -6
- package/skills/plan/references/workflows/new.md +0 -8
- package/skills/plan/references/workflows/update.md +0 -7
- package/skills/review/references/workflows/address.md +0 -11
- package/skills/review/references/workflows/auto.md +0 -18
- package/skills/review/references/workflows/complete.md +0 -7
- package/skills/review/references/workflows/edit.md +0 -7
- package/skills/review/references/workflows/help.md +0 -30
- package/skills/review/references/workflows/merge.md +0 -6
- package/skills/review/references/workflows/new.md +0 -19
- package/skills/review/references/workflows/open.md +0 -3
- package/skills/review/references/workflows/publish.md +0 -8
package/dist/index.js
CHANGED
|
@@ -176,10 +176,19 @@ var require_extend = __commonJS(function(exports, module) {
|
|
|
176
176
|
|
|
177
177
|
// src/config.ts
|
|
178
178
|
import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
179
|
-
import { z } from "zod";
|
|
179
|
+
import { z as z2 } from "zod";
|
|
180
180
|
import { homedir } from "node:os";
|
|
181
181
|
import { join } from "node:path";
|
|
182
182
|
|
|
183
|
+
// src/extensions/zodx.ts
|
|
184
|
+
import { z } from "zod";
|
|
185
|
+
var zx = {
|
|
186
|
+
cwd: z.string().optional(),
|
|
187
|
+
id: z.string().min(1).max(80).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
|
|
188
|
+
revision: z.number().int().nonnegative(),
|
|
189
|
+
text: z.string().trim().min(1)
|
|
190
|
+
};
|
|
191
|
+
|
|
183
192
|
// src/fsx.ts
|
|
184
193
|
import { readdir, readFile } from "node:fs/promises";
|
|
185
194
|
async function readDirectoryIfExists(path) {
|
|
@@ -205,12 +214,12 @@ function isMissingPath(error) {
|
|
|
205
214
|
}
|
|
206
215
|
|
|
207
216
|
// src/config.ts
|
|
208
|
-
var modelReferenceSchema =
|
|
209
|
-
var agentConfigSchema =
|
|
210
|
-
models:
|
|
217
|
+
var modelReferenceSchema = zx.text;
|
|
218
|
+
var agentConfigSchema = z2.object({
|
|
219
|
+
models: z2.array(modelReferenceSchema).optional()
|
|
211
220
|
}).strict();
|
|
212
|
-
var diffpiConfigSchema =
|
|
213
|
-
agents:
|
|
221
|
+
var diffpiConfigSchema = z2.object({
|
|
222
|
+
agents: z2.record(z2.string(), agentConfigSchema).optional()
|
|
214
223
|
}).strict();
|
|
215
224
|
function diffpiConfigPaths(homeDir = homedir()) {
|
|
216
225
|
const directory = join(homeDir, ".difflab", "diffpi");
|
|
@@ -320,7 +329,11 @@ function run(command, args, options = {}) {
|
|
|
320
329
|
const child = spawn(command, args, {
|
|
321
330
|
cwd: options.cwd,
|
|
322
331
|
env: options.env ?? process.env,
|
|
323
|
-
stdio: [
|
|
332
|
+
stdio: [
|
|
333
|
+
options.interactive ? "inherit" : options.input === undefined ? "ignore" : "pipe",
|
|
334
|
+
options.interactive ? "inherit" : "pipe",
|
|
335
|
+
"pipe"
|
|
336
|
+
],
|
|
324
337
|
signal: options.signal
|
|
325
338
|
});
|
|
326
339
|
let stdout = "";
|
|
@@ -337,6 +350,8 @@ function run(command, args, options = {}) {
|
|
|
337
350
|
});
|
|
338
351
|
child.stderr?.on("data", (chunk) => {
|
|
339
352
|
const text = chunk.toString();
|
|
353
|
+
if (options.interactive)
|
|
354
|
+
process.stderr.write(text);
|
|
340
355
|
if (unbounded)
|
|
341
356
|
stderrChunks.push(text);
|
|
342
357
|
else
|
|
@@ -588,6 +603,29 @@ function parseRemote(remote) {
|
|
|
588
603
|
function diffpiLaunchName(cwd, workflow) {
|
|
589
604
|
return `diffpi: ${basename(cwd)} / ${workflow}`;
|
|
590
605
|
}
|
|
606
|
+
async function openFileAdjacent(path, opts) {
|
|
607
|
+
const env = opts.env ?? process.env;
|
|
608
|
+
const ide = detectIde(env);
|
|
609
|
+
if (ide !== "unknown") {
|
|
610
|
+
const ideCommand = { zed: "zed", vscode: "code", cursor: "cursor", windsurf: "windsurf", jetbrains: "idea" }[ide];
|
|
611
|
+
if (await findExecutable(ideCommand)) {
|
|
612
|
+
const result = await run(ideCommand, [path], { cwd: opts.cwd, env });
|
|
613
|
+
if (result.code === 0)
|
|
614
|
+
return { launched: true, via: ide, command: `${ideCommand} ${path}` };
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
const mux = detectMux(env);
|
|
618
|
+
if (mux !== "none") {
|
|
619
|
+
const editor = await resolveEditor(env);
|
|
620
|
+
if (editor) {
|
|
621
|
+
const command = [editor, path];
|
|
622
|
+
const opened = await openMuxTab(mux, command, opts.cwd, opts.name ?? "plan", command.join(" "));
|
|
623
|
+
if (opened)
|
|
624
|
+
return opened;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
return { launched: false, via: "print", command: path, reason: "No supported IDE or multiplexer was available." };
|
|
628
|
+
}
|
|
591
629
|
async function openInNewTab(command, opts) {
|
|
592
630
|
const env = opts.env ?? process.env;
|
|
593
631
|
const name = opts.name ?? "review";
|
|
@@ -620,6 +658,15 @@ async function openInNewTab(command, opts) {
|
|
|
620
658
|
function screenWindowArgs(command, cwd, name) {
|
|
621
659
|
return ["-X", "screen", "-t", name, "sh", "-lc", 'cd -- "$1" && shift && exec "$@"', "sh", cwd, ...command];
|
|
622
660
|
}
|
|
661
|
+
async function resolveEditor(env) {
|
|
662
|
+
const configured = (env.VISUAL ?? env.EDITOR)?.trim().split(/\s+/)[0];
|
|
663
|
+
if (configured && await findExecutable(configured))
|
|
664
|
+
return configured;
|
|
665
|
+
for (const candidate of ["hx", "nvim", "vim"])
|
|
666
|
+
if (await findExecutable(candidate))
|
|
667
|
+
return candidate;
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
623
670
|
async function packageVersion() {
|
|
624
671
|
let value;
|
|
625
672
|
try {
|
|
@@ -1232,16 +1279,16 @@ async function discoverMiseTasks(cwd) {
|
|
|
1232
1279
|
return parseMiseTasks(result.stdout);
|
|
1233
1280
|
}
|
|
1234
1281
|
// src/review/types.ts
|
|
1235
|
-
import { z as
|
|
1236
|
-
var severitySchema =
|
|
1237
|
-
var findingSchema =
|
|
1238
|
-
file:
|
|
1239
|
-
line:
|
|
1282
|
+
import { z as z3 } from "zod";
|
|
1283
|
+
var severitySchema = z3.enum(["BLOCKING", "CONSIDER", "NOTE"]);
|
|
1284
|
+
var findingSchema = z3.object({
|
|
1285
|
+
file: z3.string().min(1),
|
|
1286
|
+
line: z3.number().int().nonnegative(),
|
|
1240
1287
|
severity: severitySchema,
|
|
1241
|
-
body:
|
|
1242
|
-
reference:
|
|
1288
|
+
body: z3.string().min(1),
|
|
1289
|
+
reference: z3.string().optional().default("")
|
|
1243
1290
|
});
|
|
1244
|
-
var findingsSchema =
|
|
1291
|
+
var findingsSchema = z3.array(findingSchema);
|
|
1245
1292
|
function reviewSlug(input) {
|
|
1246
1293
|
return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50);
|
|
1247
1294
|
}
|
|
@@ -1254,9 +1301,6 @@ function yymmdd(date = new Date) {
|
|
|
1254
1301
|
function reviewRecordName(target, date = new Date) {
|
|
1255
1302
|
return `${yymmdd(date)}-${reviewSlug(target) || "uncommitted"}`;
|
|
1256
1303
|
}
|
|
1257
|
-
function isLocalResponse(body) {
|
|
1258
|
-
return /^<!-- diffpi(?:-local-response:[A-Za-z0-9_-]+)? -->\n/m.test(body);
|
|
1259
|
-
}
|
|
1260
1304
|
function dedupeFindings(findings) {
|
|
1261
1305
|
const rank = { BLOCKING: 3, CONSIDER: 2, NOTE: 1 };
|
|
1262
1306
|
const byKey = new Map;
|
|
@@ -1302,18 +1346,6 @@ function renderCommentBody(finding) {
|
|
|
1302
1346
|
return `${prefix}${finding.body}${reference}`;
|
|
1303
1347
|
}
|
|
1304
1348
|
// src/review/review-markdown.ts
|
|
1305
|
-
import { z as z3 } from "zod";
|
|
1306
|
-
var reviewThreadRecordSchema = z3.object({
|
|
1307
|
-
id: z3.string().min(1),
|
|
1308
|
-
file: z3.string().nullable().optional().transform((value) => value ?? undefined),
|
|
1309
|
-
line: z3.number().int().positive().nullable().optional().transform((value) => value ?? undefined),
|
|
1310
|
-
body: z3.string(),
|
|
1311
|
-
author: z3.string().optional(),
|
|
1312
|
-
resolved: z3.boolean(),
|
|
1313
|
-
addressed: z3.boolean().optional(),
|
|
1314
|
-
question: z3.boolean(),
|
|
1315
|
-
replies: z3.array(z3.string()).optional()
|
|
1316
|
-
});
|
|
1317
1349
|
function renderReviewDoc(input) {
|
|
1318
1350
|
const anchored = dedupeFindings(input.findings).filter((finding) => finding.line > 0);
|
|
1319
1351
|
const lines = [`# Review: ${input.title}`, "", "## Metadata"];
|
|
@@ -1355,88 +1387,6 @@ function renderReviewDoc(input) {
|
|
|
1355
1387
|
`).trimEnd()}
|
|
1356
1388
|
`;
|
|
1357
1389
|
}
|
|
1358
|
-
function renderThreadArtifact(title, target, threads, options = {}) {
|
|
1359
|
-
const records = threads.map((thread) => ({
|
|
1360
|
-
id: thread.id,
|
|
1361
|
-
file: thread.file,
|
|
1362
|
-
line: thread.line,
|
|
1363
|
-
body: thread.body,
|
|
1364
|
-
author: thread.author,
|
|
1365
|
-
resolved: thread.resolved,
|
|
1366
|
-
addressed: thread.addressed,
|
|
1367
|
-
question: thread.question,
|
|
1368
|
-
replies: thread.replies
|
|
1369
|
-
}));
|
|
1370
|
-
const payload = Buffer.from(JSON.stringify(records), "utf8").toString("base64url");
|
|
1371
|
-
const lines = [
|
|
1372
|
-
`<!-- diffpi-threads:${payload} -->`,
|
|
1373
|
-
`# Review threads: ${title}`,
|
|
1374
|
-
"",
|
|
1375
|
-
"## Metadata",
|
|
1376
|
-
"",
|
|
1377
|
-
`- Target: ${target}`,
|
|
1378
|
-
`- Pulled: ${options.timestamp ?? new Date().toISOString()}`
|
|
1379
|
-
];
|
|
1380
|
-
if (options.number !== undefined)
|
|
1381
|
-
lines.push(`- PR/MR: #${options.number}${options.url ? ` — ${options.url}` : ""}`);
|
|
1382
|
-
lines.push("", "## Replies", "");
|
|
1383
|
-
for (const thread of threads) {
|
|
1384
|
-
const id = Buffer.from(thread.id, "utf8").toString("base64url");
|
|
1385
|
-
lines.push(`### ${thread.file ?? "review"}:${thread.line ?? "n/a"} (${thread.id})`, "", `<!-- diffpi-reply-start:${id} -->`, thread.reply ?? "", `<!-- diffpi-reply-end:${id} -->`, "");
|
|
1386
|
-
}
|
|
1387
|
-
lines.push("## Source comments", "");
|
|
1388
|
-
for (const thread of threads) {
|
|
1389
|
-
lines.push(`### ${thread.file ?? "review"}:${thread.line ?? "n/a"} — ${thread.author ?? "unknown"}`, "", `Thread: ${thread.id}`, `Status: ${thread.resolved ? "resolved" : thread.addressed ? "addressed" : "open"}`, "", thread.body, "", "---", "");
|
|
1390
|
-
}
|
|
1391
|
-
return `${lines.join(`
|
|
1392
|
-
`).trimEnd()}
|
|
1393
|
-
`;
|
|
1394
|
-
}
|
|
1395
|
-
function parseThreadArtifact(content) {
|
|
1396
|
-
const payload = content.match(/^<!-- diffpi-threads:([A-Za-z0-9_-]+) -->$/m)?.[1];
|
|
1397
|
-
if (!payload)
|
|
1398
|
-
throw new Error("This file is not a Diffpi thread artifact.");
|
|
1399
|
-
let threads;
|
|
1400
|
-
try {
|
|
1401
|
-
const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
1402
|
-
threads = z3.array(reviewThreadRecordSchema).parse(decoded);
|
|
1403
|
-
} catch {
|
|
1404
|
-
throw new Error("Cannot parse the Diffpi thread artifact payload.");
|
|
1405
|
-
}
|
|
1406
|
-
const replies = content.split(/^## Source comments$/m, 1)[0] ?? "";
|
|
1407
|
-
return threads.map((thread) => {
|
|
1408
|
-
const id = Buffer.from(thread.id, "utf8").toString("base64url");
|
|
1409
|
-
const startMarker = `<!-- diffpi-reply-start:${id} -->
|
|
1410
|
-
`;
|
|
1411
|
-
const endMarker = `
|
|
1412
|
-
<!-- diffpi-reply-end:${id} -->`;
|
|
1413
|
-
const start = replies.indexOf(startMarker);
|
|
1414
|
-
const end = start < 0 ? -1 : replies.indexOf(endMarker, start + startMarker.length);
|
|
1415
|
-
const reply = start >= 0 && end >= 0 ? replies.slice(start + startMarker.length, end).trim() : "";
|
|
1416
|
-
return reply ? { ...thread, reply } : thread;
|
|
1417
|
-
});
|
|
1418
|
-
}
|
|
1419
|
-
function upsertThreadReply(content, threadId, body, question, resolved) {
|
|
1420
|
-
const threads = parseThreadArtifact(content);
|
|
1421
|
-
const thread = threads.find((candidate) => candidate.id === threadId);
|
|
1422
|
-
if (!thread)
|
|
1423
|
-
throw new Error(`Review thread ${threadId} was not found in the local artifact.`);
|
|
1424
|
-
thread.reply = body;
|
|
1425
|
-
thread.addressed = true;
|
|
1426
|
-
if (question !== undefined)
|
|
1427
|
-
thread.question = question;
|
|
1428
|
-
if (resolved !== undefined)
|
|
1429
|
-
thread.resolved = resolved;
|
|
1430
|
-
const title = content.match(/^# Review threads: (.+)$/m)?.[1] ?? "review";
|
|
1431
|
-
const target = content.match(/^- Target: (.+)$/m)?.[1] ?? "local";
|
|
1432
|
-
const timestamp = content.match(/^- Pulled: (.+)$/m)?.[1];
|
|
1433
|
-
const pr = content.match(/^- PR\/MR: #(\d+)(?: — (.+))?$/m);
|
|
1434
|
-
return renderThreadArtifact(title, target, threads, {
|
|
1435
|
-
timestamp,
|
|
1436
|
-
number: pr ? Number.parseInt(pr[1], 10) : undefined,
|
|
1437
|
-
url: pr?.[2]
|
|
1438
|
-
});
|
|
1439
|
-
}
|
|
1440
1390
|
// src/review/github-review-backend.ts
|
|
1441
1391
|
function GitHubReviewBackend(vcs, number) {
|
|
1442
1392
|
const pendingReview = async () => {
|
|
@@ -1498,9 +1448,6 @@ function GitHubReviewBackend(vcs, number) {
|
|
|
1498
1448
|
id: thread.id,
|
|
1499
1449
|
file: thread.path ?? undefined,
|
|
1500
1450
|
line: thread.line ?? undefined,
|
|
1501
|
-
rootCommentId: comment?.id,
|
|
1502
|
-
commentIds: nodes.flatMap((node) => [node.id, node.databaseId?.toString()].filter((id) => Boolean(id))),
|
|
1503
|
-
commentNodeIds: nodes.flatMap((node) => node.id ? [node.id] : []),
|
|
1504
1451
|
body,
|
|
1505
1452
|
author: comment?.author?.login,
|
|
1506
1453
|
resolved: thread.isResolved,
|
|
@@ -1510,7 +1457,6 @@ function GitHubReviewBackend(vcs, number) {
|
|
|
1510
1457
|
});
|
|
1511
1458
|
};
|
|
1512
1459
|
return {
|
|
1513
|
-
kind: "remote",
|
|
1514
1460
|
async stage(draft) {
|
|
1515
1461
|
const pending = await pendingReview();
|
|
1516
1462
|
if (!pending) {
|
|
@@ -1568,33 +1514,6 @@ function GitHubReviewBackend(vcs, number) {
|
|
|
1568
1514
|
await ghChecked(args);
|
|
1569
1515
|
}
|
|
1570
1516
|
},
|
|
1571
|
-
async readDraft() {
|
|
1572
|
-
const pending = await pendingReview();
|
|
1573
|
-
if (!pending)
|
|
1574
|
-
return { comments: [], body: "" };
|
|
1575
|
-
const review = await ghChecked(["api", `/repos/${vcs.owner}/${vcs.repo}/pulls/${number}/reviews/${pending.id}`]);
|
|
1576
|
-
const comments = await ghChecked([
|
|
1577
|
-
"api",
|
|
1578
|
-
`/repos/${vcs.owner}/${vcs.repo}/pulls/${number}/reviews/${pending.id}/comments`
|
|
1579
|
-
]);
|
|
1580
|
-
let reviewData;
|
|
1581
|
-
let commentData;
|
|
1582
|
-
try {
|
|
1583
|
-
reviewData = JSON.parse(review.stdout);
|
|
1584
|
-
commentData = JSON.parse(comments.stdout);
|
|
1585
|
-
} catch {
|
|
1586
|
-
throw new Error("Cannot parse the pending GitHub review as JSON.");
|
|
1587
|
-
}
|
|
1588
|
-
return {
|
|
1589
|
-
body: reviewData.body ?? "",
|
|
1590
|
-
comments: commentData.map((comment) => ({
|
|
1591
|
-
file: comment.path,
|
|
1592
|
-
line: comment.line ?? comment.original_line ?? undefined,
|
|
1593
|
-
side: comment.side ?? undefined,
|
|
1594
|
-
body: comment.body
|
|
1595
|
-
}))
|
|
1596
|
-
};
|
|
1597
|
-
},
|
|
1598
1517
|
listThreads,
|
|
1599
1518
|
async reply(input) {
|
|
1600
1519
|
const threads = await listThreads();
|
|
@@ -1611,32 +1530,14 @@ function GitHubReviewBackend(vcs, number) {
|
|
|
1611
1530
|
`body=${input.body}`
|
|
1612
1531
|
]);
|
|
1613
1532
|
}
|
|
1614
|
-
if (input.resolve)
|
|
1615
|
-
await this.setResolved?.(input.threadId, true);
|
|
1616
|
-
},
|
|
1617
|
-
async setResolved(threadId, resolved) {
|
|
1618
|
-
await ghChecked([
|
|
1619
|
-
"api",
|
|
1620
|
-
"graphql",
|
|
1621
|
-
"-f",
|
|
1622
|
-
`query=${resolved ? GITHUB_RESOLVE_MUTATION : GITHUB_UNRESOLVE_MUTATION}`,
|
|
1623
|
-
"-f",
|
|
1624
|
-
`threadId=${threadId}`
|
|
1625
|
-
]);
|
|
1626
|
-
},
|
|
1627
|
-
async deleteThread(threadId) {
|
|
1628
|
-
const thread = (await listThreads()).find((candidate) => candidate.id === threadId);
|
|
1629
|
-
const commentNodeIds = thread?.commentNodeIds ?? (thread?.rootCommentId ? [thread.rootCommentId] : []);
|
|
1630
|
-
if (!commentNodeIds.length)
|
|
1631
|
-
throw new Error(`Cannot delete GitHub thread ${threadId}: comment node IDs are missing.`);
|
|
1632
|
-
for (const commentId of commentNodeIds) {
|
|
1533
|
+
if (input.resolve) {
|
|
1633
1534
|
await ghChecked([
|
|
1634
1535
|
"api",
|
|
1635
1536
|
"graphql",
|
|
1636
1537
|
"-f",
|
|
1637
|
-
`query=${
|
|
1538
|
+
`query=${GITHUB_RESOLVE_MUTATION}`,
|
|
1638
1539
|
"-f",
|
|
1639
|
-
`
|
|
1540
|
+
`threadId=${input.threadId}`
|
|
1640
1541
|
]);
|
|
1641
1542
|
}
|
|
1642
1543
|
},
|
|
@@ -1655,13 +1556,11 @@ function GitHubReviewBackend(vcs, number) {
|
|
|
1655
1556
|
function githubReviewSubmissionEndpoint(owner, repo, id, pendingReviewId) {
|
|
1656
1557
|
return pendingReviewId ? `/repos/${owner}/${repo}/pulls/${id}/reviews/${pendingReviewId}/events` : `/repos/${owner}/${repo}/pulls/${id}/reviews`;
|
|
1657
1558
|
}
|
|
1658
|
-
var GITHUB_THREADS_QUERY = `query($owner:String!,$repo:String!,$number:Int!,$after:String){repository(owner:$owner,name:$repo){pullRequest(number:$number){reviewThreads(first:100,after:$after){nodes{id,isResolved,path,line,comments(first:100){nodes{
|
|
1559
|
+
var GITHUB_THREADS_QUERY = `query($owner:String!,$repo:String!,$number:Int!,$after:String){repository(owner:$owner,name:$repo){pullRequest(number:$number){reviewThreads(first:100,after:$after){nodes{id,isResolved,path,line,comments(first:100){nodes{body,author{login}}}}pageInfo{hasNextPage,endCursor}}}}}`;
|
|
1659
1560
|
var GITHUB_ADD_THREAD_MUTATION = `mutation($reviewId:ID!,$body:String!,$path:String!,$line:Int!,$side:DiffSide!){addPullRequestReviewThread(input:{pullRequestReviewId:$reviewId,body:$body,path:$path,line:$line,side:$side}){thread{id}}}`;
|
|
1660
1561
|
var GITHUB_ADD_FILE_THREAD_MUTATION = `mutation($reviewId:ID!,$body:String!,$path:String!){addPullRequestReviewThread(input:{pullRequestReviewId:$reviewId,body:$body,path:$path,subjectType:FILE}){thread{id}}}`;
|
|
1661
1562
|
var GITHUB_REPLY_MUTATION = `mutation($threadId:ID!,$body:String!){addPullRequestReviewThreadReply(input:{pullRequestReviewThreadId:$threadId,body:$body}){comment{id}}}`;
|
|
1662
1563
|
var GITHUB_RESOLVE_MUTATION = `mutation($threadId:ID!){resolveReviewThread(input:{threadId:$threadId}){thread{isResolved}}}`;
|
|
1663
|
-
var GITHUB_UNRESOLVE_MUTATION = `mutation($threadId:ID!){unresolveReviewThread(input:{threadId:$threadId}){thread{isResolved}}}`;
|
|
1664
|
-
var GITHUB_DELETE_COMMENT_MUTATION = `mutation($commentId:ID!){deletePullRequestReviewComment(input:{id:$commentId}){clientMutationId}}`;
|
|
1665
1564
|
|
|
1666
1565
|
// src/review/gitlab-review-backend.ts
|
|
1667
1566
|
function GitLabReviewBackend(vcs, number) {
|
|
@@ -1698,7 +1597,6 @@ function GitLabReviewBackend(vcs, number) {
|
|
|
1698
1597
|
});
|
|
1699
1598
|
};
|
|
1700
1599
|
return {
|
|
1701
|
-
kind: "remote",
|
|
1702
1600
|
async stage(draft) {
|
|
1703
1601
|
const endpoint = `${mergeRequestEndpoint()}/draft_notes`;
|
|
1704
1602
|
if (draft.body.trim()) {
|
|
@@ -1727,36 +1625,6 @@ function GitLabReviewBackend(vcs, number) {
|
|
|
1727
1625
|
});
|
|
1728
1626
|
}
|
|
1729
1627
|
},
|
|
1730
|
-
async readDraft() {
|
|
1731
|
-
const result = await glabChecked(["api", `${mergeRequestEndpoint()}/draft_notes`]);
|
|
1732
|
-
let notes;
|
|
1733
|
-
try {
|
|
1734
|
-
notes = JSON.parse(result.stdout);
|
|
1735
|
-
} catch {
|
|
1736
|
-
throw new Error("Cannot parse GitLab draft notes as JSON.");
|
|
1737
|
-
}
|
|
1738
|
-
const comments = [];
|
|
1739
|
-
const body = [];
|
|
1740
|
-
for (const note of notes) {
|
|
1741
|
-
if (!note.note)
|
|
1742
|
-
continue;
|
|
1743
|
-
const file = note.position?.new_path ?? note.position?.old_path;
|
|
1744
|
-
const line = note.position?.new_line ?? note.position?.old_line;
|
|
1745
|
-
if (file && line) {
|
|
1746
|
-
comments.push({
|
|
1747
|
-
file,
|
|
1748
|
-
line,
|
|
1749
|
-
side: note.position?.new_line ? "RIGHT" : "LEFT",
|
|
1750
|
-
body: note.note
|
|
1751
|
-
});
|
|
1752
|
-
} else {
|
|
1753
|
-
body.push(note.note);
|
|
1754
|
-
}
|
|
1755
|
-
}
|
|
1756
|
-
return { comments, body: body.join(`
|
|
1757
|
-
|
|
1758
|
-
`) };
|
|
1759
|
-
},
|
|
1760
1628
|
listThreads,
|
|
1761
1629
|
async reply(input) {
|
|
1762
1630
|
const endpoint = `${mergeRequestEndpoint()}/discussions/${encodeURIComponent(input.threadId)}`;
|
|
@@ -1768,15 +1636,7 @@ function GitLabReviewBackend(vcs, number) {
|
|
|
1768
1636
|
});
|
|
1769
1637
|
}
|
|
1770
1638
|
if (input.resolve)
|
|
1771
|
-
await
|
|
1772
|
-
},
|
|
1773
|
-
async setResolved(threadId, resolved) {
|
|
1774
|
-
const endpoint = `${mergeRequestEndpoint()}/discussions/${encodeURIComponent(threadId)}`;
|
|
1775
|
-
await glabChecked(["api", "--method", "PUT", `${endpoint}?resolved=${resolved}`]);
|
|
1776
|
-
},
|
|
1777
|
-
async deleteThread(threadId) {
|
|
1778
|
-
const endpoint = `${mergeRequestEndpoint()}/discussions/${encodeURIComponent(threadId)}`;
|
|
1779
|
-
await glabChecked(["api", "--method", "DELETE", endpoint]);
|
|
1639
|
+
await glabChecked(["api", "--method", "PUT", `${endpoint}?resolved=true`]);
|
|
1780
1640
|
},
|
|
1781
1641
|
async publish(event) {
|
|
1782
1642
|
assertReviewEventSupported(vcs.provider, event);
|
|
@@ -1816,12 +1676,105 @@ function hasGitlabDraftNotes(input) {
|
|
|
1816
1676
|
}
|
|
1817
1677
|
}
|
|
1818
1678
|
|
|
1819
|
-
// src/review/
|
|
1820
|
-
|
|
1679
|
+
// src/review/review-backend.ts
|
|
1680
|
+
function createRemoteReviewBackend(vcs, number) {
|
|
1681
|
+
if (vcs.provider === "github")
|
|
1682
|
+
return GitHubReviewBackend(vcs, number);
|
|
1683
|
+
if (vcs.provider === "gitlab")
|
|
1684
|
+
return GitLabReviewBackend(vcs, number);
|
|
1685
|
+
throw new Error("A remote review backend requires a GitHub or GitLab repository.");
|
|
1686
|
+
}
|
|
1687
|
+
// src/review/local-reviews.ts
|
|
1688
|
+
import { mkdir as mkdir4, readdir as readdir2, readFile as readFile6, unlink as unlink2, writeFile as writeFile3 } from "node:fs/promises";
|
|
1689
|
+
import { join as join9 } from "node:path";
|
|
1690
|
+
|
|
1691
|
+
// src/store.ts
|
|
1692
|
+
import { createHash } from "node:crypto";
|
|
1693
|
+
import { lstat, mkdir as mkdir3, readlink, realpath as realpath2, symlink, unlink } from "node:fs/promises";
|
|
1694
|
+
import { homedir as homedir4 } from "node:os";
|
|
1695
|
+
import { dirname as dirname4, isAbsolute as isAbsolute2, join as join8, resolve as resolve2 } from "node:path";
|
|
1696
|
+
var STORE_LINK = ".diffpi";
|
|
1697
|
+
var LEGACY_STORE_LINK = join8(".pi", "diffpi");
|
|
1698
|
+
async function computeProjectSlug(cwd) {
|
|
1699
|
+
return projectSlug(await inspectGitRepository(cwd));
|
|
1700
|
+
}
|
|
1701
|
+
function storeGlobalRoot(homeDir = homedir4()) {
|
|
1702
|
+
return join8(homeDir, ".difflab", "diffpi", "projects");
|
|
1703
|
+
}
|
|
1704
|
+
async function ensureStore(cwd, homeDir = homedir4()) {
|
|
1705
|
+
const repository = await inspectGitRepository(cwd);
|
|
1706
|
+
const root = repository.root;
|
|
1707
|
+
const slug = projectSlug(repository);
|
|
1708
|
+
const dest = join8(storeGlobalRoot(homeDir), slug);
|
|
1709
|
+
const link = join8(root, STORE_LINK);
|
|
1710
|
+
await mkdir3(dest, { recursive: true });
|
|
1711
|
+
try {
|
|
1712
|
+
await assertStoreLink(link, dest);
|
|
1713
|
+
} catch (error) {
|
|
1714
|
+
if (error.code !== "ENOENT")
|
|
1715
|
+
throw error;
|
|
1716
|
+
await symlink(dest, link);
|
|
1717
|
+
}
|
|
1718
|
+
await removeLegacyStoreLink(join8(root, LEGACY_STORE_LINK), dest);
|
|
1719
|
+
return { slug, root, dest, link, linked: true };
|
|
1720
|
+
}
|
|
1721
|
+
async function storeDir(cwd, homeDir = homedir4()) {
|
|
1722
|
+
const store = await ensureStore(cwd, homeDir);
|
|
1723
|
+
return store.dest;
|
|
1724
|
+
}
|
|
1725
|
+
async function plansDir(cwd, homeDir = homedir4()) {
|
|
1726
|
+
const store = await ensureStore(cwd, homeDir);
|
|
1727
|
+
const dir = join8(store.link, "plan");
|
|
1728
|
+
await mkdir3(dir, { recursive: true });
|
|
1729
|
+
return dir;
|
|
1730
|
+
}
|
|
1731
|
+
async function reviewsDir(cwd, homeDir = homedir4()) {
|
|
1732
|
+
const store = await ensureStore(cwd, homeDir);
|
|
1733
|
+
const dir = join8(store.link, "review");
|
|
1734
|
+
await mkdir3(dir, { recursive: true });
|
|
1735
|
+
return dir;
|
|
1736
|
+
}
|
|
1737
|
+
async function assertStoreLink(path, dest) {
|
|
1738
|
+
const entry = await lstat(path);
|
|
1739
|
+
if (!entry.isSymbolicLink())
|
|
1740
|
+
throw new Error(`${path} exists and is not a symlink.`);
|
|
1741
|
+
const target = await symlinkTarget(path);
|
|
1742
|
+
if (target !== await canonicalPath2(dest))
|
|
1743
|
+
throw new Error(`${path} points to ${target}, not ${dest}.`);
|
|
1744
|
+
}
|
|
1745
|
+
async function removeLegacyStoreLink(path, dest) {
|
|
1746
|
+
try {
|
|
1747
|
+
const entry = await lstat(path);
|
|
1748
|
+
if (!entry.isSymbolicLink())
|
|
1749
|
+
return;
|
|
1750
|
+
const target = await symlinkTarget(path);
|
|
1751
|
+
if (target === await canonicalPath2(dest))
|
|
1752
|
+
await unlink(path);
|
|
1753
|
+
} catch (error) {
|
|
1754
|
+
if (error.code !== "ENOENT")
|
|
1755
|
+
throw error;
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
async function symlinkTarget(path) {
|
|
1759
|
+
const target = await readlink(path);
|
|
1760
|
+
return canonicalPath2(isAbsolute2(target) ? target : resolve2(dirname4(path), target));
|
|
1761
|
+
}
|
|
1762
|
+
async function canonicalPath2(path) {
|
|
1763
|
+
try {
|
|
1764
|
+
return await realpath2(path);
|
|
1765
|
+
} catch {
|
|
1766
|
+
return resolve2(path);
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
function projectSlug(repository) {
|
|
1770
|
+
const readable = repository.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "unnamed";
|
|
1771
|
+
const digest = createHash("sha256").update(repository.identity).digest("hex").slice(0, 12);
|
|
1772
|
+
return `${readable}-${digest}`;
|
|
1773
|
+
}
|
|
1821
1774
|
|
|
1822
1775
|
// src/extensions/tuicrx.ts
|
|
1823
|
-
import { readFile as readFile5, realpath as
|
|
1824
|
-
import { resolve as
|
|
1776
|
+
import { readFile as readFile5, realpath as realpath3 } from "node:fs/promises";
|
|
1777
|
+
import { resolve as resolve3 } from "node:path";
|
|
1825
1778
|
async function tuicrAvailable() {
|
|
1826
1779
|
return Boolean(await findExecutable("tuicr"));
|
|
1827
1780
|
}
|
|
@@ -1897,7 +1850,7 @@ async function resolvePrSession(cwd, owner, repo, number) {
|
|
|
1897
1850
|
});
|
|
1898
1851
|
}
|
|
1899
1852
|
async function findMatchingSession(sessions, cwd, branch) {
|
|
1900
|
-
const repository = await
|
|
1853
|
+
const repository = await canonicalPath3(await gitToplevel(cwd));
|
|
1901
1854
|
for (const session of sessions) {
|
|
1902
1855
|
if (session.kind !== "local")
|
|
1903
1856
|
continue;
|
|
@@ -1905,7 +1858,7 @@ async function findMatchingSession(sessions, cwd, branch) {
|
|
|
1905
1858
|
const data = await readSession(session.path);
|
|
1906
1859
|
if (data.branch_name !== branch || !data.repo_path)
|
|
1907
1860
|
continue;
|
|
1908
|
-
if (await
|
|
1861
|
+
if (await canonicalPath3(data.repo_path) === repository)
|
|
1909
1862
|
return session;
|
|
1910
1863
|
} catch {}
|
|
1911
1864
|
}
|
|
@@ -1919,53 +1872,33 @@ async function readSession(path) {
|
|
|
1919
1872
|
throw new Error(`Cannot parse tuicr session JSON: ${path}`);
|
|
1920
1873
|
}
|
|
1921
1874
|
}
|
|
1922
|
-
function
|
|
1923
|
-
const
|
|
1924
|
-
const
|
|
1925
|
-
const publishableBody = (comment) => {
|
|
1875
|
+
function toLocalReviewThreads(session) {
|
|
1876
|
+
const threads = [];
|
|
1877
|
+
const add = (comment, location = {}) => {
|
|
1926
1878
|
const author = commentAuthor(comment);
|
|
1927
|
-
|
|
1928
|
-
|
|
1879
|
+
threads.push({
|
|
1880
|
+
id: comment.id ?? `local-${threads.length + 1}`,
|
|
1881
|
+
...location,
|
|
1882
|
+
body: comment.content,
|
|
1883
|
+
...author ? { author } : {},
|
|
1884
|
+
resolved: false,
|
|
1885
|
+
question: /\?\s*$/.test(comment.content.trim())
|
|
1886
|
+
});
|
|
1929
1887
|
};
|
|
1930
|
-
const
|
|
1888
|
+
for (const comment of session.review_comments ?? [])
|
|
1889
|
+
add(comment);
|
|
1931
1890
|
for (const [file, entry] of Object.entries(session.files ?? {})) {
|
|
1932
|
-
for (const
|
|
1933
|
-
|
|
1934
|
-
continue;
|
|
1935
|
-
const comment = {
|
|
1936
|
-
file,
|
|
1937
|
-
body: fileComment.content,
|
|
1938
|
-
...fileComment.id ? { sourceCommentId: String(fileComment.id) } : {}
|
|
1939
|
-
};
|
|
1940
|
-
const author = commentAuthor(fileComment);
|
|
1941
|
-
if (author)
|
|
1942
|
-
comment.author = author;
|
|
1943
|
-
comments.push(comment);
|
|
1944
|
-
}
|
|
1891
|
+
for (const comment of entry.file_comments ?? [])
|
|
1892
|
+
add(comment, { file });
|
|
1945
1893
|
for (const [lineKey, lineComments] of Object.entries(entry.line_comments ?? {})) {
|
|
1946
1894
|
const line = Number.parseInt(lineKey, 10);
|
|
1947
1895
|
if (!Number.isFinite(line))
|
|
1948
1896
|
continue;
|
|
1949
|
-
for (const
|
|
1950
|
-
|
|
1951
|
-
continue;
|
|
1952
|
-
const comment = {
|
|
1953
|
-
file,
|
|
1954
|
-
line,
|
|
1955
|
-
side: lineComment.side === "old" ? "LEFT" : "RIGHT",
|
|
1956
|
-
body: lineComment.content,
|
|
1957
|
-
...lineComment.id ? { sourceCommentId: String(lineComment.id) } : {}
|
|
1958
|
-
};
|
|
1959
|
-
const author = commentAuthor(lineComment);
|
|
1960
|
-
if (author)
|
|
1961
|
-
comment.author = author;
|
|
1962
|
-
comments.push(comment);
|
|
1963
|
-
}
|
|
1897
|
+
for (const comment of lineComments)
|
|
1898
|
+
add(comment, { file, line });
|
|
1964
1899
|
}
|
|
1965
1900
|
}
|
|
1966
|
-
return
|
|
1967
|
-
|
|
1968
|
-
`) };
|
|
1901
|
+
return threads;
|
|
1969
1902
|
}
|
|
1970
1903
|
async function requireTuicr() {
|
|
1971
1904
|
if (!await tuicrAvailable())
|
|
@@ -1974,207 +1907,70 @@ async function requireTuicr() {
|
|
|
1974
1907
|
function commentAuthor(comment) {
|
|
1975
1908
|
return comment.username ?? comment.author;
|
|
1976
1909
|
}
|
|
1977
|
-
async function
|
|
1910
|
+
async function canonicalPath3(path) {
|
|
1978
1911
|
try {
|
|
1979
|
-
return await
|
|
1912
|
+
return await realpath3(path);
|
|
1980
1913
|
} catch {
|
|
1981
|
-
return
|
|
1914
|
+
return resolve3(path);
|
|
1982
1915
|
}
|
|
1983
1916
|
}
|
|
1984
1917
|
|
|
1985
|
-
// src/review/local-
|
|
1986
|
-
function
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
},
|
|
2004
|
-
async listThreads() {
|
|
2005
|
-
try {
|
|
2006
|
-
return parseThreadArtifact(await readFile6(options.artifactPath, "utf8"));
|
|
2007
|
-
} catch (error) {
|
|
2008
|
-
if (error.code === "ENOENT")
|
|
2009
|
-
return [];
|
|
2010
|
-
throw error;
|
|
2011
|
-
}
|
|
2012
|
-
},
|
|
2013
|
-
async reply(input) {
|
|
2014
|
-
const content = await readFile6(options.artifactPath, "utf8");
|
|
2015
|
-
await writeFile3(options.artifactPath, upsertThreadReply(content, input.threadId, input.body, input.question, input.resolve), "utf8");
|
|
2016
|
-
},
|
|
2017
|
-
async publish() {
|
|
2018
|
-
throw new Error("Promote a local draft through a remote review backend before publishing it.");
|
|
2019
|
-
}
|
|
1918
|
+
// src/review/local-reviews.ts
|
|
1919
|
+
async function captureLocalReview(input) {
|
|
1920
|
+
const id = reviewSlug(input.branch) || "local-review";
|
|
1921
|
+
const dir = join9(await reviewsDir(input.cwd, input.homeDir), id);
|
|
1922
|
+
await mkdir4(dir, { recursive: true });
|
|
1923
|
+
const revision = await nextRevision(dir);
|
|
1924
|
+
const output = await readFile6(input.session.path, "utf8");
|
|
1925
|
+
const comments = toLocalReviewThreads(await readSession(input.session.path));
|
|
1926
|
+
const dump = {
|
|
1927
|
+
schemaVersion: 1,
|
|
1928
|
+
id,
|
|
1929
|
+
revision,
|
|
1930
|
+
branch: input.branch,
|
|
1931
|
+
base: input.base,
|
|
1932
|
+
createdAt: (input.now ?? (() => new Date))().toISOString(),
|
|
1933
|
+
diff: input.diff,
|
|
1934
|
+
comments,
|
|
1935
|
+
tuicr: { slug: input.session.slug, output }
|
|
2020
1936
|
};
|
|
2021
|
-
}
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
}
|
|
2034
|
-
// src/review/review-state.ts
|
|
2035
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
2036
|
-
import { readFile as readFile7, rename, writeFile as writeFile4 } from "node:fs/promises";
|
|
2037
|
-
import { join as join9 } from "node:path";
|
|
2038
|
-
import { z as z4 } from "zod";
|
|
2039
|
-
|
|
2040
|
-
// src/store.ts
|
|
2041
|
-
import { createHash } from "node:crypto";
|
|
2042
|
-
import { lstat, mkdir as mkdir3, readlink, realpath as realpath3, symlink, unlink } from "node:fs/promises";
|
|
2043
|
-
import { homedir as homedir4 } from "node:os";
|
|
2044
|
-
import { dirname as dirname4, isAbsolute as isAbsolute2, join as join8, resolve as resolve3 } from "node:path";
|
|
2045
|
-
var STORE_LINK = ".diffpi";
|
|
2046
|
-
var LEGACY_STORE_LINK = join8(".pi", "diffpi");
|
|
2047
|
-
async function computeProjectSlug(cwd) {
|
|
2048
|
-
return projectSlug(await inspectGitRepository(cwd));
|
|
2049
|
-
}
|
|
2050
|
-
function storeGlobalRoot(homeDir = homedir4()) {
|
|
2051
|
-
return join8(homeDir, ".difflab", "diffpi", "projects");
|
|
2052
|
-
}
|
|
2053
|
-
async function ensureStore(cwd, homeDir = homedir4()) {
|
|
2054
|
-
const repository = await inspectGitRepository(cwd);
|
|
2055
|
-
const root = repository.root;
|
|
2056
|
-
const slug = projectSlug(repository);
|
|
2057
|
-
const dest = join8(storeGlobalRoot(homeDir), slug);
|
|
2058
|
-
const link = join8(root, STORE_LINK);
|
|
2059
|
-
await mkdir3(dest, { recursive: true });
|
|
1937
|
+
const path = join9(dir, `${revision}.json`);
|
|
1938
|
+
await writeFile3(path, `${JSON.stringify(dump, null, 2)}
|
|
1939
|
+
`, { encoding: "utf8", flag: "wx" });
|
|
1940
|
+
await unlink2(input.session.path);
|
|
1941
|
+
return { path, dump };
|
|
1942
|
+
}
|
|
1943
|
+
async function readLocalReview(cwd, branch, revision, homeDir) {
|
|
1944
|
+
const id = reviewSlug(branch) || "local-review";
|
|
1945
|
+
const dir = join9(await reviewsDir(cwd, homeDir), id);
|
|
1946
|
+
const selected = revision ?? await latestRevision(dir);
|
|
1947
|
+
if (!selected)
|
|
1948
|
+
return;
|
|
2060
1949
|
try {
|
|
2061
|
-
await
|
|
1950
|
+
return JSON.parse(await readFile6(join9(dir, `${selected}.json`), "utf8"));
|
|
2062
1951
|
} catch (error) {
|
|
2063
|
-
if (error.code
|
|
2064
|
-
throw error;
|
|
2065
|
-
await symlink(dest, link);
|
|
2066
|
-
}
|
|
2067
|
-
await removeLegacyStoreLink(join8(root, LEGACY_STORE_LINK), dest);
|
|
2068
|
-
return { slug, root, dest, link, linked: true };
|
|
2069
|
-
}
|
|
2070
|
-
async function storeDir(cwd, homeDir = homedir4()) {
|
|
2071
|
-
const store = await ensureStore(cwd, homeDir);
|
|
2072
|
-
return store.dest;
|
|
2073
|
-
}
|
|
2074
|
-
async function plansDir(cwd, homeDir = homedir4()) {
|
|
2075
|
-
const store = await ensureStore(cwd, homeDir);
|
|
2076
|
-
const dir = join8(store.link, "plan");
|
|
2077
|
-
await mkdir3(dir, { recursive: true });
|
|
2078
|
-
return dir;
|
|
2079
|
-
}
|
|
2080
|
-
async function reviewsDir(cwd, homeDir = homedir4()) {
|
|
2081
|
-
const store = await ensureStore(cwd, homeDir);
|
|
2082
|
-
const dir = join8(store.link, "review");
|
|
2083
|
-
await mkdir3(dir, { recursive: true });
|
|
2084
|
-
return dir;
|
|
2085
|
-
}
|
|
2086
|
-
async function sessionsDir(cwd, homeDir = homedir4()) {
|
|
2087
|
-
const store = await ensureStore(cwd, homeDir);
|
|
2088
|
-
const dir = join8(store.link, "sessions");
|
|
2089
|
-
await mkdir3(dir, { recursive: true });
|
|
2090
|
-
return dir;
|
|
2091
|
-
}
|
|
2092
|
-
async function assertStoreLink(path, dest) {
|
|
2093
|
-
const entry = await lstat(path);
|
|
2094
|
-
if (!entry.isSymbolicLink())
|
|
2095
|
-
throw new Error(`${path} exists and is not a symlink.`);
|
|
2096
|
-
const target = await symlinkTarget(path);
|
|
2097
|
-
if (target !== await canonicalPath3(dest))
|
|
2098
|
-
throw new Error(`${path} points to ${target}, not ${dest}.`);
|
|
2099
|
-
}
|
|
2100
|
-
async function removeLegacyStoreLink(path, dest) {
|
|
2101
|
-
try {
|
|
2102
|
-
const entry = await lstat(path);
|
|
2103
|
-
if (!entry.isSymbolicLink())
|
|
1952
|
+
if (error.code === "ENOENT")
|
|
2104
1953
|
return;
|
|
2105
|
-
|
|
2106
|
-
if (target === await canonicalPath3(dest))
|
|
2107
|
-
await unlink(path);
|
|
2108
|
-
} catch (error) {
|
|
2109
|
-
if (error.code !== "ENOENT")
|
|
2110
|
-
throw error;
|
|
2111
|
-
}
|
|
2112
|
-
}
|
|
2113
|
-
async function symlinkTarget(path) {
|
|
2114
|
-
const target = await readlink(path);
|
|
2115
|
-
return canonicalPath3(isAbsolute2(target) ? target : resolve3(dirname4(path), target));
|
|
2116
|
-
}
|
|
2117
|
-
async function canonicalPath3(path) {
|
|
2118
|
-
try {
|
|
2119
|
-
return await realpath3(path);
|
|
2120
|
-
} catch {
|
|
2121
|
-
return resolve3(path);
|
|
1954
|
+
throw error;
|
|
2122
1955
|
}
|
|
2123
1956
|
}
|
|
2124
|
-
function
|
|
2125
|
-
|
|
2126
|
-
const digest = createHash("sha256").update(repository.identity).digest("hex").slice(0, 12);
|
|
2127
|
-
return `${readable}-${digest}`;
|
|
2128
|
-
}
|
|
2129
|
-
|
|
2130
|
-
// src/review/review-state.ts
|
|
2131
|
-
var reviewPublicationStateSchema = z4.object({
|
|
2132
|
-
target: z4.string().optional(),
|
|
2133
|
-
bodies: z4.array(z4.string()).default([]),
|
|
2134
|
-
comments: z4.array(z4.string()).default([]),
|
|
2135
|
-
stagedComments: z4.array(z4.string()).default([]),
|
|
2136
|
-
replies: z4.array(z4.string()).default([]),
|
|
2137
|
-
overlayPath: z4.string().optional()
|
|
2138
|
-
});
|
|
2139
|
-
function reviewBodyFingerprint(body) {
|
|
2140
|
-
return digest(body);
|
|
2141
|
-
}
|
|
2142
|
-
function reviewCommentFingerprint(comment) {
|
|
2143
|
-
return digest([comment.file, String(comment.line), comment.side ?? "RIGHT", comment.body].join("\x00"));
|
|
2144
|
-
}
|
|
2145
|
-
function reviewReplyFingerprint(threadId, body) {
|
|
2146
|
-
return digest(`${threadId}\x00${body}`);
|
|
1957
|
+
async function nextRevision(dir) {
|
|
1958
|
+
return await latestRevision(dir) + 1;
|
|
2147
1959
|
}
|
|
2148
|
-
function
|
|
2149
|
-
return comments.filter((comment) => !knownFingerprints.has(reviewCommentFingerprint(comment)));
|
|
2150
|
-
}
|
|
2151
|
-
async function loadReviewPublicationState(cwd, vcs, number, homeDir) {
|
|
2152
|
-
const target = `${vcs.provider}:${vcs.owner}/${vcs.repo}#${number}`;
|
|
2153
|
-
const path = join9(await sessionsDir(cwd, homeDir), `review-publish-${digest(target).slice(0, 16)}.json`);
|
|
1960
|
+
async function latestRevision(dir) {
|
|
2154
1961
|
try {
|
|
2155
|
-
|
|
2156
|
-
|
|
1962
|
+
return (await readdir2(dir)).reduce((latest, name) => {
|
|
1963
|
+
const match = name.match(/^(\d+)\.json$/);
|
|
1964
|
+
return match ? Math.max(latest, Number(match[1])) : latest;
|
|
1965
|
+
}, 0);
|
|
2157
1966
|
} catch (error) {
|
|
2158
|
-
if (error.code === "ENOENT")
|
|
2159
|
-
return
|
|
2160
|
-
}
|
|
2161
|
-
if (error instanceof SyntaxError || error instanceof z4.ZodError) {
|
|
2162
|
-
throw new Error(`Cannot parse review publication state: ${path}`);
|
|
2163
|
-
}
|
|
1967
|
+
if (error.code === "ENOENT")
|
|
1968
|
+
return 0;
|
|
2164
1969
|
throw error;
|
|
2165
1970
|
}
|
|
2166
1971
|
}
|
|
2167
|
-
async function saveReviewPublicationState(path, state) {
|
|
2168
|
-
const temp = `${path}.${process.pid}.tmp`;
|
|
2169
|
-
await writeFile4(temp, `${JSON.stringify(state, null, 2)}
|
|
2170
|
-
`, "utf8");
|
|
2171
|
-
await rename(temp, path);
|
|
2172
|
-
}
|
|
2173
|
-
function digest(value) {
|
|
2174
|
-
return createHash2("sha256").update(value).digest("hex");
|
|
2175
|
-
}
|
|
2176
1972
|
// src/mcp.ts
|
|
2177
|
-
import { mkdir as
|
|
1973
|
+
import { mkdir as mkdir5, readFile as readFile7, writeFile as writeFile4 } from "node:fs/promises";
|
|
2178
1974
|
import { homedir as homedir5 } from "node:os";
|
|
2179
1975
|
import { dirname as dirname5, join as join10 } from "node:path";
|
|
2180
1976
|
var mcp = {
|
|
@@ -2192,8 +1988,8 @@ var mcp = {
|
|
|
2192
1988
|
const next = { ...current, mcpServers: nextServers };
|
|
2193
1989
|
const changed = JSON.stringify(current) !== JSON.stringify(next);
|
|
2194
1990
|
if (changed && !options.dryRun) {
|
|
2195
|
-
await
|
|
2196
|
-
await
|
|
1991
|
+
await mkdir5(dirname5(path), { recursive: true });
|
|
1992
|
+
await writeFile4(path, `${JSON.stringify(next, null, 2)}
|
|
2197
1993
|
`, "utf8");
|
|
2198
1994
|
}
|
|
2199
1995
|
return { path, changed, existed: currentText !== undefined, planned: changed && options.dryRun === true };
|
|
@@ -2222,7 +2018,7 @@ function getParsedConfig(content, path) {
|
|
|
2222
2018
|
}
|
|
2223
2019
|
async function getOptionalFile2(path) {
|
|
2224
2020
|
try {
|
|
2225
|
-
return await
|
|
2021
|
+
return await readFile7(path, "utf8");
|
|
2226
2022
|
} catch (error) {
|
|
2227
2023
|
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
2228
2024
|
return;
|
|
@@ -2237,7 +2033,7 @@ import {
|
|
|
2237
2033
|
getAgentDir,
|
|
2238
2034
|
parseFrontmatter as parseFrontmatter2
|
|
2239
2035
|
} from "@earendil-works/pi-coding-agent";
|
|
2240
|
-
import { readFile as
|
|
2036
|
+
import { readFile as readFile8 } from "node:fs/promises";
|
|
2241
2037
|
import { homedir as homedir6 } from "node:os";
|
|
2242
2038
|
import { basename as basename4, extname, join as join11 } from "node:path";
|
|
2243
2039
|
async function discoverAgentModes(options) {
|
|
@@ -2436,7 +2232,7 @@ async function loadAgentModes(directory, source, modes, diagnostics, skillName)
|
|
|
2436
2232
|
for (const entry of entries.filter((item) => item.isFile() && item.name.endsWith(".md")).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
2437
2233
|
const path = join11(directory, entry.name);
|
|
2438
2234
|
try {
|
|
2439
|
-
const content = await
|
|
2235
|
+
const content = await readFile8(path, "utf8");
|
|
2440
2236
|
const { frontmatter, body } = parseFrontmatter2(content.startsWith("\uFEFF") ? content.slice(1) : content);
|
|
2441
2237
|
if (frontmatter.enabled === false || frontmatter.inline === false)
|
|
2442
2238
|
continue;
|
|
@@ -2492,7 +2288,7 @@ function isModeBaseline(value) {
|
|
|
2492
2288
|
return (model === undefined || typeof model.provider === "string" && typeof model.id === "string") && candidate.thinkingLevel !== undefined && THINKING_LEVELS.has(candidate.thinkingLevel) && Array.isArray(candidate.tools) && candidate.tools.every((tool) => typeof tool === "string");
|
|
2493
2289
|
}
|
|
2494
2290
|
// src/pi.ts
|
|
2495
|
-
import { mkdir as
|
|
2291
|
+
import { mkdir as mkdir6, writeFile as writeFile5 } from "node:fs/promises";
|
|
2496
2292
|
import { homedir as homedir7 } from "node:os";
|
|
2497
2293
|
import { dirname as dirname6, join as join12 } from "node:path";
|
|
2498
2294
|
var pi = {
|
|
@@ -2511,8 +2307,8 @@ async function ensurePiAgent(filename, content, agentDir = resolvePiAgentDir(),
|
|
|
2511
2307
|
const currentText = await readTextIfExists(path);
|
|
2512
2308
|
const changed = currentText !== content;
|
|
2513
2309
|
if (changed && !dryRun) {
|
|
2514
|
-
await
|
|
2515
|
-
await
|
|
2310
|
+
await mkdir6(dirname6(path), { recursive: true });
|
|
2311
|
+
await writeFile5(path, content, "utf8");
|
|
2516
2312
|
}
|
|
2517
2313
|
return { path, changed, existed: currentText !== undefined, planned: changed && dryRun };
|
|
2518
2314
|
}
|
|
@@ -2548,8 +2344,8 @@ async function ensurePiConfig(path, update, dryRun = false) {
|
|
|
2548
2344
|
const next = update(current);
|
|
2549
2345
|
const changed = JSON.stringify(current) !== JSON.stringify(next);
|
|
2550
2346
|
if (changed && !dryRun) {
|
|
2551
|
-
await
|
|
2552
|
-
await
|
|
2347
|
+
await mkdir6(dirname6(path), { recursive: true });
|
|
2348
|
+
await writeFile5(path, `${JSON.stringify(next, null, 2)}
|
|
2553
2349
|
`, "utf8");
|
|
2554
2350
|
}
|
|
2555
2351
|
return { path, changed, existed: currentText !== undefined, planned: changed && dryRun };
|
|
@@ -2581,271 +2377,93 @@ function parseJsonObject(content, path) {
|
|
|
2581
2377
|
} catch {}
|
|
2582
2378
|
throw new Error(`Expected valid JSON object in ${path}.`);
|
|
2583
2379
|
}
|
|
2584
|
-
// src/
|
|
2585
|
-
import {
|
|
2586
|
-
|
|
2587
|
-
correlation: z5.object({
|
|
2588
|
-
workflow: z5.string().min(1),
|
|
2589
|
-
executionId: z5.string().min(1).optional(),
|
|
2590
|
-
unitId: z5.string().min(1).optional(),
|
|
2591
|
-
parentUnitId: z5.string().min(1).optional(),
|
|
2592
|
-
metadata: z5.record(z5.string(), z5.string()).optional()
|
|
2593
|
-
}).strict(),
|
|
2594
|
-
blocker: z5.string().min(1),
|
|
2595
|
-
attempts: z5.array(z5.string()),
|
|
2596
|
-
evidence: z5.array(z5.string()),
|
|
2597
|
-
needsUserDecision: z5.boolean()
|
|
2598
|
-
}).strict();
|
|
2599
|
-
var ESCALATION_OPEN = "<diffpi-subagent-escalation>";
|
|
2600
|
-
var ESCALATION_CLOSE = "</diffpi-subagent-escalation>";
|
|
2601
|
-
function renderSubagentEscalation(value) {
|
|
2602
|
-
return `${ESCALATION_OPEN}${JSON.stringify(escalationSchema.parse(value))}${ESCALATION_CLOSE}`;
|
|
2603
|
-
}
|
|
2604
|
-
|
|
2605
|
-
// src/plan/annotations.ts
|
|
2606
|
-
import { spawn as spawn2 } from "node:child_process";
|
|
2607
|
-
import { readFile as readFile12, rename as rename2, writeFile as writeFile8 } from "node:fs/promises";
|
|
2608
|
-
import { basename as basename5, dirname as dirname7, join as join14 } from "node:path";
|
|
2609
|
-
import { z as z6 } from "zod";
|
|
2610
|
-
|
|
2611
|
-
// src/log.ts
|
|
2612
|
-
import { randomUUID } from "node:crypto";
|
|
2613
|
-
import { appendFile, readFile as readFile10 } from "node:fs/promises";
|
|
2614
|
-
async function appendLogEntry(logPath, event) {
|
|
2615
|
-
const entry = {
|
|
2616
|
-
...event,
|
|
2617
|
-
version: 1,
|
|
2618
|
-
eventId: event.eventId ?? randomUUID(),
|
|
2619
|
-
timestamp: event.timestamp ?? new Date().toISOString()
|
|
2620
|
-
};
|
|
2621
|
-
await appendFile(logPath, `${JSON.stringify(entry)}
|
|
2622
|
-
`, { encoding: "utf8", mode: 384 });
|
|
2623
|
-
return entry;
|
|
2624
|
-
}
|
|
2380
|
+
// src/plan/store.ts
|
|
2381
|
+
import { mkdir as mkdir8, readdir as readdir3, readFile as readFile11, realpath as realpath4, rm as rm2 } from "node:fs/promises";
|
|
2382
|
+
import { basename as basename6, dirname as dirname8, join as join16, resolve as resolve4, sep } from "node:path";
|
|
2625
2383
|
|
|
2626
|
-
// src/
|
|
2627
|
-
|
|
2628
|
-
|
|
2384
|
+
// src/extensions/fsx.ts
|
|
2385
|
+
import { mkdir as mkdir7, rename, rm, writeFile as writeFile6 } from "node:fs/promises";
|
|
2386
|
+
import { basename as basename5, dirname as dirname7, join as join13 } from "node:path";
|
|
2387
|
+
async function atomicWrite(path, content) {
|
|
2388
|
+
const temp = join13(dirname7(path), `.${basename5(path)}.${crypto.randomUUID()}.tmp`);
|
|
2389
|
+
await writeFile6(temp, content, { encoding: "utf8", mode: 384, flag: "wx" });
|
|
2390
|
+
await rename(temp, path);
|
|
2629
2391
|
}
|
|
2630
|
-
|
|
2631
|
-
// src/plan/lock.ts
|
|
2632
|
-
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2633
|
-
import { mkdir as mkdir6, readFile as readFile11, rm, writeFile as writeFile7 } from "node:fs/promises";
|
|
2634
|
-
import { hostname } from "node:os";
|
|
2635
|
-
import { join as join13 } from "node:path";
|
|
2636
|
-
async function withPlanLock(planDir, operation, options = {}) {
|
|
2637
|
-
const lockDir = join13(planDir, ".lock");
|
|
2392
|
+
async function withDirectoryLock(lockDir, operation, options = {}) {
|
|
2638
2393
|
const waitMs = options.waitMs ?? 5000;
|
|
2639
2394
|
const pollMs = options.pollMs ?? 25;
|
|
2640
|
-
const owner = {
|
|
2641
|
-
token: randomUUID2(),
|
|
2642
|
-
pid: process.pid,
|
|
2643
|
-
hostname: hostname(),
|
|
2644
|
-
operation: options.operation ?? "plan mutation",
|
|
2645
|
-
acquiredAt: new Date().toISOString()
|
|
2646
|
-
};
|
|
2647
2395
|
const started = Date.now();
|
|
2648
2396
|
while (true) {
|
|
2649
2397
|
try {
|
|
2650
|
-
await
|
|
2651
|
-
await writeFile7(join13(lockDir, "owner.json"), `${JSON.stringify(owner)}
|
|
2652
|
-
`, { mode: 384 });
|
|
2398
|
+
await mkdir7(lockDir);
|
|
2653
2399
|
break;
|
|
2654
2400
|
} catch (error) {
|
|
2655
2401
|
if (error.code !== "EEXIST")
|
|
2656
2402
|
throw error;
|
|
2657
|
-
if (Date.now() - started >= waitMs)
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
existing = (await readFile11(join13(lockDir, "owner.json"), "utf8")).trim();
|
|
2661
|
-
} catch {}
|
|
2662
|
-
throw new Error(`Timed out waiting for plan lock ${lockDir}; owner: ${existing}. Remove it only after confirming the owner is stale.`);
|
|
2663
|
-
}
|
|
2664
|
-
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
2403
|
+
if (Date.now() - started >= waitMs)
|
|
2404
|
+
throw new Error(`Timed out waiting to ${options.operation ?? "acquire lock"}: ${lockDir}`);
|
|
2405
|
+
await sleep(pollMs);
|
|
2665
2406
|
}
|
|
2666
2407
|
}
|
|
2667
2408
|
try {
|
|
2668
2409
|
return await operation();
|
|
2669
2410
|
} finally {
|
|
2670
|
-
|
|
2671
|
-
try {
|
|
2672
|
-
current = JSON.parse(await readFile11(join13(lockDir, "owner.json"), "utf8"));
|
|
2673
|
-
} catch {}
|
|
2674
|
-
if (current?.token === owner.token)
|
|
2675
|
-
await rm(lockDir, { recursive: true, force: true });
|
|
2411
|
+
await rm(lockDir, { recursive: true, force: true });
|
|
2676
2412
|
}
|
|
2677
2413
|
}
|
|
2414
|
+
function sleep(ms) {
|
|
2415
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2416
|
+
}
|
|
2678
2417
|
|
|
2679
|
-
// src/
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
if (result.code !== 0)
|
|
2693
|
-
throw new Error(result.stderr.trim() || `tuicr exited with status ${result.code}.`);
|
|
2694
|
-
if (!sessionSlug)
|
|
2695
|
-
throw new Error("tuicr did not report a tuicr-session marker.");
|
|
2696
|
-
const state = await withPlanLock(record.dir, async () => {
|
|
2697
|
-
const previous = await readAnnotationState(record).catch((error) => {
|
|
2698
|
-
if (error.code === "ENOENT")
|
|
2699
|
-
return;
|
|
2700
|
-
throw error;
|
|
2701
|
-
});
|
|
2702
|
-
const next = {
|
|
2703
|
-
schemaVersion: 1,
|
|
2704
|
-
sessionSlug,
|
|
2705
|
-
updatedAt: new Date().toISOString(),
|
|
2706
|
-
appliedCommentIds: previous?.appliedCommentIds ?? []
|
|
2707
|
-
};
|
|
2708
|
-
await atomicJson(annotationStatePath(record), next);
|
|
2709
|
-
return next;
|
|
2710
|
-
}, { operation: "save annotation session" });
|
|
2711
|
-
return { sessionSlug, code: result.code, state };
|
|
2418
|
+
// src/log.ts
|
|
2419
|
+
import { randomUUID } from "node:crypto";
|
|
2420
|
+
import { appendFile, readFile as readFile9 } from "node:fs/promises";
|
|
2421
|
+
async function appendLogEntry(logPath, event) {
|
|
2422
|
+
const entry = {
|
|
2423
|
+
...event,
|
|
2424
|
+
version: 1,
|
|
2425
|
+
eventId: event.eventId ?? randomUUID(),
|
|
2426
|
+
timestamp: event.timestamp ?? new Date().toISOString()
|
|
2427
|
+
};
|
|
2428
|
+
await appendFile(logPath, `${JSON.stringify(entry)}
|
|
2429
|
+
`, { encoding: "utf8", mode: 384 });
|
|
2430
|
+
return entry;
|
|
2712
2431
|
}
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
const
|
|
2722
|
-
const
|
|
2723
|
-
if (
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
try {
|
|
2730
|
-
raw = JSON.parse(response.stdout);
|
|
2731
|
-
} catch {
|
|
2732
|
-
throw new Error("tuicr returned malformed comment JSON.");
|
|
2733
|
-
}
|
|
2734
|
-
if (!Array.isArray(raw))
|
|
2735
|
-
throw new Error("tuicr comment output must be a JSON array.");
|
|
2736
|
-
const lines = record.source.split(`
|
|
2737
|
-
`);
|
|
2738
|
-
const applied = new Set(state.appliedCommentIds);
|
|
2739
|
-
const comments = raw.map((value, index) => normalizeComment(value, index, lines, applied));
|
|
2740
|
-
const pending = comments.filter((comment) => !comment.applied);
|
|
2741
|
-
return { state, comments: options.includeApplied ? comments : pending, pending };
|
|
2742
|
-
}
|
|
2743
|
-
async function acknowledgePlanAnnotations(record, commentIds, summary) {
|
|
2744
|
-
if (!summary.trim())
|
|
2745
|
-
throw new Error("Annotation acknowledgement summary is required.");
|
|
2746
|
-
return withPlanLock(record.dir, async () => {
|
|
2747
|
-
const state = await readAnnotationState(record);
|
|
2748
|
-
const next = {
|
|
2749
|
-
...state,
|
|
2750
|
-
updatedAt: new Date().toISOString(),
|
|
2751
|
-
appliedCommentIds: [...new Set([...state.appliedCommentIds, ...commentIds])]
|
|
2752
|
-
};
|
|
2753
|
-
await atomicJson(annotationStatePath(record), next);
|
|
2754
|
-
await appendPlanLog(record.logPath, {
|
|
2755
|
-
planRevision: record.document.revision,
|
|
2756
|
-
kind: "annotation",
|
|
2757
|
-
actor: "planner",
|
|
2758
|
-
message: summary,
|
|
2759
|
-
data: { commentIds: [...commentIds], sessionSlug: state.sessionSlug }
|
|
2760
|
-
});
|
|
2761
|
-
return next;
|
|
2762
|
-
}, { operation: "acknowledge annotations" });
|
|
2432
|
+
|
|
2433
|
+
// src/templates.ts
|
|
2434
|
+
import { readFile as readFile10 } from "node:fs/promises";
|
|
2435
|
+
import { homedir as homedir8 } from "node:os";
|
|
2436
|
+
import { join as join14, normalize } from "node:path";
|
|
2437
|
+
async function loadTemplate(name, options = {}) {
|
|
2438
|
+
const relative = templateRelativePath(name);
|
|
2439
|
+
const userPath = join14(options.homeDir ?? homedir8(), ".difflab", "diffpi", "templates", relative);
|
|
2440
|
+
const bundledPath = join14(options.bundledDir ?? resolveBundledTemplatesDir(), relative);
|
|
2441
|
+
const user = await readOptionalFile(userPath);
|
|
2442
|
+
if (user !== undefined)
|
|
2443
|
+
return { name, path: userPath, source: "user", content: user };
|
|
2444
|
+
const bundled = await readOptionalFile(bundledPath);
|
|
2445
|
+
if (bundled !== undefined)
|
|
2446
|
+
return { name, path: bundledPath, source: "bundled", content: bundled };
|
|
2447
|
+
throw new Error(`Template "${name}" was not found at ${userPath} or ${bundledPath}.`);
|
|
2763
2448
|
}
|
|
2764
|
-
function
|
|
2765
|
-
return
|
|
2449
|
+
function renderTemplate(content, variables) {
|
|
2450
|
+
return content.replace(/\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g, (match, key) => variables[key] ?? match);
|
|
2766
2451
|
}
|
|
2767
|
-
|
|
2768
|
-
const
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
value = JSON.parse(source);
|
|
2772
|
-
} catch {
|
|
2773
|
-
throw new Error(`Malformed annotation state: ${annotationStatePath(record)}.`);
|
|
2774
|
-
}
|
|
2775
|
-
if (value.schemaVersion !== 1 || typeof value.sessionSlug !== "string" || !Array.isArray(value.appliedCommentIds)) {
|
|
2776
|
-
throw new Error(`Malformed annotation state: ${annotationStatePath(record)}.`);
|
|
2452
|
+
function templateRelativePath(name) {
|
|
2453
|
+
const normalized = normalize(name.replaceAll("\\", "/")).replace(/^\.\//, "");
|
|
2454
|
+
if (!normalized || normalized.startsWith("../") || normalized.includes("/../") || normalized.startsWith("/")) {
|
|
2455
|
+
throw new Error(`Invalid template name: ${name}`);
|
|
2777
2456
|
}
|
|
2778
|
-
return
|
|
2779
|
-
}
|
|
2780
|
-
function normalizeComment(value, index, lines, applied) {
|
|
2781
|
-
const parsed = commentSchema.safeParse(value);
|
|
2782
|
-
if (!parsed.success)
|
|
2783
|
-
throw new Error(`Malformed tuicr comment at index ${index}.`);
|
|
2784
|
-
const raw = parsed.data;
|
|
2785
|
-
const line = raw.start_line ?? raw.line;
|
|
2786
|
-
const endLine = raw.end_line ?? line;
|
|
2787
|
-
const targetPath = raw.path;
|
|
2788
|
-
const appliesToPlan = !targetPath || basename5(targetPath) === basename5("PLAN.md");
|
|
2789
|
-
const validAnchor = appliesToPlan && line !== undefined && line > 0 && line <= lines.length;
|
|
2790
|
-
return {
|
|
2791
|
-
id: raw.id,
|
|
2792
|
-
body: raw.content,
|
|
2793
|
-
file: targetPath,
|
|
2794
|
-
line,
|
|
2795
|
-
endLine,
|
|
2796
|
-
context: validAnchor ? lines.slice(line - 1, Math.min(endLine ?? line, lines.length)).join(`
|
|
2797
|
-
`) : undefined,
|
|
2798
|
-
stale: line !== undefined && !validAnchor,
|
|
2799
|
-
applied: applied.has(raw.id)
|
|
2800
|
-
};
|
|
2457
|
+
return normalized.endsWith(".md") ? normalized : `${normalized}.md`;
|
|
2801
2458
|
}
|
|
2802
|
-
async function
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2459
|
+
async function readOptionalFile(path) {
|
|
2460
|
+
try {
|
|
2461
|
+
return await readFile10(path, "utf8");
|
|
2462
|
+
} catch (error) {
|
|
2463
|
+
if (error.code === "ENOENT")
|
|
2464
|
+
return;
|
|
2465
|
+
throw error;
|
|
2806
2466
|
}
|
|
2807
|
-
return new Promise((resolve, reject) => {
|
|
2808
|
-
const child = spawn2(command, args, { cwd, stdio: ["inherit", "inherit", "pipe"] });
|
|
2809
|
-
let stderr = "";
|
|
2810
|
-
child.stderr.on("data", (chunk) => {
|
|
2811
|
-
const text = chunk.toString();
|
|
2812
|
-
stderr += text;
|
|
2813
|
-
process.stderr.write(text);
|
|
2814
|
-
});
|
|
2815
|
-
child.on("error", reject);
|
|
2816
|
-
child.on("close", (code) => resolve({ code: code ?? 1, stdout: "", stderr }));
|
|
2817
|
-
});
|
|
2818
|
-
}
|
|
2819
|
-
async function atomicJson(path, value) {
|
|
2820
|
-
const temp = join14(dirname7(path), `.${basename5(path)}.${crypto.randomUUID()}.tmp`);
|
|
2821
|
-
await writeFile8(temp, `${JSON.stringify(value, null, 2)}
|
|
2822
|
-
`, { mode: 384, flag: "wx" });
|
|
2823
|
-
await rename2(temp, path);
|
|
2824
|
-
}
|
|
2825
|
-
|
|
2826
|
-
// src/plan/execution.ts
|
|
2827
|
-
function createExecutionPacket(plan, coordinator) {
|
|
2828
|
-
if (!plan.execution?.active)
|
|
2829
|
-
throw new Error(`Plan ${plan.id} has no active execution.`);
|
|
2830
|
-
return {
|
|
2831
|
-
version: 1,
|
|
2832
|
-
planId: plan.id,
|
|
2833
|
-
executionId: plan.execution.id,
|
|
2834
|
-
policy: plan.execution.policy,
|
|
2835
|
-
cwd: plan.execution.cwd,
|
|
2836
|
-
branch: plan.execution.branch,
|
|
2837
|
-
coordinator
|
|
2838
|
-
};
|
|
2839
|
-
}
|
|
2840
|
-
function renderExecutionPrompt(packet) {
|
|
2841
|
-
return [
|
|
2842
|
-
`Execute the durable Diffpi plan using this packet: ${JSON.stringify(packet)}.`,
|
|
2843
|
-
"Call plan_context first and keep plan tools as the source of truth for eligibility, ownership, status, gates, and commits.",
|
|
2844
|
-
"Use SubagentWorkflow for deterministic multi-task coordination: pipeline dependent work, parallelize only tasks with non-overlapping declared file scopes, and use structured schemas for worker outcomes.",
|
|
2845
|
-
"The extension cannot launch workflow children over RPC, so the active orchestrator must invoke SubagentWorkflow itself.",
|
|
2846
|
-
"Persist every transition, progress event, issue, and deviation. Do not edit PLAN.md directly. Delegated implementation workers never commit or restructure the plan.",
|
|
2847
|
-
"For commit-per-phase execution, commit after local gates, push immediately, record pending CI on the completed phase, and launch a bounded background Worker to call plan_watch_ci for the exact SHA while the next phase executes. Collect that monitor before the next push and collect all monitors before plan completion; failed or timed-out CI blocks execution."
|
|
2848
|
-
].join(" ");
|
|
2849
2467
|
}
|
|
2850
2468
|
|
|
2851
2469
|
// src/plan/markdown.ts
|
|
@@ -7485,25 +7103,6 @@ function looksLikeAValue(value) {
|
|
|
7485
7103
|
function isUint8Array2(value) {
|
|
7486
7104
|
return Boolean(value && typeof value === "object" && "byteLength" in value && "byteOffset" in value);
|
|
7487
7105
|
}
|
|
7488
|
-
// src/plan/ids.ts
|
|
7489
|
-
var STABLE_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
7490
|
-
function isStableId(value) {
|
|
7491
|
-
return STABLE_ID.test(value) && value.length <= 80;
|
|
7492
|
-
}
|
|
7493
|
-
function assertStableId(value, label = "identifier") {
|
|
7494
|
-
if (!isStableId(value))
|
|
7495
|
-
throw new Error(`${label} must be a lowercase stable slug, not a path: ${value}`);
|
|
7496
|
-
}
|
|
7497
|
-
function assertUniqueIds(ids, label) {
|
|
7498
|
-
const seen = new Set;
|
|
7499
|
-
for (const id of ids) {
|
|
7500
|
-
assertStableId(id, label);
|
|
7501
|
-
if (seen.has(id))
|
|
7502
|
-
throw new Error(`Duplicate ${label}: ${id}.`);
|
|
7503
|
-
seen.add(id);
|
|
7504
|
-
}
|
|
7505
|
-
}
|
|
7506
|
-
|
|
7507
7106
|
// src/plan/markdown.ts
|
|
7508
7107
|
var PLAN_MARKER = /<!-- diffpi-plan: (\{[^\n]+\}) -->/;
|
|
7509
7108
|
var PHASE_MARKER = /<!-- diffpi-phase: (\{[^\n]+\}) -->/g;
|
|
@@ -7551,8 +7150,6 @@ function validatePlanDocument(plan, options = {}) {
|
|
|
7551
7150
|
add("design-too-long", "error", `Design is ${designWords} words; finalization allows at most 800.`);
|
|
7552
7151
|
if (options.strict && plan.phases.length === 0)
|
|
7553
7152
|
add("no-phases", "error", "Finalization requires at least one phase.");
|
|
7554
|
-
if (options.strict && (options.pendingAnnotations ?? 0) > 0)
|
|
7555
|
-
add("pending-annotations", "error", `${options.pendingAnnotations} annotations remain pending.`);
|
|
7556
7153
|
return issues;
|
|
7557
7154
|
}
|
|
7558
7155
|
function renderPlanDocument(plan, previousSource) {
|
|
@@ -7894,6 +7491,19 @@ function parseCsv(value) {
|
|
|
7894
7491
|
return [];
|
|
7895
7492
|
return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
7896
7493
|
}
|
|
7494
|
+
function assertStableId(value, label) {
|
|
7495
|
+
if (!zx.id.safeParse(value).success)
|
|
7496
|
+
throw new Error(`${label} must be a lowercase stable slug, not a path: ${value}`);
|
|
7497
|
+
}
|
|
7498
|
+
function assertUniqueIds(ids, label) {
|
|
7499
|
+
const seen = new Set;
|
|
7500
|
+
for (const id of ids) {
|
|
7501
|
+
assertStableId(id, label);
|
|
7502
|
+
if (seen.has(id))
|
|
7503
|
+
throw new Error(`Duplicate ${label}: ${id}.`);
|
|
7504
|
+
seen.add(id);
|
|
7505
|
+
}
|
|
7506
|
+
}
|
|
7897
7507
|
function cleanPlaceholder(value) {
|
|
7898
7508
|
return value.replace(/<!--[^]*?-->/g, "").trim();
|
|
7899
7509
|
}
|
|
@@ -7925,46 +7535,6 @@ function escapeRegExp(value) {
|
|
|
7925
7535
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7926
7536
|
}
|
|
7927
7537
|
|
|
7928
|
-
// src/plan/store.ts
|
|
7929
|
-
import { open, mkdir as mkdir7, readdir as readdir2, readFile as readFile14, realpath as realpath4, rename as rename3, rm as rm2 } from "node:fs/promises";
|
|
7930
|
-
import { basename as basename6, dirname as dirname8, join as join17, resolve as resolve4, sep } from "node:path";
|
|
7931
|
-
|
|
7932
|
-
// src/templates.ts
|
|
7933
|
-
import { readFile as readFile13 } from "node:fs/promises";
|
|
7934
|
-
import { homedir as homedir8 } from "node:os";
|
|
7935
|
-
import { join as join16, normalize } from "node:path";
|
|
7936
|
-
async function loadTemplate(name, options = {}) {
|
|
7937
|
-
const relative = templateRelativePath(name);
|
|
7938
|
-
const userPath = join16(options.homeDir ?? homedir8(), ".difflab", "diffpi", "templates", relative);
|
|
7939
|
-
const bundledPath = join16(options.bundledDir ?? resolveBundledTemplatesDir(), relative);
|
|
7940
|
-
const user = await readOptionalFile(userPath);
|
|
7941
|
-
if (user !== undefined)
|
|
7942
|
-
return { name, path: userPath, source: "user", content: user };
|
|
7943
|
-
const bundled = await readOptionalFile(bundledPath);
|
|
7944
|
-
if (bundled !== undefined)
|
|
7945
|
-
return { name, path: bundledPath, source: "bundled", content: bundled };
|
|
7946
|
-
throw new Error(`Template "${name}" was not found at ${userPath} or ${bundledPath}.`);
|
|
7947
|
-
}
|
|
7948
|
-
function renderTemplate(content, variables) {
|
|
7949
|
-
return content.replace(/\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g, (match, key) => variables[key] ?? match);
|
|
7950
|
-
}
|
|
7951
|
-
function templateRelativePath(name) {
|
|
7952
|
-
const normalized = normalize(name.replaceAll("\\", "/")).replace(/^\.\//, "");
|
|
7953
|
-
if (!normalized || normalized.startsWith("../") || normalized.includes("/../") || normalized.startsWith("/")) {
|
|
7954
|
-
throw new Error(`Invalid template name: ${name}`);
|
|
7955
|
-
}
|
|
7956
|
-
return normalized.endsWith(".md") ? normalized : `${normalized}.md`;
|
|
7957
|
-
}
|
|
7958
|
-
async function readOptionalFile(path) {
|
|
7959
|
-
try {
|
|
7960
|
-
return await readFile13(path, "utf8");
|
|
7961
|
-
} catch (error) {
|
|
7962
|
-
if (error.code === "ENOENT")
|
|
7963
|
-
return;
|
|
7964
|
-
throw error;
|
|
7965
|
-
}
|
|
7966
|
-
}
|
|
7967
|
-
|
|
7968
7538
|
// src/plan/store.ts
|
|
7969
7539
|
function planRecordName(shortSlug, date = new Date) {
|
|
7970
7540
|
const slug = normalizeSlug(shortSlug);
|
|
@@ -7972,7 +7542,7 @@ function planRecordName(shortSlug, date = new Date) {
|
|
|
7972
7542
|
}
|
|
7973
7543
|
async function resolvePlan(cwd, query, filters = {}, options = {}) {
|
|
7974
7544
|
const root = await plansDir(cwd, options.homeDir);
|
|
7975
|
-
const entries = await
|
|
7545
|
+
const entries = await readdir3(root, { withFileTypes: true });
|
|
7976
7546
|
const normalizedQuery = query ? normalizeQuery(query) : undefined;
|
|
7977
7547
|
const records = [];
|
|
7978
7548
|
for (const entry of entries) {
|
|
@@ -7980,7 +7550,7 @@ async function resolvePlan(cwd, query, filters = {}, options = {}) {
|
|
|
7980
7550
|
continue;
|
|
7981
7551
|
if (normalizedQuery && entry.name !== normalizedQuery && stripDate(entry.name) !== normalizedQuery)
|
|
7982
7552
|
continue;
|
|
7983
|
-
const dir =
|
|
7553
|
+
const dir = join16(root, entry.name);
|
|
7984
7554
|
await assertContained(root, dir);
|
|
7985
7555
|
try {
|
|
7986
7556
|
const record = await readRecord(dir);
|
|
@@ -8006,10 +7576,10 @@ function createPlanStore(options = {}) {
|
|
|
8006
7576
|
async init(input) {
|
|
8007
7577
|
const root = await plansDir(input.cwd, options.homeDir);
|
|
8008
7578
|
const id = planRecordName(input.shortSlug, now());
|
|
8009
|
-
const dir =
|
|
7579
|
+
const dir = join16(root, id);
|
|
8010
7580
|
await assertContained(root, dirname8(dir));
|
|
8011
7581
|
try {
|
|
8012
|
-
await
|
|
7582
|
+
await mkdir8(dir);
|
|
8013
7583
|
} catch (error) {
|
|
8014
7584
|
if (error.code === "EEXIST")
|
|
8015
7585
|
throw new Error(`Plan ${id} already exists.`);
|
|
@@ -8026,25 +7596,19 @@ function createPlanStore(options = {}) {
|
|
|
8026
7596
|
branch: input.branch,
|
|
8027
7597
|
title: input.title?.trim() || titleFromSlug(input.shortSlug),
|
|
8028
7598
|
intent: input.intent?.trim() || "<!-- Describe the intended outcome. -->",
|
|
7599
|
+
issue_id: input.issueId?.trim() || "<!-- Add issue tracker ID. -->",
|
|
7600
|
+
issue_url: input.issueUrl?.trim() || "<!-- Add issue tracker URL. -->",
|
|
8029
7601
|
created_at: timestamp,
|
|
8030
7602
|
updated_at: timestamp
|
|
8031
7603
|
});
|
|
8032
|
-
const document = parsePlanDocument(source,
|
|
8033
|
-
await atomicWrite(
|
|
8034
|
-
await ensureImplementationFiles(dir, document, options);
|
|
8035
|
-
await atomicWrite(join17(dir, "logs.txt"), "");
|
|
8036
|
-
await appendPlanLog(join17(dir, "logs.txt"), {
|
|
8037
|
-
planRevision: document.revision,
|
|
8038
|
-
kind: "created",
|
|
8039
|
-
actor: "diffpi",
|
|
8040
|
-
message: `Created plan ${id}.`
|
|
8041
|
-
});
|
|
7604
|
+
const document = parsePlanDocument(source, join16(dir, "PLAN.md"));
|
|
7605
|
+
await atomicWrite(join16(dir, "PLAN.md"), source);
|
|
8042
7606
|
return {
|
|
8043
7607
|
id,
|
|
8044
7608
|
dir,
|
|
8045
|
-
planPath:
|
|
8046
|
-
logPath:
|
|
8047
|
-
implementationDir:
|
|
7609
|
+
planPath: join16(dir, "PLAN.md"),
|
|
7610
|
+
logPath: join16(dir, "logs.txt"),
|
|
7611
|
+
implementationDir: join16(dir, "implementation"),
|
|
8048
7612
|
document,
|
|
8049
7613
|
source
|
|
8050
7614
|
};
|
|
@@ -8064,7 +7628,7 @@ function createPlanStore(options = {}) {
|
|
|
8064
7628
|
},
|
|
8065
7629
|
async mutate(cwd, query, operation, update) {
|
|
8066
7630
|
const initial = await this.read(cwd, query);
|
|
8067
|
-
return
|
|
7631
|
+
return withDirectoryLock(join16(initial.dir, ".lock"), async () => {
|
|
8068
7632
|
const current = await readRecord(initial.dir);
|
|
8069
7633
|
const next = await update(structuredClone(current.document));
|
|
8070
7634
|
if (next.id !== current.id)
|
|
@@ -8078,41 +7642,41 @@ function createPlanStore(options = {}) {
|
|
|
8078
7642
|
parsePlanDocument(source, current.planPath);
|
|
8079
7643
|
await atomicWrite(current.planPath, source);
|
|
8080
7644
|
await ensureImplementationFiles(current.dir, document, options);
|
|
8081
|
-
return { ...current, document, source, implementationDir:
|
|
7645
|
+
return { ...current, document, source, implementationDir: join16(current.dir, "implementation") };
|
|
8082
7646
|
}, { operation });
|
|
8083
7647
|
},
|
|
8084
7648
|
async log(cwd, query, event) {
|
|
8085
7649
|
const record = await this.read(cwd, query);
|
|
8086
|
-
return
|
|
7650
|
+
return appendLogEntry(record.logPath, event);
|
|
8087
7651
|
}
|
|
8088
7652
|
};
|
|
8089
7653
|
}
|
|
8090
7654
|
async function readRecord(dir) {
|
|
8091
|
-
const planPath =
|
|
8092
|
-
const logPath =
|
|
8093
|
-
const source = await
|
|
7655
|
+
const planPath = join16(dir, "PLAN.md");
|
|
7656
|
+
const logPath = join16(dir, "logs.txt");
|
|
7657
|
+
const source = await readFile11(planPath, "utf8");
|
|
8094
7658
|
const document = parsePlanDocument(source, planPath);
|
|
8095
7659
|
return {
|
|
8096
7660
|
id: basename6(dir),
|
|
8097
7661
|
dir,
|
|
8098
7662
|
planPath,
|
|
8099
7663
|
logPath,
|
|
8100
|
-
implementationDir:
|
|
7664
|
+
implementationDir: join16(dir, "implementation"),
|
|
8101
7665
|
document,
|
|
8102
7666
|
source
|
|
8103
7667
|
};
|
|
8104
7668
|
}
|
|
8105
7669
|
async function ensureImplementationFiles(dir, document, options) {
|
|
8106
|
-
const implementationDir =
|
|
8107
|
-
await
|
|
7670
|
+
const implementationDir = join16(dir, "implementation");
|
|
7671
|
+
await mkdir8(implementationDir, { recursive: true });
|
|
8108
7672
|
const template = await loadTemplate("plan/implementation", {
|
|
8109
7673
|
homeDir: options.homeDir,
|
|
8110
7674
|
bundledDir: options.bundledTemplatesDir
|
|
8111
7675
|
});
|
|
8112
7676
|
for (const phase of document.phases) {
|
|
8113
|
-
const path =
|
|
7677
|
+
const path = join16(implementationDir, `phase-${phase.id}.md`);
|
|
8114
7678
|
try {
|
|
8115
|
-
await
|
|
7679
|
+
await readFile11(path, "utf8");
|
|
8116
7680
|
} catch (error) {
|
|
8117
7681
|
if (error.code !== "ENOENT")
|
|
8118
7682
|
throw error;
|
|
@@ -8126,25 +7690,6 @@ async function ensureImplementationFiles(dir, document, options) {
|
|
|
8126
7690
|
}
|
|
8127
7691
|
}
|
|
8128
7692
|
}
|
|
8129
|
-
async function atomicWrite(path, content) {
|
|
8130
|
-
const temp = join17(dirname8(path), `.${basename6(path)}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
8131
|
-
const file = await open(temp, "wx", 384);
|
|
8132
|
-
try {
|
|
8133
|
-
await file.writeFile(content, "utf8");
|
|
8134
|
-
await file.sync();
|
|
8135
|
-
} finally {
|
|
8136
|
-
await file.close();
|
|
8137
|
-
}
|
|
8138
|
-
await rename3(temp, path);
|
|
8139
|
-
try {
|
|
8140
|
-
const directory = await open(dirname8(path), "r");
|
|
8141
|
-
try {
|
|
8142
|
-
await directory.sync();
|
|
8143
|
-
} finally {
|
|
8144
|
-
await directory.close();
|
|
8145
|
-
}
|
|
8146
|
-
} catch {}
|
|
8147
|
-
}
|
|
8148
7693
|
async function assertContained(root, candidate) {
|
|
8149
7694
|
const canonicalRoot = await realpath4(root);
|
|
8150
7695
|
let canonicalCandidate;
|
|
@@ -8162,7 +7707,8 @@ function normalizeSlug(value) {
|
|
|
8162
7707
|
throw new Error(`Invalid plan slug: ${value}.`);
|
|
8163
7708
|
}
|
|
8164
7709
|
const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
8165
|
-
|
|
7710
|
+
if (!zx.id.safeParse(slug).success)
|
|
7711
|
+
throw new Error(`Invalid plan slug: ${value}.`);
|
|
8166
7712
|
return slug;
|
|
8167
7713
|
}
|
|
8168
7714
|
function normalizeQuery(value) {
|
|
@@ -8177,345 +7723,11 @@ function stripDate(value) {
|
|
|
8177
7723
|
function titleFromSlug(value) {
|
|
8178
7724
|
return normalizeSlug(value).replace(/^(?:[a-z]+-\d+-)/, "").split("-").map((part) => part[0].toUpperCase() + part.slice(1)).join(" ");
|
|
8179
7725
|
}
|
|
8180
|
-
|
|
8181
|
-
// src/plan/controller.ts
|
|
8182
|
-
function createPlanController(options = {}) {
|
|
8183
|
-
const store = createPlanStore(options);
|
|
8184
|
-
const controller = {
|
|
8185
|
-
context: store.context,
|
|
8186
|
-
create: store.init,
|
|
8187
|
-
read: store.read,
|
|
8188
|
-
update: store.mutate,
|
|
8189
|
-
appendLog: store.log,
|
|
8190
|
-
updateStatus: (cwd, query, input) => updateStatus(store, cwd, query, input),
|
|
8191
|
-
updateCi: (cwd, query, input) => updateCi(store, cwd, query, input),
|
|
8192
|
-
retryCi: (cwd, query, input) => retryCi(store, cwd, query, input),
|
|
8193
|
-
assertPhasePushEligible,
|
|
8194
|
-
validate: validatePlanDocument,
|
|
8195
|
-
countDesignWords,
|
|
8196
|
-
annotate: annotatePlan,
|
|
8197
|
-
annotations: readPlanAnnotations,
|
|
8198
|
-
acknowledgeAnnotations: acknowledgePlanAnnotations,
|
|
8199
|
-
executionPacket: createExecutionPacket,
|
|
8200
|
-
executionPrompt: renderExecutionPrompt,
|
|
8201
|
-
renderEscalation: renderSubagentEscalation,
|
|
8202
|
-
assertStableId
|
|
8203
|
-
};
|
|
8204
|
-
return controller;
|
|
8205
|
-
}
|
|
8206
|
-
async function updateStatus(store, cwd, query, input) {
|
|
8207
|
-
let escalation;
|
|
8208
|
-
const record = await store.mutate(cwd, query, "update status", (plan) => {
|
|
8209
|
-
if (input.target.type === "plan") {
|
|
8210
|
-
if (plan.status !== input.expectedStatus)
|
|
8211
|
-
throw new Error(`Expected plan status ${input.expectedStatus}, found ${plan.status}.`);
|
|
8212
|
-
assertPlanTransition(plan.status, input.status);
|
|
8213
|
-
if (input.status === "ready") {
|
|
8214
|
-
const errors = validatePlanDocument(plan, { strict: true }).filter((issue) => issue.severity === "error");
|
|
8215
|
-
if (errors.length)
|
|
8216
|
-
throw new Error(`Plan cannot become ready: ${errors.map((issue) => issue.message).join(" ")}`);
|
|
8217
|
-
}
|
|
8218
|
-
if ((input.status === "blocked" || input.status === "completed") && plan.status === "in_progress")
|
|
8219
|
-
assertExecutionOwner(plan, input.executionId);
|
|
8220
|
-
if (input.status === "completed" && plan.phases.some((phase) => phase.status !== "completed" && phase.status !== "skipped"))
|
|
8221
|
-
throw new Error("Every phase must be complete or skipped before the plan completes.");
|
|
8222
|
-
if (input.status === "completed" && plan.execution?.policy === "commit-per-phase") {
|
|
8223
|
-
const unsettled = plan.phases.filter((phase) => phase.status === "completed" && (!phase.commit?.pushedAt || !["passed", "skipped"].includes(phase.commit.ci?.status ?? "pending")));
|
|
8224
|
-
if (unsettled.length)
|
|
8225
|
-
throw new Error(`CI must settle successfully for phases: ${unsettled.map((phase) => phase.id).join(", ")}.`);
|
|
8226
|
-
}
|
|
8227
|
-
return {
|
|
8228
|
-
...heartbeat(plan),
|
|
8229
|
-
status: input.status,
|
|
8230
|
-
execution: input.status === "completed" || input.status === "blocked" ? plan.execution && { ...plan.execution, active: false, heartbeatAt: new Date().toISOString() } : plan.execution
|
|
8231
|
-
};
|
|
8232
|
-
}
|
|
8233
|
-
if (!input.target.id)
|
|
8234
|
-
throw new Error("Target ID is required.");
|
|
8235
|
-
const phase = input.target.type === "phase" ? getPhase(plan, input.target.id) : findTaskPhase(plan, input.target.id);
|
|
8236
|
-
if (input.target.type === "phase") {
|
|
8237
|
-
if (phase.status !== input.expectedStatus)
|
|
8238
|
-
throw new Error(`Expected phase status ${input.expectedStatus}, found ${phase.status}.`);
|
|
8239
|
-
if (phase.status === "completed")
|
|
8240
|
-
throw new Error(`Completed phase ${phase.id} cannot transition again.`);
|
|
8241
|
-
assertExecutionOwner(plan, input.executionId);
|
|
8242
|
-
assertPhaseTransition(phase.status, input.status);
|
|
8243
|
-
if (input.status === "in_progress" || input.status === "completed")
|
|
8244
|
-
assertPhaseDependenciesSatisfied(plan, phase);
|
|
8245
|
-
if (input.status === "completed") {
|
|
8246
|
-
if (phase.tasks.some((task) => task.status !== "completed" && task.status !== "skipped"))
|
|
8247
|
-
throw new Error("All phase tasks must be complete or skipped.");
|
|
8248
|
-
if (phase.gate.status !== "passed")
|
|
8249
|
-
throw new Error("Phase gates must pass before completion.");
|
|
8250
|
-
if (plan.execution?.policy === "commit-per-phase")
|
|
8251
|
-
assertPhasePushEligible(plan, phase.id, input.executionId);
|
|
8252
|
-
if (plan.execution?.policy === "commit-per-phase" && !input.commit)
|
|
8253
|
-
throw new Error("Commit-per-phase execution requires commit metadata before phase completion.");
|
|
8254
|
-
if (plan.execution?.policy === "commit-per-phase" && (!input.commit?.pushedAt || input.commit.ci?.status !== "pending"))
|
|
8255
|
-
throw new Error("Commit-per-phase execution requires a pushed commit with pending CI monitoring.");
|
|
8256
|
-
if (plan.execution?.policy === "no-commit" && input.commit)
|
|
8257
|
-
throw new Error("No-commit execution cannot record a phase commit.");
|
|
8258
|
-
}
|
|
8259
|
-
const updated = {
|
|
8260
|
-
...phase,
|
|
8261
|
-
status: input.status,
|
|
8262
|
-
revision: phase.revision + 1,
|
|
8263
|
-
commit: input.commit ?? phase.commit,
|
|
8264
|
-
blocker: input.blockedReason ? blocker(input) : phase.blocker
|
|
8265
|
-
};
|
|
8266
|
-
if (input.status === "blocked" && input.blockedReason)
|
|
8267
|
-
escalation = escalationFor(plan, updated.id, undefined, input);
|
|
8268
|
-
const phases = plan.phases.map((item) => {
|
|
8269
|
-
if (item.id === updated.id)
|
|
8270
|
-
return updated;
|
|
8271
|
-
if (input.status === "completed" && plan.execution?.policy === "commit-per-phase" && input.commit && item.status !== "completed" && item.status !== "skipped" && item.gate.status === "passed")
|
|
8272
|
-
return { ...item, gate: { ...item.gate, status: "stale" } };
|
|
8273
|
-
return item;
|
|
8274
|
-
});
|
|
8275
|
-
return { ...heartbeat(plan), phases };
|
|
8276
|
-
}
|
|
8277
|
-
const task = getTask(phase, input.target.id);
|
|
8278
|
-
if (task.status !== input.expectedStatus)
|
|
8279
|
-
throw new Error(`Expected task status ${input.expectedStatus}, found ${task.status}.`);
|
|
8280
|
-
assertExecutionOwner(plan, input.executionId);
|
|
8281
|
-
assertTaskTransition(task, input.status, input.executionId, input.actor);
|
|
8282
|
-
if (input.status === "in_progress" || input.status === "completed") {
|
|
8283
|
-
if (phase.status !== "in_progress")
|
|
8284
|
-
throw new Error(`Phase ${phase.id} is not in progress.`);
|
|
8285
|
-
assertTaskDependenciesSatisfied(plan, task);
|
|
8286
|
-
}
|
|
8287
|
-
const updatedTask = {
|
|
8288
|
-
...task,
|
|
8289
|
-
status: input.status,
|
|
8290
|
-
revision: task.revision + 1,
|
|
8291
|
-
owner: input.status === "in_progress" ? input.actor : task.owner,
|
|
8292
|
-
executionId: input.status === "in_progress" ? input.executionId : task.executionId,
|
|
8293
|
-
blocker: input.blockedReason ? blocker(input) : task.blocker
|
|
8294
|
-
};
|
|
8295
|
-
if (input.status === "blocked" && input.blockedReason)
|
|
8296
|
-
escalation = escalationFor(plan, phase.id, task.id, input);
|
|
8297
|
-
const updatedPhase = {
|
|
8298
|
-
...phase,
|
|
8299
|
-
revision: phase.revision + 1,
|
|
8300
|
-
tasks: phase.tasks.map((item) => item.id === updatedTask.id ? updatedTask : item)
|
|
8301
|
-
};
|
|
8302
|
-
return {
|
|
8303
|
-
...heartbeat(plan),
|
|
8304
|
-
phases: plan.phases.map((item) => item.id === updatedPhase.id ? updatedPhase : item)
|
|
8305
|
-
};
|
|
8306
|
-
});
|
|
8307
|
-
await store.log(cwd, record.id, {
|
|
8308
|
-
planRevision: record.document.revision,
|
|
8309
|
-
kind: input.status === "blocked" ? "blocker" : "status",
|
|
8310
|
-
actor: input.actor,
|
|
8311
|
-
message: input.message,
|
|
8312
|
-
executionId: input.executionId,
|
|
8313
|
-
phaseId: input.target.type === "phase" ? input.target.id : escalation?.correlation.parentUnitId ?? escalation?.correlation.unitId,
|
|
8314
|
-
taskId: input.target.type === "task" ? input.target.id : undefined,
|
|
8315
|
-
evidence: input.evidence
|
|
8316
|
-
});
|
|
8317
|
-
return { record, escalation };
|
|
8318
|
-
}
|
|
8319
|
-
async function updateCi(store, cwd, query, input) {
|
|
8320
|
-
const record = await store.mutate(cwd, query, "update CI status", (plan) => {
|
|
8321
|
-
assertExecutionOwner(plan, input.executionId);
|
|
8322
|
-
if (plan.execution?.policy !== "commit-per-phase")
|
|
8323
|
-
throw new Error("CI status applies only to commit-per-phase execution.");
|
|
8324
|
-
const phase = getPhase(plan, input.phaseId);
|
|
8325
|
-
if (phase.status !== "completed")
|
|
8326
|
-
throw new Error(`Phase ${phase.id} must be completed before CI can settle.`);
|
|
8327
|
-
if (!phase.commit || phase.commit.sha !== input.sha)
|
|
8328
|
-
throw new Error(`Phase ${phase.id} is not committed at ${input.sha}.`);
|
|
8329
|
-
const current = phase.commit.ci?.status;
|
|
8330
|
-
if (current !== "pending")
|
|
8331
|
-
throw new Error(`CI for phase ${phase.id} is already ${current ?? "untracked"}.`);
|
|
8332
|
-
const timestamp = new Date().toISOString();
|
|
8333
|
-
const updated = {
|
|
8334
|
-
...phase,
|
|
8335
|
-
revision: phase.revision + 1,
|
|
8336
|
-
commit: {
|
|
8337
|
-
...phase.commit,
|
|
8338
|
-
ci: {
|
|
8339
|
-
...phase.commit.ci,
|
|
8340
|
-
status: input.status,
|
|
8341
|
-
startedAt: phase.commit.ci?.startedAt ?? timestamp,
|
|
8342
|
-
completedAt: timestamp,
|
|
8343
|
-
detail: input.detail
|
|
8344
|
-
}
|
|
8345
|
-
}
|
|
8346
|
-
};
|
|
8347
|
-
return {
|
|
8348
|
-
...heartbeat(plan),
|
|
8349
|
-
phases: plan.phases.map((item) => item.id === updated.id ? updated : item)
|
|
8350
|
-
};
|
|
8351
|
-
});
|
|
8352
|
-
await store.log(cwd, record.id, {
|
|
8353
|
-
planRevision: record.document.revision,
|
|
8354
|
-
kind: "gate",
|
|
8355
|
-
actor: input.actor,
|
|
8356
|
-
message: `CI ${input.status} for ${input.phaseId}: ${input.detail}`,
|
|
8357
|
-
executionId: input.executionId,
|
|
8358
|
-
phaseId: input.phaseId,
|
|
8359
|
-
evidence: [input.sha]
|
|
8360
|
-
});
|
|
8361
|
-
return record;
|
|
8362
|
-
}
|
|
8363
|
-
async function retryCi(store, cwd, query, input) {
|
|
8364
|
-
let previousDetail;
|
|
8365
|
-
const record = await store.mutate(cwd, query, "retry CI", (plan) => {
|
|
8366
|
-
assertExecutionOwner(plan, input.executionId);
|
|
8367
|
-
if (plan.execution?.policy !== "commit-per-phase")
|
|
8368
|
-
throw new Error("CI retry applies only to commit-per-phase execution.");
|
|
8369
|
-
const phase = getPhase(plan, input.phaseId);
|
|
8370
|
-
if (phase.status !== "completed")
|
|
8371
|
-
throw new Error(`Phase ${phase.id} must be completed before CI can retry.`);
|
|
8372
|
-
if (!phase.commit || phase.commit.sha !== input.sha)
|
|
8373
|
-
throw new Error(`Phase ${phase.id} is not committed at ${input.sha}.`);
|
|
8374
|
-
if (phase.commit.ci?.status !== "failed")
|
|
8375
|
-
throw new Error(`CI for phase ${phase.id} cannot retry from ${phase.commit.ci?.status ?? "untracked"}.`);
|
|
8376
|
-
previousDetail = phase.commit.ci.detail;
|
|
8377
|
-
const updated = {
|
|
8378
|
-
...phase,
|
|
8379
|
-
revision: phase.revision + 1,
|
|
8380
|
-
commit: {
|
|
8381
|
-
...phase.commit,
|
|
8382
|
-
ci: { status: "pending", startedAt: new Date().toISOString() }
|
|
8383
|
-
}
|
|
8384
|
-
};
|
|
8385
|
-
return {
|
|
8386
|
-
...heartbeat(plan),
|
|
8387
|
-
phases: plan.phases.map((item) => item.id === updated.id ? updated : item)
|
|
8388
|
-
};
|
|
8389
|
-
});
|
|
8390
|
-
await store.log(cwd, record.id, {
|
|
8391
|
-
planRevision: record.document.revision,
|
|
8392
|
-
kind: "gate",
|
|
8393
|
-
actor: input.actor,
|
|
8394
|
-
message: `Retrying CI for ${input.phaseId}: ${input.reason}`,
|
|
8395
|
-
executionId: input.executionId,
|
|
8396
|
-
phaseId: input.phaseId,
|
|
8397
|
-
evidence: [input.sha],
|
|
8398
|
-
data: { previousStatus: "failed", previousDetail }
|
|
8399
|
-
});
|
|
8400
|
-
return record;
|
|
8401
|
-
}
|
|
8402
|
-
function assertPhasePushEligible(plan, phaseId, executionId) {
|
|
8403
|
-
assertExecutionOwner(plan, executionId);
|
|
8404
|
-
if (plan.execution?.policy !== "commit-per-phase")
|
|
8405
|
-
return;
|
|
8406
|
-
const phaseIndex = plan.phases.findIndex((phase) => phase.id === phaseId);
|
|
8407
|
-
if (phaseIndex < 0)
|
|
8408
|
-
throw new Error(`Unknown phase: ${phaseId}.`);
|
|
8409
|
-
const unsettled = plan.phases.slice(0, phaseIndex).filter((phase) => {
|
|
8410
|
-
if (phase.status === "skipped")
|
|
8411
|
-
return false;
|
|
8412
|
-
return phase.status !== "completed" || !phase.commit?.pushedAt || !["passed", "skipped"].includes(phase.commit.ci?.status ?? "pending");
|
|
8413
|
-
});
|
|
8414
|
-
if (unsettled.length)
|
|
8415
|
-
throw new Error(`Prior phase CI must settle successfully before another push: ${unsettled.map((phase) => phase.id).join(", ")}.`);
|
|
8416
|
-
}
|
|
8417
|
-
function assertPhaseDependenciesSatisfied(plan, phase) {
|
|
8418
|
-
const unsettled = phase.dependencies.map((id) => getPhase(plan, id)).filter((dependency) => !dependencySatisfied(dependency.status));
|
|
8419
|
-
if (unsettled.length)
|
|
8420
|
-
throw new Error(`Phase ${phase.id} has incomplete dependencies: ${unsettled.map((dependency) => `${dependency.id} (${dependency.status})`).join(", ")}.`);
|
|
8421
|
-
}
|
|
8422
|
-
function assertTaskDependenciesSatisfied(plan, task) {
|
|
8423
|
-
const tasks = plan.phases.flatMap((phase) => phase.tasks);
|
|
8424
|
-
const unsettled = task.dependencies.map((id) => {
|
|
8425
|
-
const dependency = tasks.find((candidate) => candidate.id === id);
|
|
8426
|
-
if (!dependency)
|
|
8427
|
-
throw new Error(`Unknown task dependency: ${id}.`);
|
|
8428
|
-
return dependency;
|
|
8429
|
-
}).filter((dependency) => !dependencySatisfied(dependency.status));
|
|
8430
|
-
if (unsettled.length)
|
|
8431
|
-
throw new Error(`Task ${task.id} has incomplete dependencies: ${unsettled.map((dependency) => `${dependency.id} (${dependency.status})`).join(", ")}.`);
|
|
8432
|
-
}
|
|
8433
|
-
function dependencySatisfied(status) {
|
|
8434
|
-
return status === "completed" || status === "skipped";
|
|
8435
|
-
}
|
|
8436
|
-
var PLAN_TRANSITIONS = {
|
|
8437
|
-
draft: ["ready"],
|
|
8438
|
-
ready: ["draft", "in_progress"],
|
|
8439
|
-
in_progress: ["blocked", "completed"],
|
|
8440
|
-
blocked: ["draft", "ready", "in_progress"],
|
|
8441
|
-
completed: []
|
|
8442
|
-
};
|
|
8443
|
-
var WORK_TRANSITIONS = {
|
|
8444
|
-
pending: ["in_progress", "skipped"],
|
|
8445
|
-
in_progress: ["blocked", "completed"],
|
|
8446
|
-
blocked: ["pending", "in_progress", "skipped"],
|
|
8447
|
-
completed: [],
|
|
8448
|
-
skipped: []
|
|
8449
|
-
};
|
|
8450
|
-
function assertPlanTransition(from, to) {
|
|
8451
|
-
if (from !== to && !PLAN_TRANSITIONS[from].includes(to))
|
|
8452
|
-
throw new Error(`Invalid plan status transition: ${from} -> ${to}.`);
|
|
8453
|
-
}
|
|
8454
|
-
function assertPhaseTransition(from, to) {
|
|
8455
|
-
if (from !== to && !WORK_TRANSITIONS[from].includes(to))
|
|
8456
|
-
throw new Error(`Invalid phase status transition: ${from} -> ${to}.`);
|
|
8457
|
-
}
|
|
8458
|
-
function assertTaskTransition(task, to, executionId, actor) {
|
|
8459
|
-
if (task.status !== to && !WORK_TRANSITIONS[task.status].includes(to))
|
|
8460
|
-
throw new Error(`Invalid task status transition: ${task.status} -> ${to}.`);
|
|
8461
|
-
if (task.status === "in_progress" && task.executionId && executionId !== task.executionId)
|
|
8462
|
-
throw new Error(`Task ${task.id} is owned by execution ${task.executionId}.`);
|
|
8463
|
-
if (task.status === "in_progress" && task.owner && actor && actor !== task.owner)
|
|
8464
|
-
throw new Error(`Task ${task.id} is owned by ${task.owner}.`);
|
|
8465
|
-
}
|
|
8466
|
-
function getPhase(plan, phaseId) {
|
|
8467
|
-
const phase = plan.phases.find((item) => item.id === phaseId);
|
|
8468
|
-
if (!phase)
|
|
8469
|
-
throw new Error(`Unknown phase: ${phaseId}.`);
|
|
8470
|
-
return phase;
|
|
8471
|
-
}
|
|
8472
|
-
function findTaskPhase(plan, taskId) {
|
|
8473
|
-
const phase = plan.phases.find((item) => item.tasks.some((task) => task.id === taskId));
|
|
8474
|
-
if (!phase)
|
|
8475
|
-
throw new Error(`Unknown task: ${taskId}.`);
|
|
8476
|
-
return phase;
|
|
8477
|
-
}
|
|
8478
|
-
function getTask(phase, taskId) {
|
|
8479
|
-
const task = phase.tasks.find((item) => item.id === taskId);
|
|
8480
|
-
if (!task)
|
|
8481
|
-
throw new Error(`Unknown task: ${taskId}.`);
|
|
8482
|
-
return task;
|
|
8483
|
-
}
|
|
8484
|
-
function assertExecutionOwner(plan, executionId) {
|
|
8485
|
-
if (!plan.execution?.active || !executionId || plan.execution.id !== executionId)
|
|
8486
|
-
throw new Error(`Execution ${executionId ?? "(missing)"} does not own this plan.`);
|
|
8487
|
-
}
|
|
8488
|
-
function blocker(input) {
|
|
8489
|
-
return {
|
|
8490
|
-
reason: input.blockedReason,
|
|
8491
|
-
attempts: input.attempts,
|
|
8492
|
-
evidence: input.evidence,
|
|
8493
|
-
needsUserDecision: input.needsUserDecision
|
|
8494
|
-
};
|
|
8495
|
-
}
|
|
8496
|
-
function escalationFor(plan, phaseId, taskId, input) {
|
|
8497
|
-
return {
|
|
8498
|
-
correlation: {
|
|
8499
|
-
workflow: "plan",
|
|
8500
|
-
executionId: input.executionId,
|
|
8501
|
-
unitId: taskId ?? phaseId,
|
|
8502
|
-
parentUnitId: taskId ? phaseId : undefined,
|
|
8503
|
-
metadata: { planId: plan.id }
|
|
8504
|
-
},
|
|
8505
|
-
blocker: input.blockedReason,
|
|
8506
|
-
attempts: input.attempts ?? [],
|
|
8507
|
-
evidence: input.evidence ?? [],
|
|
8508
|
-
needsUserDecision: input.needsUserDecision ?? false
|
|
8509
|
-
};
|
|
8510
|
-
}
|
|
8511
|
-
function heartbeat(plan) {
|
|
8512
|
-
return plan.execution?.active ? { ...plan, execution: { ...plan.execution, heartbeatAt: new Date().toISOString() } } : plan;
|
|
8513
|
-
}
|
|
8514
7726
|
// src/setup.ts
|
|
8515
7727
|
import { parseFrontmatter as parseFrontmatter3 } from "@earendil-works/pi-coding-agent";
|
|
8516
|
-
import { readdir as
|
|
7728
|
+
import { readdir as readdir4, readFile as readFile12 } from "node:fs/promises";
|
|
8517
7729
|
import { homedir as homedir9 } from "node:os";
|
|
8518
|
-
import { basename as basename7, join as
|
|
7730
|
+
import { basename as basename7, join as join17 } from "node:path";
|
|
8519
7731
|
var MISE_DEPENDENCIES = [
|
|
8520
7732
|
{ name: "node", tool: "node", spec: "node@22", minimumVersion: "22.19.0" },
|
|
8521
7733
|
{ name: "zellij", tool: "zellij", spec: "zellij@latest", minimumVersion: undefined },
|
|
@@ -8554,7 +7766,7 @@ var FORGE_DEPENDENCIES = {
|
|
|
8554
7766
|
};
|
|
8555
7767
|
async function ensureMise(options = {}) {
|
|
8556
7768
|
const homeDir = options.homeDir ?? homedir9();
|
|
8557
|
-
const current = await mise.executableCheck() ?? await mise.executableCheck(
|
|
7769
|
+
const current = await mise.executableCheck() ?? await mise.executableCheck(join17(homeDir, ".local", "bin", "mise"));
|
|
8558
7770
|
if (current)
|
|
8559
7771
|
return { executable: current, action: createSetupAction("mise", "ready", current) };
|
|
8560
7772
|
reportProgress(options, "Installing mise");
|
|
@@ -8603,9 +7815,9 @@ async function ensureMiseDeps(miseExecutable, options = {}) {
|
|
|
8603
7815
|
async function ensurePiPlugins(options = {}) {
|
|
8604
7816
|
const actions = await ensurePiPackages(PI_PACKAGES, options);
|
|
8605
7817
|
const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
|
|
8606
|
-
const webSearch = await pi.configEnsure(
|
|
7818
|
+
const webSearch = await pi.configEnsure(join17(agentDir, "web-search.json"), (config) => ({ ...config, workflow: "auto-summary" }), options.dryRun);
|
|
8607
7819
|
actions.push(getConfigSetupAction("web search settings", webSearch));
|
|
8608
|
-
const lsp = await pi.configEnsure(
|
|
7820
|
+
const lsp = await pi.configEnsure(join17(agentDir, "pi-lsp.json"), (config) => ({
|
|
8609
7821
|
...config,
|
|
8610
7822
|
progressive: { ...getRecord(config.progressive), enabled: true, inject: "none" }
|
|
8611
7823
|
}), options.dryRun);
|
|
@@ -8616,11 +7828,11 @@ async function ensurePiAgents(options = {}) {
|
|
|
8616
7828
|
const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
|
|
8617
7829
|
const bundledAgentsDir = options.bundledAgentsDir ?? BUNDLED_AGENTS_DIR2;
|
|
8618
7830
|
const userConfig = await loadDiffpiConfig({ homeDir: options.homeDir });
|
|
8619
|
-
const entries = (await
|
|
7831
|
+
const entries = (await readdir4(bundledAgentsDir, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.startsWith("diffpi-") && entry.name.endsWith(".md")).sort((left, right) => left.name.localeCompare(right.name));
|
|
8620
7832
|
const actions = [];
|
|
8621
7833
|
for (const entry of entries) {
|
|
8622
7834
|
const id = basename7(entry.name, ".md").replace(/^diffpi-/, "");
|
|
8623
|
-
const source = await
|
|
7835
|
+
const source = await readFile12(join17(bundledAgentsDir, entry.name), "utf8");
|
|
8624
7836
|
const content = materializeAgentModels(source, id, userConfig.config, options.availableModels);
|
|
8625
7837
|
const result = await pi.agentEnsure(entry.name, content, agentDir, options.dryRun);
|
|
8626
7838
|
actions.push(getConfigSetupAction(`pi agent ${id}`, result));
|
|
@@ -8629,7 +7841,7 @@ async function ensurePiAgents(options = {}) {
|
|
|
8629
7841
|
}
|
|
8630
7842
|
async function ensurePiSkills(miseExecutable, options = {}) {
|
|
8631
7843
|
const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
|
|
8632
|
-
const sharedSkillsDir =
|
|
7844
|
+
const sharedSkillsDir = join17(options.homeDir ?? homedir9(), ".agents", "skills");
|
|
8633
7845
|
const actions = [];
|
|
8634
7846
|
for (const source of PI_SKILL_SOURCES) {
|
|
8635
7847
|
const missing = [];
|
|
@@ -8825,7 +8037,7 @@ function reportProgress(options, message) {
|
|
|
8825
8037
|
options.onProgress?.(message);
|
|
8826
8038
|
}
|
|
8827
8039
|
async function packageVersion2() {
|
|
8828
|
-
const source = await
|
|
8040
|
+
const source = await readFile12(join17(resolveBundledAgentsDir(), "..", "package.json"), "utf8");
|
|
8829
8041
|
let value;
|
|
8830
8042
|
try {
|
|
8831
8043
|
value = JSON.parse(source);
|
|
@@ -8850,13 +8062,13 @@ export {
|
|
|
8850
8062
|
addComment,
|
|
8851
8063
|
assertGitHubMergeReady,
|
|
8852
8064
|
assertReviewEventSupported,
|
|
8065
|
+
captureLocalReview,
|
|
8853
8066
|
checkConventionalSubject,
|
|
8854
8067
|
computeProjectSlug,
|
|
8855
8068
|
createForge,
|
|
8856
8069
|
createForgeBackend,
|
|
8857
|
-
createLocalReviewBackend,
|
|
8858
8070
|
createModeController,
|
|
8859
|
-
|
|
8071
|
+
createPlanStore,
|
|
8860
8072
|
createRemoteReviewBackend,
|
|
8861
8073
|
createVcsBackend,
|
|
8862
8074
|
dedupeFindings,
|
|
@@ -8887,45 +8099,36 @@ export {
|
|
|
8887
8099
|
launch,
|
|
8888
8100
|
listSessions,
|
|
8889
8101
|
loadDiffpiConfig,
|
|
8890
|
-
loadReviewPublicationState,
|
|
8891
8102
|
loadTemplate,
|
|
8892
8103
|
localReviewAuthor,
|
|
8893
8104
|
mcp,
|
|
8894
8105
|
mise,
|
|
8106
|
+
openFileAdjacent,
|
|
8895
8107
|
openInNewTab,
|
|
8896
8108
|
parseGitlabDiffRefs,
|
|
8897
8109
|
parseRemote,
|
|
8898
|
-
parseThreadArtifact,
|
|
8899
8110
|
pi,
|
|
8900
8111
|
plansDir,
|
|
8112
|
+
readLocalReview,
|
|
8901
8113
|
readSession,
|
|
8902
8114
|
renderReviewDoc,
|
|
8903
8115
|
renderTemplate,
|
|
8904
|
-
renderThreadArtifact,
|
|
8905
8116
|
resolveAgentMode,
|
|
8906
8117
|
resolveAgentModelPreferences,
|
|
8907
8118
|
resolvePrSession,
|
|
8908
8119
|
resolveReviewSession,
|
|
8909
8120
|
resolveSession,
|
|
8910
|
-
reviewBodyFingerprint,
|
|
8911
|
-
reviewCommentFingerprint,
|
|
8912
8121
|
reviewRecordName,
|
|
8913
|
-
reviewReplyFingerprint,
|
|
8914
8122
|
reviewSlug,
|
|
8915
8123
|
reviewsDir,
|
|
8916
8124
|
runMiseGates,
|
|
8917
|
-
saveReviewPublicationState,
|
|
8918
|
-
sessionsDir,
|
|
8919
8125
|
setupPi,
|
|
8920
8126
|
severitySchema,
|
|
8921
8127
|
storeDir,
|
|
8922
8128
|
storeGlobalRoot,
|
|
8923
8129
|
templateRelativePath,
|
|
8924
|
-
toFindings,
|
|
8925
8130
|
toReviewComments,
|
|
8926
8131
|
tuicrAvailable,
|
|
8927
|
-
unpublishedReviewComments,
|
|
8928
|
-
upsertThreadReply,
|
|
8929
8132
|
withRemoteProvenance,
|
|
8930
8133
|
yymmdd,
|
|
8931
8134
|
zedKeymapPath,
|