@difflab/pi 0.4.0-rc.202609231122.7b4400b → 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 +811 -306
- 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 +2458 -6098
- 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 +724 -188
- 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 +5 -2
- package/dist/plan/index.d.ts.map +1 -1
- package/dist/plan/lock.d.ts +11 -0
- package/dist/plan/lock.d.ts.map +1 -0
- package/dist/plan/log.d.ts +6 -0
- package/dist/plan/log.d.ts.map +1 -0
- 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 +3 -6
- 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 +2096 -1776
- 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/fsx.d.ts +0 -8
- package/dist/extensions/fsx.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,93 +2581,271 @@ 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
|
-
|
|
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}`;
|
|
2603
|
+
}
|
|
2634
2604
|
|
|
2635
|
-
// src/
|
|
2636
|
-
import {
|
|
2637
|
-
import {
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
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;
|
|
2642
2624
|
}
|
|
2643
|
-
|
|
2625
|
+
|
|
2626
|
+
// src/plan/log.ts
|
|
2627
|
+
async function appendPlanLog(logPath, event) {
|
|
2628
|
+
return appendLogEntry(logPath, event);
|
|
2629
|
+
}
|
|
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");
|
|
2644
2638
|
const waitMs = options.waitMs ?? 5000;
|
|
2645
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
|
+
};
|
|
2646
2647
|
const started = Date.now();
|
|
2647
2648
|
while (true) {
|
|
2648
2649
|
try {
|
|
2649
2650
|
await mkdir6(lockDir);
|
|
2651
|
+
await writeFile7(join13(lockDir, "owner.json"), `${JSON.stringify(owner)}
|
|
2652
|
+
`, { mode: 384 });
|
|
2650
2653
|
break;
|
|
2651
2654
|
} catch (error) {
|
|
2652
2655
|
if (error.code !== "EEXIST")
|
|
2653
2656
|
throw error;
|
|
2654
|
-
if (Date.now() - started >= waitMs)
|
|
2655
|
-
|
|
2656
|
-
|
|
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));
|
|
2657
2665
|
}
|
|
2658
2666
|
}
|
|
2659
2667
|
try {
|
|
2660
2668
|
return await operation();
|
|
2661
2669
|
} finally {
|
|
2662
|
-
|
|
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 });
|
|
2663
2676
|
}
|
|
2664
2677
|
}
|
|
2665
|
-
function sleep(ms) {
|
|
2666
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2667
|
-
}
|
|
2668
2678
|
|
|
2669
|
-
// src/
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
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 };
|
|
2682
2712
|
}
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
const
|
|
2692
|
-
const
|
|
2693
|
-
if (
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
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" });
|
|
2699
2763
|
}
|
|
2700
|
-
function
|
|
2701
|
-
return
|
|
2764
|
+
function annotationStatePath(record) {
|
|
2765
|
+
return join14(record.dir, "annotations.json");
|
|
2702
2766
|
}
|
|
2703
|
-
function
|
|
2704
|
-
const
|
|
2705
|
-
|
|
2706
|
-
|
|
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)}.`);
|
|
2707
2774
|
}
|
|
2708
|
-
|
|
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;
|
|
2709
2779
|
}
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
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 };
|
|
2717
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(" ");
|
|
2718
2849
|
}
|
|
2719
2850
|
|
|
2720
2851
|
// src/plan/markdown.ts
|
|
@@ -7354,6 +7485,25 @@ function looksLikeAValue(value) {
|
|
|
7354
7485
|
function isUint8Array2(value) {
|
|
7355
7486
|
return Boolean(value && typeof value === "object" && "byteLength" in value && "byteOffset" in value);
|
|
7356
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
|
+
|
|
7357
7507
|
// src/plan/markdown.ts
|
|
7358
7508
|
var PLAN_MARKER = /<!-- diffpi-plan: (\{[^\n]+\}) -->/;
|
|
7359
7509
|
var PHASE_MARKER = /<!-- diffpi-phase: (\{[^\n]+\}) -->/g;
|
|
@@ -7401,6 +7551,8 @@ function validatePlanDocument(plan, options = {}) {
|
|
|
7401
7551
|
add("design-too-long", "error", `Design is ${designWords} words; finalization allows at most 800.`);
|
|
7402
7552
|
if (options.strict && plan.phases.length === 0)
|
|
7403
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.`);
|
|
7404
7556
|
return issues;
|
|
7405
7557
|
}
|
|
7406
7558
|
function renderPlanDocument(plan, previousSource) {
|
|
@@ -7742,19 +7894,6 @@ function parseCsv(value) {
|
|
|
7742
7894
|
return [];
|
|
7743
7895
|
return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
7744
7896
|
}
|
|
7745
|
-
function assertStableId(value, label) {
|
|
7746
|
-
if (!zx.id.safeParse(value).success)
|
|
7747
|
-
throw new Error(`${label} must be a lowercase stable slug, not a path: ${value}`);
|
|
7748
|
-
}
|
|
7749
|
-
function assertUniqueIds(ids, label) {
|
|
7750
|
-
const seen = new Set;
|
|
7751
|
-
for (const id of ids) {
|
|
7752
|
-
assertStableId(id, label);
|
|
7753
|
-
if (seen.has(id))
|
|
7754
|
-
throw new Error(`Duplicate ${label}: ${id}.`);
|
|
7755
|
-
seen.add(id);
|
|
7756
|
-
}
|
|
7757
|
-
}
|
|
7758
7897
|
function cleanPlaceholder(value) {
|
|
7759
7898
|
return value.replace(/<!--[^]*?-->/g, "").trim();
|
|
7760
7899
|
}
|
|
@@ -7786,6 +7925,46 @@ function escapeRegExp(value) {
|
|
|
7786
7925
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7787
7926
|
}
|
|
7788
7927
|
|
|
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
|
+
|
|
7789
7968
|
// src/plan/store.ts
|
|
7790
7969
|
function planRecordName(shortSlug, date = new Date) {
|
|
7791
7970
|
const slug = normalizeSlug(shortSlug);
|
|
@@ -7801,7 +7980,7 @@ async function resolvePlan(cwd, query, filters = {}, options = {}) {
|
|
|
7801
7980
|
continue;
|
|
7802
7981
|
if (normalizedQuery && entry.name !== normalizedQuery && stripDate(entry.name) !== normalizedQuery)
|
|
7803
7982
|
continue;
|
|
7804
|
-
const dir =
|
|
7983
|
+
const dir = join17(root, entry.name);
|
|
7805
7984
|
await assertContained(root, dir);
|
|
7806
7985
|
try {
|
|
7807
7986
|
const record = await readRecord(dir);
|
|
@@ -7827,7 +8006,7 @@ function createPlanStore(options = {}) {
|
|
|
7827
8006
|
async init(input) {
|
|
7828
8007
|
const root = await plansDir(input.cwd, options.homeDir);
|
|
7829
8008
|
const id = planRecordName(input.shortSlug, now());
|
|
7830
|
-
const dir =
|
|
8009
|
+
const dir = join17(root, id);
|
|
7831
8010
|
await assertContained(root, dirname8(dir));
|
|
7832
8011
|
try {
|
|
7833
8012
|
await mkdir7(dir);
|
|
@@ -7847,19 +8026,25 @@ function createPlanStore(options = {}) {
|
|
|
7847
8026
|
branch: input.branch,
|
|
7848
8027
|
title: input.title?.trim() || titleFromSlug(input.shortSlug),
|
|
7849
8028
|
intent: input.intent?.trim() || "<!-- Describe the intended outcome. -->",
|
|
7850
|
-
issue_id: input.issueId?.trim() || "<!-- Add issue tracker ID. -->",
|
|
7851
|
-
issue_url: input.issueUrl?.trim() || "<!-- Add issue tracker URL. -->",
|
|
7852
8029
|
created_at: timestamp,
|
|
7853
8030
|
updated_at: timestamp
|
|
7854
8031
|
});
|
|
7855
|
-
const document = parsePlanDocument(source,
|
|
7856
|
-
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
|
+
});
|
|
7857
8042
|
return {
|
|
7858
8043
|
id,
|
|
7859
8044
|
dir,
|
|
7860
|
-
planPath:
|
|
7861
|
-
logPath:
|
|
7862
|
-
implementationDir:
|
|
8045
|
+
planPath: join17(dir, "PLAN.md"),
|
|
8046
|
+
logPath: join17(dir, "logs.txt"),
|
|
8047
|
+
implementationDir: join17(dir, "implementation"),
|
|
7863
8048
|
document,
|
|
7864
8049
|
source
|
|
7865
8050
|
};
|
|
@@ -7879,7 +8064,7 @@ function createPlanStore(options = {}) {
|
|
|
7879
8064
|
},
|
|
7880
8065
|
async mutate(cwd, query, operation, update) {
|
|
7881
8066
|
const initial = await this.read(cwd, query);
|
|
7882
|
-
return
|
|
8067
|
+
return withPlanLock(initial.dir, async () => {
|
|
7883
8068
|
const current = await readRecord(initial.dir);
|
|
7884
8069
|
const next = await update(structuredClone(current.document));
|
|
7885
8070
|
if (next.id !== current.id)
|
|
@@ -7893,41 +8078,41 @@ function createPlanStore(options = {}) {
|
|
|
7893
8078
|
parsePlanDocument(source, current.planPath);
|
|
7894
8079
|
await atomicWrite(current.planPath, source);
|
|
7895
8080
|
await ensureImplementationFiles(current.dir, document, options);
|
|
7896
|
-
return { ...current, document, source, implementationDir:
|
|
8081
|
+
return { ...current, document, source, implementationDir: join17(current.dir, "implementation") };
|
|
7897
8082
|
}, { operation });
|
|
7898
8083
|
},
|
|
7899
8084
|
async log(cwd, query, event) {
|
|
7900
8085
|
const record = await this.read(cwd, query);
|
|
7901
|
-
return
|
|
8086
|
+
return withPlanLock(record.dir, () => appendPlanLog(record.logPath, event), { operation: "append log" });
|
|
7902
8087
|
}
|
|
7903
8088
|
};
|
|
7904
8089
|
}
|
|
7905
8090
|
async function readRecord(dir) {
|
|
7906
|
-
const planPath =
|
|
7907
|
-
const logPath =
|
|
7908
|
-
const source = await
|
|
8091
|
+
const planPath = join17(dir, "PLAN.md");
|
|
8092
|
+
const logPath = join17(dir, "logs.txt");
|
|
8093
|
+
const source = await readFile14(planPath, "utf8");
|
|
7909
8094
|
const document = parsePlanDocument(source, planPath);
|
|
7910
8095
|
return {
|
|
7911
8096
|
id: basename6(dir),
|
|
7912
8097
|
dir,
|
|
7913
8098
|
planPath,
|
|
7914
8099
|
logPath,
|
|
7915
|
-
implementationDir:
|
|
8100
|
+
implementationDir: join17(dir, "implementation"),
|
|
7916
8101
|
document,
|
|
7917
8102
|
source
|
|
7918
8103
|
};
|
|
7919
8104
|
}
|
|
7920
8105
|
async function ensureImplementationFiles(dir, document, options) {
|
|
7921
|
-
const implementationDir =
|
|
8106
|
+
const implementationDir = join17(dir, "implementation");
|
|
7922
8107
|
await mkdir7(implementationDir, { recursive: true });
|
|
7923
8108
|
const template = await loadTemplate("plan/implementation", {
|
|
7924
8109
|
homeDir: options.homeDir,
|
|
7925
8110
|
bundledDir: options.bundledTemplatesDir
|
|
7926
8111
|
});
|
|
7927
8112
|
for (const phase of document.phases) {
|
|
7928
|
-
const path =
|
|
8113
|
+
const path = join17(implementationDir, `phase-${phase.id}.md`);
|
|
7929
8114
|
try {
|
|
7930
|
-
await
|
|
8115
|
+
await readFile14(path, "utf8");
|
|
7931
8116
|
} catch (error) {
|
|
7932
8117
|
if (error.code !== "ENOENT")
|
|
7933
8118
|
throw error;
|
|
@@ -7941,6 +8126,25 @@ async function ensureImplementationFiles(dir, document, options) {
|
|
|
7941
8126
|
}
|
|
7942
8127
|
}
|
|
7943
8128
|
}
|
|
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
|
+
}
|
|
7944
8148
|
async function assertContained(root, candidate) {
|
|
7945
8149
|
const canonicalRoot = await realpath4(root);
|
|
7946
8150
|
let canonicalCandidate;
|
|
@@ -7958,8 +8162,7 @@ function normalizeSlug(value) {
|
|
|
7958
8162
|
throw new Error(`Invalid plan slug: ${value}.`);
|
|
7959
8163
|
}
|
|
7960
8164
|
const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
7961
|
-
|
|
7962
|
-
throw new Error(`Invalid plan slug: ${value}.`);
|
|
8165
|
+
assertStableId(slug, "plan slug");
|
|
7963
8166
|
return slug;
|
|
7964
8167
|
}
|
|
7965
8168
|
function normalizeQuery(value) {
|
|
@@ -7974,11 +8177,345 @@ function stripDate(value) {
|
|
|
7974
8177
|
function titleFromSlug(value) {
|
|
7975
8178
|
return normalizeSlug(value).replace(/^(?:[a-z]+-\d+-)/, "").split("-").map((part) => part[0].toUpperCase() + part.slice(1)).join(" ");
|
|
7976
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
|
+
}
|
|
7977
8514
|
// src/setup.ts
|
|
7978
8515
|
import { parseFrontmatter as parseFrontmatter3 } from "@earendil-works/pi-coding-agent";
|
|
7979
|
-
import { readdir as readdir3, readFile as
|
|
8516
|
+
import { readdir as readdir3, readFile as readFile15 } from "node:fs/promises";
|
|
7980
8517
|
import { homedir as homedir9 } from "node:os";
|
|
7981
|
-
import { basename as basename7, join as
|
|
8518
|
+
import { basename as basename7, join as join18 } from "node:path";
|
|
7982
8519
|
var MISE_DEPENDENCIES = [
|
|
7983
8520
|
{ name: "node", tool: "node", spec: "node@22", minimumVersion: "22.19.0" },
|
|
7984
8521
|
{ name: "zellij", tool: "zellij", spec: "zellij@latest", minimumVersion: undefined },
|
|
@@ -8017,7 +8554,7 @@ var FORGE_DEPENDENCIES = {
|
|
|
8017
8554
|
};
|
|
8018
8555
|
async function ensureMise(options = {}) {
|
|
8019
8556
|
const homeDir = options.homeDir ?? homedir9();
|
|
8020
|
-
const current = await mise.executableCheck() ?? await mise.executableCheck(
|
|
8557
|
+
const current = await mise.executableCheck() ?? await mise.executableCheck(join18(homeDir, ".local", "bin", "mise"));
|
|
8021
8558
|
if (current)
|
|
8022
8559
|
return { executable: current, action: createSetupAction("mise", "ready", current) };
|
|
8023
8560
|
reportProgress(options, "Installing mise");
|
|
@@ -8066,9 +8603,9 @@ async function ensureMiseDeps(miseExecutable, options = {}) {
|
|
|
8066
8603
|
async function ensurePiPlugins(options = {}) {
|
|
8067
8604
|
const actions = await ensurePiPackages(PI_PACKAGES, options);
|
|
8068
8605
|
const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
|
|
8069
|
-
const webSearch = await pi.configEnsure(
|
|
8606
|
+
const webSearch = await pi.configEnsure(join18(agentDir, "web-search.json"), (config) => ({ ...config, workflow: "auto-summary" }), options.dryRun);
|
|
8070
8607
|
actions.push(getConfigSetupAction("web search settings", webSearch));
|
|
8071
|
-
const lsp = await pi.configEnsure(
|
|
8608
|
+
const lsp = await pi.configEnsure(join18(agentDir, "pi-lsp.json"), (config) => ({
|
|
8072
8609
|
...config,
|
|
8073
8610
|
progressive: { ...getRecord(config.progressive), enabled: true, inject: "none" }
|
|
8074
8611
|
}), options.dryRun);
|
|
@@ -8083,7 +8620,7 @@ async function ensurePiAgents(options = {}) {
|
|
|
8083
8620
|
const actions = [];
|
|
8084
8621
|
for (const entry of entries) {
|
|
8085
8622
|
const id = basename7(entry.name, ".md").replace(/^diffpi-/, "");
|
|
8086
|
-
const source = await
|
|
8623
|
+
const source = await readFile15(join18(bundledAgentsDir, entry.name), "utf8");
|
|
8087
8624
|
const content = materializeAgentModels(source, id, userConfig.config, options.availableModels);
|
|
8088
8625
|
const result = await pi.agentEnsure(entry.name, content, agentDir, options.dryRun);
|
|
8089
8626
|
actions.push(getConfigSetupAction(`pi agent ${id}`, result));
|
|
@@ -8092,7 +8629,7 @@ async function ensurePiAgents(options = {}) {
|
|
|
8092
8629
|
}
|
|
8093
8630
|
async function ensurePiSkills(miseExecutable, options = {}) {
|
|
8094
8631
|
const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
|
|
8095
|
-
const sharedSkillsDir =
|
|
8632
|
+
const sharedSkillsDir = join18(options.homeDir ?? homedir9(), ".agents", "skills");
|
|
8096
8633
|
const actions = [];
|
|
8097
8634
|
for (const source of PI_SKILL_SOURCES) {
|
|
8098
8635
|
const missing = [];
|
|
@@ -8288,7 +8825,7 @@ function reportProgress(options, message) {
|
|
|
8288
8825
|
options.onProgress?.(message);
|
|
8289
8826
|
}
|
|
8290
8827
|
async function packageVersion2() {
|
|
8291
|
-
const source = await
|
|
8828
|
+
const source = await readFile15(join18(resolveBundledAgentsDir(), "..", "package.json"), "utf8");
|
|
8292
8829
|
let value;
|
|
8293
8830
|
try {
|
|
8294
8831
|
value = JSON.parse(source);
|
|
@@ -8319,7 +8856,7 @@ export {
|
|
|
8319
8856
|
createForgeBackend,
|
|
8320
8857
|
createLocalReviewBackend,
|
|
8321
8858
|
createModeController,
|
|
8322
|
-
|
|
8859
|
+
createPlanController,
|
|
8323
8860
|
createRemoteReviewBackend,
|
|
8324
8861
|
createVcsBackend,
|
|
8325
8862
|
dedupeFindings,
|
|
@@ -8355,7 +8892,6 @@ export {
|
|
|
8355
8892
|
localReviewAuthor,
|
|
8356
8893
|
mcp,
|
|
8357
8894
|
mise,
|
|
8358
|
-
openFileAdjacent,
|
|
8359
8895
|
openInNewTab,
|
|
8360
8896
|
parseGitlabDiffRefs,
|
|
8361
8897
|
parseRemote,
|