@difflab/pi 0.6.0 → 0.7.0-rc.202609271854.6af8600

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 (95) hide show
  1. package/README.md +11 -9
  2. package/agents/diffpi-orchestrator.md +6 -4
  3. package/agents/diffpi-plan-reviewer.md +27 -0
  4. package/agents/diffpi-planner.md +5 -5
  5. package/agents/diffpi-reviewer.md +4 -4
  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 +446 -160
  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 -2
  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 +1841 -3858
  17. package/dist/extensions/tuicrx.d.ts +8 -1
  18. package/dist/extensions/tuicrx.d.ts.map +1 -1
  19. package/dist/index.d.ts +8 -7
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +951 -349
  22. package/dist/plan/markdown.d.ts +4 -1
  23. package/dist/plan/markdown.d.ts.map +1 -1
  24. package/dist/{tools/plan-schema.d.ts → plan/schema.d.ts} +113 -75
  25. package/dist/plan/schema.d.ts.map +1 -0
  26. package/dist/plan/store.d.ts +39 -1
  27. package/dist/plan/store.d.ts.map +1 -1
  28. package/dist/plan/types.d.ts +27 -3
  29. package/dist/plan/types.d.ts.map +1 -1
  30. package/dist/review/github-review-backend.d.ts.map +1 -1
  31. package/dist/review/gitlab-review-backend.d.ts.map +1 -1
  32. package/dist/review/index.d.ts +6 -6
  33. package/dist/review/index.d.ts.map +1 -1
  34. package/dist/review/local-review-backend.d.ts +3 -0
  35. package/dist/review/local-review-backend.d.ts.map +1 -0
  36. package/dist/review/review-backend.d.ts +3 -2
  37. package/dist/review/review-backend.d.ts.map +1 -1
  38. package/dist/review/review-markdown.d.ts +4 -1
  39. package/dist/review/review-markdown.d.ts.map +1 -1
  40. package/dist/review/review-state.d.ts +20 -0
  41. package/dist/review/review-state.d.ts.map +1 -0
  42. package/dist/review/types.d.ts +28 -0
  43. package/dist/review/types.d.ts.map +1 -1
  44. package/dist/store.d.ts +2 -0
  45. package/dist/store.d.ts.map +1 -1
  46. package/dist/tools/index.js +1668 -607
  47. package/dist/tools/plan.d.ts.map +1 -1
  48. package/dist/tools/review.d.ts +38 -0
  49. package/dist/tools/review.d.ts.map +1 -1
  50. package/package.json +2 -4
  51. package/skills/plan/SKILL.md +8 -14
  52. package/skills/plan/references/design-quality.md +30 -0
  53. package/skills/plan/references/workflows/annotate.md +11 -0
  54. package/skills/plan/references/workflows/finalize.md +7 -0
  55. package/skills/plan/references/workflows/go.md +8 -0
  56. package/skills/plan/references/workflows/help.md +10 -0
  57. package/skills/plan/references/workflows/init.md +5 -0
  58. package/skills/plan/references/workflows/new.md +7 -0
  59. package/skills/plan/references/workflows/update.md +7 -0
  60. package/skills/review/SKILL.md +7 -7
  61. package/skills/review/references/review-standards.md +8 -1
  62. package/skills/review/references/workflows/address.md +11 -0
  63. package/skills/review/references/workflows/auto.md +18 -0
  64. package/skills/review/references/workflows/complete.md +7 -0
  65. package/skills/review/references/workflows/edit.md +7 -0
  66. package/skills/review/references/workflows/help.md +36 -0
  67. package/skills/review/references/workflows/merge.md +6 -0
  68. package/skills/review/references/workflows/new.md +19 -0
  69. package/skills/review/references/workflows/open.md +3 -0
  70. package/skills/review/references/workflows/publish.md +8 -0
  71. package/skills/review/references/workflows/status.md +3 -0
  72. package/templates/plan/PLAN.md +17 -11
  73. package/templates/plan/implementation.md +34 -12
  74. package/dist/commands.d.ts +0 -22
  75. package/dist/commands.d.ts.map +0 -1
  76. package/dist/extensions/cmdtsx.d.ts +0 -2
  77. package/dist/extensions/cmdtsx.d.ts.map +0 -1
  78. package/dist/tools/plan-schema.d.ts.map +0 -1
  79. package/dist/workflow.d.ts +0 -80
  80. package/dist/workflow.d.ts.map +0 -1
  81. package/workflows/plan/annotate.md +0 -13
  82. package/workflows/plan/finalize.md +0 -15
  83. package/workflows/plan/go.md +0 -20
  84. package/workflows/plan/help.md +0 -20
  85. package/workflows/plan/init.md +0 -13
  86. package/workflows/plan/new.md +0 -20
  87. package/workflows/plan/update.md +0 -18
  88. package/workflows/review/address.md +0 -8
  89. package/workflows/review/auto.md +0 -8
  90. package/workflows/review/complete.md +0 -7
  91. package/workflows/review/edit.md +0 -7
  92. package/workflows/review/help.md +0 -14
  93. package/workflows/review/merge.md +0 -6
  94. package/workflows/review/new.md +0 -7
  95. package/workflows/review/publish.md +0 -6
package/dist/cli.js CHANGED
@@ -2383,8 +2383,9 @@ function appendBounded(current, next) {
2383
2383
  }
2384
2384
 
2385
2385
  // src/plan/store.ts
2386
- import { mkdir as mkdir3, readdir, readFile as readFile3, realpath as realpath3, rm as rm2 } from "node:fs/promises";
2387
- import { basename as basename3, dirname as dirname4, join as join7, resolve as resolve3, sep } from "node:path";
2386
+ import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
2387
+ import { mkdir as mkdir3, readdir, readFile as readFile2, realpath as realpath3, rename as rename2, rm as rm2 } from "node:fs/promises";
2388
+ import { basename as basename3, dirname as dirname3, join as join5, resolve as resolve3, sep } from "node:path";
2388
2389
 
2389
2390
  // src/extensions/fsx.ts
2390
2391
  import { mkdir, rename, rm, writeFile } from "node:fs/promises";
@@ -2556,57 +2557,6 @@ function projectSlug(repository) {
2556
2557
  return `${readable}-${digest}`;
2557
2558
  }
2558
2559
 
2559
- // src/templates.ts
2560
- import { readFile as readFile2 } from "node:fs/promises";
2561
- import { homedir as homedir2 } from "node:os";
2562
- import { join as join5, normalize } from "node:path";
2563
-
2564
- // src/assets.ts
2565
- import { existsSync } from "node:fs";
2566
- import { dirname as dirname3, join as join4 } from "node:path";
2567
- import { fileURLToPath } from "node:url";
2568
- function resolveBundledAssetDir(name, moduleUrl = import.meta.url) {
2569
- const moduleDir = dirname3(fileURLToPath(moduleUrl));
2570
- const candidates = [join4(moduleDir, name), join4(moduleDir, "..", name), join4(moduleDir, "..", "..", name)];
2571
- return candidates.find((path) => existsSync(path)) ?? candidates[1];
2572
- }
2573
- function resolveBundledTemplatesDir(moduleUrl = import.meta.url) {
2574
- return resolveBundledAssetDir("templates", moduleUrl);
2575
- }
2576
-
2577
- // src/templates.ts
2578
- async function loadTemplate(name, options = {}) {
2579
- const relative = templateRelativePath(name);
2580
- const userPath = join5(options.homeDir ?? homedir2(), ".difflab", "diffpi", "templates", relative);
2581
- const bundledPath = join5(options.bundledDir ?? resolveBundledTemplatesDir(), relative);
2582
- const user = await readOptionalFile(userPath);
2583
- if (user !== undefined)
2584
- return { name, path: userPath, source: "user", content: user };
2585
- const bundled = await readOptionalFile(bundledPath);
2586
- if (bundled !== undefined)
2587
- return { name, path: bundledPath, source: "bundled", content: bundled };
2588
- throw new Error(`Template "${name}" was not found at ${userPath} or ${bundledPath}.`);
2589
- }
2590
- function renderTemplate(content, variables) {
2591
- return content.replace(/\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g, (match, key) => variables[key] ?? match);
2592
- }
2593
- function templateRelativePath(name) {
2594
- const normalized = normalize(name.replaceAll("\\", "/")).replace(/^\.\//, "");
2595
- if (!normalized || normalized.startsWith("../") || normalized.includes("/../") || normalized.startsWith("/")) {
2596
- throw new Error(`Invalid template name: ${name}`);
2597
- }
2598
- return normalized.endsWith(".md") ? normalized : `${normalized}.md`;
2599
- }
2600
- async function readOptionalFile(path) {
2601
- try {
2602
- return await readFile2(path, "utf8");
2603
- } catch (error) {
2604
- if (error.code === "ENOENT")
2605
- return;
2606
- throw error;
2607
- }
2608
- }
2609
-
2610
2560
  // src/plan/markdown.ts
2611
2561
  var import_mdast_builder = __toESM(require_src(), 1);
2612
2562
 
@@ -3953,7 +3903,7 @@ var handle = {
3953
3903
  };
3954
3904
 
3955
3905
  // ../../node_modules/mdast-util-to-markdown/lib/join.js
3956
- var join6 = [joinDefaults];
3906
+ var join4 = [joinDefaults];
3957
3907
  function joinDefaults(left, right, parent, state) {
3958
3908
  if (right.type === "code" && formatCodeAsIndented(right, state) && (left.type === "list" || left.type === right.type && formatCodeAsIndented(left, state))) {
3959
3909
  return false;
@@ -6499,7 +6449,7 @@ function toMarkdown(tree, options) {
6499
6449
  handle: undefined,
6500
6450
  indentLines,
6501
6451
  indexStack: [],
6502
- join: [...join6],
6452
+ join: [...join4],
6503
6453
  options: {},
6504
6454
  safe: safeBound,
6505
6455
  stack: [],
@@ -6803,7 +6753,7 @@ import { default as default2 } from "node:path";
6803
6753
  import { default as default3 } from "node:process";
6804
6754
 
6805
6755
  // ../../node_modules/vfile/lib/minurl.js
6806
- import { fileURLToPath as fileURLToPath2 } from "node:url";
6756
+ import { fileURLToPath } from "node:url";
6807
6757
 
6808
6758
  // ../../node_modules/vfile/lib/minurl.shared.js
6809
6759
  function isUrl(fileUrlOrPath) {
@@ -6889,7 +6839,7 @@ class VFile {
6889
6839
  }
6890
6840
  set path(path) {
6891
6841
  if (isUrl(path)) {
6892
- path = fileURLToPath2(path);
6842
+ path = fileURLToPath(path);
6893
6843
  }
6894
6844
  assertNonEmpty(path, "path");
6895
6845
  if (this.path !== path) {
@@ -7276,11 +7226,13 @@ function validatePlanDocument(plan, options = {}) {
7276
7226
  if (!taskIds.has(dependency))
7277
7227
  add("dangling-dependency", "error", `Task ${task.id} depends on missing ${dependency}.`);
7278
7228
  }
7279
- if (options.strict && task.acceptanceCriteria.length === 0)
7280
- add("acceptance", "error", `Task ${task.id} has no acceptance criteria.`, task.id);
7229
+ if (options.strict && isPlaceholder(task.title))
7230
+ add("placeholder", "error", `Task ${task.id} has placeholder content.`, task.id);
7281
7231
  }
7282
7232
  if (options.strict && phase.tasks.length === 0)
7283
7233
  add("empty-phase", "error", `Phase ${phase.id} has no tasks.`, phase.id);
7234
+ if (options.strict && (isPlaceholder(phase.title) || isPlaceholder(phase.objective)))
7235
+ add("placeholder", "error", `Phase ${phase.id} has placeholder content.`, phase.id);
7284
7236
  }
7285
7237
  for (const cycle of dependencyCycles(plan))
7286
7238
  add("dependency-cycle", "error", `Dependency cycle: ${cycle.join(" -> ")}.`);
@@ -7289,10 +7241,131 @@ function validatePlanDocument(plan, options = {}) {
7289
7241
  add("design-length", "warning", `Design is ${designWords} words; prefer 300 or fewer.`);
7290
7242
  if (options.strict && designWords > 800)
7291
7243
  add("design-too-long", "error", `Design is ${designWords} words; finalization allows at most 800.`);
7292
- if (options.strict && plan.phases.length === 0)
7293
- add("no-phases", "error", "Finalization requires at least one phase.");
7244
+ if (options.strict) {
7245
+ if (plan.phases.length === 0)
7246
+ add("no-phases", "error", "Finalization requires at least one phase.");
7247
+ if (isPlaceholder(plan.intent))
7248
+ add("placeholder", "error", "Intent must contain concrete content.", "intent");
7249
+ if (!plan.requirements.length || plan.requirements.some(isPlaceholder))
7250
+ add("requirements", "error", "Requirements must contain concrete content.", "requirements");
7251
+ for (const [key, value] of Object.entries(plan.design)) {
7252
+ if (isPlaceholder(value))
7253
+ add("design", "error", `Design/${key} must contain concrete content.`, `design.${key}`);
7254
+ }
7255
+ }
7294
7256
  return issues;
7295
7257
  }
7258
+ function validatePlanBriefs(plan, briefs) {
7259
+ const issues = [];
7260
+ const phaseIds = plan.phases.map((phase) => phase.id);
7261
+ const briefIds = briefs.map((brief) => brief.phaseId);
7262
+ if (!sameIds(phaseIds, briefIds))
7263
+ issues.push({
7264
+ code: "brief-phase-ids",
7265
+ severity: "error",
7266
+ message: `Brief phase IDs must exactly match plan phases in order: ${phaseIds.join(", ")}.`
7267
+ });
7268
+ for (const [index, phase] of plan.phases.entries()) {
7269
+ const brief = briefs[index];
7270
+ if (!brief || brief.phaseId !== phase.id)
7271
+ continue;
7272
+ if (isPlaceholder(brief.summary))
7273
+ issues.push({ code: "brief-placeholder", severity: "error", message: `Brief ${phase.id} needs a summary.` });
7274
+ const taskIds = phase.tasks.map((task) => task.id);
7275
+ const briefTaskIds = brief.tasks.map((task) => task.taskId);
7276
+ if (!sameIds(taskIds, briefTaskIds))
7277
+ issues.push({
7278
+ code: "brief-task-ids",
7279
+ severity: "error",
7280
+ message: `Brief ${phase.id} task IDs must exactly match phase tasks in order: ${taskIds.join(", ")}.`
7281
+ });
7282
+ for (const task of brief.tasks) {
7283
+ for (const [field, values] of [
7284
+ ["ordered steps", task.steps],
7285
+ ["file scopes", task.fileScopes],
7286
+ ["acceptance criteria", task.acceptanceCriteria]
7287
+ ]) {
7288
+ if (!values.length || values.some(isPlaceholder))
7289
+ issues.push({
7290
+ code: "brief-incomplete",
7291
+ severity: "error",
7292
+ message: `Brief task ${task.taskId} needs concrete ${field}.`,
7293
+ path: task.taskId
7294
+ });
7295
+ }
7296
+ }
7297
+ for (const [field, values] of [
7298
+ ["API changes", brief.apiChanges],
7299
+ ["libraries", brief.libraries],
7300
+ ["constraints", brief.constraints]
7301
+ ]) {
7302
+ if (values.some(isPlaceholder))
7303
+ issues.push({
7304
+ code: "brief-placeholder",
7305
+ severity: "error",
7306
+ message: `Brief ${phase.id} has placeholder ${field}.`,
7307
+ path: phase.id
7308
+ });
7309
+ }
7310
+ }
7311
+ return issues;
7312
+ }
7313
+ function renderImplementationBrief(planRevision, ordinal, phase, brief) {
7314
+ const marker = { schemaVersion: 1, planRevision, ordinal, phaseId: phase.id };
7315
+ const tasks = phase.tasks.map((task, index) => {
7316
+ const details = brief.tasks[index];
7317
+ if (!details || details.taskId !== task.id)
7318
+ throw new Error(`Missing implementation details for ${task.id}.`);
7319
+ return `<!-- diffpi-brief-task: ${json({ id: task.id })} -->
7320
+ ### Task: ${task.title}
7321
+
7322
+ #### Ordered Steps
7323
+
7324
+ ${bulletList(details.steps)}
7325
+
7326
+ #### File Scopes
7327
+
7328
+ ${bulletList(details.fileScopes)}
7329
+
7330
+ #### Acceptance Criteria
7331
+
7332
+ ${bulletList(details.acceptanceCriteria)}
7333
+
7334
+ <!-- /diffpi-brief-task -->`;
7335
+ }).join(`
7336
+
7337
+ `);
7338
+ return `<!-- diffpi-implementation: ${json(marker)} -->
7339
+ # Phase ${ordinal}: ${phase.title}
7340
+
7341
+ - **Phase ID:** ${phase.id}
7342
+ - **Plan Revision:** ${planRevision}
7343
+
7344
+ ## Summary
7345
+
7346
+ ${brief.summary}
7347
+
7348
+ ## Objective
7349
+
7350
+ ${phase.objective}
7351
+
7352
+ ## Tasks
7353
+
7354
+ ${tasks}
7355
+
7356
+ ## API Changes
7357
+
7358
+ ${bulletList(brief.apiChanges, "None.")}
7359
+
7360
+ ## Libraries and Algorithms
7361
+
7362
+ ${bulletList(brief.libraries, "None.")}
7363
+
7364
+ ## Implementation Constraints
7365
+
7366
+ ${bulletList(brief.constraints, "None.")}
7367
+ `;
7368
+ }
7296
7369
  function renderPlanDocument(plan, previousSource) {
7297
7370
  if (previousSource && canPatch(previousSource, plan))
7298
7371
  return patchMarkers(previousSource, plan);
@@ -7301,6 +7374,8 @@ function renderPlanDocument(plan, previousSource) {
7301
7374
  id: plan.id,
7302
7375
  revision: plan.revision,
7303
7376
  branch: plan.branch,
7377
+ issueId: plan.issueId,
7378
+ issueUrl: plan.issueUrl,
7304
7379
  status: plan.status,
7305
7380
  execution: plan.execution,
7306
7381
  createdAt: plan.createdAt,
@@ -7309,6 +7384,8 @@ function renderPlanDocument(plan, previousSource) {
7309
7384
  const metadata = import_mdast_builder.list("unordered", [
7310
7385
  ["Plan ID", plan.id],
7311
7386
  ["Branch", plan.branch],
7387
+ ...plan.issueId ? [["Issue", plan.issueId]] : [],
7388
+ ...plan.issueUrl ? [["Issue URL", plan.issueUrl]] : [],
7312
7389
  ["Status", plan.status],
7313
7390
  ["Revision", String(plan.revision)]
7314
7391
  ].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}`)]))));
@@ -7330,7 +7407,7 @@ function renderPlanDocument(plan, previousSource) {
7330
7407
  import_mdast_builder.heading(3, import_mdast_builder.text("Consequences")),
7331
7408
  import_mdast_builder.html(plan.design.consequences || "<!-- Describe trade-offs and limitations. -->"),
7332
7409
  import_mdast_builder.heading(2, import_mdast_builder.text("Implementation")),
7333
- ...plan.phases.length ? plan.phases.flatMap((phase) => [import_mdast_builder.html(renderPhase(phase))]) : [import_mdast_builder.html("<!-- Add phases with plan_add_phase. -->")],
7410
+ ...plan.phases.length ? plan.phases.flatMap((phase) => [import_mdast_builder.html(renderPhase(phase))]) : [import_mdast_builder.html("<!-- Add phases with plan_apply_revision. -->")],
7334
7411
  import_mdast_builder.heading(2, import_mdast_builder.text("References")),
7335
7412
  references
7336
7413
  ]);
@@ -7404,10 +7481,7 @@ function renderTask(task) {
7404
7481
  const checked = task.status === "completed" || task.status === "skipped" ? "x" : " ";
7405
7482
  return `<!-- diffpi-task: ${json(marker)} -->
7406
7483
  - [${checked}] **${task.title}**
7407
- ${taskList("Steps", task.steps ?? [])}
7408
7484
  - Dependencies: ${list3(task.dependencies)}
7409
- ${taskList("File scopes", task.fileScopes)}
7410
- ${taskList("Acceptance criteria", task.acceptanceCriteria)}
7411
7485
  <!-- /diffpi-task -->`;
7412
7486
  }
7413
7487
  function parsePhases(input, ref) {
@@ -7457,10 +7531,7 @@ function parseTasks(input, ref) {
7457
7531
  return {
7458
7532
  ...marker,
7459
7533
  title,
7460
- steps: parseListValue(body, "Steps"),
7461
- dependencies: parseListValue(body, "Dependencies"),
7462
- fileScopes: parseListValue(body, "File scopes"),
7463
- acceptanceCriteria: parseListValue(body, "Acceptance criteria")
7534
+ dependencies: parseListValue(body, "Dependencies")
7464
7535
  };
7465
7536
  });
7466
7537
  assertUniqueIds(tasks.map((task) => task.id), "task id");
@@ -7487,6 +7558,8 @@ function patchMarkers(source, plan) {
7487
7558
  id: plan.id,
7488
7559
  revision: plan.revision,
7489
7560
  branch: plan.branch,
7561
+ issueId: plan.issueId,
7562
+ issueUrl: plan.issueUrl,
7490
7563
  status: plan.status,
7491
7564
  execution: plan.execution,
7492
7565
  createdAt: plan.createdAt,
@@ -7536,19 +7609,12 @@ function contentShape(plan) {
7536
7609
  requirements: plan.requirements,
7537
7610
  design: plan.design,
7538
7611
  references: plan.references,
7539
- phases: plan.phases.map(({ id, title, objective, dependencies, tasks }) => ({
7540
- id,
7541
- title,
7542
- objective,
7543
- dependencies,
7544
- tasks: tasks.map(({ id: taskId, title: taskTitle, steps, dependencies: taskDependencies, fileScopes, acceptanceCriteria }) => ({
7545
- id: taskId,
7546
- title: taskTitle,
7547
- steps,
7548
- dependencies: taskDependencies,
7549
- fileScopes,
7550
- acceptanceCriteria
7551
- }))
7612
+ phases: plan.phases.map((phase) => ({
7613
+ id: phase.id,
7614
+ title: phase.title,
7615
+ objective: phase.objective,
7616
+ dependencies: phase.dependencies,
7617
+ tasks: phase.tasks.map(({ id, title, dependencies }) => ({ id, title, dependencies }))
7552
7618
  }))
7553
7619
  };
7554
7620
  }
@@ -7648,6 +7714,13 @@ function assertUniqueIds(ids, label) {
7648
7714
  function cleanPlaceholder(value) {
7649
7715
  return value.replace(/<!--[^]*?-->/g, "").trim();
7650
7716
  }
7717
+ function isPlaceholder(value) {
7718
+ const normalized = cleanPlaceholder(value).trim();
7719
+ return normalized.length === 0 || /^(?:todo|tbd|none|n\/a)$/i.test(normalized) || /define during implementation|describe the|add requirements|add implementation/i.test(normalized);
7720
+ }
7721
+ function sameIds(expected, actual) {
7722
+ return expected.length === actual.length && expected.every((value, index) => actual[index] === value);
7723
+ }
7651
7724
  function parseMarker(value, label) {
7652
7725
  try {
7653
7726
  return JSON.parse(value);
@@ -7655,15 +7728,14 @@ function parseMarker(value, label) {
7655
7728
  throw new Error(`Malformed ${label} marker JSON.`);
7656
7729
  }
7657
7730
  }
7658
- function taskList(label, values) {
7731
+ function bulletList(values, empty = "") {
7659
7732
  if (!values.length)
7660
- return ` - ${label}: none`;
7661
- const items = values.flatMap((value) => {
7733
+ return empty;
7734
+ return values.flatMap((value) => {
7662
7735
  const [first = "", ...continuations] = value.split(`
7663
7736
  `);
7664
- return [` - ${first}`, ...continuations.map((line) => ` ${line}`)];
7665
- });
7666
- return [` - ${label}:`, ...items].join(`
7737
+ return [`- ${first}`, ...continuations.map((line) => ` ${line}`)];
7738
+ }).join(`
7667
7739
  `);
7668
7740
  }
7669
7741
  function list3(values) {
@@ -7691,7 +7763,7 @@ async function resolvePlan(cwd, query, filters = {}, options = {}) {
7691
7763
  continue;
7692
7764
  if (normalizedQuery && entry.name !== normalizedQuery && stripDate(entry.name) !== normalizedQuery)
7693
7765
  continue;
7694
- const dir = join7(root, entry.name);
7766
+ const dir = join5(root, entry.name);
7695
7767
  await assertContained(root, dir);
7696
7768
  try {
7697
7769
  const record = await readRecord(dir);
@@ -7710,6 +7782,15 @@ async function resolvePlan(cwd, query, filters = {}, options = {}) {
7710
7782
  }
7711
7783
  function createPlanStore(options = {}) {
7712
7784
  const now = options.now ?? (() => new Date);
7785
+ const read = async (cwd, query, filters) => {
7786
+ const result = await resolvePlan(cwd, query, filters, options);
7787
+ if (!result.record) {
7788
+ if (result.ambiguous)
7789
+ throw new Error(`Plan query "${query}" is ambiguous: ${result.candidates.map((item) => item.id).join(", ")}.`);
7790
+ throw new Error(`Plan "${query}" was not found.`);
7791
+ }
7792
+ return result.record;
7793
+ };
7713
7794
  return {
7714
7795
  context(cwd, query, filters) {
7715
7796
  return resolvePlan(cwd, query, filters, options);
@@ -7717,120 +7798,322 @@ function createPlanStore(options = {}) {
7717
7798
  async init(input) {
7718
7799
  const root = await plansDir(input.cwd, options.homeDir);
7719
7800
  const id = planRecordName(input.shortSlug, now());
7720
- const dir = join7(root, id);
7721
- await assertContained(root, dirname4(dir));
7722
- try {
7723
- await mkdir3(dir);
7724
- } catch (error) {
7725
- if (error.code === "EEXIST")
7726
- throw new Error(`Plan ${id} already exists.`);
7727
- throw error;
7728
- }
7801
+ const dir = join5(root, id);
7802
+ await createPlanDirectory(root, dir, id);
7729
7803
  try {
7730
7804
  const timestamp = now().toISOString();
7731
- const template = await loadTemplate("plan/PLAN", {
7732
- homeDir: options.homeDir,
7733
- bundledDir: options.bundledTemplatesDir
7734
- });
7735
- const source = renderTemplate(template.content, {
7805
+ const title = input.title?.trim() || titleFromSlug(input.shortSlug);
7806
+ const document = {
7807
+ schemaVersion: 1,
7736
7808
  id,
7809
+ revision: 0,
7810
+ title,
7737
7811
  branch: input.branch,
7738
- title: input.title?.trim() || titleFromSlug(input.shortSlug),
7812
+ ...input.issueId?.trim() ? { issueId: input.issueId.trim() } : {},
7813
+ ...input.issueUrl?.trim() ? { issueUrl: input.issueUrl.trim() } : {},
7739
7814
  intent: input.intent?.trim() || "<!-- Describe the intended outcome. -->",
7740
- issue_id: input.issueId?.trim() || "<!-- Add issue tracker ID. -->",
7741
- issue_url: input.issueUrl?.trim() || "<!-- Add issue tracker URL. -->",
7742
- created_at: timestamp,
7743
- updated_at: timestamp
7744
- });
7745
- const document = parsePlanDocument(source, join7(dir, "PLAN.md"));
7746
- await atomicWrite(join7(dir, "PLAN.md"), source);
7747
- return {
7748
- id,
7749
- dir,
7750
- planPath: join7(dir, "PLAN.md"),
7751
- logPath: join7(dir, "logs.txt"),
7752
- implementationDir: join7(dir, "implementation"),
7753
- document,
7754
- source
7815
+ requirements: [],
7816
+ design: { bigIdeas: "", keyApiUpdates: "", consequences: "" },
7817
+ phases: [],
7818
+ references: [],
7819
+ status: "draft",
7820
+ createdAt: timestamp,
7821
+ updatedAt: timestamp
7755
7822
  };
7823
+ const source = renderPlanDocument(document);
7824
+ parsePlanDocument(source, join5(dir, "PLAN.md"));
7825
+ const authoringRequest = input.request ?? { kind: "user", text: input.intent?.trim() || `Initialize phase-less plan ${title}.` };
7826
+ await writeRevisionSnapshot(dir, document, source, authoringRequest, []);
7827
+ await writeLatestView(dir, document, source, []);
7828
+ return await readRecord(dir);
7756
7829
  } catch (error) {
7757
7830
  await rm2(dir, { recursive: true, force: true });
7758
7831
  throw error;
7759
7832
  }
7760
7833
  },
7761
- async read(cwd, query, filters) {
7762
- const result = await resolvePlan(cwd, query, filters, options);
7763
- if (!result.record) {
7764
- if (result.ambiguous)
7765
- throw new Error(`Plan query "${query}" is ambiguous: ${result.candidates.map((item) => item.id).join(", ")}.`);
7766
- throw new Error(`Plan "${query}" was not found.`);
7767
- }
7768
- return result.record;
7834
+ read,
7835
+ async applyRevision(cwd, request) {
7836
+ if (request.mode === "create") {
7837
+ const root = await plansDir(cwd, options.homeDir);
7838
+ const id = planRecordName(request.shortSlug, now());
7839
+ const dir = join5(root, id);
7840
+ await createPlanDirectory(root, dir, id);
7841
+ try {
7842
+ const timestamp = now().toISOString();
7843
+ const document = normalizeRevision(undefined, request, id, 0, timestamp);
7844
+ assertCompleteRevision(document, request.briefs);
7845
+ const source = renderPlanDocument(document);
7846
+ parsePlanDocument(source, join5(dir, "PLAN.md"));
7847
+ await writeRevisionSnapshot(dir, document, source, request.request, request.briefs);
7848
+ await writeLatestView(dir, document, source, request.briefs);
7849
+ return await readRecord(dir);
7850
+ } catch (error) {
7851
+ await rm2(dir, { recursive: true, force: true });
7852
+ throw error;
7853
+ }
7854
+ }
7855
+ const initial = await read(cwd, request.plan);
7856
+ return withDirectoryLock(join5(initial.dir, ".lock"), async () => {
7857
+ const current = await readRecord(initial.dir);
7858
+ if (current.document.revision !== request.expectedPlanRevision)
7859
+ throw new Error(`Expected plan revision ${request.expectedPlanRevision}, found ${current.document.revision}.`);
7860
+ const revision = current.document.revision + 1;
7861
+ const document = normalizeRevision(current.document, request, current.id, revision, now().toISOString());
7862
+ assertCompleteRevision(document, request.briefs);
7863
+ const source = renderPlanDocument(document);
7864
+ parsePlanDocument(source, current.planPath);
7865
+ await writeRevisionSnapshot(current.dir, document, source, request.request, request.briefs);
7866
+ try {
7867
+ await writeLatestView(current.dir, document, source, request.briefs);
7868
+ } catch (error) {
7869
+ await rm2(join5(current.dir, "revisions", String(revision)), { recursive: true, force: true });
7870
+ throw error;
7871
+ }
7872
+ return await readRecord(current.dir);
7873
+ }, { operation: "apply revision" });
7769
7874
  },
7770
7875
  async mutate(cwd, query, operation, update) {
7771
- const initial = await this.read(cwd, query);
7772
- return withDirectoryLock(join7(initial.dir, ".lock"), async () => {
7876
+ const initial = await read(cwd, query);
7877
+ return withDirectoryLock(join5(initial.dir, ".lock"), async () => {
7773
7878
  const current = await readRecord(initial.dir);
7774
7879
  const next = await update(structuredClone(current.document));
7775
7880
  if (next.id !== current.id)
7776
7881
  throw new Error("A plan mutation cannot change the plan ID.");
7777
7882
  const document = {
7778
7883
  ...next,
7779
- revision: current.document.revision + 1,
7884
+ revision: current.document.revision,
7780
7885
  updatedAt: now().toISOString()
7781
7886
  };
7782
7887
  const source = renderPlanDocument(document, current.source);
7783
7888
  parsePlanDocument(source, current.planPath);
7784
7889
  await atomicWrite(current.planPath, source);
7785
- await ensureImplementationFiles(current.dir, document, options);
7786
- return { ...current, document, source, implementationDir: join7(current.dir, "implementation") };
7890
+ return { ...current, document, source };
7787
7891
  }, { operation });
7788
7892
  },
7789
7893
  async log(cwd, query, event) {
7790
- const record = await this.read(cwd, query);
7894
+ const record = await read(cwd, query);
7791
7895
  return appendLogEntry(record.logPath, event);
7792
7896
  }
7793
7897
  };
7794
7898
  }
7795
7899
  async function readRecord(dir) {
7796
- const planPath = join7(dir, "PLAN.md");
7797
- const logPath = join7(dir, "logs.txt");
7798
- const source = await readFile3(planPath, "utf8");
7900
+ const planPath = join5(dir, "PLAN.md");
7901
+ const logPath = join5(dir, "logs.txt");
7902
+ const source = await readFile2(planPath, "utf8");
7799
7903
  const document = parsePlanDocument(source, planPath);
7800
7904
  return {
7801
7905
  id: basename3(dir),
7802
7906
  dir,
7803
7907
  planPath,
7804
7908
  logPath,
7805
- implementationDir: join7(dir, "implementation"),
7909
+ implementationDir: join5(dir, "implementation"),
7806
7910
  document,
7807
7911
  source
7808
7912
  };
7809
7913
  }
7810
- async function ensureImplementationFiles(dir, document, options) {
7811
- const implementationDir = join7(dir, "implementation");
7812
- await mkdir3(implementationDir, { recursive: true });
7813
- const template = await loadTemplate("plan/implementation", {
7814
- homeDir: options.homeDir,
7815
- bundledDir: options.bundledTemplatesDir
7816
- });
7817
- for (const phase of document.phases) {
7818
- const path = join7(implementationDir, `phase-${phase.id}.md`);
7914
+ async function createPlanDirectory(root, dir, id) {
7915
+ await assertContained(root, dirname3(dir));
7916
+ try {
7917
+ await mkdir3(dir);
7918
+ } catch (error) {
7919
+ if (error.code === "EEXIST")
7920
+ throw new Error(`Plan ${id} already exists.`);
7921
+ throw error;
7922
+ }
7923
+ }
7924
+ async function writeRevisionSnapshot(planDir, document, source, request, briefs) {
7925
+ if (!request.text.trim())
7926
+ throw new Error("An exact non-empty authoring request is required.");
7927
+ if (request.kind === "annotation" && !request.response?.trim())
7928
+ throw new Error("Annotation requests require a non-empty response.");
7929
+ const revisionsDir = join5(planDir, "revisions");
7930
+ await mkdir3(revisionsDir, { recursive: true });
7931
+ const target = join5(revisionsDir, String(document.revision));
7932
+ try {
7933
+ await readFile2(join5(target, "metadata.json"), "utf8");
7934
+ throw new Error(`Revision ${document.revision} already exists and is immutable.`);
7935
+ } catch (error) {
7936
+ if (error.code !== "ENOENT")
7937
+ throw error;
7938
+ }
7939
+ const staging = join5(revisionsDir, `.tmp-${document.revision}-${randomUUID2()}`);
7940
+ await mkdir3(staging);
7941
+ try {
7942
+ const requestSource = request.kind === "annotation" ? `## Original annotation
7943
+ ${request.text}
7944
+
7945
+ ## LLM response
7946
+ ${request.response}` : request.text;
7947
+ await atomicWrite(join5(staging, "request.md"), requestSource);
7948
+ await atomicWrite(join5(staging, "metadata.json"), `${JSON.stringify({
7949
+ schemaVersion: 1,
7950
+ id: document.id,
7951
+ revision: document.revision,
7952
+ requestKind: request.kind,
7953
+ requestSha256: sha256(request.text),
7954
+ ...request.kind === "annotation" ? { originalAnnotationSha256: sha256(request.text), responseSha256: sha256(request.response) } : {},
7955
+ createdAt: document.updatedAt
7956
+ }, null, 2)}
7957
+ `);
7958
+ await atomicWrite(join5(staging, "PLAN.md"), source);
7959
+ await writeImplementationDirectory(staging, document, briefs);
7960
+ await rename2(staging, target);
7961
+ } catch (error) {
7962
+ await rm2(staging, { recursive: true, force: true });
7963
+ throw error;
7964
+ }
7965
+ }
7966
+ async function writeLatestView(planDir, document, source, briefs) {
7967
+ const current = join5(planDir, "implementation");
7968
+ const stagingRoot = join5(planDir, `.latest-${randomUUID2()}`);
7969
+ const stagedImplementation = join5(stagingRoot, "implementation");
7970
+ const backup = join5(planDir, `.implementation-backup-${randomUUID2()}`);
7971
+ await mkdir3(stagingRoot);
7972
+ await writeImplementationDirectory(stagingRoot, document, briefs);
7973
+ let backedUp = false;
7974
+ try {
7819
7975
  try {
7820
- await readFile3(path, "utf8");
7976
+ await rename2(current, backup);
7977
+ backedUp = true;
7821
7978
  } catch (error) {
7822
7979
  if (error.code !== "ENOENT")
7823
7980
  throw error;
7824
- const tasks = phase.tasks.map((task) => `- ${task.id}: ${task.title}`).join("\\n") || "- Add implementation tasks.";
7825
- await atomicWrite(path, renderTemplate(template.content, {
7826
- phase_id: phase.id,
7827
- phase_title: phase.title,
7828
- phase_objective: phase.objective,
7829
- phase_tasks: tasks
7830
- }));
7831
7981
  }
7982
+ await rename2(stagedImplementation, current);
7983
+ await atomicWrite(join5(planDir, "PLAN.md"), source);
7984
+ if (backedUp)
7985
+ await rm2(backup, { recursive: true, force: true });
7986
+ } catch (error) {
7987
+ await rm2(current, { recursive: true, force: true });
7988
+ if (backedUp)
7989
+ await rename2(backup, current);
7990
+ throw error;
7991
+ } finally {
7992
+ await rm2(stagingRoot, { recursive: true, force: true });
7832
7993
  }
7833
7994
  }
7995
+ async function writeImplementationDirectory(baseDir, document, briefs) {
7996
+ const implementationDir = join5(baseDir, "implementation");
7997
+ await mkdir3(implementationDir, { recursive: true });
7998
+ for (const [index, phase] of document.phases.entries()) {
7999
+ const brief = briefs[index];
8000
+ if (!brief)
8001
+ throw new Error(`Missing implementation brief for phase ${phase.id}.`);
8002
+ await atomicWrite(join5(implementationDir, `phase-${index + 1}.md`), renderImplementationBrief(document.revision, index + 1, phase, brief));
8003
+ }
8004
+ }
8005
+ function normalizeRevision(current, request, id, revision, updatedAt) {
8006
+ if (current?.status === "completed")
8007
+ throw new Error("Completed plans cannot be amended.");
8008
+ if (current?.execution?.active && request.request.kind !== "blocker")
8009
+ throw new Error("An active execution accepts only a blocker amendment.");
8010
+ const currentPhases = new Map(current?.phases.map((phase) => [phase.id, phase]) ?? []);
8011
+ for (const phase of current?.phases ?? []) {
8012
+ if (!request.phases.some((candidate) => candidate.id === phase.id) && (phase.status === "completed" || phase.status === "in_progress"))
8013
+ throw new Error(`${phase.status === "completed" ? "Completed" : "Active"} phase ${phase.id} cannot be removed.`);
8014
+ }
8015
+ const phases = request.phases.map((draft) => reconcilePhase(currentPhases.get(draft.id), draft, current));
8016
+ const createdAt = current?.createdAt ?? updatedAt;
8017
+ return {
8018
+ schemaVersion: 1,
8019
+ id,
8020
+ revision,
8021
+ title: request.title,
8022
+ branch: current?.branch ?? (request.mode === "create" ? request.branch : ""),
8023
+ issueId: current?.issueId ?? (request.mode === "create" ? request.issueId : undefined),
8024
+ issueUrl: current?.issueUrl ?? (request.mode === "create" ? request.issueUrl : undefined),
8025
+ intent: request.intent,
8026
+ requirements: request.requirements,
8027
+ design: request.design,
8028
+ references: request.references,
8029
+ phases,
8030
+ status: nextPlanStatus(current),
8031
+ execution: current?.execution,
8032
+ createdAt,
8033
+ updatedAt
8034
+ };
8035
+ }
8036
+ function reconcilePhase(current, draft, plan) {
8037
+ if (!current)
8038
+ return {
8039
+ ...draft,
8040
+ revision: 0,
8041
+ status: "pending",
8042
+ gate: { phaseRevision: 0, status: "pending", results: [] },
8043
+ tasks: draft.tasks.map(newTask)
8044
+ };
8045
+ if (current.status === "completed") {
8046
+ if (JSON.stringify(phaseContent(current)) !== JSON.stringify(draft))
8047
+ throw new Error(`Completed phase ${current.id} cannot be changed.`);
8048
+ return current;
8049
+ }
8050
+ const byId = new Map(current.tasks.map((task) => [task.id, task]));
8051
+ for (const task of current.tasks) {
8052
+ if (!draft.tasks.some((candidate) => candidate.id === task.id) && (task.status === "completed" || task.status === "in_progress"))
8053
+ throw new Error(`${task.status === "completed" ? "Completed" : "Active"} task ${task.id} cannot be removed.`);
8054
+ }
8055
+ const tasks = draft.tasks.map((task) => reconcileTask(byId.get(task.id), task));
8056
+ const changed = JSON.stringify(phaseContent(current)) !== JSON.stringify(draft);
8057
+ const revision = changed ? current.revision + 1 : current.revision;
8058
+ const wasBlocked = current.status === "blocked";
8059
+ return {
8060
+ ...current,
8061
+ ...draft,
8062
+ revision,
8063
+ tasks,
8064
+ status: wasBlocked ? plan?.execution?.active ? "in_progress" : "pending" : current.status,
8065
+ gate: changed || wasBlocked ? { phaseRevision: revision, status: "stale", results: [] } : current.gate,
8066
+ blocker: wasBlocked ? undefined : current.blocker
8067
+ };
8068
+ }
8069
+ function reconcileTask(current, draft) {
8070
+ if (!current)
8071
+ return newTask(draft);
8072
+ const changed = JSON.stringify(taskContent(current)) !== JSON.stringify(draft);
8073
+ if ((current.status === "completed" || current.status === "in_progress") && changed)
8074
+ throw new Error(`${current.status === "completed" ? "Completed" : "Active"} task ${current.id} cannot be changed.`);
8075
+ if (current.status === "completed" || current.status === "in_progress")
8076
+ return current;
8077
+ return {
8078
+ ...current,
8079
+ ...draft,
8080
+ revision: changed ? current.revision + 1 : current.revision,
8081
+ status: current.status === "blocked" ? "pending" : current.status,
8082
+ owner: current.status === "blocked" ? undefined : current.owner,
8083
+ executionId: current.status === "blocked" ? undefined : current.executionId,
8084
+ blocker: current.status === "blocked" ? undefined : current.blocker
8085
+ };
8086
+ }
8087
+ function newTask(draft) {
8088
+ return { ...draft, revision: 0, status: "pending" };
8089
+ }
8090
+ function phaseContent(phase) {
8091
+ return {
8092
+ id: phase.id,
8093
+ title: phase.title,
8094
+ objective: phase.objective,
8095
+ dependencies: phase.dependencies,
8096
+ tasks: phase.tasks.map(taskContent)
8097
+ };
8098
+ }
8099
+ function taskContent(task) {
8100
+ return { id: task.id, title: task.title, dependencies: task.dependencies };
8101
+ }
8102
+ function nextPlanStatus(current) {
8103
+ if (!current)
8104
+ return "draft";
8105
+ if (current.execution?.active)
8106
+ return current.status === "blocked" ? "in_progress" : current.status;
8107
+ return current.status === "ready" || current.status === "blocked" ? "draft" : current.status;
8108
+ }
8109
+ function assertCompleteRevision(document, briefs) {
8110
+ const errors = [...validatePlanDocument(document, { strict: true }), ...validatePlanBriefs(document, briefs)].filter((issue) => issue.severity === "error");
8111
+ if (errors.length)
8112
+ throw new Error(`Invalid plan revision: ${errors.map((issue) => issue.message).join(" ")}`);
8113
+ }
8114
+ function sha256(value) {
8115
+ return createHash2("sha256").update(value).digest("hex");
8116
+ }
7834
8117
  async function assertContained(root, candidate) {
7835
8118
  const canonicalRoot = await realpath3(root);
7836
8119
  let canonicalCandidate;
@@ -7862,11 +8145,14 @@ function stripDate(value) {
7862
8145
  return value.replace(/^\d{6}-/, "");
7863
8146
  }
7864
8147
  function titleFromSlug(value) {
7865
- return normalizeSlug(value).replace(/^(?:[a-z]+-\d+-)/, "").split("-").map((part) => part[0].toUpperCase() + part.slice(1)).join(" ");
8148
+ const slug = normalizeSlug(value).replace(/^(?:[a-z]+-\d+-)/, "");
8149
+ return slug.replace(/(^|-)([a-z])/g, (_match, separator, letter) => {
8150
+ return `${separator ? " " : ""}${letter.toUpperCase()}`;
8151
+ });
7866
8152
  }
7867
8153
  // src/plan/reviews.ts
7868
- import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile2 } from "node:fs/promises";
7869
- import { dirname as dirname5, join as join8 } from "node:path";
8154
+ import { mkdir as mkdir4, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
8155
+ import { dirname as dirname4, join as join6 } from "node:path";
7870
8156
  async function createPlanReview(record, runtime = {}) {
7871
8157
  if (await readPlanReview(record))
7872
8158
  throw new Error(`Plan revision ${record.document.revision} already has a review.`);
@@ -7899,7 +8185,7 @@ async function readPlanReview(record) {
7899
8185
  const path = reviewPath(record);
7900
8186
  let source;
7901
8187
  try {
7902
- source = await readFile4(path, "utf8");
8188
+ source = await readFile3(path, "utf8");
7903
8189
  } catch (error) {
7904
8190
  if (error.code === "ENOENT")
7905
8191
  return;
@@ -7916,10 +8202,10 @@ async function readPlanReview(record) {
7916
8202
  return { path, dump };
7917
8203
  }
7918
8204
  function reviewPath(record) {
7919
- return join8(record.dir, "reviews", `${record.document.revision}.json`);
8205
+ return join6(record.dir, "reviews", `${record.document.revision}.json`);
7920
8206
  }
7921
8207
  async function writeReview(review) {
7922
- await mkdir4(dirname5(review.path), { recursive: true });
8208
+ await mkdir4(dirname4(review.path), { recursive: true });
7923
8209
  await writeFile2(review.path, `${JSON.stringify(review.dump, null, 2)}
7924
8210
  `, {
7925
8211
  encoding: "utf8",