@difflab/pi 0.5.0 → 0.6.0-rc.202609262054.4a8e556

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 (70) hide show
  1. package/README.md +8 -6
  2. package/agents/diffpi-copilot.md +6 -3
  3. package/agents/diffpi-orchestrator.md +6 -4
  4. package/agents/diffpi-planner.md +3 -3
  5. package/agents/diffpi-reviewer.md +3 -3
  6. package/agents/diffpi-worker.md +1 -1
  7. package/dist/assets.d.ts +0 -1
  8. package/dist/assets.d.ts.map +1 -1
  9. package/dist/cli.js +417 -84
  10. package/dist/commands/plan.d.ts +2 -2
  11. package/dist/commands/plan.d.ts.map +1 -1
  12. package/dist/commands/review.d.ts +2 -3
  13. package/dist/commands/review.d.ts.map +1 -1
  14. package/dist/environment.d.ts +2 -0
  15. package/dist/environment.d.ts.map +1 -1
  16. package/dist/extensions/index.js +1114 -3634
  17. package/dist/index.js +437 -99
  18. package/dist/plan/markdown.d.ts +4 -1
  19. package/dist/plan/markdown.d.ts.map +1 -1
  20. package/dist/{tools/plan-schema.d.ts → plan/schema.d.ts} +113 -75
  21. package/dist/plan/schema.d.ts.map +1 -0
  22. package/dist/plan/store.d.ts +39 -1
  23. package/dist/plan/store.d.ts.map +1 -1
  24. package/dist/plan/types.d.ts +27 -3
  25. package/dist/plan/types.d.ts.map +1 -1
  26. package/dist/review/local-reviews.d.ts +31 -0
  27. package/dist/review/local-reviews.d.ts.map +1 -0
  28. package/dist/review/review-markdown.d.ts.map +1 -1
  29. package/dist/tools/index.js +836 -410
  30. package/dist/tools/plan.d.ts.map +1 -1
  31. package/dist/tools/review.d.ts +6 -0
  32. package/dist/tools/review.d.ts.map +1 -1
  33. package/package.json +2 -4
  34. package/skills/plan/SKILL.md +8 -14
  35. package/skills/plan/references/design-quality.md +41 -0
  36. package/skills/plan/references/workflows/annotate.md +11 -0
  37. package/skills/plan/references/workflows/finalize.md +7 -0
  38. package/skills/plan/references/workflows/go.md +8 -0
  39. package/skills/plan/references/workflows/help.md +10 -0
  40. package/skills/plan/references/workflows/init.md +5 -0
  41. package/skills/plan/references/workflows/new.md +7 -0
  42. package/skills/plan/references/workflows/update.md +7 -0
  43. package/skills/review/SKILL.md +7 -9
  44. package/skills/review/references/review-standards.md +8 -1
  45. package/skills/review/references/workflows/address.md +4 -4
  46. package/skills/review/references/workflows/help.md +8 -2
  47. package/skills/review/references/workflows/open.md +1 -1
  48. package/skills/review/references/workflows/status.md +3 -0
  49. package/templates/plan/PLAN.md +2 -2
  50. package/templates/plan/implementation.md +16 -10
  51. package/dist/commands.d.ts +0 -22
  52. package/dist/commands.d.ts.map +0 -1
  53. package/dist/extensions/cmdtsx.d.ts +0 -2
  54. package/dist/extensions/cmdtsx.d.ts.map +0 -1
  55. package/dist/tools/plan-schema.d.ts.map +0 -1
  56. package/dist/workflow.d.ts +0 -33
  57. package/dist/workflow.d.ts.map +0 -1
  58. package/skills/review/references/workflows/auto.md +0 -18
  59. package/skills/review/references/workflows/complete.md +0 -7
  60. package/skills/review/references/workflows/edit.md +0 -7
  61. package/skills/review/references/workflows/merge.md +0 -6
  62. package/skills/review/references/workflows/new.md +0 -19
  63. package/skills/review/references/workflows/publish.md +0 -8
  64. package/workflows/plan/annotate.md +0 -13
  65. package/workflows/plan/finalize.md +0 -15
  66. package/workflows/plan/go.md +0 -20
  67. package/workflows/plan/help.md +0 -20
  68. package/workflows/plan/init.md +0 -13
  69. package/workflows/plan/new.md +0 -20
  70. package/workflows/plan/update.md +0 -18
@@ -503,6 +503,16 @@ function diffpiLaunchName(cwd, workflow) {
503
503
  }
504
504
  async function openFileAdjacent(path, opts) {
505
505
  const env = opts.env ?? process.env;
506
+ const mux = detectMux(env);
507
+ if (mux !== "none") {
508
+ const editor = await resolveEditor(env);
509
+ if (editor) {
510
+ const command = persistentEditorArgs(editor, path, opts.cwd, env.SHELL);
511
+ const opened = await openMuxTab(mux, command, opts.cwd, opts.name ?? "plan", [editor, path].join(" "), command);
512
+ if (opened)
513
+ return opened;
514
+ }
515
+ }
506
516
  const ide = detectIde(env);
507
517
  if (ide !== "unknown") {
508
518
  const ideCommand = { zed: "zed", vscode: "code", cursor: "cursor", windsurf: "windsurf", jetbrains: "idea" }[ide];
@@ -512,16 +522,6 @@ async function openFileAdjacent(path, opts) {
512
522
  return { launched: true, via: ide, command: `${ideCommand} ${path}` };
513
523
  }
514
524
  }
515
- const mux = detectMux(env);
516
- if (mux !== "none") {
517
- const editor = await resolveEditor(env);
518
- if (editor) {
519
- const command = [editor, path];
520
- const opened = await openMuxTab(mux, command, opts.cwd, opts.name ?? "plan", command.join(" "));
521
- if (opened)
522
- return opened;
523
- }
524
- }
525
525
  return { launched: false, via: "print", command: path, reason: "No supported IDE or multiplexer was available." };
526
526
  }
527
527
  async function openInNewTab(command, opts) {
@@ -556,10 +556,15 @@ async function openInNewTab(command, opts) {
556
556
  function screenWindowArgs(command, cwd, name) {
557
557
  return ["-X", "screen", "-t", name, "sh", "-lc", 'cd -- "$1" && shift && exec "$@"', "sh", cwd, ...command];
558
558
  }
559
+ function persistentEditorArgs(editor, path, cwd, shell = "sh") {
560
+ return ["bash", "-lc", 'cd -- "$1" && shift && "$@"; exec "$0" -i', shell, cwd, editor, path];
561
+ }
559
562
  async function resolveEditor(env) {
560
- const configured = (env.VISUAL ?? env.EDITOR)?.trim().split(/\s+/)[0];
561
- if (configured && await findExecutable(configured))
562
- return configured;
563
+ for (const configured of [env.EDITOR, env.VISUAL]) {
564
+ const editor = configured?.trim();
565
+ if (editor && await findExecutable(editor))
566
+ return editor;
567
+ }
563
568
  for (const candidate of ["hx", "nvim", "vim"])
564
569
  if (await findExecutable(candidate))
565
570
  return candidate;
@@ -576,7 +581,7 @@ async function packageVersion() {
576
581
  throw new Error("Cannot resolve the installed @difflab/pi version.");
577
582
  return value.version;
578
583
  }
579
- async function openMuxTab(mux, command, cwd, name, printable) {
584
+ async function openMuxTab(mux, command, cwd, name, printable, tmuxCommand) {
580
585
  if (mux === "zellij" && await findExecutable("zellij")) {
581
586
  const result = await run("zellij", ["action", "new-tab", "--cwd", cwd, "--name", name, "--", ...command]);
582
587
  if (result.code === 0)
@@ -586,7 +591,7 @@ async function openMuxTab(mux, command, cwd, name, printable) {
586
591
  return { launched: true, via: "zellij-run", command: printable };
587
592
  }
588
593
  if (mux === "tmux" && await findExecutable("tmux")) {
589
- const result = await run("tmux", ["new-window", "-c", cwd, "-n", name, printable]);
594
+ const result = tmuxCommand ? await run("tmux", ["new-window", "-c", cwd, "-n", name, ...tmuxCommand]) : await run("tmux", ["new-window", "-c", cwd, "-n", name, printable]);
590
595
  if (result.code === 0)
591
596
  return { launched: true, via: "tmux", command: printable };
592
597
  }
@@ -1285,7 +1290,7 @@ function sanitize(value) {
1285
1290
  }
1286
1291
 
1287
1292
  // src/tools/plan.ts
1288
- import { join as join14 } from "node:path";
1293
+ import { join as join15 } from "node:path";
1289
1294
  import { pathToFileURL } from "node:url";
1290
1295
  import { defineTool as defineTool5 } from "@earendil-works/pi-coding-agent";
1291
1296
  import { z as z7 } from "zod";
@@ -1522,6 +1527,9 @@ async function discoverMiseTasks(cwd) {
1522
1527
 
1523
1528
  // src/plan/markdown.ts
1524
1529
  var import_mdast_builder = __toESM(require_src(), 1);
1530
+ import { createHash as createHash2 } from "node:crypto";
1531
+ import { readFile as readFile5, readdir } from "node:fs/promises";
1532
+ import { join as join10 } from "node:path";
1525
1533
 
1526
1534
  // ../../node_modules/zwitch/index.js
1527
1535
  var own = {}.hasOwnProperty;
@@ -6161,6 +6169,8 @@ function isUint8Array2(value) {
6161
6169
  var PLAN_MARKER = /<!-- diffpi-plan: (\{[^\n]+\}) -->/;
6162
6170
  var PHASE_MARKER = /<!-- diffpi-phase: (\{[^\n]+\}) -->/g;
6163
6171
  var TASK_MARKER = /<!-- diffpi-task: (\{[^\n]+\}) -->/g;
6172
+ var BRIEF_MARKER = /<!-- diffpi-implementation: (\{[^\n]+\}) -->/;
6173
+ var BRIEF_TASK_MARKER = /<!-- diffpi-brief-task: (\{[^\n]+\}) -->/g;
6164
6174
  var SUPPORTED_SCHEMA = 1;
6165
6175
  function countDesignWords(plan) {
6166
6176
  return [plan.design.bigIdeas, plan.design.keyApiUpdates, plan.design.consequences].join(" ").trim().split(/\s+/).filter(Boolean).length;
@@ -6189,11 +6199,13 @@ function validatePlanDocument(plan, options = {}) {
6189
6199
  if (!taskIds.has(dependency))
6190
6200
  add("dangling-dependency", "error", `Task ${task.id} depends on missing ${dependency}.`);
6191
6201
  }
6192
- if (options.strict && task.acceptanceCriteria.length === 0)
6193
- add("acceptance", "error", `Task ${task.id} has no acceptance criteria.`, task.id);
6202
+ if (options.strict && isPlaceholder(task.title))
6203
+ add("placeholder", "error", `Task ${task.id} has placeholder content.`, task.id);
6194
6204
  }
6195
6205
  if (options.strict && phase.tasks.length === 0)
6196
6206
  add("empty-phase", "error", `Phase ${phase.id} has no tasks.`, phase.id);
6207
+ if (options.strict && (isPlaceholder(phase.title) || isPlaceholder(phase.objective)))
6208
+ add("placeholder", "error", `Phase ${phase.id} has placeholder content.`, phase.id);
6197
6209
  }
6198
6210
  for (const cycle of dependencyCycles(plan))
6199
6211
  add("dependency-cycle", "error", `Dependency cycle: ${cycle.join(" -> ")}.`);
@@ -6202,10 +6214,140 @@ function validatePlanDocument(plan, options = {}) {
6202
6214
  add("design-length", "warning", `Design is ${designWords} words; prefer 300 or fewer.`);
6203
6215
  if (options.strict && designWords > 800)
6204
6216
  add("design-too-long", "error", `Design is ${designWords} words; finalization allows at most 800.`);
6205
- if (options.strict && plan.phases.length === 0)
6206
- add("no-phases", "error", "Finalization requires at least one phase.");
6217
+ if (options.strict) {
6218
+ if (plan.phases.length === 0)
6219
+ add("no-phases", "error", "Finalization requires at least one phase.");
6220
+ if (isPlaceholder(plan.intent))
6221
+ add("placeholder", "error", "Intent must contain concrete content.", "intent");
6222
+ if (!plan.requirements.length || plan.requirements.some(isPlaceholder))
6223
+ add("requirements", "error", "Requirements must contain concrete content.", "requirements");
6224
+ for (const [key, value] of Object.entries(plan.design)) {
6225
+ if (isPlaceholder(value))
6226
+ add("design", "error", `Design/${key} must contain concrete content.`, `design.${key}`);
6227
+ }
6228
+ }
6229
+ return issues;
6230
+ }
6231
+ function validatePlanBriefs(plan, briefs) {
6232
+ const issues = [];
6233
+ const phaseIds = plan.phases.map((phase) => phase.id);
6234
+ const briefIds = briefs.map((brief) => brief.phaseId);
6235
+ if (!sameIds(phaseIds, briefIds))
6236
+ issues.push({
6237
+ code: "brief-phase-ids",
6238
+ severity: "error",
6239
+ message: `Brief phase IDs must exactly match plan phases in order: ${phaseIds.join(", ")}.`
6240
+ });
6241
+ for (const [index, phase] of plan.phases.entries()) {
6242
+ const brief = briefs[index];
6243
+ if (!brief || brief.phaseId !== phase.id)
6244
+ continue;
6245
+ if (isPlaceholder(brief.summary))
6246
+ issues.push({ code: "brief-placeholder", severity: "error", message: `Brief ${phase.id} needs a summary.` });
6247
+ const taskIds = phase.tasks.map((task) => task.id);
6248
+ const briefTaskIds = brief.tasks.map((task) => task.taskId);
6249
+ if (!sameIds(taskIds, briefTaskIds))
6250
+ issues.push({
6251
+ code: "brief-task-ids",
6252
+ severity: "error",
6253
+ message: `Brief ${phase.id} task IDs must exactly match phase tasks in order: ${taskIds.join(", ")}.`
6254
+ });
6255
+ for (const task of brief.tasks) {
6256
+ for (const [field, values] of [
6257
+ ["ordered steps", task.steps],
6258
+ ["file scopes", task.fileScopes],
6259
+ ["acceptance criteria", task.acceptanceCriteria]
6260
+ ]) {
6261
+ if (!values.length || values.some(isPlaceholder))
6262
+ issues.push({
6263
+ code: "brief-incomplete",
6264
+ severity: "error",
6265
+ message: `Brief task ${task.taskId} needs concrete ${field}.`,
6266
+ path: task.taskId
6267
+ });
6268
+ }
6269
+ }
6270
+ for (const [field, values] of [
6271
+ ["API changes", brief.apiChanges],
6272
+ ["libraries", brief.libraries],
6273
+ ["constraints", brief.constraints]
6274
+ ]) {
6275
+ if (values.some(isPlaceholder))
6276
+ issues.push({
6277
+ code: "brief-placeholder",
6278
+ severity: "error",
6279
+ message: `Brief ${phase.id} has placeholder ${field}.`,
6280
+ path: phase.id
6281
+ });
6282
+ }
6283
+ }
6284
+ return issues;
6285
+ }
6286
+ async function validatePlanRecord(record, options = {}) {
6287
+ const issues = validatePlanDocument(record.document, options);
6288
+ if (!options.strict)
6289
+ return issues;
6290
+ await validateImplementationDirectory(record.dir, record.document, issues, "latest");
6291
+ if (options.checkSnapshots !== false)
6292
+ await validateRevisionSnapshots(record, issues);
6207
6293
  return issues;
6208
6294
  }
6295
+ function renderImplementationBrief(planRevision, ordinal, phase, brief) {
6296
+ const marker = { schemaVersion: 1, planRevision, ordinal, phaseId: phase.id };
6297
+ const tasks = phase.tasks.map((task, index) => {
6298
+ const details = brief.tasks[index];
6299
+ if (!details || details.taskId !== task.id)
6300
+ throw new Error(`Missing implementation details for ${task.id}.`);
6301
+ return `<!-- diffpi-brief-task: ${json({ id: task.id })} -->
6302
+ ### Task: ${task.title}
6303
+
6304
+ #### Ordered Steps
6305
+
6306
+ ${bulletList(details.steps)}
6307
+
6308
+ #### File Scopes
6309
+
6310
+ ${bulletList(details.fileScopes)}
6311
+
6312
+ #### Acceptance Criteria
6313
+
6314
+ ${bulletList(details.acceptanceCriteria)}
6315
+
6316
+ <!-- /diffpi-brief-task -->`;
6317
+ }).join(`
6318
+
6319
+ `);
6320
+ return `<!-- diffpi-implementation: ${json(marker)} -->
6321
+ # Phase ${ordinal}: ${phase.title}
6322
+
6323
+ - **Phase ID:** ${phase.id}
6324
+ - **Plan Revision:** ${planRevision}
6325
+
6326
+ ## Summary
6327
+
6328
+ ${brief.summary}
6329
+
6330
+ ## Objective
6331
+
6332
+ ${phase.objective}
6333
+
6334
+ ## Tasks
6335
+
6336
+ ${tasks}
6337
+
6338
+ ## API Changes
6339
+
6340
+ ${bulletList(brief.apiChanges, "None.")}
6341
+
6342
+ ## Libraries and Algorithms
6343
+
6344
+ ${bulletList(brief.libraries, "None.")}
6345
+
6346
+ ## Implementation Constraints
6347
+
6348
+ ${bulletList(brief.constraints, "None.")}
6349
+ `;
6350
+ }
6209
6351
  function renderPlanDocument(plan, previousSource) {
6210
6352
  if (previousSource && canPatch(previousSource, plan))
6211
6353
  return patchMarkers(previousSource, plan);
@@ -6214,6 +6356,8 @@ function renderPlanDocument(plan, previousSource) {
6214
6356
  id: plan.id,
6215
6357
  revision: plan.revision,
6216
6358
  branch: plan.branch,
6359
+ issueId: plan.issueId,
6360
+ issueUrl: plan.issueUrl,
6217
6361
  status: plan.status,
6218
6362
  execution: plan.execution,
6219
6363
  createdAt: plan.createdAt,
@@ -6222,6 +6366,8 @@ function renderPlanDocument(plan, previousSource) {
6222
6366
  const metadata = import_mdast_builder.list("unordered", [
6223
6367
  ["Plan ID", plan.id],
6224
6368
  ["Branch", plan.branch],
6369
+ ...plan.issueId ? [["Issue", plan.issueId]] : [],
6370
+ ...plan.issueUrl ? [["Issue URL", plan.issueUrl]] : [],
6225
6371
  ["Status", plan.status],
6226
6372
  ["Revision", String(plan.revision)]
6227
6373
  ].map(([label, value]) => import_mdast_builder.listItem(import_mdast_builder.paragraph([import_mdast_builder.strong(import_mdast_builder.text(`${label}:`)), import_mdast_builder.text(` ${value}`)]))));
@@ -6243,7 +6389,7 @@ function renderPlanDocument(plan, previousSource) {
6243
6389
  import_mdast_builder.heading(3, import_mdast_builder.text("Consequences")),
6244
6390
  import_mdast_builder.html(plan.design.consequences || "<!-- Describe trade-offs and limitations. -->"),
6245
6391
  import_mdast_builder.heading(2, import_mdast_builder.text("Implementation")),
6246
- ...plan.phases.length ? plan.phases.flatMap((phase) => [import_mdast_builder.html(renderPhase(phase))]) : [import_mdast_builder.html("<!-- Add phases with plan_add_phase. -->")],
6392
+ ...plan.phases.length ? plan.phases.flatMap((phase) => [import_mdast_builder.html(renderPhase(phase))]) : [import_mdast_builder.html("<!-- Add phases with plan_apply_revision. -->")],
6247
6393
  import_mdast_builder.heading(2, import_mdast_builder.text("References")),
6248
6394
  references
6249
6395
  ]);
@@ -6317,10 +6463,7 @@ function renderTask(task) {
6317
6463
  const checked = task.status === "completed" || task.status === "skipped" ? "x" : " ";
6318
6464
  return `<!-- diffpi-task: ${json(marker)} -->
6319
6465
  - [${checked}] **${task.title}**
6320
- ${taskList("Steps", task.steps ?? [])}
6321
6466
  - Dependencies: ${list3(task.dependencies)}
6322
- ${taskList("File scopes", task.fileScopes)}
6323
- ${taskList("Acceptance criteria", task.acceptanceCriteria)}
6324
6467
  <!-- /diffpi-task -->`;
6325
6468
  }
6326
6469
  function parsePhases(input, ref) {
@@ -6370,10 +6513,7 @@ function parseTasks(input, ref) {
6370
6513
  return {
6371
6514
  ...marker,
6372
6515
  title,
6373
- steps: parseListValue(body, "Steps"),
6374
- dependencies: parseListValue(body, "Dependencies"),
6375
- fileScopes: parseListValue(body, "File scopes"),
6376
- acceptanceCriteria: parseListValue(body, "Acceptance criteria")
6516
+ dependencies: parseListValue(body, "Dependencies")
6377
6517
  };
6378
6518
  });
6379
6519
  assertUniqueIds(tasks.map((task) => task.id), "task id");
@@ -6400,6 +6540,8 @@ function patchMarkers(source, plan) {
6400
6540
  id: plan.id,
6401
6541
  revision: plan.revision,
6402
6542
  branch: plan.branch,
6543
+ issueId: plan.issueId,
6544
+ issueUrl: plan.issueUrl,
6403
6545
  status: plan.status,
6404
6546
  execution: plan.execution,
6405
6547
  createdAt: plan.createdAt,
@@ -6449,19 +6591,12 @@ function contentShape(plan) {
6449
6591
  requirements: plan.requirements,
6450
6592
  design: plan.design,
6451
6593
  references: plan.references,
6452
- phases: plan.phases.map(({ id, title, objective, dependencies, tasks }) => ({
6453
- id,
6454
- title,
6455
- objective,
6456
- dependencies,
6457
- tasks: tasks.map(({ id: taskId, title: taskTitle, steps, dependencies: taskDependencies, fileScopes, acceptanceCriteria }) => ({
6458
- id: taskId,
6459
- title: taskTitle,
6460
- steps,
6461
- dependencies: taskDependencies,
6462
- fileScopes,
6463
- acceptanceCriteria
6464
- }))
6594
+ phases: plan.phases.map((phase) => ({
6595
+ id: phase.id,
6596
+ title: phase.title,
6597
+ objective: phase.objective,
6598
+ dependencies: phase.dependencies,
6599
+ tasks: phase.tasks.map(({ id, title, dependencies }) => ({ id, title, dependencies }))
6465
6600
  }))
6466
6601
  };
6467
6602
  }
@@ -6491,6 +6626,152 @@ function dependencyCycles(plan) {
6491
6626
  walk(id, []);
6492
6627
  return cycles;
6493
6628
  }
6629
+ async function validateRevisionSnapshots(record, issues) {
6630
+ const revisionsDir = join10(record.dir, "revisions");
6631
+ let entries;
6632
+ try {
6633
+ entries = await readdir(revisionsDir);
6634
+ } catch {
6635
+ issues.push({ code: "snapshot-missing", severity: "error", message: "Missing revisions directory." });
6636
+ return;
6637
+ }
6638
+ const expected = Array.from({ length: record.document.revision + 1 }, (_, index) => String(index));
6639
+ if (!sameIds(expected, entries.filter((entry) => /^\d+$/.test(entry)).sort((a, b) => Number(a) - Number(b))))
6640
+ issues.push({
6641
+ code: "snapshot-sequence",
6642
+ severity: "error",
6643
+ message: `Revision snapshots must be exactly ${expected.join(", ")}.`,
6644
+ path: revisionsDir
6645
+ });
6646
+ for (const entry of entries) {
6647
+ if (!/^\d+$/.test(entry))
6648
+ issues.push({ code: "snapshot-entry", severity: "error", message: `Unexpected revisions entry: ${entry}.` });
6649
+ }
6650
+ for (const revision of expected) {
6651
+ const dir = join10(revisionsDir, revision);
6652
+ try {
6653
+ const [request, metadataSource, planSource] = await Promise.all([
6654
+ readFile5(join10(dir, "request.md"), "utf8"),
6655
+ readFile5(join10(dir, "metadata.json"), "utf8"),
6656
+ readFile5(join10(dir, "PLAN.md"), "utf8")
6657
+ ]);
6658
+ if (isPlaceholder(request))
6659
+ issues.push({ code: "snapshot-request", severity: "error", message: `Revision ${revision} request is empty.` });
6660
+ const metadata = JSON.parse(metadataSource);
6661
+ const annotation = metadata.requestKind === "annotation" ? /^## Original annotation\n([\s\S]*?)\n\n## LLM response\n([\s\S]*)$/.exec(request) : undefined;
6662
+ const requestText = annotation?.[1];
6663
+ const responseText = annotation?.[2];
6664
+ if (metadata.schemaVersion !== 1 || metadata.id !== record.id || metadata.revision !== Number(revision) || !["user", "annotation", "blocker"].includes(metadata.requestKind) || (metadata.requestKind === "annotation" ? !annotation || !requestText?.trim() || !responseText?.trim() || metadata.requestSha256 !== sha256(requestText) || metadata.originalAnnotationSha256 !== sha256(requestText) || metadata.responseSha256 !== sha256(responseText) : metadata.requestSha256 !== sha256(request)))
6665
+ issues.push({
6666
+ code: "snapshot-metadata",
6667
+ severity: "error",
6668
+ message: `Revision ${revision} metadata does not match its immutable request.`
6669
+ });
6670
+ const snapshotPlan = parsePlanDocument(planSource, join10(dir, "PLAN.md"));
6671
+ if (snapshotPlan.id !== record.id || snapshotPlan.revision !== Number(revision))
6672
+ issues.push({
6673
+ code: "snapshot-plan",
6674
+ severity: "error",
6675
+ message: `Revision ${revision} PLAN.md has mismatched identity or revision.`
6676
+ });
6677
+ await validateImplementationDirectory(dir, snapshotPlan, issues, `revision ${revision}`);
6678
+ } catch (error) {
6679
+ issues.push({
6680
+ code: "snapshot-missing",
6681
+ severity: "error",
6682
+ message: `Revision ${revision} is incomplete: ${error.message}`,
6683
+ path: dir
6684
+ });
6685
+ }
6686
+ }
6687
+ }
6688
+ async function validateImplementationDirectory(baseDir, plan, issues, label) {
6689
+ const dir = join10(baseDir, "implementation");
6690
+ let entries;
6691
+ try {
6692
+ entries = await readdir(dir);
6693
+ } catch {
6694
+ issues.push({
6695
+ code: "brief-directory",
6696
+ severity: "error",
6697
+ message: `${label} is missing implementation/.`,
6698
+ path: dir
6699
+ });
6700
+ return;
6701
+ }
6702
+ const expected = plan.phases.map((_phase, index) => `phase-${index + 1}.md`);
6703
+ const actual = entries.filter((entry) => entry.endsWith(".md")).sort((left, right) => left.localeCompare(right));
6704
+ if (!sameIds([...expected].sort((left, right) => left.localeCompare(right)), actual))
6705
+ issues.push({
6706
+ code: "brief-filenames",
6707
+ severity: "error",
6708
+ message: `${label} briefs must use exactly: ${expected.join(", ") || "(none)"}.`,
6709
+ path: dir
6710
+ });
6711
+ for (const [index, phase] of plan.phases.entries()) {
6712
+ const path = join10(dir, `phase-${index + 1}.md`);
6713
+ try {
6714
+ validateBriefSource(await readFile5(path, "utf8"), plan.revision, index + 1, phase, path, issues);
6715
+ } catch (error) {
6716
+ issues.push({
6717
+ code: "brief-missing",
6718
+ severity: "error",
6719
+ message: `${label} brief phase-${index + 1}.md is missing: ${error.message}`,
6720
+ path
6721
+ });
6722
+ }
6723
+ }
6724
+ }
6725
+ function validateBriefSource(source, planRevision, ordinal, phase, path, issues) {
6726
+ const matches = source.match(new RegExp(BRIEF_MARKER.source, "g")) ?? [];
6727
+ const markerMatch = source.match(BRIEF_MARKER);
6728
+ if (matches.length !== 1 || !markerMatch) {
6729
+ issues.push({ code: "brief-marker", severity: "error", message: `${path} needs one implementation marker.`, path });
6730
+ return;
6731
+ }
6732
+ const marker = parseMarker(markerMatch[1], `${path} implementation`);
6733
+ if (marker.schemaVersion !== 1 || marker.planRevision !== planRevision || marker.ordinal !== ordinal || marker.phaseId !== phase.id)
6734
+ issues.push({
6735
+ code: "brief-marker",
6736
+ severity: "error",
6737
+ message: `${path} marker must identify phase ${ordinal} (${phase.id}) at revision ${planRevision}.`,
6738
+ path
6739
+ });
6740
+ const taskIds = [...source.matchAll(BRIEF_TASK_MARKER)].map((match) => parseMarker(match[1], `${path} task`).id);
6741
+ if (!sameIds(phase.tasks.map((task) => task.id), taskIds))
6742
+ issues.push({
6743
+ code: "brief-task-ids",
6744
+ severity: "error",
6745
+ message: `${path} task markers must exactly match phase task IDs in order.`,
6746
+ path
6747
+ });
6748
+ const prose = source.replace(BRIEF_MARKER, "").replace(BRIEF_TASK_MARKER, "").replace(/<!-- \/diffpi-brief-task -->/g, "");
6749
+ if (/<!--[^]*?-->|\b(?:TODO|TBD)\b|define during implementation/i.test(prose))
6750
+ issues.push({
6751
+ code: "brief-placeholder",
6752
+ severity: "error",
6753
+ message: `${path} contains placeholder content.`,
6754
+ path
6755
+ });
6756
+ try {
6757
+ if (isPlaceholder(section(source, "Summary")))
6758
+ throw new Error("Summary is empty.");
6759
+ for (const task of phase.tasks) {
6760
+ const taskStart = source.indexOf(`<!-- diffpi-brief-task: ${json({ id: task.id })} -->`);
6761
+ const taskEnd = source.indexOf("<!-- /diffpi-brief-task -->", taskStart);
6762
+ if (taskStart < 0 || taskEnd < 0)
6763
+ throw new Error(`Task ${task.id} body is missing.`);
6764
+ const body = source.slice(taskStart, taskEnd);
6765
+ for (const heading of ["Ordered Steps", "File Scopes", "Acceptance Criteria"]) {
6766
+ const content = briefSubsection(body, heading);
6767
+ if (!parseBullets(content).length || parseBullets(content).some(isPlaceholder))
6768
+ throw new Error(`Task ${task.id} ${heading} is incomplete.`);
6769
+ }
6770
+ }
6771
+ } catch (error) {
6772
+ issues.push({ code: "brief-incomplete", severity: "error", message: `${path}: ${error.message}`, path });
6773
+ }
6774
+ }
6494
6775
  function section(source, heading) {
6495
6776
  const match = source.match(new RegExp(`^## ${escapeRegExp(heading)}\\s*$`, "m"));
6496
6777
  if (!match?.index) {
@@ -6515,6 +6796,14 @@ function subsection(source, parent, heading) {
6515
6796
  function parseBullets(input) {
6516
6797
  return [...input.matchAll(/^- (?!<!--)(.+)$/gm)].map((match) => match[1].trim());
6517
6798
  }
6799
+ function briefSubsection(source, heading) {
6800
+ const match = source.match(new RegExp(`^#### ${escapeRegExp(heading)}\\s*$`, "m"));
6801
+ if (match?.index === undefined)
6802
+ throw new Error(`Missing required brief heading: ${heading}.`);
6803
+ const rest = source.slice(match.index + match[0].length);
6804
+ const end = rest.search(/^#### |^<!-- \/diffpi-brief-task -->/m);
6805
+ return (end < 0 ? rest : rest.slice(0, end)).trim();
6806
+ }
6518
6807
  function parseListValue(body, label) {
6519
6808
  const match = new RegExp(`^ {2}- ${escapeRegExp(label)}:[ \\t]*(.*)$`, "m").exec(body);
6520
6809
  if (!match)
@@ -6561,6 +6850,16 @@ function assertUniqueIds(ids, label) {
6561
6850
  function cleanPlaceholder(value) {
6562
6851
  return value.replace(/<!--[^]*?-->/g, "").trim();
6563
6852
  }
6853
+ function isPlaceholder(value) {
6854
+ const normalized = cleanPlaceholder(value).trim();
6855
+ return normalized.length === 0 || /^(?:todo|tbd|none|n\/a)$/i.test(normalized) || /define during implementation|describe the|add requirements|add implementation/i.test(normalized);
6856
+ }
6857
+ function sameIds(expected, actual) {
6858
+ return expected.length === actual.length && expected.every((value, index) => actual[index] === value);
6859
+ }
6860
+ function sha256(value) {
6861
+ return createHash2("sha256").update(value).digest("hex");
6862
+ }
6564
6863
  function parseMarker(value, label) {
6565
6864
  try {
6566
6865
  return JSON.parse(value);
@@ -6568,15 +6867,14 @@ function parseMarker(value, label) {
6568
6867
  throw new Error(`Malformed ${label} marker JSON.`);
6569
6868
  }
6570
6869
  }
6571
- function taskList(label, values) {
6870
+ function bulletList(values, empty = "") {
6572
6871
  if (!values.length)
6573
- return ` - ${label}: none`;
6574
- const items = values.flatMap((value) => {
6872
+ return empty;
6873
+ return values.flatMap((value) => {
6575
6874
  const [first = "", ...continuations] = value.split(`
6576
6875
  `);
6577
- return [` - ${first}`, ...continuations.map((line) => ` ${line}`)];
6578
- });
6579
- return [` - ${label}:`, ...items].join(`
6876
+ return [`- ${first}`, ...continuations.map((line) => ` ${line}`)];
6877
+ }).join(`
6580
6878
  `);
6581
6879
  }
6582
6880
  function list3(values) {
@@ -6902,13 +7200,13 @@ function heartbeat(plan) {
6902
7200
  }
6903
7201
 
6904
7202
  // src/plan/reviews.ts
6905
- import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile3 } from "node:fs/promises";
6906
- import { dirname as dirname5, join as join10 } from "node:path";
7203
+ import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile3 } from "node:fs/promises";
7204
+ import { dirname as dirname5, join as join11 } from "node:path";
6907
7205
  async function readPlanReview(record) {
6908
7206
  const path = reviewPath(record);
6909
7207
  let source;
6910
7208
  try {
6911
- source = await readFile5(path, "utf8");
7209
+ source = await readFile6(path, "utf8");
6912
7210
  } catch (error) {
6913
7211
  if (error.code === "ENOENT")
6914
7212
  return;
@@ -6925,7 +7223,7 @@ async function readPlanReview(record) {
6925
7223
  return { path, dump };
6926
7224
  }
6927
7225
  function reviewPath(record) {
6928
- return join10(record.dir, "reviews", `${record.document.revision}.json`);
7226
+ return join11(record.dir, "reviews", `${record.document.revision}.json`);
6929
7227
  }
6930
7228
  function isPlanReviewDump(value) {
6931
7229
  if (!value || typeof value !== "object")
@@ -6935,14 +7233,15 @@ function isPlanReviewDump(value) {
6935
7233
  }
6936
7234
 
6937
7235
  // src/plan/store.ts
6938
- import { mkdir as mkdir6, readdir, readFile as readFile7, realpath as realpath3, rm as rm2 } from "node:fs/promises";
6939
- import { basename as basename5, dirname as dirname7, join as join13, resolve as resolve3, sep } from "node:path";
7236
+ import { createHash as createHash3, randomUUID as randomUUID2 } from "node:crypto";
7237
+ import { mkdir as mkdir6, readdir as readdir2, readFile as readFile8, realpath as realpath3, rename as rename2, rm as rm2 } from "node:fs/promises";
7238
+ import { basename as basename5, dirname as dirname7, join as join14, resolve as resolve3, sep } from "node:path";
6940
7239
 
6941
7240
  // src/extensions/fsx.ts
6942
7241
  import { mkdir as mkdir5, rename, rm, writeFile as writeFile4 } from "node:fs/promises";
6943
- import { basename as basename4, dirname as dirname6, join as join11 } from "node:path";
7242
+ import { basename as basename4, dirname as dirname6, join as join12 } from "node:path";
6944
7243
  async function atomicWrite(path, content) {
6945
- const temp = join11(dirname6(path), `.${basename4(path)}.${crypto.randomUUID()}.tmp`);
7244
+ const temp = join12(dirname6(path), `.${basename4(path)}.${crypto.randomUUID()}.tmp`);
6946
7245
  await writeFile4(temp, content, { encoding: "utf8", mode: 384, flag: "wx" });
6947
7246
  await rename(temp, path);
6948
7247
  }
@@ -6973,13 +7272,13 @@ function sleep(ms) {
6973
7272
  }
6974
7273
 
6975
7274
  // src/templates.ts
6976
- import { readFile as readFile6 } from "node:fs/promises";
7275
+ import { readFile as readFile7 } from "node:fs/promises";
6977
7276
  import { homedir as homedir4 } from "node:os";
6978
- import { join as join12, normalize } from "node:path";
7277
+ import { join as join13, normalize } from "node:path";
6979
7278
  async function loadTemplate(name, options = {}) {
6980
7279
  const relative = templateRelativePath(name);
6981
- const userPath = join12(options.homeDir ?? homedir4(), ".difflab", "diffpi", "templates", relative);
6982
- const bundledPath = join12(options.bundledDir ?? resolveBundledTemplatesDir(), relative);
7280
+ const userPath = join13(options.homeDir ?? homedir4(), ".difflab", "diffpi", "templates", relative);
7281
+ const bundledPath = join13(options.bundledDir ?? resolveBundledTemplatesDir(), relative);
6983
7282
  const user = await readOptionalFile(userPath);
6984
7283
  if (user !== undefined)
6985
7284
  return { name, path: userPath, source: "user", content: user };
@@ -7000,7 +7299,7 @@ function templateRelativePath(name) {
7000
7299
  }
7001
7300
  async function readOptionalFile(path) {
7002
7301
  try {
7003
- return await readFile6(path, "utf8");
7302
+ return await readFile7(path, "utf8");
7004
7303
  } catch (error) {
7005
7304
  if (error.code === "ENOENT")
7006
7305
  return;
@@ -7015,7 +7314,7 @@ function planRecordName(shortSlug, date = new Date) {
7015
7314
  }
7016
7315
  async function resolvePlan(cwd, query, filters = {}, options = {}) {
7017
7316
  const root = await plansDir(cwd, options.homeDir);
7018
- const entries = await readdir(root, { withFileTypes: true });
7317
+ const entries = await readdir2(root, { withFileTypes: true });
7019
7318
  const normalizedQuery = query ? normalizeQuery(query) : undefined;
7020
7319
  const records = [];
7021
7320
  for (const entry of entries) {
@@ -7023,7 +7322,7 @@ async function resolvePlan(cwd, query, filters = {}, options = {}) {
7023
7322
  continue;
7024
7323
  if (normalizedQuery && entry.name !== normalizedQuery && stripDate(entry.name) !== normalizedQuery)
7025
7324
  continue;
7026
- const dir = join13(root, entry.name);
7325
+ const dir = join14(root, entry.name);
7027
7326
  await assertContained(root, dir);
7028
7327
  try {
7029
7328
  const record = await readRecord(dir);
@@ -7042,6 +7341,15 @@ async function resolvePlan(cwd, query, filters = {}, options = {}) {
7042
7341
  }
7043
7342
  function createPlanStore(options = {}) {
7044
7343
  const now = options.now ?? (() => new Date);
7344
+ const read = async (cwd, query, filters) => {
7345
+ const result = await resolvePlan(cwd, query, filters, options);
7346
+ if (!result.record) {
7347
+ if (result.ambiguous)
7348
+ throw new Error(`Plan query "${query}" is ambiguous: ${result.candidates.map((item) => item.id).join(", ")}.`);
7349
+ throw new Error(`Plan "${query}" was not found.`);
7350
+ }
7351
+ return result.record;
7352
+ };
7045
7353
  return {
7046
7354
  context(cwd, query, filters) {
7047
7355
  return resolvePlan(cwd, query, filters, options);
@@ -7049,120 +7357,318 @@ function createPlanStore(options = {}) {
7049
7357
  async init(input) {
7050
7358
  const root = await plansDir(input.cwd, options.homeDir);
7051
7359
  const id = planRecordName(input.shortSlug, now());
7052
- const dir = join13(root, id);
7053
- await assertContained(root, dirname7(dir));
7054
- try {
7055
- await mkdir6(dir);
7056
- } catch (error) {
7057
- if (error.code === "EEXIST")
7058
- throw new Error(`Plan ${id} already exists.`);
7059
- throw error;
7060
- }
7360
+ const dir = join14(root, id);
7361
+ await createPlanDirectory(root, dir, id);
7061
7362
  try {
7062
7363
  const timestamp = now().toISOString();
7063
7364
  const template = await loadTemplate("plan/PLAN", {
7064
7365
  homeDir: options.homeDir,
7065
7366
  bundledDir: options.bundledTemplatesDir
7066
7367
  });
7368
+ const title = input.title?.trim() || titleFromSlug(input.shortSlug);
7067
7369
  const source = renderTemplate(template.content, {
7068
7370
  id,
7069
7371
  branch: input.branch,
7070
- title: input.title?.trim() || titleFromSlug(input.shortSlug),
7372
+ title,
7071
7373
  intent: input.intent?.trim() || "<!-- Describe the intended outcome. -->",
7072
- issue_id: input.issueId?.trim() || "<!-- Add issue tracker ID. -->",
7073
- issue_url: input.issueUrl?.trim() || "<!-- Add issue tracker URL. -->",
7374
+ issue_id: input.issueId?.trim() || "",
7375
+ issue_url: input.issueUrl?.trim() || "",
7074
7376
  created_at: timestamp,
7075
7377
  updated_at: timestamp
7076
7378
  });
7077
- const document = parsePlanDocument(source, join13(dir, "PLAN.md"));
7078
- await atomicWrite(join13(dir, "PLAN.md"), source);
7079
- return {
7080
- id,
7081
- dir,
7082
- planPath: join13(dir, "PLAN.md"),
7083
- logPath: join13(dir, "logs.txt"),
7084
- implementationDir: join13(dir, "implementation"),
7085
- document,
7086
- source
7087
- };
7379
+ const document = parsePlanDocument(source, join14(dir, "PLAN.md"));
7380
+ const authoringRequest = input.request ?? { kind: "user", text: input.intent?.trim() || `Initialize phase-less plan ${title}.` };
7381
+ await writeRevisionSnapshot(dir, document, source, authoringRequest, []);
7382
+ await writeLatestView(dir, document, source, []);
7383
+ return await readRecord(dir);
7088
7384
  } catch (error) {
7089
7385
  await rm2(dir, { recursive: true, force: true });
7090
7386
  throw error;
7091
7387
  }
7092
7388
  },
7093
- async read(cwd, query, filters) {
7094
- const result = await resolvePlan(cwd, query, filters, options);
7095
- if (!result.record) {
7096
- if (result.ambiguous)
7097
- throw new Error(`Plan query "${query}" is ambiguous: ${result.candidates.map((item) => item.id).join(", ")}.`);
7098
- throw new Error(`Plan "${query}" was not found.`);
7389
+ read,
7390
+ async applyRevision(cwd, request) {
7391
+ if (request.mode === "create") {
7392
+ const root = await plansDir(cwd, options.homeDir);
7393
+ const id = planRecordName(request.shortSlug, now());
7394
+ const dir = join14(root, id);
7395
+ await createPlanDirectory(root, dir, id);
7396
+ try {
7397
+ const timestamp = now().toISOString();
7398
+ const document = normalizeRevision(undefined, request, id, 0, timestamp);
7399
+ assertCompleteRevision(document, request.briefs);
7400
+ const source = renderPlanDocument(document);
7401
+ parsePlanDocument(source, join14(dir, "PLAN.md"));
7402
+ await writeRevisionSnapshot(dir, document, source, request.request, request.briefs);
7403
+ await writeLatestView(dir, document, source, request.briefs);
7404
+ return await readRecord(dir);
7405
+ } catch (error) {
7406
+ await rm2(dir, { recursive: true, force: true });
7407
+ throw error;
7408
+ }
7099
7409
  }
7100
- return result.record;
7410
+ const initial = await read(cwd, request.plan);
7411
+ return withDirectoryLock(join14(initial.dir, ".lock"), async () => {
7412
+ const current = await readRecord(initial.dir);
7413
+ if (current.document.revision !== request.expectedPlanRevision)
7414
+ throw new Error(`Expected plan revision ${request.expectedPlanRevision}, found ${current.document.revision}.`);
7415
+ const revision = current.document.revision + 1;
7416
+ const document = normalizeRevision(current.document, request, current.id, revision, now().toISOString());
7417
+ assertCompleteRevision(document, request.briefs);
7418
+ const source = renderPlanDocument(document);
7419
+ parsePlanDocument(source, current.planPath);
7420
+ await writeRevisionSnapshot(current.dir, document, source, request.request, request.briefs);
7421
+ try {
7422
+ await writeLatestView(current.dir, document, source, request.briefs);
7423
+ } catch (error) {
7424
+ await rm2(join14(current.dir, "revisions", String(revision)), { recursive: true, force: true });
7425
+ throw error;
7426
+ }
7427
+ return await readRecord(current.dir);
7428
+ }, { operation: "apply revision" });
7101
7429
  },
7102
7430
  async mutate(cwd, query, operation, update) {
7103
- const initial = await this.read(cwd, query);
7104
- return withDirectoryLock(join13(initial.dir, ".lock"), async () => {
7431
+ const initial = await read(cwd, query);
7432
+ return withDirectoryLock(join14(initial.dir, ".lock"), async () => {
7105
7433
  const current = await readRecord(initial.dir);
7106
7434
  const next = await update(structuredClone(current.document));
7107
7435
  if (next.id !== current.id)
7108
7436
  throw new Error("A plan mutation cannot change the plan ID.");
7109
7437
  const document = {
7110
7438
  ...next,
7111
- revision: current.document.revision + 1,
7439
+ revision: current.document.revision,
7112
7440
  updatedAt: now().toISOString()
7113
7441
  };
7114
7442
  const source = renderPlanDocument(document, current.source);
7115
7443
  parsePlanDocument(source, current.planPath);
7116
7444
  await atomicWrite(current.planPath, source);
7117
- await ensureImplementationFiles(current.dir, document, options);
7118
- return { ...current, document, source, implementationDir: join13(current.dir, "implementation") };
7445
+ return { ...current, document, source };
7119
7446
  }, { operation });
7120
7447
  },
7121
7448
  async log(cwd, query, event) {
7122
- const record = await this.read(cwd, query);
7449
+ const record = await read(cwd, query);
7123
7450
  return appendLogEntry(record.logPath, event);
7124
7451
  }
7125
7452
  };
7126
7453
  }
7127
7454
  async function readRecord(dir) {
7128
- const planPath = join13(dir, "PLAN.md");
7129
- const logPath = join13(dir, "logs.txt");
7130
- const source = await readFile7(planPath, "utf8");
7455
+ const planPath = join14(dir, "PLAN.md");
7456
+ const logPath = join14(dir, "logs.txt");
7457
+ const source = await readFile8(planPath, "utf8");
7131
7458
  const document = parsePlanDocument(source, planPath);
7132
7459
  return {
7133
7460
  id: basename5(dir),
7134
7461
  dir,
7135
7462
  planPath,
7136
7463
  logPath,
7137
- implementationDir: join13(dir, "implementation"),
7464
+ implementationDir: join14(dir, "implementation"),
7138
7465
  document,
7139
7466
  source
7140
7467
  };
7141
7468
  }
7142
- async function ensureImplementationFiles(dir, document, options) {
7143
- const implementationDir = join13(dir, "implementation");
7144
- await mkdir6(implementationDir, { recursive: true });
7145
- const template = await loadTemplate("plan/implementation", {
7146
- homeDir: options.homeDir,
7147
- bundledDir: options.bundledTemplatesDir
7148
- });
7149
- for (const phase of document.phases) {
7150
- const path = join13(implementationDir, `phase-${phase.id}.md`);
7469
+ async function createPlanDirectory(root, dir, id) {
7470
+ await assertContained(root, dirname7(dir));
7471
+ try {
7472
+ await mkdir6(dir);
7473
+ } catch (error) {
7474
+ if (error.code === "EEXIST")
7475
+ throw new Error(`Plan ${id} already exists.`);
7476
+ throw error;
7477
+ }
7478
+ }
7479
+ async function writeRevisionSnapshot(planDir, document, source, request, briefs) {
7480
+ if (!request.text.trim())
7481
+ throw new Error("An exact non-empty authoring request is required.");
7482
+ if (request.kind === "annotation" && !request.response?.trim())
7483
+ throw new Error("Annotation requests require a non-empty response.");
7484
+ const revisionsDir = join14(planDir, "revisions");
7485
+ await mkdir6(revisionsDir, { recursive: true });
7486
+ const target = join14(revisionsDir, String(document.revision));
7487
+ try {
7488
+ await readFile8(join14(target, "metadata.json"), "utf8");
7489
+ throw new Error(`Revision ${document.revision} already exists and is immutable.`);
7490
+ } catch (error) {
7491
+ if (error.code !== "ENOENT")
7492
+ throw error;
7493
+ }
7494
+ const staging = join14(revisionsDir, `.tmp-${document.revision}-${randomUUID2()}`);
7495
+ await mkdir6(staging);
7496
+ try {
7497
+ const requestSource = request.kind === "annotation" ? `## Original annotation
7498
+ ${request.text}
7499
+
7500
+ ## LLM response
7501
+ ${request.response}` : request.text;
7502
+ await atomicWrite(join14(staging, "request.md"), requestSource);
7503
+ await atomicWrite(join14(staging, "metadata.json"), `${JSON.stringify({
7504
+ schemaVersion: 1,
7505
+ id: document.id,
7506
+ revision: document.revision,
7507
+ requestKind: request.kind,
7508
+ requestSha256: sha2562(request.text),
7509
+ ...request.kind === "annotation" ? { originalAnnotationSha256: sha2562(request.text), responseSha256: sha2562(request.response) } : {},
7510
+ createdAt: document.updatedAt
7511
+ }, null, 2)}
7512
+ `);
7513
+ await atomicWrite(join14(staging, "PLAN.md"), source);
7514
+ await writeImplementationDirectory(staging, document, briefs);
7515
+ await rename2(staging, target);
7516
+ } catch (error) {
7517
+ await rm2(staging, { recursive: true, force: true });
7518
+ throw error;
7519
+ }
7520
+ }
7521
+ async function writeLatestView(planDir, document, source, briefs) {
7522
+ const current = join14(planDir, "implementation");
7523
+ const stagingRoot = join14(planDir, `.latest-${randomUUID2()}`);
7524
+ const stagedImplementation = join14(stagingRoot, "implementation");
7525
+ const backup = join14(planDir, `.implementation-backup-${randomUUID2()}`);
7526
+ await mkdir6(stagingRoot);
7527
+ await writeImplementationDirectory(stagingRoot, document, briefs);
7528
+ let backedUp = false;
7529
+ try {
7151
7530
  try {
7152
- await readFile7(path, "utf8");
7531
+ await rename2(current, backup);
7532
+ backedUp = true;
7153
7533
  } catch (error) {
7154
7534
  if (error.code !== "ENOENT")
7155
7535
  throw error;
7156
- const tasks = phase.tasks.map((task) => `- ${task.id}: ${task.title}`).join("\\n") || "- Add implementation tasks.";
7157
- await atomicWrite(path, renderTemplate(template.content, {
7158
- phase_id: phase.id,
7159
- phase_title: phase.title,
7160
- phase_objective: phase.objective,
7161
- phase_tasks: tasks
7162
- }));
7163
7536
  }
7537
+ await rename2(stagedImplementation, current);
7538
+ await atomicWrite(join14(planDir, "PLAN.md"), source);
7539
+ if (backedUp)
7540
+ await rm2(backup, { recursive: true, force: true });
7541
+ } catch (error) {
7542
+ await rm2(current, { recursive: true, force: true });
7543
+ if (backedUp)
7544
+ await rename2(backup, current);
7545
+ throw error;
7546
+ } finally {
7547
+ await rm2(stagingRoot, { recursive: true, force: true });
7164
7548
  }
7165
7549
  }
7550
+ async function writeImplementationDirectory(baseDir, document, briefs) {
7551
+ const implementationDir = join14(baseDir, "implementation");
7552
+ await mkdir6(implementationDir, { recursive: true });
7553
+ for (const [index, phase] of document.phases.entries()) {
7554
+ const brief = briefs[index];
7555
+ if (!brief)
7556
+ throw new Error(`Missing implementation brief for phase ${phase.id}.`);
7557
+ await atomicWrite(join14(implementationDir, `phase-${index + 1}.md`), renderImplementationBrief(document.revision, index + 1, phase, brief));
7558
+ }
7559
+ }
7560
+ function normalizeRevision(current, request, id, revision, updatedAt) {
7561
+ if (current?.status === "completed")
7562
+ throw new Error("Completed plans cannot be amended.");
7563
+ if (current?.execution?.active && request.request.kind !== "blocker")
7564
+ throw new Error("An active execution accepts only a blocker amendment.");
7565
+ const currentPhases = new Map(current?.phases.map((phase) => [phase.id, phase]) ?? []);
7566
+ for (const phase of current?.phases ?? []) {
7567
+ if (!request.phases.some((candidate) => candidate.id === phase.id) && (phase.status === "completed" || phase.status === "in_progress"))
7568
+ throw new Error(`${phase.status === "completed" ? "Completed" : "Active"} phase ${phase.id} cannot be removed.`);
7569
+ }
7570
+ const phases = request.phases.map((draft) => reconcilePhase(currentPhases.get(draft.id), draft, current));
7571
+ const createdAt = current?.createdAt ?? updatedAt;
7572
+ return {
7573
+ schemaVersion: 1,
7574
+ id,
7575
+ revision,
7576
+ title: request.title,
7577
+ branch: current?.branch ?? (request.mode === "create" ? request.branch : ""),
7578
+ issueId: current?.issueId ?? (request.mode === "create" ? request.issueId : undefined),
7579
+ issueUrl: current?.issueUrl ?? (request.mode === "create" ? request.issueUrl : undefined),
7580
+ intent: request.intent,
7581
+ requirements: request.requirements,
7582
+ design: request.design,
7583
+ references: request.references,
7584
+ phases,
7585
+ status: nextPlanStatus(current),
7586
+ execution: current?.execution,
7587
+ createdAt,
7588
+ updatedAt
7589
+ };
7590
+ }
7591
+ function reconcilePhase(current, draft, plan) {
7592
+ if (!current)
7593
+ return {
7594
+ ...draft,
7595
+ revision: 0,
7596
+ status: "pending",
7597
+ gate: { phaseRevision: 0, status: "pending", results: [] },
7598
+ tasks: draft.tasks.map(newTask)
7599
+ };
7600
+ if (current.status === "completed") {
7601
+ if (JSON.stringify(phaseContent(current)) !== JSON.stringify(draft))
7602
+ throw new Error(`Completed phase ${current.id} cannot be changed.`);
7603
+ return current;
7604
+ }
7605
+ const byId = new Map(current.tasks.map((task) => [task.id, task]));
7606
+ for (const task of current.tasks) {
7607
+ if (!draft.tasks.some((candidate) => candidate.id === task.id) && (task.status === "completed" || task.status === "in_progress"))
7608
+ throw new Error(`${task.status === "completed" ? "Completed" : "Active"} task ${task.id} cannot be removed.`);
7609
+ }
7610
+ const tasks = draft.tasks.map((task) => reconcileTask(byId.get(task.id), task));
7611
+ const changed = JSON.stringify(phaseContent(current)) !== JSON.stringify(draft);
7612
+ const revision = changed ? current.revision + 1 : current.revision;
7613
+ const wasBlocked = current.status === "blocked";
7614
+ return {
7615
+ ...current,
7616
+ ...draft,
7617
+ revision,
7618
+ tasks,
7619
+ status: wasBlocked ? plan?.execution?.active ? "in_progress" : "pending" : current.status,
7620
+ gate: changed || wasBlocked ? { phaseRevision: revision, status: "stale", results: [] } : current.gate,
7621
+ blocker: wasBlocked ? undefined : current.blocker
7622
+ };
7623
+ }
7624
+ function reconcileTask(current, draft) {
7625
+ if (!current)
7626
+ return newTask(draft);
7627
+ const changed = JSON.stringify(taskContent(current)) !== JSON.stringify(draft);
7628
+ if ((current.status === "completed" || current.status === "in_progress") && changed)
7629
+ throw new Error(`${current.status === "completed" ? "Completed" : "Active"} task ${current.id} cannot be changed.`);
7630
+ if (current.status === "completed" || current.status === "in_progress")
7631
+ return current;
7632
+ return {
7633
+ ...current,
7634
+ ...draft,
7635
+ revision: changed ? current.revision + 1 : current.revision,
7636
+ status: current.status === "blocked" ? "pending" : current.status,
7637
+ owner: current.status === "blocked" ? undefined : current.owner,
7638
+ executionId: current.status === "blocked" ? undefined : current.executionId,
7639
+ blocker: current.status === "blocked" ? undefined : current.blocker
7640
+ };
7641
+ }
7642
+ function newTask(draft) {
7643
+ return { ...draft, revision: 0, status: "pending" };
7644
+ }
7645
+ function phaseContent(phase) {
7646
+ return {
7647
+ id: phase.id,
7648
+ title: phase.title,
7649
+ objective: phase.objective,
7650
+ dependencies: phase.dependencies,
7651
+ tasks: phase.tasks.map(taskContent)
7652
+ };
7653
+ }
7654
+ function taskContent(task) {
7655
+ return { id: task.id, title: task.title, dependencies: task.dependencies };
7656
+ }
7657
+ function nextPlanStatus(current) {
7658
+ if (!current)
7659
+ return "draft";
7660
+ if (current.execution?.active)
7661
+ return current.status === "blocked" ? "in_progress" : current.status;
7662
+ return current.status === "ready" || current.status === "blocked" ? "draft" : current.status;
7663
+ }
7664
+ function assertCompleteRevision(document, briefs) {
7665
+ const errors = [...validatePlanDocument(document, { strict: true }), ...validatePlanBriefs(document, briefs)].filter((issue) => issue.severity === "error");
7666
+ if (errors.length)
7667
+ throw new Error(`Invalid plan revision: ${errors.map((issue) => issue.message).join(" ")}`);
7668
+ }
7669
+ function sha2562(value) {
7670
+ return createHash3("sha256").update(value).digest("hex");
7671
+ }
7166
7672
  async function assertContained(root, candidate) {
7167
7673
  const canonicalRoot = await realpath3(root);
7168
7674
  let canonicalCandidate;
@@ -7194,34 +7700,54 @@ function stripDate(value) {
7194
7700
  return value.replace(/^\d{6}-/, "");
7195
7701
  }
7196
7702
  function titleFromSlug(value) {
7197
- return normalizeSlug(value).replace(/^(?:[a-z]+-\d+-)/, "").split("-").map((part) => part[0].toUpperCase() + part.slice(1)).join(" ");
7703
+ const slug = normalizeSlug(value).replace(/^(?:[a-z]+-\d+-)/, "");
7704
+ return slug.replace(/(^|-)([a-z])/g, (_match, separator, letter) => {
7705
+ return `${separator ? " " : ""}${letter.toUpperCase()}`;
7706
+ });
7198
7707
  }
7199
7708
 
7200
- // src/tools/plan-schema.ts
7709
+ // src/plan/schema.ts
7201
7710
  import { z as z6 } from "zod";
7202
7711
  var planStatusSchema = z6.enum(["draft", "ready", "in_progress", "blocked", "completed"]);
7203
7712
  var planUnitStatusSchema = z6.enum(["draft", "ready", "in_progress", "blocked", "completed", "pending", "skipped"]);
7204
- var planTaskDraftSchema = z6.object({
7205
- id: zx.id,
7206
- title: zx.text,
7207
- steps: z6.array(zx.text).optional(),
7208
- dependencies: z6.array(zx.id).optional(),
7209
- fileScopes: z6.array(zx.text).optional(),
7210
- acceptanceCriteria: z6.array(zx.text).optional()
7211
- }).strict();
7713
+ var planTaskDraftSchema = z6.object({ id: zx.id, title: zx.text, dependencies: z6.array(zx.id) }).strict();
7212
7714
  var planPhaseDraftSchema = z6.object({
7213
7715
  id: zx.id,
7214
7716
  title: zx.text,
7215
7717
  objective: zx.text,
7216
- dependencies: z6.array(zx.id).optional(),
7217
- tasks: z6.array(planTaskDraftSchema).optional()
7718
+ dependencies: z6.array(zx.id),
7719
+ tasks: z6.array(planTaskDraftSchema).min(1)
7218
7720
  }).strict();
7219
- var planReferenceSchema = z6.object({ id: zx.id, value: zx.text }).strict();
7220
- var planDesignSchema = z6.object({
7221
- bigIdeas: z6.string(),
7222
- keyApiUpdates: z6.string(),
7223
- consequences: z6.string()
7721
+ var planBriefSchema = z6.object({
7722
+ phaseId: zx.id,
7723
+ summary: zx.text,
7724
+ apiChanges: z6.array(zx.text),
7725
+ libraries: z6.array(zx.text),
7726
+ constraints: z6.array(zx.text),
7727
+ tasks: z6.array(z6.object({
7728
+ taskId: zx.id,
7729
+ steps: z6.array(zx.text).min(1),
7730
+ fileScopes: z6.array(zx.text).min(1),
7731
+ acceptanceCriteria: z6.array(zx.text).min(1)
7732
+ }).strict()).min(1)
7224
7733
  }).strict();
7734
+ var planReferenceSchema = z6.object({ id: zx.id, value: zx.text }).strict();
7735
+ var planDesignSchema = z6.object({ bigIdeas: zx.text, keyApiUpdates: zx.text, consequences: zx.text }).strict();
7736
+ var planRequestSchema = z6.discriminatedUnion("kind", [
7737
+ z6.object({ kind: z6.literal("annotation"), text: zx.text, response: zx.text }).strict(),
7738
+ z6.object({ kind: z6.literal("user"), text: zx.text, response: zx.text.optional() }).strict(),
7739
+ z6.object({ kind: z6.literal("blocker"), text: zx.text, response: zx.text.optional() }).strict()
7740
+ ]);
7741
+ var revisionContentFields = {
7742
+ request: planRequestSchema,
7743
+ title: zx.text,
7744
+ intent: zx.text,
7745
+ requirements: z6.array(zx.text).min(1),
7746
+ design: planDesignSchema,
7747
+ references: z6.array(planReferenceSchema),
7748
+ phases: z6.array(planPhaseDraftSchema).min(1),
7749
+ briefs: z6.array(planBriefSchema).min(1)
7750
+ };
7225
7751
  var planContextParametersSchema = z6.object({
7226
7752
  cwd: zx.cwd,
7227
7753
  plan: z6.string().optional(),
@@ -7235,46 +7761,28 @@ var planInitParametersSchema = z6.object({
7235
7761
  title: zx.text.optional(),
7236
7762
  intent: zx.text.optional(),
7237
7763
  issueId: zx.text.optional(),
7238
- issueUrl: z6.string().url().optional(),
7764
+ issueUrl: z6.url().optional(),
7765
+ request: planRequestSchema.optional(),
7239
7766
  open: z6.boolean().optional()
7240
7767
  }).strict();
7241
- var planUpdateOverviewParametersSchema = z6.object({
7242
- cwd: zx.cwd,
7243
- plan: zx.text,
7244
- expectedPlanRevision: zx.revision,
7245
- intent: zx.text.optional(),
7246
- requirements: z6.array(zx.text).optional(),
7247
- design: planDesignSchema.optional(),
7248
- references: z6.array(planReferenceSchema).optional()
7249
- }).strict().refine((value) => value.intent || value.requirements || value.design || value.references, {
7250
- message: "At least one overview field is required."
7251
- });
7252
- var planAddPhaseParametersSchema = z6.object({
7253
- cwd: zx.cwd,
7254
- plan: zx.text,
7255
- expectedPlanRevision: zx.revision,
7256
- afterPhaseId: zx.id.optional(),
7257
- phase: planPhaseDraftSchema
7258
- }).strict();
7259
- var planRemovePhaseParametersSchema = z6.object({
7260
- cwd: zx.cwd,
7261
- plan: zx.text,
7262
- expectedPlanRevision: zx.revision,
7263
- phaseId: zx.id,
7264
- reason: zx.text
7265
- }).strict();
7266
- var planUpdatePhaseParametersSchema = z6.object({
7267
- cwd: zx.cwd,
7268
- plan: zx.text,
7269
- expectedPlanRevision: zx.revision,
7270
- phaseId: zx.id,
7271
- patch: z6.object({
7272
- title: zx.text.optional(),
7273
- objective: zx.text.optional(),
7274
- dependencies: z6.array(zx.id).optional(),
7275
- tasks: z6.array(planTaskDraftSchema).optional()
7768
+ var planApplyRevisionParametersSchema = z6.discriminatedUnion("mode", [
7769
+ z6.object({
7770
+ mode: z6.literal("create"),
7771
+ cwd: zx.cwd,
7772
+ shortSlug: zx.id,
7773
+ branch: z6.string().min(1).optional(),
7774
+ issueId: zx.text.optional(),
7775
+ issueUrl: z6.url().optional(),
7776
+ ...revisionContentFields
7777
+ }).strict(),
7778
+ z6.object({
7779
+ mode: z6.literal("amend"),
7780
+ cwd: zx.cwd,
7781
+ plan: zx.text,
7782
+ expectedPlanRevision: zx.revision,
7783
+ ...revisionContentFields
7276
7784
  }).strict()
7277
- }).strict();
7785
+ ]);
7278
7786
  var planValidateParametersSchema = z6.object({ cwd: zx.cwd, plan: zx.text, strict: z6.boolean().optional() }).strict();
7279
7787
  var planLogProgressParametersSchema = z6.object({
7280
7788
  cwd: zx.cwd,
@@ -7374,116 +7882,17 @@ function createPlanTools(_pi, _modes, store = createPlanStore()) {
7374
7882
  }
7375
7883
  }),
7376
7884
  defineTool5({
7377
- name: "plan_update_overview",
7378
- label: "plan update overview",
7379
- description: "Replace selected plan overview fields using an expected plan revision.",
7380
- parameters: parameters(planUpdateOverviewParametersSchema),
7381
- executionMode: "sequential",
7382
- async execute(_id, input) {
7383
- const params = planUpdateOverviewParametersSchema.parse(input);
7384
- const record = await store.mutate(params.cwd ?? process.cwd(), params.plan, "update overview", (plan) => {
7385
- assertAuthoringRevision(plan, params.expectedPlanRevision);
7386
- assertAuthorable(plan);
7387
- return resetDraft({
7388
- ...plan,
7389
- intent: params.intent ?? plan.intent,
7390
- requirements: params.requirements ?? plan.requirements,
7391
- design: params.design ?? plan.design,
7392
- references: params.references ?? plan.references
7393
- });
7394
- });
7395
- return result2(`Updated ${record.id} to revision ${record.document.revision}.`, { record });
7396
- }
7397
- }),
7398
- defineTool5({
7399
- name: "plan_add_phase",
7400
- label: "plan add phase",
7401
- description: "Add an ordered phase with stable IDs.",
7402
- parameters: parameters(planAddPhaseParametersSchema),
7403
- executionMode: "sequential",
7404
- async execute(_id, input) {
7405
- const params = planAddPhaseParametersSchema.parse(input);
7406
- const record = await store.mutate(params.cwd ?? process.cwd(), params.plan, "add phase", (plan) => {
7407
- assertAuthoringRevision(plan, params.expectedPlanRevision);
7408
- assertAuthorable(plan);
7409
- if (allIds(plan).has(params.phase.id))
7410
- throw new Error(`Duplicate stable ID: ${params.phase.id}.`);
7411
- const phase = newPhase(params.phase);
7412
- const phases = [...plan.phases];
7413
- if (params.afterPhaseId) {
7414
- const index = phases.findIndex((item) => item.id === params.afterPhaseId);
7415
- if (index < 0)
7416
- throw new Error(`Unknown phase: ${params.afterPhaseId}.`);
7417
- phases.splice(index + 1, 0, phase);
7418
- } else
7419
- phases.push(phase);
7420
- assertNewIds(plan, phase);
7421
- return resetDraft({ ...plan, phases });
7422
- });
7423
- return result2(`Added phase ${params.phase.id} to ${record.id}.`, { record });
7424
- }
7425
- }),
7426
- defineTool5({
7427
- name: "plan_remove_phase",
7428
- label: "plan remove phase",
7429
- description: "Remove an unfinished phase that has no dependents.",
7430
- parameters: parameters(planRemovePhaseParametersSchema),
7885
+ name: "plan_apply_revision",
7886
+ label: "plan apply revision",
7887
+ description: "Create mode writes a complete revision 0 without plan_init; amend mode atomically writes the next complete authoring revision.",
7888
+ parameters: parameters(planApplyRevisionParametersSchema),
7431
7889
  executionMode: "sequential",
7432
7890
  async execute(_id, input) {
7433
- const params = planRemovePhaseParametersSchema.parse(input);
7434
- const record = await store.mutate(params.cwd ?? process.cwd(), params.plan, "remove phase", (plan) => {
7435
- assertAuthoringRevision(plan, params.expectedPlanRevision);
7436
- assertAuthorable(plan);
7437
- const phase = getPhase2(plan, params.phaseId);
7438
- if (phase.status === "completed" || phase.tasks.some((task) => task.status === "completed"))
7439
- throw new Error(`Completed phase ${phase.id} cannot be removed.`);
7440
- if (plan.phases.some((item) => item.dependencies.includes(phase.id)))
7441
- throw new Error(`Phase ${phase.id} has dependents and cannot be removed.`);
7442
- return resetDraft({ ...plan, phases: plan.phases.filter((item) => item.id !== phase.id) });
7443
- });
7444
- await store.log(params.cwd ?? process.cwd(), record.id, {
7445
- planRevision: record.document.revision,
7446
- kind: "updated",
7447
- actor: "planner",
7448
- message: `Removed phase ${params.phaseId}: ${params.reason}`,
7449
- phaseId: params.phaseId
7450
- });
7451
- return result2(`Removed phase ${params.phaseId}.`, { record });
7452
- }
7453
- }),
7454
- defineTool5({
7455
- name: "plan_update_phase",
7456
- label: "plan update phase",
7457
- description: "Update an unfinished phase and its ordered tasks.",
7458
- parameters: parameters(planUpdatePhaseParametersSchema),
7459
- executionMode: "sequential",
7460
- async execute(_id, input) {
7461
- const params = planUpdatePhaseParametersSchema.parse(input);
7462
- const record = await store.mutate(params.cwd ?? process.cwd(), params.plan, "update phase", (plan) => {
7463
- assertAuthoringRevision(plan, params.expectedPlanRevision);
7464
- assertAuthorable(plan, params.phaseId);
7465
- const existing = getPhase2(plan, params.phaseId);
7466
- if (existing.status === "completed")
7467
- throw new Error(`Completed phase ${existing.id} cannot be changed.`);
7468
- const tasks = params.patch.tasks ? reconcileTasks(existing.tasks, params.patch.tasks) : existing.tasks;
7469
- const updated = {
7470
- ...existing,
7471
- title: params.patch.title ?? existing.title,
7472
- objective: params.patch.objective ?? existing.objective,
7473
- dependencies: params.patch.dependencies ?? existing.dependencies,
7474
- tasks,
7475
- revision: existing.revision + 1,
7476
- status: existing.status === "blocked" ? "in_progress" : existing.status,
7477
- gate: { ...existing.gate, status: "stale" },
7478
- blocker: existing.status === "blocked" ? undefined : existing.blocker
7479
- };
7480
- assertNewIds({ ...plan, phases: plan.phases.filter((phase) => phase.id !== existing.id) }, updated);
7481
- return resetDraft({
7482
- ...plan,
7483
- phases: plan.phases.map((phase) => phase.id === existing.id ? updated : phase)
7484
- });
7485
- });
7486
- return result2(`Updated phase ${params.phaseId}.`, { record });
7891
+ const params = planApplyRevisionParametersSchema.parse(input);
7892
+ const workingDirectory = params.cwd ?? process.cwd();
7893
+ const request = params.mode === "create" ? { ...params, branch: params.branch ?? await currentBranch(workingDirectory) } : params;
7894
+ const record = await store.applyRevision(workingDirectory, request);
7895
+ return result2(`Applied revision ${record.document.revision} to ${record.id}.`, { record });
7487
7896
  }
7488
7897
  }),
7489
7898
  defineTool5({
@@ -7495,7 +7904,7 @@ function createPlanTools(_pi, _modes, store = createPlanStore()) {
7495
7904
  async execute(_id, input) {
7496
7905
  const params = planValidateParametersSchema.parse(input);
7497
7906
  const record = await store.read(params.cwd ?? process.cwd(), params.plan);
7498
- const issues = validatePlanDocument(record.document, { strict: params.strict });
7907
+ const issues = await validatePlanRecord(record, { strict: params.strict });
7499
7908
  const words = countDesignWords(record.document);
7500
7909
  return result2(issues.length ? issues.map((issue) => `- ${issue.severity}: ${issue.message}`).join(`
7501
7910
  `) : "Plan is valid.", { record, issues, designWordCount: words, ready: !issues.some((issue) => issue.severity === "error") });
@@ -7631,7 +8040,7 @@ function createPlanTools(_pi, _modes, store = createPlanStore()) {
7631
8040
  const resolution = await store.context(workingDirectory, params.plan);
7632
8041
  if (!resolution.record)
7633
8042
  throw new Error(resolution.ambiguous ? `Plan is ambiguous: ${resolution.candidates.map((item) => item.id).join(", ")}.` : "No matching plan.");
7634
- const cliPath = join14(resolveBundledAgentsDir(), "..", "dist", "cli.js");
8043
+ const cliPath = join15(resolveBundledAgentsDir(), "..", "dist", "cli.js");
7635
8044
  const command = [
7636
8045
  process.execPath,
7637
8046
  cliPath,
@@ -7726,8 +8135,8 @@ function renderExecutionPrompt(packet) {
7726
8135
  return [
7727
8136
  `Execute the durable Diffpi plan using this packet: ${JSON.stringify(packet)}.`,
7728
8137
  "Call plan_context first and keep plan tools as the source of truth for eligibility, ownership, status, gates, and commits.",
7729
- "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.",
7730
- "The extension cannot launch workflow children over RPC, so the active orchestrator must invoke SubagentWorkflow itself.",
8138
+ "Use bounded Agent delegation for eligible tasks. Run dependent tasks in order; parallelize only tasks with non-overlapping declared file scopes. Collect and verify worker results before changing plan state.",
8139
+ "Use a multi-agent SubagentWorkflow only when the user explicitly opts into that orchestration.",
7731
8140
  "Persist every transition, progress event, issue, and deviation. Do not edit PLAN.md directly. Delegated implementation workers never commit or restructure the plan.",
7732
8141
  "In commit mode, create one local commit after each phase passes its gates and do not push. In push mode, commit and push each phase, record pending CI, and launch a bounded background Worker to call watch_ci for the exact SHA and then plan_record_ci for the phase 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."
7733
8142
  ].join(" ");
@@ -7791,81 +8200,6 @@ ${dirty.stdout.trim()}`);
7791
8200
  prompt: renderExecutionPrompt(packet)
7792
8201
  });
7793
8202
  }
7794
- function newPhase(input) {
7795
- return {
7796
- id: input.id,
7797
- revision: 0,
7798
- title: input.title,
7799
- objective: input.objective,
7800
- dependencies: input.dependencies ?? [],
7801
- tasks: (input.tasks ?? []).map(newTask),
7802
- status: "pending",
7803
- gate: { phaseRevision: 0, status: "pending", results: [] }
7804
- };
7805
- }
7806
- function newTask(input) {
7807
- return {
7808
- id: input.id,
7809
- revision: 0,
7810
- title: input.title,
7811
- steps: input.steps,
7812
- dependencies: input.dependencies ?? [],
7813
- fileScopes: input.fileScopes ?? [],
7814
- acceptanceCriteria: input.acceptanceCriteria ?? [],
7815
- status: "pending"
7816
- };
7817
- }
7818
- function reconcileTasks(existing, drafts) {
7819
- const byId = new Map(existing.map((task) => [task.id, task]));
7820
- const requested = new Set(drafts.map((task) => task.id));
7821
- const protectedTask = existing.find((task) => (task.status === "completed" || task.status === "in_progress") && !requested.has(task.id));
7822
- if (protectedTask)
7823
- throw new Error(`${protectedTask.status === "completed" ? "Completed" : "Active"} task ${protectedTask.id} cannot be removed.`);
7824
- return drafts.map((draft) => {
7825
- const current = byId.get(draft.id);
7826
- if (current?.status === "completed" || current?.status === "in_progress")
7827
- return current;
7828
- return current ? {
7829
- ...newTask(draft),
7830
- revision: current.revision + 1,
7831
- status: current.status === "blocked" ? "pending" : current.status,
7832
- owner: current.status === "blocked" ? undefined : current.owner,
7833
- executionId: current.status === "blocked" ? undefined : current.executionId,
7834
- blocker: current.status === "blocked" ? undefined : current.blocker
7835
- } : newTask(draft);
7836
- });
7837
- }
7838
- function assertAuthoringRevision(plan, expected) {
7839
- if (plan.revision !== expected)
7840
- throw new Error(`Stale plan revision: expected ${expected}, found ${plan.revision}.`);
7841
- }
7842
- function assertAuthorable(plan, blockedPhaseId) {
7843
- if (plan.status === "completed")
7844
- throw new Error("Completed plans cannot be structurally changed.");
7845
- if (!plan.execution?.active)
7846
- return;
7847
- if (!blockedPhaseId)
7848
- throw new Error("An active execution owns this plan.");
7849
- const phase = getPhase2(plan, blockedPhaseId);
7850
- if (phase.status !== "blocked" && !phase.tasks.some((task) => task.status === "blocked"))
7851
- throw new Error("Planner may amend only blocked work during an active execution.");
7852
- }
7853
- function resetDraft(plan) {
7854
- if (plan.status === "blocked" && plan.execution?.active)
7855
- return { ...plan, status: "in_progress" };
7856
- return plan.status === "ready" || plan.status === "blocked" ? { ...plan, status: "draft" } : plan;
7857
- }
7858
- function allIds(plan) {
7859
- return new Set([plan.id, ...plan.phases.flatMap((phase) => [phase.id, ...phase.tasks.map((task) => task.id)])]);
7860
- }
7861
- function assertNewIds(plan, phase) {
7862
- const known = allIds(plan);
7863
- for (const candidate of [phase.id, ...phase.tasks.map((task) => task.id)]) {
7864
- if (known.has(candidate))
7865
- throw new Error(`Duplicate stable ID: ${candidate}.`);
7866
- known.add(candidate);
7867
- }
7868
- }
7869
8203
  function getPhase2(plan, phaseId) {
7870
8204
  const phase = plan.phases.find((item) => item.id === phaseId);
7871
8205
  if (!phase)
@@ -7903,8 +8237,8 @@ function result2(text, details = {}) {
7903
8237
 
7904
8238
  // src/tools/review.ts
7905
8239
  import { existsSync as existsSync2 } from "node:fs";
7906
- import { mkdir as mkdir7, readdir as readdir2, readFile as readFile11, rename as rename3, stat, unlink as unlink2, writeFile as writeFile7 } from "node:fs/promises";
7907
- import { join as join16 } from "node:path";
8240
+ import { mkdir as mkdir7, readdir as readdir3, readFile as readFile12, rename as rename4, stat, unlink as unlink2, writeFile as writeFile7 } from "node:fs/promises";
8241
+ import { join as join17 } from "node:path";
7908
8242
  import { defineTool as defineTool6 } from "@earendil-works/pi-coding-agent";
7909
8243
  import { z as z11 } from "zod";
7910
8244
 
@@ -8075,7 +8409,7 @@ function renderThreadArtifact(title, target, threads, options = {}) {
8075
8409
  }
8076
8410
  lines.push("## Source comments", "");
8077
8411
  for (const thread of threads) {
8078
- lines.push(`### ${thread.file ?? "review"}:${thread.line ?? "n/a"} — ${thread.author ?? "unknown"}`, "", `Thread: ${thread.id}`, `Status: ${thread.resolved ? "resolved" : thread.addressed ? "addressed" : "open"}`, "", thread.body, "", "---", "");
8412
+ lines.push(`### ${thread.file ?? "review"}:${thread.line ?? "n/a"} — ${thread.author ?? "unknown"}`, "", `Thread: ${thread.id}`, `Status: ${thread.resolved ? "resolved" : thread.addressed ? "addressed" : "open"}`, "", "**Source comment**", "", thread.body, "", "**Recorded response**", "", thread.reply?.trim() || "_No response recorded._", "", "---", "");
8079
8413
  }
8080
8414
  return `${lines.join(`
8081
8415
  `).trimEnd()}
@@ -8506,10 +8840,10 @@ function hasGitlabDraftNotes(input) {
8506
8840
  }
8507
8841
 
8508
8842
  // src/review/local-review-backend.ts
8509
- import { readFile as readFile9, writeFile as writeFile5 } from "node:fs/promises";
8843
+ import { readFile as readFile10, writeFile as writeFile5 } from "node:fs/promises";
8510
8844
 
8511
8845
  // src/extensions/tuicrx.ts
8512
- import { readFile as readFile8, realpath as realpath4 } from "node:fs/promises";
8846
+ import { readFile as readFile9, realpath as realpath4 } from "node:fs/promises";
8513
8847
  import { resolve as resolve4 } from "node:path";
8514
8848
  async function tuicrAvailable() {
8515
8849
  return Boolean(await findExecutable("tuicr"));
@@ -8606,7 +8940,7 @@ async function findMatchingSession(sessions, cwd, branch) {
8606
8940
  return;
8607
8941
  }
8608
8942
  async function readSession(path) {
8609
- const content = await readFile8(path, "utf8");
8943
+ const content = await readFile9(path, "utf8");
8610
8944
  try {
8611
8945
  return JSON.parse(content);
8612
8946
  } catch {
@@ -8727,7 +9061,7 @@ function LocalReviewBackend(options) {
8727
9061
  },
8728
9062
  async listThreads() {
8729
9063
  try {
8730
- return parseThreadArtifact(await readFile9(options.artifactPath, "utf8"));
9064
+ return parseThreadArtifact(await readFile10(options.artifactPath, "utf8"));
8731
9065
  } catch (error) {
8732
9066
  if (error.code === "ENOENT")
8733
9067
  return [];
@@ -8735,7 +9069,7 @@ function LocalReviewBackend(options) {
8735
9069
  }
8736
9070
  },
8737
9071
  async reply(input) {
8738
- const content = await readFile9(options.artifactPath, "utf8");
9072
+ const content = await readFile10(options.artifactPath, "utf8");
8739
9073
  await writeFile5(options.artifactPath, upsertThreadReply(content, input.threadId, input.body, input.question, input.resolve), "utf8");
8740
9074
  },
8741
9075
  async publish() {
@@ -8756,9 +9090,9 @@ function createLocalReviewBackend(options) {
8756
9090
  return LocalReviewBackend(options);
8757
9091
  }
8758
9092
  // src/review/review-state.ts
8759
- import { createHash as createHash2 } from "node:crypto";
8760
- import { readFile as readFile10, rename as rename2, writeFile as writeFile6 } from "node:fs/promises";
8761
- import { join as join15 } from "node:path";
9093
+ import { createHash as createHash4 } from "node:crypto";
9094
+ import { readFile as readFile11, rename as rename3, writeFile as writeFile6 } from "node:fs/promises";
9095
+ import { join as join16 } from "node:path";
8762
9096
  import { z as z10 } from "zod";
8763
9097
  var reviewPublicationStateSchema = z10.object({
8764
9098
  target: z10.string().optional(),
@@ -8782,9 +9116,9 @@ function unpublishedReviewComments(comments, knownFingerprints) {
8782
9116
  }
8783
9117
  async function loadReviewPublicationState(cwd, vcs, number, homeDir) {
8784
9118
  const target = `${vcs.provider}:${vcs.owner}/${vcs.repo}#${number}`;
8785
- const path = join15(await sessionsDir(cwd, homeDir), `review-publish-${digest(target).slice(0, 16)}.json`);
9119
+ const path = join16(await sessionsDir(cwd, homeDir), `review-publish-${digest(target).slice(0, 16)}.json`);
8786
9120
  try {
8787
- const parsed = reviewPublicationStateSchema.parse(JSON.parse(await readFile10(path, "utf8")));
9121
+ const parsed = reviewPublicationStateSchema.parse(JSON.parse(await readFile11(path, "utf8")));
8788
9122
  return { path, state: { ...parsed, target } };
8789
9123
  } catch (error) {
8790
9124
  if (error.code === "ENOENT") {
@@ -8800,10 +9134,10 @@ async function saveReviewPublicationState(path, state) {
8800
9134
  const temp = `${path}.${process.pid}.tmp`;
8801
9135
  await writeFile6(temp, `${JSON.stringify(state, null, 2)}
8802
9136
  `, "utf8");
8803
- await rename2(temp, path);
9137
+ await rename3(temp, path);
8804
9138
  }
8805
9139
  function digest(value) {
8806
- return createHash2("sha256").update(value).digest("hex");
9140
+ return createHash4("sha256").update(value).digest("hex");
8807
9141
  }
8808
9142
  // src/tools/review.ts
8809
9143
  var contextSchema = z11.object({
@@ -8845,6 +9179,17 @@ var completeSchema = localSchema.extend({
8845
9179
  function hasReviewDraft(comments, body) {
8846
9180
  return comments.length > 0 || body.trim().length > 0;
8847
9181
  }
9182
+ function reviewBrowserCommand(url, platform = process.platform) {
9183
+ return platform === "darwin" ? { command: "open", args: [url] } : platform === "win32" ? { command: "cmd", args: ["/c", "start", "", url] } : { command: "xdg-open", args: [url] };
9184
+ }
9185
+ async function openReviewUrl(url, execute = run, platform = process.platform) {
9186
+ const launch = reviewBrowserCommand(url, platform);
9187
+ try {
9188
+ return (await execute(launch.command, launch.args)).code === 0;
9189
+ } catch {
9190
+ return false;
9191
+ }
9192
+ }
8848
9193
  function partitionReviewComments(comments, knownFingerprints, model) {
8849
9194
  const normalized = comments.map((comment) => {
8850
9195
  const published = { ...comment, body: withCommentProvenance(comment, model) };
@@ -8899,7 +9244,9 @@ function createReviewTools() {
8899
9244
  const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
8900
9245
  const store = await ensureStore(review.cwd);
8901
9246
  const env = { ide: detectIde(), mux: detectMux(), shell: detectShell() };
8902
- const session = await resolveTuicrSession(review, Boolean(params.local || params.workingTree));
9247
+ const session = await resolveTuicrSession(review, Boolean(params.local || params.workingTree)).catch(() => {
9248
+ return;
9249
+ });
8903
9250
  const baseRef = review.pr?.baseRef ?? (review.forge ? await review.forge.defaultBranch() : "local");
8904
9251
  const backend = params.local || params.workingTree ? "tuicr" : review.forge?.provider ?? "unsupported";
8905
9252
  return result3([
@@ -8923,6 +9270,21 @@ function createReviewTools() {
8923
9270
  });
8924
9271
  }
8925
9272
  }),
9273
+ defineTool6({
9274
+ name: "review_status",
9275
+ label: "review status",
9276
+ description: "Report local and remote review lifecycle status without requiring a forge or tuicr.",
9277
+ promptSnippet: "Call review_status for review lifecycle state",
9278
+ promptGuidelines: ["Use this for status even when no forge or tuicr binary is available."],
9279
+ parameters: parameters2(localSchema),
9280
+ executionMode: "parallel",
9281
+ async execute(_id, input) {
9282
+ const params = localSchema.parse(input);
9283
+ const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
9284
+ const status = await collectReviewStatus(review);
9285
+ return result3(status.text, status.details);
9286
+ }
9287
+ }),
8926
9288
  defineTool6({
8927
9289
  name: "review_new",
8928
9290
  label: "review new",
@@ -8966,6 +9328,31 @@ ${openMessage}`, {
8966
9328
  });
8967
9329
  }
8968
9330
  }),
9331
+ defineTool6({
9332
+ name: "review_open",
9333
+ label: "review open",
9334
+ description: "Open an existing remote PR/MR in the system browser; never creates one.",
9335
+ promptSnippet: "Call review_open to open an existing PR/MR",
9336
+ promptGuidelines: ["Direct users to review_new when no remote review exists."],
9337
+ parameters: parameters2(localSchema),
9338
+ executionMode: "sequential",
9339
+ async execute(_id, input) {
9340
+ const params = localSchema.parse(input);
9341
+ const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
9342
+ if (params.local || params.workingTree)
9343
+ return launchLocalReview(review, true);
9344
+ if (!review.forge)
9345
+ return result3(`${unsupportedForgeMessage()} Use review_new with local=true for an offline review.`);
9346
+ if (!review.pr)
9347
+ return result3("No remote PR/MR exists. Use review_new to create a draft review first.");
9348
+ const opened = await openReviewUrl(review.pr.url);
9349
+ return result3(`${opened ? "Opened" : "Open this URL"} PR/MR #${review.pr.number}: ${review.pr.url}`, {
9350
+ pr: review.pr,
9351
+ opened,
9352
+ url: review.pr.url
9353
+ });
9354
+ }
9355
+ }),
8969
9356
  defineTool6({
8970
9357
  name: "review_edit",
8971
9358
  label: "review edit",
@@ -9272,7 +9659,7 @@ ${params.body}`, {
9272
9659
  if (!existsSync2(artifact))
9273
9660
  return result3("No local review artifact. Run review_comments with local=true first.");
9274
9661
  const archived = await uniqueRecordPath(await completedReviewsDir(review.cwd), reviewSlug(session.slug));
9275
- await rename3(artifact, archived);
9662
+ await rename4(artifact, archived);
9276
9663
  await unlink2(session.path);
9277
9664
  return result3(`Archived ${archived} and deleted the tuicr session ${session.slug}.`, {
9278
9665
  artifact: archived,
@@ -9332,7 +9719,7 @@ ${params.body}`, {
9332
9719
  async function syncLocalThreadArtifact(path, title, sessionSlug, threads) {
9333
9720
  let previous = [];
9334
9721
  try {
9335
- previous = parseThreadArtifact(await readFile11(path, "utf8"));
9722
+ previous = parseThreadArtifact(await readFile12(path, "utf8"));
9336
9723
  } catch (error) {
9337
9724
  if (error.code !== "ENOENT")
9338
9725
  throw error;
@@ -9365,6 +9752,45 @@ function result3(text, details = {}) {
9365
9752
  function unsupportedForgeMessage() {
9366
9753
  return "No supported GitHub or GitLab remote was detected. Use local=true for an offline tuicr review.";
9367
9754
  }
9755
+ async function collectReviewStatus(review) {
9756
+ const [porcelain, branch, localSession, remoteSession] = await Promise.all([
9757
+ runChecked("git", ["-C", review.cwd, "status", "--porcelain"]),
9758
+ runChecked("git", ["-C", review.cwd, "branch", "--show-current"]),
9759
+ resolveTuicrSession(review, true).catch(() => {
9760
+ return;
9761
+ }),
9762
+ resolveTuicrSession(review, false).catch(() => {
9763
+ return;
9764
+ })
9765
+ ]);
9766
+ const entries = porcelain.stdout.split("\\n").filter(Boolean);
9767
+ const staged = entries.filter((line) => !line.startsWith("??") && line[0] !== " ").length;
9768
+ const unstaged = entries.filter((line) => !line.startsWith("??") && line[1] !== " ").length;
9769
+ const untracked = entries.filter((line) => line.startsWith("??")).length;
9770
+ const remote = review.pr ? `yes (#${review.pr.number})` : "no";
9771
+ const url = review.pr?.url ?? "none";
9772
+ const text = [
9773
+ `Branch: ${branch.stdout.trim() || review.vcs.branch}`,
9774
+ `Worktree: ${entries.length ? "dirty" : "clean"} (staged=${staged}, unstaged=${unstaged}, untracked=${untracked})`,
9775
+ `Local working-tree tuicr review: ${localSession ? `yes (${localSession.slug}, ${localSession.commentCount} comments)` : "no"}`,
9776
+ `Remote PR/MR: ${remote}`,
9777
+ `Remote URL: ${url}`,
9778
+ `Remote tuicr session: ${remoteSession ? `yes (${remoteSession.slug}, ${remoteSession.commentCount} comments)` : "no"}`
9779
+ ].join("\\n");
9780
+ return {
9781
+ text,
9782
+ details: {
9783
+ branch: branch.stdout.trim(),
9784
+ staged,
9785
+ unstaged,
9786
+ untracked,
9787
+ localSession,
9788
+ remoteSession,
9789
+ pr: review.pr,
9790
+ url
9791
+ }
9792
+ };
9793
+ }
9368
9794
  function modelRoute(ctx) {
9369
9795
  if (!ctx.model)
9370
9796
  throw new Error("Cannot record review provenance because Pi did not provide an active model route.");
@@ -9588,11 +10014,11 @@ async function newReviewArtifactPath(cwd, target) {
9588
10014
  }
9589
10015
  async function localThreadArtifactPath(cwd, sessionSlug) {
9590
10016
  const dir = await reviewsDir(cwd);
9591
- return join16(dir, `${reviewSlug(sessionSlug) || "local-review"}.md`);
10017
+ return join17(dir, `${reviewSlug(sessionSlug) || "local-review"}.md`);
9592
10018
  }
9593
10019
  async function readThreadArtifact(path) {
9594
10020
  try {
9595
- return parseThreadArtifact(await readFile11(path, "utf8"));
10021
+ return parseThreadArtifact(await readFile12(path, "utf8"));
9596
10022
  } catch (error) {
9597
10023
  if (error.code === "ENOENT") {
9598
10024
  throw new Error(`Expected local reply overlay is missing: ${path}`);
@@ -9616,9 +10042,9 @@ async function findLatestThreadArtifact(cwd, review, target) {
9616
10042
  return [];
9617
10043
  return [
9618
10044
  (async () => {
9619
- const path = join16(dir, name);
10045
+ const path = join17(dir, name);
9620
10046
  const info = await stat(path);
9621
- return { path, name, content: await readFile11(path, "utf8"), modified: info.mtimeMs };
10047
+ return { path, name, content: await readFile12(path, "utf8"), modified: info.mtimeMs };
9622
10048
  })()
9623
10049
  ];
9624
10050
  }));
@@ -9632,7 +10058,7 @@ async function findLatestThreadArtifact(cwd, review, target) {
9632
10058
  }
9633
10059
  async function listReviewArtifactNames(dir) {
9634
10060
  try {
9635
- return await readdir2(dir);
10061
+ return await readdir3(dir);
9636
10062
  } catch (error) {
9637
10063
  throw new Error(`Cannot read review artifacts in ${dir}.`, { cause: error });
9638
10064
  }
@@ -9641,10 +10067,10 @@ function deriveTitle(branch) {
9641
10067
  return branch.replace(/^(feature|feat|fix|bug|chore)\//, "").replace(/^eng-\d+-/i, "").replace(/[-_]+/g, " ").replace(/^\w/, (char) => char.toUpperCase());
9642
10068
  }
9643
10069
  function uniqueRecordPath(dir, base) {
9644
- let path = join16(dir, `${base}.md`);
10070
+ let path = join17(dir, `${base}.md`);
9645
10071
  let count = 2;
9646
10072
  while (existsSync2(path))
9647
- path = join16(dir, `${base}-${count++}.md`);
10073
+ path = join17(dir, `${base}-${count++}.md`);
9648
10074
  return path;
9649
10075
  }
9650
10076
  function artifactNameMatches(name, suffix) {
@@ -9665,21 +10091,21 @@ import { z as z13 } from "zod";
9665
10091
 
9666
10092
  // src/setup.ts
9667
10093
  import { parseFrontmatter as parseFrontmatter2 } from "@earendil-works/pi-coding-agent";
9668
- import { readdir as readdir4, readFile as readFile14 } from "node:fs/promises";
10094
+ import { readdir as readdir5, readFile as readFile15 } from "node:fs/promises";
9669
10095
  import { homedir as homedir8 } from "node:os";
9670
- import { basename as basename6, join as join20 } from "node:path";
10096
+ import { basename as basename6, join as join21 } from "node:path";
9671
10097
 
9672
10098
  // src/config.ts
9673
10099
  import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
9674
10100
  import { z as z12 } from "zod";
9675
10101
  import { homedir as homedir5 } from "node:os";
9676
- import { join as join17 } from "node:path";
10102
+ import { join as join18 } from "node:path";
9677
10103
 
9678
10104
  // src/fsx.ts
9679
- import { readdir as readdir3, readFile as readFile12 } from "node:fs/promises";
10105
+ import { readdir as readdir4, readFile as readFile13 } from "node:fs/promises";
9680
10106
  async function readTextIfExists(path) {
9681
10107
  try {
9682
- return await readFile12(path, "utf8");
10108
+ return await readFile13(path, "utf8");
9683
10109
  } catch (error) {
9684
10110
  if (isMissingPath(error))
9685
10111
  return;
@@ -9699,10 +10125,10 @@ var diffpiConfigSchema = z12.object({
9699
10125
  agents: z12.record(z12.string(), agentConfigSchema).optional()
9700
10126
  }).strict();
9701
10127
  function diffpiConfigPaths(homeDir = homedir5()) {
9702
- const directory = join17(homeDir, ".difflab", "diffpi");
10128
+ const directory = join18(homeDir, ".difflab", "diffpi");
9703
10129
  return {
9704
- yaml: join17(directory, "config.yaml"),
9705
- json: join17(directory, "config.json")
10130
+ yaml: join18(directory, "config.yaml"),
10131
+ json: join18(directory, "config.json")
9706
10132
  };
9707
10133
  }
9708
10134
  async function loadDiffpiConfig(options = {}) {
@@ -9758,12 +10184,12 @@ function normalizeModelReference(value) {
9758
10184
  }
9759
10185
 
9760
10186
  // src/mcp.ts
9761
- import { mkdir as mkdir8, readFile as readFile13, writeFile as writeFile8 } from "node:fs/promises";
10187
+ import { mkdir as mkdir8, readFile as readFile14, writeFile as writeFile8 } from "node:fs/promises";
9762
10188
  import { homedir as homedir6 } from "node:os";
9763
- import { dirname as dirname8, join as join18 } from "node:path";
10189
+ import { dirname as dirname8, join as join19 } from "node:path";
9764
10190
  var mcp = {
9765
10191
  globalConfigPath(homeDir = homedir6()) {
9766
- return join18(homeDir, ".config", "mcp", "mcp.json");
10192
+ return join19(homeDir, ".config", "mcp", "mcp.json");
9767
10193
  },
9768
10194
  async serversEnsure(servers, options = {}) {
9769
10195
  const path = options.path ?? mcp.globalConfigPath();
@@ -9806,7 +10232,7 @@ function getParsedConfig(content, path) {
9806
10232
  }
9807
10233
  async function getOptionalFile2(path) {
9808
10234
  try {
9809
- return await readFile13(path, "utf8");
10235
+ return await readFile14(path, "utf8");
9810
10236
  } catch (error) {
9811
10237
  if (error instanceof Error && "code" in error && error.code === "ENOENT")
9812
10238
  return;
@@ -9820,7 +10246,7 @@ function isRecord(value) {
9820
10246
  // src/pi.ts
9821
10247
  import { mkdir as mkdir9, writeFile as writeFile9 } from "node:fs/promises";
9822
10248
  import { homedir as homedir7 } from "node:os";
9823
- import { dirname as dirname9, join as join19 } from "node:path";
10249
+ import { dirname as dirname9, join as join20 } from "node:path";
9824
10250
  var pi = {
9825
10251
  executableCheck: findPiExecutable,
9826
10252
  packageList: listPiPackages,
@@ -9833,7 +10259,7 @@ var pi = {
9833
10259
  configEnsure: ensurePiConfig
9834
10260
  };
9835
10261
  async function ensurePiAgent(filename, content, agentDir = resolvePiAgentDir(), dryRun = false) {
9836
- const path = join19(agentDir, "agents", filename);
10262
+ const path = join20(agentDir, "agents", filename);
9837
10263
  const currentText = await readTextIfExists(path);
9838
10264
  const changed = currentText !== content;
9839
10265
  if (changed && !dryRun) {
@@ -9842,10 +10268,10 @@ async function ensurePiAgent(filename, content, agentDir = resolvePiAgentDir(),
9842
10268
  }
9843
10269
  return { path, changed, existed: currentText !== undefined, planned: changed && dryRun };
9844
10270
  }
9845
- async function checkGlobalPiSkill(name, agentDir = resolvePiAgentDir(), sharedSkillsDir = join19(homedir7(), ".agents", "skills")) {
9846
- const roots = [join19(agentDir, "skills"), sharedSkillsDir];
10271
+ async function checkGlobalPiSkill(name, agentDir = resolvePiAgentDir(), sharedSkillsDir = join20(homedir7(), ".agents", "skills")) {
10272
+ const roots = [join20(agentDir, "skills"), sharedSkillsDir];
9847
10273
  for (const root of roots) {
9848
- if (await readTextIfExists(join19(root, name, "SKILL.md")) !== undefined)
10274
+ if (await readTextIfExists(join20(root, name, "SKILL.md")) !== undefined)
9849
10275
  return true;
9850
10276
  }
9851
10277
  return false;
@@ -9895,7 +10321,7 @@ async function installPiPackage(executable, source) {
9895
10321
  await runChecked(executable, ["install", source]);
9896
10322
  }
9897
10323
  function resolvePiAgentDir(homeDir = homedir7()) {
9898
- return process.env.PI_CODING_AGENT_DIR ?? (process.env.XDG_CONFIG_HOME ? join19(process.env.XDG_CONFIG_HOME, "pi") : join19(homeDir, ".pi", "agent"));
10324
+ return process.env.PI_CODING_AGENT_DIR ?? (process.env.XDG_CONFIG_HOME ? join20(process.env.XDG_CONFIG_HOME, "pi") : join20(homeDir, ".pi", "agent"));
9899
10325
  }
9900
10326
  function parseJsonObject(content, path) {
9901
10327
  if (!content?.trim())
@@ -9947,7 +10373,7 @@ var FORGE_DEPENDENCIES = {
9947
10373
  };
9948
10374
  async function ensureMise(options = {}) {
9949
10375
  const homeDir = options.homeDir ?? homedir8();
9950
- const current = await mise.executableCheck() ?? await mise.executableCheck(join20(homeDir, ".local", "bin", "mise"));
10376
+ const current = await mise.executableCheck() ?? await mise.executableCheck(join21(homeDir, ".local", "bin", "mise"));
9951
10377
  if (current)
9952
10378
  return { executable: current, action: createSetupAction("mise", "ready", current) };
9953
10379
  reportProgress(options, "Installing mise");
@@ -9996,9 +10422,9 @@ async function ensureMiseDeps(miseExecutable, options = {}) {
9996
10422
  async function ensurePiPlugins(options = {}) {
9997
10423
  const actions = await ensurePiPackages(PI_PACKAGES, options);
9998
10424
  const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
9999
- const webSearch = await pi.configEnsure(join20(agentDir, "web-search.json"), (config) => ({ ...config, workflow: "auto-summary" }), options.dryRun);
10425
+ const webSearch = await pi.configEnsure(join21(agentDir, "web-search.json"), (config) => ({ ...config, workflow: "auto-summary" }), options.dryRun);
10000
10426
  actions.push(getConfigSetupAction("web search settings", webSearch));
10001
- const lsp = await pi.configEnsure(join20(agentDir, "pi-lsp.json"), (config) => ({
10427
+ const lsp = await pi.configEnsure(join21(agentDir, "pi-lsp.json"), (config) => ({
10002
10428
  ...config,
10003
10429
  progressive: { ...getRecord(config.progressive), enabled: true, inject: "none" }
10004
10430
  }), options.dryRun);
@@ -10009,11 +10435,11 @@ async function ensurePiAgents(options = {}) {
10009
10435
  const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
10010
10436
  const bundledAgentsDir = options.bundledAgentsDir ?? BUNDLED_AGENTS_DIR;
10011
10437
  const userConfig = await loadDiffpiConfig({ homeDir: options.homeDir });
10012
- const entries = (await readdir4(bundledAgentsDir, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.startsWith("diffpi-") && entry.name.endsWith(".md")).sort((left, right) => left.name.localeCompare(right.name));
10438
+ const entries = (await readdir5(bundledAgentsDir, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.startsWith("diffpi-") && entry.name.endsWith(".md")).sort((left, right) => left.name.localeCompare(right.name));
10013
10439
  const actions = [];
10014
10440
  for (const entry of entries) {
10015
10441
  const id = basename6(entry.name, ".md").replace(/^diffpi-/, "");
10016
- const source = await readFile14(join20(bundledAgentsDir, entry.name), "utf8");
10442
+ const source = await readFile15(join21(bundledAgentsDir, entry.name), "utf8");
10017
10443
  const content = materializeAgentModels(source, id, userConfig.config, options.availableModels);
10018
10444
  const result = await pi.agentEnsure(entry.name, content, agentDir, options.dryRun);
10019
10445
  actions.push(getConfigSetupAction(`pi agent ${id}`, result));
@@ -10022,7 +10448,7 @@ async function ensurePiAgents(options = {}) {
10022
10448
  }
10023
10449
  async function ensurePiSkills(miseExecutable, options = {}) {
10024
10450
  const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
10025
- const sharedSkillsDir = join20(options.homeDir ?? homedir8(), ".agents", "skills");
10451
+ const sharedSkillsDir = join21(options.homeDir ?? homedir8(), ".agents", "skills");
10026
10452
  const actions = [];
10027
10453
  for (const source of PI_SKILL_SOURCES) {
10028
10454
  const missing = [];
@@ -10218,7 +10644,7 @@ function reportProgress(options, message) {
10218
10644
  options.onProgress?.(message);
10219
10645
  }
10220
10646
  async function packageVersion2() {
10221
- const source = await readFile14(join20(resolveBundledAgentsDir(), "..", "package.json"), "utf8");
10647
+ const source = await readFile15(join21(resolveBundledAgentsDir(), "..", "package.json"), "utf8");
10222
10648
  let value;
10223
10649
  try {
10224
10650
  value = JSON.parse(source);