@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.
Files changed (78) hide show
  1. package/README.md +4 -4
  2. package/agents/diffpi-orchestrator.md +1 -1
  3. package/agents/diffpi-planner.md +3 -3
  4. package/agents/diffpi-worker.md +2 -2
  5. package/dist/assets.d.ts +0 -1
  6. package/dist/assets.d.ts.map +1 -1
  7. package/dist/cli/plan.d.ts +3 -7
  8. package/dist/cli/plan.d.ts.map +1 -1
  9. package/dist/cli.js +816 -314
  10. package/dist/commands/plan.d.ts +1 -0
  11. package/dist/commands/plan.d.ts.map +1 -1
  12. package/dist/config.d.ts.map +1 -1
  13. package/dist/environment.d.ts +1 -2
  14. package/dist/environment.d.ts.map +1 -1
  15. package/dist/extensions/index.js +2509 -6152
  16. package/dist/extensions/processx.d.ts +0 -1
  17. package/dist/extensions/processx.d.ts.map +1 -1
  18. package/dist/extensions/subagentx.d.ts +1 -1
  19. package/dist/extensions/subagentx.d.ts.map +1 -1
  20. package/dist/index.d.ts +3 -3
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +728 -195
  23. package/dist/plan/annotations.d.ts +25 -0
  24. package/dist/plan/annotations.d.ts.map +1 -0
  25. package/dist/plan/controller.d.ts +65 -0
  26. package/dist/plan/controller.d.ts.map +1 -0
  27. package/dist/plan/execution.d.ts +13 -0
  28. package/dist/plan/execution.d.ts.map +1 -0
  29. package/dist/plan/ids.d.ts +5 -0
  30. package/dist/plan/ids.d.ts.map +1 -0
  31. package/dist/plan/index.d.ts +4 -2
  32. package/dist/plan/index.d.ts.map +1 -1
  33. package/dist/plan/lock.d.ts +7 -2
  34. package/dist/plan/lock.d.ts.map +1 -1
  35. package/dist/plan/markdown.d.ts.map +1 -1
  36. package/dist/plan/parser.d.ts +18 -0
  37. package/dist/plan/parser.d.ts.map +1 -0
  38. package/dist/plan/store.d.ts +0 -2
  39. package/dist/plan/store.d.ts.map +1 -1
  40. package/dist/plan/types.d.ts +22 -3
  41. package/dist/plan/types.d.ts.map +1 -1
  42. package/dist/tools/index.d.ts +0 -1
  43. package/dist/tools/index.d.ts.map +1 -1
  44. package/dist/tools/index.js +2086 -1769
  45. package/dist/tools/log.d.ts.map +1 -1
  46. package/dist/tools/modes.d.ts.map +1 -1
  47. package/dist/tools/plan.d.ts +2 -2
  48. package/dist/tools/plan.d.ts.map +1 -1
  49. package/package.json +2 -4
  50. package/skills/plan/SKILL.md +7 -15
  51. package/skills/plan/references/workflows/annotate.md +6 -0
  52. package/skills/plan/references/workflows/finalize.md +7 -0
  53. package/skills/plan/references/workflows/go.md +8 -0
  54. package/{workflows/plan → skills/plan/references/workflows}/help.md +4 -11
  55. package/skills/plan/references/workflows/init.md +6 -0
  56. package/skills/plan/references/workflows/new.md +8 -0
  57. package/skills/plan/references/workflows/update.md +7 -0
  58. package/templates/plan/PLAN.md +0 -2
  59. package/dist/commands.d.ts +0 -22
  60. package/dist/commands.d.ts.map +0 -1
  61. package/dist/extensions/zodx.d.ts +0 -8
  62. package/dist/extensions/zodx.d.ts.map +0 -1
  63. package/dist/plan/operations.d.ts +0 -43
  64. package/dist/plan/operations.d.ts.map +0 -1
  65. package/dist/plan/reviews.d.ts +0 -28
  66. package/dist/plan/reviews.d.ts.map +0 -1
  67. package/dist/tools/ci.d.ts +0 -6
  68. package/dist/tools/ci.d.ts.map +0 -1
  69. package/dist/tools/plan-schema.d.ts +0 -211
  70. package/dist/tools/plan-schema.d.ts.map +0 -1
  71. package/dist/workflow.d.ts +0 -33
  72. package/dist/workflow.d.ts.map +0 -1
  73. package/workflows/plan/annotate.md +0 -13
  74. package/workflows/plan/finalize.md +0 -15
  75. package/workflows/plan/go.md +0 -20
  76. package/workflows/plan/init.md +0 -13
  77. package/workflows/plan/new.md +0 -20
  78. 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 as z2 } from "zod";
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 = zx.text;
218
- var agentConfigSchema = z2.object({
219
- models: z2.array(modelReferenceSchema).optional()
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 = z2.object({
222
- agents: z2.record(z2.string(), agentConfigSchema).optional()
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 z3 } from "zod";
1283
- var severitySchema = z3.enum(["BLOCKING", "CONSIDER", "NOTE"]);
1284
- var findingSchema = z3.object({
1285
- file: z3.string().min(1),
1286
- line: z3.number().int().nonnegative(),
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: z3.string().min(1),
1289
- reference: z3.string().optional().default("")
1241
+ body: z2.string().min(1),
1242
+ reference: z2.string().optional().default("")
1290
1243
  });
1291
- var findingsSchema = z3.array(findingSchema);
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 z4 } from "zod";
1353
- var reviewThreadRecordSchema = z4.object({
1354
- id: z4.string().min(1),
1355
- file: z4.string().nullable().optional().transform((value) => value ?? undefined),
1356
- line: z4.number().int().positive().nullable().optional().transform((value) => value ?? undefined),
1357
- body: z4.string(),
1358
- author: z4.string().optional(),
1359
- resolved: z4.boolean(),
1360
- addressed: z4.boolean().optional(),
1361
- question: z4.boolean(),
1362
- replies: z4.array(z4.string()).optional()
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 = z4.array(reviewThreadRecordSchema).parse(decoded);
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 z5 } from "zod";
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 = z5.object({
2179
- target: z5.string().optional(),
2180
- bodies: z5.array(z5.string()).default([]),
2181
- comments: z5.array(z5.string()).default([]),
2182
- stagedComments: z5.array(z5.string()).default([]),
2183
- replies: z5.array(z5.string()).default([]),
2184
- overlayPath: z5.string().optional()
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 z5.ZodError) {
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/plan/store.ts
2632
- import { mkdir as mkdir7, readdir as readdir2, readFile as readFile12, realpath as realpath4, rename as rename2, rm as rm2, writeFile as writeFile7 } from "node:fs/promises";
2633
- import { basename as basename5, dirname as dirname7, join as join16, resolve as resolve4, sep } from "node:path";
2634
-
2635
- // src/templates.ts
2636
- import { readFile as readFile10 } from "node:fs/promises";
2637
- import { homedir as homedir8 } from "node:os";
2638
- import { join as join13, normalize } from "node:path";
2639
- async function loadTemplate(name, options = {}) {
2640
- const relative = templateRelativePath(name);
2641
- const userPath = join13(options.homeDir ?? homedir8(), ".difflab", "diffpi", "templates", relative);
2642
- const bundledPath = join13(options.bundledDir ?? resolveBundledTemplatesDir(), relative);
2643
- const user = await readOptionalFile(userPath);
2644
- if (user !== undefined)
2645
- return { name, path: userPath, source: "user", content: user };
2646
- const bundled = await readOptionalFile(bundledPath);
2647
- if (bundled !== undefined)
2648
- return { name, path: bundledPath, source: "bundled", content: bundled };
2649
- throw new Error(`Template "${name}" was not found at ${userPath} or ${bundledPath}.`);
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 readFile11 } from "node:fs/promises";
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 join14 = [joinDefaults];
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: [...join14],
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/lock.ts
7761
- import { mkdir as mkdir6, rm } from "node:fs/promises";
7762
- import { join as join15 } from "node:path";
7763
- async function withPlanLock(planDir, operation, options = {}) {
7764
- const lockDir = join15(planDir, ".lock");
7765
- const waitMs = options.waitMs ?? 5000;
7766
- const pollMs = options.pollMs ?? 25;
7767
- const started = Date.now();
7768
- while (true) {
7769
- try {
7770
- await mkdir6(lockDir);
7771
- break;
7772
- } catch (error) {
7773
- if (error.code !== "EEXIST")
7774
- throw error;
7775
- if (Date.now() - started >= waitMs)
7776
- throw new Error(`Timed out waiting to ${options.operation ?? "update plan"}: ${lockDir}`);
7777
- await new Promise((resolve) => setTimeout(resolve, pollMs));
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 operation();
7782
- } finally {
7783
- await rm(lockDir, { recursive: true, force: true });
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 = join16(root, entry.name);
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 = join16(root, id);
7829
- await assertContained(root, dirname7(dir));
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, join16(dir, "PLAN.md"));
7854
- await atomicWrite(join16(dir, "PLAN.md"), source);
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: join16(dir, "PLAN.md"),
7859
- logPath: join16(dir, "logs.txt"),
7860
- implementationDir: join16(dir, "implementation"),
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: join16(current.dir, "implementation") };
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 = join16(dir, "PLAN.md");
7905
- const logPath = join16(dir, "logs.txt");
7906
- const source = await readFile12(planPath, "utf8");
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: basename5(dir),
8096
+ id: basename6(dir),
7910
8097
  dir,
7911
8098
  planPath,
7912
8099
  logPath,
7913
- implementationDir: join16(dir, "implementation"),
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 = join16(dir, "implementation");
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 = join16(implementationDir, `phase-${phase.id}.md`);
8113
+ const path = join17(implementationDir, `phase-${phase.id}.md`);
7927
8114
  try {
7928
- await readFile12(path, "utf8");
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 = join16(dirname7(path), `.${basename5(path)}.${crypto.randomUUID()}.tmp`);
7944
- await writeFile7(temp, content, { encoding: "utf8", mode: 384, flag: "wx" });
7945
- await rename2(temp, path);
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
- if (!zx.id.safeParse(slug).success)
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 readFile13 } from "node:fs/promises";
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 basename6, join as join17 } from "node:path";
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(join17(homeDir, ".local", "bin", "mise"));
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(join17(agentDir, "web-search.json"), (config) => ({ ...config, workflow: "auto-summary" }), options.dryRun);
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(join17(agentDir, "pi-lsp.json"), (config) => ({
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 = basename6(entry.name, ".md").replace(/^diffpi-/, "");
8089
- const source = await readFile13(join17(bundledAgentsDir, entry.name), "utf8");
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 = join17(options.homeDir ?? homedir9(), ".agents", "skills");
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 readFile13(join17(resolveBundledAgentsDir(), "..", "package.json"), "utf8");
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
- createPlanStore,
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,