@difflab/pi 0.4.0-rc.202609231106.845dbe5 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/agents/diffpi-orchestrator.md +1 -1
- package/agents/diffpi-planner.md +3 -3
- package/agents/diffpi-worker.md +2 -2
- package/dist/assets.d.ts +0 -1
- package/dist/assets.d.ts.map +1 -1
- package/dist/cli/plan.d.ts +3 -7
- package/dist/cli/plan.d.ts.map +1 -1
- package/dist/cli.js +816 -314
- package/dist/commands/plan.d.ts +1 -0
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/environment.d.ts +1 -2
- package/dist/environment.d.ts.map +1 -1
- package/dist/extensions/index.js +2509 -6152
- package/dist/extensions/processx.d.ts +0 -1
- 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/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +728 -195
- package/dist/plan/annotations.d.ts +25 -0
- package/dist/plan/annotations.d.ts.map +1 -0
- package/dist/plan/controller.d.ts +65 -0
- package/dist/plan/controller.d.ts.map +1 -0
- package/dist/plan/execution.d.ts +13 -0
- package/dist/plan/execution.d.ts.map +1 -0
- package/dist/plan/ids.d.ts +5 -0
- package/dist/plan/ids.d.ts.map +1 -0
- package/dist/plan/index.d.ts +4 -2
- package/dist/plan/index.d.ts.map +1 -1
- package/dist/plan/lock.d.ts +7 -2
- package/dist/plan/lock.d.ts.map +1 -1
- package/dist/plan/markdown.d.ts.map +1 -1
- package/dist/plan/parser.d.ts +18 -0
- package/dist/plan/parser.d.ts.map +1 -0
- package/dist/plan/store.d.ts +0 -2
- package/dist/plan/store.d.ts.map +1 -1
- package/dist/plan/types.d.ts +22 -3
- package/dist/plan/types.d.ts.map +1 -1
- package/dist/tools/index.d.ts +0 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +2086 -1769
- package/dist/tools/log.d.ts.map +1 -1
- package/dist/tools/modes.d.ts.map +1 -1
- package/dist/tools/plan.d.ts +2 -2
- package/dist/tools/plan.d.ts.map +1 -1
- package/package.json +2 -4
- package/skills/plan/SKILL.md +7 -15
- package/skills/plan/references/workflows/annotate.md +6 -0
- package/skills/plan/references/workflows/finalize.md +7 -0
- package/skills/plan/references/workflows/go.md +8 -0
- package/{workflows/plan → skills/plan/references/workflows}/help.md +4 -11
- package/skills/plan/references/workflows/init.md +6 -0
- package/skills/plan/references/workflows/new.md +8 -0
- package/skills/plan/references/workflows/update.md +7 -0
- package/templates/plan/PLAN.md +0 -2
- package/dist/commands.d.ts +0 -22
- package/dist/commands.d.ts.map +0 -1
- package/dist/extensions/zodx.d.ts +0 -8
- package/dist/extensions/zodx.d.ts.map +0 -1
- package/dist/plan/operations.d.ts +0 -43
- package/dist/plan/operations.d.ts.map +0 -1
- package/dist/plan/reviews.d.ts +0 -28
- package/dist/plan/reviews.d.ts.map +0 -1
- package/dist/tools/ci.d.ts +0 -6
- package/dist/tools/ci.d.ts.map +0 -1
- package/dist/tools/plan-schema.d.ts +0 -211
- package/dist/tools/plan-schema.d.ts.map +0 -1
- package/dist/workflow.d.ts +0 -33
- package/dist/workflow.d.ts.map +0 -1
- package/workflows/plan/annotate.md +0 -13
- package/workflows/plan/finalize.md +0 -15
- package/workflows/plan/go.md +0 -20
- package/workflows/plan/init.md +0 -13
- package/workflows/plan/new.md +0 -20
- package/workflows/plan/update.md +0 -18
package/dist/index.js
CHANGED
|
@@ -176,19 +176,10 @@ 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
|
|
179
|
+
import { z } 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
|
-
|
|
192
183
|
// src/fsx.ts
|
|
193
184
|
import { readdir, readFile } from "node:fs/promises";
|
|
194
185
|
async function readDirectoryIfExists(path) {
|
|
@@ -214,12 +205,12 @@ function isMissingPath(error) {
|
|
|
214
205
|
}
|
|
215
206
|
|
|
216
207
|
// src/config.ts
|
|
217
|
-
var modelReferenceSchema =
|
|
218
|
-
var agentConfigSchema =
|
|
219
|
-
models:
|
|
208
|
+
var modelReferenceSchema = z.string().trim().min(1);
|
|
209
|
+
var agentConfigSchema = z.object({
|
|
210
|
+
models: z.array(modelReferenceSchema).optional()
|
|
220
211
|
}).strict();
|
|
221
|
-
var diffpiConfigSchema =
|
|
222
|
-
agents:
|
|
212
|
+
var diffpiConfigSchema = z.object({
|
|
213
|
+
agents: z.record(z.string(), agentConfigSchema).optional()
|
|
223
214
|
}).strict();
|
|
224
215
|
function diffpiConfigPaths(homeDir = homedir()) {
|
|
225
216
|
const directory = join(homeDir, ".difflab", "diffpi");
|
|
@@ -329,11 +320,7 @@ function run(command, args, options = {}) {
|
|
|
329
320
|
const child = spawn(command, args, {
|
|
330
321
|
cwd: options.cwd,
|
|
331
322
|
env: options.env ?? process.env,
|
|
332
|
-
stdio: [
|
|
333
|
-
options.interactive ? "inherit" : options.input === undefined ? "ignore" : "pipe",
|
|
334
|
-
options.interactive ? "inherit" : "pipe",
|
|
335
|
-
"pipe"
|
|
336
|
-
],
|
|
323
|
+
stdio: [options.input === undefined ? "ignore" : "pipe", "pipe", "pipe"],
|
|
337
324
|
signal: options.signal
|
|
338
325
|
});
|
|
339
326
|
let stdout = "";
|
|
@@ -350,8 +337,6 @@ function run(command, args, options = {}) {
|
|
|
350
337
|
});
|
|
351
338
|
child.stderr?.on("data", (chunk) => {
|
|
352
339
|
const text = chunk.toString();
|
|
353
|
-
if (options.interactive)
|
|
354
|
-
process.stderr.write(text);
|
|
355
340
|
if (unbounded)
|
|
356
341
|
stderrChunks.push(text);
|
|
357
342
|
else
|
|
@@ -603,29 +588,6 @@ function parseRemote(remote) {
|
|
|
603
588
|
function diffpiLaunchName(cwd, workflow) {
|
|
604
589
|
return `diffpi: ${basename(cwd)} / ${workflow}`;
|
|
605
590
|
}
|
|
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
|
-
}
|
|
629
591
|
async function openInNewTab(command, opts) {
|
|
630
592
|
const env = opts.env ?? process.env;
|
|
631
593
|
const name = opts.name ?? "review";
|
|
@@ -658,15 +620,6 @@ async function openInNewTab(command, opts) {
|
|
|
658
620
|
function screenWindowArgs(command, cwd, name) {
|
|
659
621
|
return ["-X", "screen", "-t", name, "sh", "-lc", 'cd -- "$1" && shift && exec "$@"', "sh", cwd, ...command];
|
|
660
622
|
}
|
|
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
|
-
}
|
|
670
623
|
async function packageVersion() {
|
|
671
624
|
let value;
|
|
672
625
|
try {
|
|
@@ -1279,16 +1232,16 @@ async function discoverMiseTasks(cwd) {
|
|
|
1279
1232
|
return parseMiseTasks(result.stdout);
|
|
1280
1233
|
}
|
|
1281
1234
|
// src/review/types.ts
|
|
1282
|
-
import { z as
|
|
1283
|
-
var severitySchema =
|
|
1284
|
-
var findingSchema =
|
|
1285
|
-
file:
|
|
1286
|
-
line:
|
|
1235
|
+
import { z as z2 } from "zod";
|
|
1236
|
+
var severitySchema = z2.enum(["BLOCKING", "CONSIDER", "NOTE"]);
|
|
1237
|
+
var findingSchema = z2.object({
|
|
1238
|
+
file: z2.string().min(1),
|
|
1239
|
+
line: z2.number().int().nonnegative(),
|
|
1287
1240
|
severity: severitySchema,
|
|
1288
|
-
body:
|
|
1289
|
-
reference:
|
|
1241
|
+
body: z2.string().min(1),
|
|
1242
|
+
reference: z2.string().optional().default("")
|
|
1290
1243
|
});
|
|
1291
|
-
var findingsSchema =
|
|
1244
|
+
var findingsSchema = z2.array(findingSchema);
|
|
1292
1245
|
function reviewSlug(input) {
|
|
1293
1246
|
return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50);
|
|
1294
1247
|
}
|
|
@@ -1349,17 +1302,17 @@ function renderCommentBody(finding) {
|
|
|
1349
1302
|
return `${prefix}${finding.body}${reference}`;
|
|
1350
1303
|
}
|
|
1351
1304
|
// src/review/review-markdown.ts
|
|
1352
|
-
import { z as
|
|
1353
|
-
var reviewThreadRecordSchema =
|
|
1354
|
-
id:
|
|
1355
|
-
file:
|
|
1356
|
-
line:
|
|
1357
|
-
body:
|
|
1358
|
-
author:
|
|
1359
|
-
resolved:
|
|
1360
|
-
addressed:
|
|
1361
|
-
question:
|
|
1362
|
-
replies:
|
|
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()
|
|
1363
1316
|
});
|
|
1364
1317
|
function renderReviewDoc(input) {
|
|
1365
1318
|
const anchored = dedupeFindings(input.findings).filter((finding) => finding.line > 0);
|
|
@@ -1446,7 +1399,7 @@ function parseThreadArtifact(content) {
|
|
|
1446
1399
|
let threads;
|
|
1447
1400
|
try {
|
|
1448
1401
|
const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
1449
|
-
threads =
|
|
1402
|
+
threads = z3.array(reviewThreadRecordSchema).parse(decoded);
|
|
1450
1403
|
} catch {
|
|
1451
1404
|
throw new Error("Cannot parse the Diffpi thread artifact payload.");
|
|
1452
1405
|
}
|
|
@@ -2082,7 +2035,7 @@ function createLocalReviewBackend(options) {
|
|
|
2082
2035
|
import { createHash as createHash2 } from "node:crypto";
|
|
2083
2036
|
import { readFile as readFile7, rename, writeFile as writeFile4 } from "node:fs/promises";
|
|
2084
2037
|
import { join as join9 } from "node:path";
|
|
2085
|
-
import { z as
|
|
2038
|
+
import { z as z4 } from "zod";
|
|
2086
2039
|
|
|
2087
2040
|
// src/store.ts
|
|
2088
2041
|
import { createHash } from "node:crypto";
|
|
@@ -2175,13 +2128,13 @@ function projectSlug(repository) {
|
|
|
2175
2128
|
}
|
|
2176
2129
|
|
|
2177
2130
|
// src/review/review-state.ts
|
|
2178
|
-
var reviewPublicationStateSchema =
|
|
2179
|
-
target:
|
|
2180
|
-
bodies:
|
|
2181
|
-
comments:
|
|
2182
|
-
stagedComments:
|
|
2183
|
-
replies:
|
|
2184
|
-
overlayPath:
|
|
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()
|
|
2185
2138
|
});
|
|
2186
2139
|
function reviewBodyFingerprint(body) {
|
|
2187
2140
|
return digest(body);
|
|
@@ -2205,7 +2158,7 @@ async function loadReviewPublicationState(cwd, vcs, number, homeDir) {
|
|
|
2205
2158
|
if (error.code === "ENOENT") {
|
|
2206
2159
|
return { path, state: { target, bodies: [], comments: [], stagedComments: [], replies: [] } };
|
|
2207
2160
|
}
|
|
2208
|
-
if (error instanceof SyntaxError || error instanceof
|
|
2161
|
+
if (error instanceof SyntaxError || error instanceof z4.ZodError) {
|
|
2209
2162
|
throw new Error(`Cannot parse review publication state: ${path}`);
|
|
2210
2163
|
}
|
|
2211
2164
|
throw error;
|
|
@@ -2628,49 +2581,36 @@ function parseJsonObject(content, path) {
|
|
|
2628
2581
|
} catch {}
|
|
2629
2582
|
throw new Error(`Expected valid JSON object in ${path}.`);
|
|
2630
2583
|
}
|
|
2631
|
-
// src/
|
|
2632
|
-
import {
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
}
|
|
2651
|
-
function renderTemplate(content, variables) {
|
|
2652
|
-
return content.replace(/\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g, (match, key) => variables[key] ?? match);
|
|
2653
|
-
}
|
|
2654
|
-
function templateRelativePath(name) {
|
|
2655
|
-
const normalized = normalize(name.replaceAll("\\", "/")).replace(/^\.\//, "");
|
|
2656
|
-
if (!normalized || normalized.startsWith("../") || normalized.includes("/../") || normalized.startsWith("/")) {
|
|
2657
|
-
throw new Error(`Invalid template name: ${name}`);
|
|
2658
|
-
}
|
|
2659
|
-
return normalized.endsWith(".md") ? normalized : `${normalized}.md`;
|
|
2660
|
-
}
|
|
2661
|
-
async function readOptionalFile(path) {
|
|
2662
|
-
try {
|
|
2663
|
-
return await readFile10(path, "utf8");
|
|
2664
|
-
} catch (error) {
|
|
2665
|
-
if (error.code === "ENOENT")
|
|
2666
|
-
return;
|
|
2667
|
-
throw error;
|
|
2668
|
-
}
|
|
2584
|
+
// src/extensions/subagentx.ts
|
|
2585
|
+
import { z as z5 } from "zod";
|
|
2586
|
+
var escalationSchema = z5.object({
|
|
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}`;
|
|
2669
2603
|
}
|
|
2670
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
|
+
|
|
2671
2611
|
// src/log.ts
|
|
2672
2612
|
import { randomUUID } from "node:crypto";
|
|
2673
|
-
import { appendFile, readFile as
|
|
2613
|
+
import { appendFile, readFile as readFile10 } from "node:fs/promises";
|
|
2674
2614
|
async function appendLogEntry(logPath, event) {
|
|
2675
2615
|
const entry = {
|
|
2676
2616
|
...event,
|
|
@@ -2688,6 +2628,226 @@ async function appendPlanLog(logPath, event) {
|
|
|
2688
2628
|
return appendLogEntry(logPath, event);
|
|
2689
2629
|
}
|
|
2690
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");
|
|
2638
|
+
const waitMs = options.waitMs ?? 5000;
|
|
2639
|
+
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
|
+
const started = Date.now();
|
|
2648
|
+
while (true) {
|
|
2649
|
+
try {
|
|
2650
|
+
await mkdir6(lockDir);
|
|
2651
|
+
await writeFile7(join13(lockDir, "owner.json"), `${JSON.stringify(owner)}
|
|
2652
|
+
`, { mode: 384 });
|
|
2653
|
+
break;
|
|
2654
|
+
} catch (error) {
|
|
2655
|
+
if (error.code !== "EEXIST")
|
|
2656
|
+
throw error;
|
|
2657
|
+
if (Date.now() - started >= waitMs) {
|
|
2658
|
+
let existing = "unknown owner";
|
|
2659
|
+
try {
|
|
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));
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
try {
|
|
2668
|
+
return await operation();
|
|
2669
|
+
} finally {
|
|
2670
|
+
let current;
|
|
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 });
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
// src/plan/annotations.ts
|
|
2680
|
+
var commentSchema = z6.object({
|
|
2681
|
+
id: z6.string(),
|
|
2682
|
+
content: z6.string(),
|
|
2683
|
+
path: z6.string().optional(),
|
|
2684
|
+
start_line: z6.number().int().optional(),
|
|
2685
|
+
line: z6.number().int().optional(),
|
|
2686
|
+
end_line: z6.number().int().optional()
|
|
2687
|
+
}).loose();
|
|
2688
|
+
async function annotatePlan(record, runtime = {}) {
|
|
2689
|
+
const execute = runtime.execute ?? executeProcess;
|
|
2690
|
+
const result = await execute("tuicr", ["--file", record.planPath], dirname7(record.planPath), true);
|
|
2691
|
+
const sessionSlug = [...result.stderr.matchAll(/^tuicr-session:\s*(\S+)\s*$/gm)].at(-1)?.[1];
|
|
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 };
|
|
2712
|
+
}
|
|
2713
|
+
async function readPlanAnnotations(record, options = {}) {
|
|
2714
|
+
const state = await readAnnotationState(record).catch((error) => {
|
|
2715
|
+
if (error.code === "ENOENT")
|
|
2716
|
+
return;
|
|
2717
|
+
throw error;
|
|
2718
|
+
});
|
|
2719
|
+
if (!state)
|
|
2720
|
+
return { comments: [], pending: [] };
|
|
2721
|
+
const execute = options.runtime?.execute ?? executeProcess;
|
|
2722
|
+
const response = await execute("tuicr", ["review", "comments", "--session", state.sessionSlug], record.dir, false);
|
|
2723
|
+
if (response.code !== 0) {
|
|
2724
|
+
if (/not found|no session|deleted/i.test(response.stderr))
|
|
2725
|
+
return { state, comments: [], pending: [] };
|
|
2726
|
+
throw new Error(response.stderr.trim() || "Cannot read tuicr comments.");
|
|
2727
|
+
}
|
|
2728
|
+
let raw;
|
|
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" });
|
|
2763
|
+
}
|
|
2764
|
+
function annotationStatePath(record) {
|
|
2765
|
+
return join14(record.dir, "annotations.json");
|
|
2766
|
+
}
|
|
2767
|
+
async function readAnnotationState(record) {
|
|
2768
|
+
const source = await readFile12(annotationStatePath(record), "utf8");
|
|
2769
|
+
let value;
|
|
2770
|
+
try {
|
|
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)}.`);
|
|
2777
|
+
}
|
|
2778
|
+
return value;
|
|
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
|
+
};
|
|
2801
|
+
}
|
|
2802
|
+
async function executeProcess(command, args, cwd, interactive) {
|
|
2803
|
+
if (!interactive) {
|
|
2804
|
+
const result = await run(command, args, { cwd, capture: "unbounded" });
|
|
2805
|
+
return { code: result.code, stdout: result.stdout, stderr: result.stderr };
|
|
2806
|
+
}
|
|
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
|
+
}
|
|
2850
|
+
|
|
2691
2851
|
// src/plan/markdown.ts
|
|
2692
2852
|
var import_mdast_builder = __toESM(require_src(), 1);
|
|
2693
2853
|
|
|
@@ -4034,7 +4194,7 @@ var handle = {
|
|
|
4034
4194
|
};
|
|
4035
4195
|
|
|
4036
4196
|
// ../../node_modules/mdast-util-to-markdown/lib/join.js
|
|
4037
|
-
var
|
|
4197
|
+
var join15 = [joinDefaults];
|
|
4038
4198
|
function joinDefaults(left, right, parent, state) {
|
|
4039
4199
|
if (right.type === "code" && formatCodeAsIndented(right, state) && (left.type === "list" || left.type === right.type && formatCodeAsIndented(left, state))) {
|
|
4040
4200
|
return false;
|
|
@@ -6580,7 +6740,7 @@ function toMarkdown(tree, options) {
|
|
|
6580
6740
|
handle: undefined,
|
|
6581
6741
|
indentLines,
|
|
6582
6742
|
indexStack: [],
|
|
6583
|
-
join: [...
|
|
6743
|
+
join: [...join15],
|
|
6584
6744
|
options: {},
|
|
6585
6745
|
safe: safeBound,
|
|
6586
6746
|
stack: [],
|
|
@@ -7325,6 +7485,25 @@ function looksLikeAValue(value) {
|
|
|
7325
7485
|
function isUint8Array2(value) {
|
|
7326
7486
|
return Boolean(value && typeof value === "object" && "byteLength" in value && "byteOffset" in value);
|
|
7327
7487
|
}
|
|
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
|
+
|
|
7328
7507
|
// src/plan/markdown.ts
|
|
7329
7508
|
var PLAN_MARKER = /<!-- diffpi-plan: (\{[^\n]+\}) -->/;
|
|
7330
7509
|
var PHASE_MARKER = /<!-- diffpi-phase: (\{[^\n]+\}) -->/g;
|
|
@@ -7372,6 +7551,8 @@ function validatePlanDocument(plan, options = {}) {
|
|
|
7372
7551
|
add("design-too-long", "error", `Design is ${designWords} words; finalization allows at most 800.`);
|
|
7373
7552
|
if (options.strict && plan.phases.length === 0)
|
|
7374
7553
|
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.`);
|
|
7375
7556
|
return issues;
|
|
7376
7557
|
}
|
|
7377
7558
|
function renderPlanDocument(plan, previousSource) {
|
|
@@ -7713,19 +7894,6 @@ function parseCsv(value) {
|
|
|
7713
7894
|
return [];
|
|
7714
7895
|
return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
7715
7896
|
}
|
|
7716
|
-
function assertStableId(value, label) {
|
|
7717
|
-
if (!zx.id.safeParse(value).success)
|
|
7718
|
-
throw new Error(`${label} must be a lowercase stable slug, not a path: ${value}`);
|
|
7719
|
-
}
|
|
7720
|
-
function assertUniqueIds(ids, label) {
|
|
7721
|
-
const seen = new Set;
|
|
7722
|
-
for (const id of ids) {
|
|
7723
|
-
assertStableId(id, label);
|
|
7724
|
-
if (seen.has(id))
|
|
7725
|
-
throw new Error(`Duplicate ${label}: ${id}.`);
|
|
7726
|
-
seen.add(id);
|
|
7727
|
-
}
|
|
7728
|
-
}
|
|
7729
7897
|
function cleanPlaceholder(value) {
|
|
7730
7898
|
return value.replace(/<!--[^]*?-->/g, "").trim();
|
|
7731
7899
|
}
|
|
@@ -7757,30 +7925,43 @@ function escapeRegExp(value) {
|
|
|
7757
7925
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7758
7926
|
}
|
|
7759
7927
|
|
|
7760
|
-
// src/plan/
|
|
7761
|
-
import { mkdir as
|
|
7762
|
-
import { join as
|
|
7763
|
-
|
|
7764
|
-
|
|
7765
|
-
|
|
7766
|
-
|
|
7767
|
-
|
|
7768
|
-
|
|
7769
|
-
|
|
7770
|
-
|
|
7771
|
-
|
|
7772
|
-
|
|
7773
|
-
|
|
7774
|
-
|
|
7775
|
-
|
|
7776
|
-
|
|
7777
|
-
|
|
7778
|
-
|
|
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}`);
|
|
7779
7955
|
}
|
|
7956
|
+
return normalized.endsWith(".md") ? normalized : `${normalized}.md`;
|
|
7957
|
+
}
|
|
7958
|
+
async function readOptionalFile(path) {
|
|
7780
7959
|
try {
|
|
7781
|
-
return await
|
|
7782
|
-
}
|
|
7783
|
-
|
|
7960
|
+
return await readFile13(path, "utf8");
|
|
7961
|
+
} catch (error) {
|
|
7962
|
+
if (error.code === "ENOENT")
|
|
7963
|
+
return;
|
|
7964
|
+
throw error;
|
|
7784
7965
|
}
|
|
7785
7966
|
}
|
|
7786
7967
|
|
|
@@ -7799,7 +7980,7 @@ async function resolvePlan(cwd, query, filters = {}, options = {}) {
|
|
|
7799
7980
|
continue;
|
|
7800
7981
|
if (normalizedQuery && entry.name !== normalizedQuery && stripDate(entry.name) !== normalizedQuery)
|
|
7801
7982
|
continue;
|
|
7802
|
-
const dir =
|
|
7983
|
+
const dir = join17(root, entry.name);
|
|
7803
7984
|
await assertContained(root, dir);
|
|
7804
7985
|
try {
|
|
7805
7986
|
const record = await readRecord(dir);
|
|
@@ -7825,8 +8006,8 @@ function createPlanStore(options = {}) {
|
|
|
7825
8006
|
async init(input) {
|
|
7826
8007
|
const root = await plansDir(input.cwd, options.homeDir);
|
|
7827
8008
|
const id = planRecordName(input.shortSlug, now());
|
|
7828
|
-
const dir =
|
|
7829
|
-
await assertContained(root,
|
|
8009
|
+
const dir = join17(root, id);
|
|
8010
|
+
await assertContained(root, dirname8(dir));
|
|
7830
8011
|
try {
|
|
7831
8012
|
await mkdir7(dir);
|
|
7832
8013
|
} catch (error) {
|
|
@@ -7845,19 +8026,25 @@ function createPlanStore(options = {}) {
|
|
|
7845
8026
|
branch: input.branch,
|
|
7846
8027
|
title: input.title?.trim() || titleFromSlug(input.shortSlug),
|
|
7847
8028
|
intent: input.intent?.trim() || "<!-- Describe the intended outcome. -->",
|
|
7848
|
-
issue_id: input.issueId?.trim() || "<!-- Add issue tracker ID. -->",
|
|
7849
|
-
issue_url: input.issueUrl?.trim() || "<!-- Add issue tracker URL. -->",
|
|
7850
8029
|
created_at: timestamp,
|
|
7851
8030
|
updated_at: timestamp
|
|
7852
8031
|
});
|
|
7853
|
-
const document = parsePlanDocument(source,
|
|
7854
|
-
await atomicWrite(
|
|
8032
|
+
const document = parsePlanDocument(source, join17(dir, "PLAN.md"));
|
|
8033
|
+
await atomicWrite(join17(dir, "PLAN.md"), source);
|
|
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
|
+
});
|
|
7855
8042
|
return {
|
|
7856
8043
|
id,
|
|
7857
8044
|
dir,
|
|
7858
|
-
planPath:
|
|
7859
|
-
logPath:
|
|
7860
|
-
implementationDir:
|
|
8045
|
+
planPath: join17(dir, "PLAN.md"),
|
|
8046
|
+
logPath: join17(dir, "logs.txt"),
|
|
8047
|
+
implementationDir: join17(dir, "implementation"),
|
|
7861
8048
|
document,
|
|
7862
8049
|
source
|
|
7863
8050
|
};
|
|
@@ -7891,41 +8078,41 @@ function createPlanStore(options = {}) {
|
|
|
7891
8078
|
parsePlanDocument(source, current.planPath);
|
|
7892
8079
|
await atomicWrite(current.planPath, source);
|
|
7893
8080
|
await ensureImplementationFiles(current.dir, document, options);
|
|
7894
|
-
return { ...current, document, source, implementationDir:
|
|
8081
|
+
return { ...current, document, source, implementationDir: join17(current.dir, "implementation") };
|
|
7895
8082
|
}, { operation });
|
|
7896
8083
|
},
|
|
7897
8084
|
async log(cwd, query, event) {
|
|
7898
8085
|
const record = await this.read(cwd, query);
|
|
7899
|
-
return appendPlanLog(record.logPath, event);
|
|
8086
|
+
return withPlanLock(record.dir, () => appendPlanLog(record.logPath, event), { operation: "append log" });
|
|
7900
8087
|
}
|
|
7901
8088
|
};
|
|
7902
8089
|
}
|
|
7903
8090
|
async function readRecord(dir) {
|
|
7904
|
-
const planPath =
|
|
7905
|
-
const logPath =
|
|
7906
|
-
const source = await
|
|
8091
|
+
const planPath = join17(dir, "PLAN.md");
|
|
8092
|
+
const logPath = join17(dir, "logs.txt");
|
|
8093
|
+
const source = await readFile14(planPath, "utf8");
|
|
7907
8094
|
const document = parsePlanDocument(source, planPath);
|
|
7908
8095
|
return {
|
|
7909
|
-
id:
|
|
8096
|
+
id: basename6(dir),
|
|
7910
8097
|
dir,
|
|
7911
8098
|
planPath,
|
|
7912
8099
|
logPath,
|
|
7913
|
-
implementationDir:
|
|
8100
|
+
implementationDir: join17(dir, "implementation"),
|
|
7914
8101
|
document,
|
|
7915
8102
|
source
|
|
7916
8103
|
};
|
|
7917
8104
|
}
|
|
7918
8105
|
async function ensureImplementationFiles(dir, document, options) {
|
|
7919
|
-
const implementationDir =
|
|
8106
|
+
const implementationDir = join17(dir, "implementation");
|
|
7920
8107
|
await mkdir7(implementationDir, { recursive: true });
|
|
7921
8108
|
const template = await loadTemplate("plan/implementation", {
|
|
7922
8109
|
homeDir: options.homeDir,
|
|
7923
8110
|
bundledDir: options.bundledTemplatesDir
|
|
7924
8111
|
});
|
|
7925
8112
|
for (const phase of document.phases) {
|
|
7926
|
-
const path =
|
|
8113
|
+
const path = join17(implementationDir, `phase-${phase.id}.md`);
|
|
7927
8114
|
try {
|
|
7928
|
-
await
|
|
8115
|
+
await readFile14(path, "utf8");
|
|
7929
8116
|
} catch (error) {
|
|
7930
8117
|
if (error.code !== "ENOENT")
|
|
7931
8118
|
throw error;
|
|
@@ -7940,9 +8127,23 @@ async function ensureImplementationFiles(dir, document, options) {
|
|
|
7940
8127
|
}
|
|
7941
8128
|
}
|
|
7942
8129
|
async function atomicWrite(path, content) {
|
|
7943
|
-
const temp =
|
|
7944
|
-
await
|
|
7945
|
-
|
|
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 {}
|
|
7946
8147
|
}
|
|
7947
8148
|
async function assertContained(root, candidate) {
|
|
7948
8149
|
const canonicalRoot = await realpath4(root);
|
|
@@ -7961,8 +8162,7 @@ function normalizeSlug(value) {
|
|
|
7961
8162
|
throw new Error(`Invalid plan slug: ${value}.`);
|
|
7962
8163
|
}
|
|
7963
8164
|
const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
7964
|
-
|
|
7965
|
-
throw new Error(`Invalid plan slug: ${value}.`);
|
|
8165
|
+
assertStableId(slug, "plan slug");
|
|
7966
8166
|
return slug;
|
|
7967
8167
|
}
|
|
7968
8168
|
function normalizeQuery(value) {
|
|
@@ -7977,11 +8177,345 @@ function stripDate(value) {
|
|
|
7977
8177
|
function titleFromSlug(value) {
|
|
7978
8178
|
return normalizeSlug(value).replace(/^(?:[a-z]+-\d+-)/, "").split("-").map((part) => part[0].toUpperCase() + part.slice(1)).join(" ");
|
|
7979
8179
|
}
|
|
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
|
+
}
|
|
7980
8514
|
// src/setup.ts
|
|
7981
8515
|
import { parseFrontmatter as parseFrontmatter3 } from "@earendil-works/pi-coding-agent";
|
|
7982
|
-
import { readdir as readdir3, readFile as
|
|
8516
|
+
import { readdir as readdir3, readFile as readFile15 } from "node:fs/promises";
|
|
7983
8517
|
import { homedir as homedir9 } from "node:os";
|
|
7984
|
-
import { basename as
|
|
8518
|
+
import { basename as basename7, join as join18 } from "node:path";
|
|
7985
8519
|
var MISE_DEPENDENCIES = [
|
|
7986
8520
|
{ name: "node", tool: "node", spec: "node@22", minimumVersion: "22.19.0" },
|
|
7987
8521
|
{ name: "zellij", tool: "zellij", spec: "zellij@latest", minimumVersion: undefined },
|
|
@@ -8020,7 +8554,7 @@ var FORGE_DEPENDENCIES = {
|
|
|
8020
8554
|
};
|
|
8021
8555
|
async function ensureMise(options = {}) {
|
|
8022
8556
|
const homeDir = options.homeDir ?? homedir9();
|
|
8023
|
-
const current = await mise.executableCheck() ?? await mise.executableCheck(
|
|
8557
|
+
const current = await mise.executableCheck() ?? await mise.executableCheck(join18(homeDir, ".local", "bin", "mise"));
|
|
8024
8558
|
if (current)
|
|
8025
8559
|
return { executable: current, action: createSetupAction("mise", "ready", current) };
|
|
8026
8560
|
reportProgress(options, "Installing mise");
|
|
@@ -8069,9 +8603,9 @@ async function ensureMiseDeps(miseExecutable, options = {}) {
|
|
|
8069
8603
|
async function ensurePiPlugins(options = {}) {
|
|
8070
8604
|
const actions = await ensurePiPackages(PI_PACKAGES, options);
|
|
8071
8605
|
const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
|
|
8072
|
-
const webSearch = await pi.configEnsure(
|
|
8606
|
+
const webSearch = await pi.configEnsure(join18(agentDir, "web-search.json"), (config) => ({ ...config, workflow: "auto-summary" }), options.dryRun);
|
|
8073
8607
|
actions.push(getConfigSetupAction("web search settings", webSearch));
|
|
8074
|
-
const lsp = await pi.configEnsure(
|
|
8608
|
+
const lsp = await pi.configEnsure(join18(agentDir, "pi-lsp.json"), (config) => ({
|
|
8075
8609
|
...config,
|
|
8076
8610
|
progressive: { ...getRecord(config.progressive), enabled: true, inject: "none" }
|
|
8077
8611
|
}), options.dryRun);
|
|
@@ -8085,8 +8619,8 @@ async function ensurePiAgents(options = {}) {
|
|
|
8085
8619
|
const entries = (await readdir3(bundledAgentsDir, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.startsWith("diffpi-") && entry.name.endsWith(".md")).sort((left, right) => left.name.localeCompare(right.name));
|
|
8086
8620
|
const actions = [];
|
|
8087
8621
|
for (const entry of entries) {
|
|
8088
|
-
const id =
|
|
8089
|
-
const source = await
|
|
8622
|
+
const id = basename7(entry.name, ".md").replace(/^diffpi-/, "");
|
|
8623
|
+
const source = await readFile15(join18(bundledAgentsDir, entry.name), "utf8");
|
|
8090
8624
|
const content = materializeAgentModels(source, id, userConfig.config, options.availableModels);
|
|
8091
8625
|
const result = await pi.agentEnsure(entry.name, content, agentDir, options.dryRun);
|
|
8092
8626
|
actions.push(getConfigSetupAction(`pi agent ${id}`, result));
|
|
@@ -8095,7 +8629,7 @@ async function ensurePiAgents(options = {}) {
|
|
|
8095
8629
|
}
|
|
8096
8630
|
async function ensurePiSkills(miseExecutable, options = {}) {
|
|
8097
8631
|
const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
|
|
8098
|
-
const sharedSkillsDir =
|
|
8632
|
+
const sharedSkillsDir = join18(options.homeDir ?? homedir9(), ".agents", "skills");
|
|
8099
8633
|
const actions = [];
|
|
8100
8634
|
for (const source of PI_SKILL_SOURCES) {
|
|
8101
8635
|
const missing = [];
|
|
@@ -8291,7 +8825,7 @@ function reportProgress(options, message) {
|
|
|
8291
8825
|
options.onProgress?.(message);
|
|
8292
8826
|
}
|
|
8293
8827
|
async function packageVersion2() {
|
|
8294
|
-
const source = await
|
|
8828
|
+
const source = await readFile15(join18(resolveBundledAgentsDir(), "..", "package.json"), "utf8");
|
|
8295
8829
|
let value;
|
|
8296
8830
|
try {
|
|
8297
8831
|
value = JSON.parse(source);
|
|
@@ -8322,7 +8856,7 @@ export {
|
|
|
8322
8856
|
createForgeBackend,
|
|
8323
8857
|
createLocalReviewBackend,
|
|
8324
8858
|
createModeController,
|
|
8325
|
-
|
|
8859
|
+
createPlanController,
|
|
8326
8860
|
createRemoteReviewBackend,
|
|
8327
8861
|
createVcsBackend,
|
|
8328
8862
|
dedupeFindings,
|
|
@@ -8358,7 +8892,6 @@ export {
|
|
|
8358
8892
|
localReviewAuthor,
|
|
8359
8893
|
mcp,
|
|
8360
8894
|
mise,
|
|
8361
|
-
openFileAdjacent,
|
|
8362
8895
|
openInNewTab,
|
|
8363
8896
|
parseGitlabDiffRefs,
|
|
8364
8897
|
parseRemote,
|