@difflab/pi 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +11 -9
  2. package/agents/diffpi-orchestrator.md +6 -4
  3. package/agents/diffpi-planner.md +3 -3
  4. package/agents/diffpi-reviewer.md +4 -4
  5. package/agents/diffpi-worker.md +1 -1
  6. package/dist/assets.d.ts +0 -1
  7. package/dist/assets.d.ts.map +1 -1
  8. package/dist/cli.js +417 -84
  9. package/dist/commands/plan.d.ts +2 -2
  10. package/dist/commands/plan.d.ts.map +1 -1
  11. package/dist/commands/review.d.ts +2 -2
  12. package/dist/commands/review.d.ts.map +1 -1
  13. package/dist/environment.d.ts +2 -0
  14. package/dist/environment.d.ts.map +1 -1
  15. package/dist/extensions/index.js +1778 -3799
  16. package/dist/extensions/tuicrx.d.ts +8 -1
  17. package/dist/extensions/tuicrx.d.ts.map +1 -1
  18. package/dist/index.d.ts +8 -7
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +891 -292
  21. package/dist/plan/markdown.d.ts +4 -1
  22. package/dist/plan/markdown.d.ts.map +1 -1
  23. package/dist/{tools/plan-schema.d.ts → plan/schema.d.ts} +113 -75
  24. package/dist/plan/schema.d.ts.map +1 -0
  25. package/dist/plan/store.d.ts +39 -1
  26. package/dist/plan/store.d.ts.map +1 -1
  27. package/dist/plan/types.d.ts +27 -3
  28. package/dist/plan/types.d.ts.map +1 -1
  29. package/dist/review/github-review-backend.d.ts.map +1 -1
  30. package/dist/review/gitlab-review-backend.d.ts.map +1 -1
  31. package/dist/review/index.d.ts +6 -6
  32. package/dist/review/index.d.ts.map +1 -1
  33. package/dist/review/local-review-backend.d.ts +3 -0
  34. package/dist/review/local-review-backend.d.ts.map +1 -0
  35. package/dist/review/review-backend.d.ts +3 -2
  36. package/dist/review/review-backend.d.ts.map +1 -1
  37. package/dist/review/review-markdown.d.ts +4 -1
  38. package/dist/review/review-markdown.d.ts.map +1 -1
  39. package/dist/review/review-state.d.ts +20 -0
  40. package/dist/review/review-state.d.ts.map +1 -0
  41. package/dist/review/types.d.ts +28 -0
  42. package/dist/review/types.d.ts.map +1 -1
  43. package/dist/store.d.ts +2 -0
  44. package/dist/store.d.ts.map +1 -1
  45. package/dist/tools/index.js +1632 -575
  46. package/dist/tools/plan.d.ts.map +1 -1
  47. package/dist/tools/review.d.ts +38 -0
  48. package/dist/tools/review.d.ts.map +1 -1
  49. package/package.json +2 -4
  50. package/skills/plan/SKILL.md +8 -14
  51. package/skills/plan/references/design-quality.md +41 -0
  52. package/skills/plan/references/workflows/annotate.md +11 -0
  53. package/skills/plan/references/workflows/finalize.md +7 -0
  54. package/skills/plan/references/workflows/go.md +8 -0
  55. package/skills/plan/references/workflows/help.md +10 -0
  56. package/skills/plan/references/workflows/init.md +5 -0
  57. package/skills/plan/references/workflows/new.md +7 -0
  58. package/skills/plan/references/workflows/update.md +7 -0
  59. package/skills/review/SKILL.md +7 -7
  60. package/skills/review/references/review-standards.md +8 -1
  61. package/skills/review/references/workflows/address.md +11 -0
  62. package/skills/review/references/workflows/auto.md +18 -0
  63. package/skills/review/references/workflows/complete.md +7 -0
  64. package/skills/review/references/workflows/edit.md +7 -0
  65. package/skills/review/references/workflows/help.md +36 -0
  66. package/skills/review/references/workflows/merge.md +6 -0
  67. package/skills/review/references/workflows/new.md +19 -0
  68. package/skills/review/references/workflows/open.md +3 -0
  69. package/skills/review/references/workflows/publish.md +8 -0
  70. package/skills/review/references/workflows/status.md +3 -0
  71. package/templates/plan/PLAN.md +2 -2
  72. package/templates/plan/implementation.md +16 -10
  73. package/dist/commands.d.ts +0 -22
  74. package/dist/commands.d.ts.map +0 -1
  75. package/dist/extensions/cmdtsx.d.ts +0 -2
  76. package/dist/extensions/cmdtsx.d.ts.map +0 -1
  77. package/dist/tools/plan-schema.d.ts.map +0 -1
  78. package/dist/workflow.d.ts +0 -80
  79. package/dist/workflow.d.ts.map +0 -1
  80. package/workflows/plan/annotate.md +0 -13
  81. package/workflows/plan/finalize.md +0 -15
  82. package/workflows/plan/go.md +0 -20
  83. package/workflows/plan/help.md +0 -20
  84. package/workflows/plan/init.md +0 -13
  85. package/workflows/plan/new.md +0 -20
  86. package/workflows/plan/update.md +0 -18
  87. package/workflows/review/address.md +0 -8
  88. package/workflows/review/auto.md +0 -8
  89. package/workflows/review/complete.md +0 -7
  90. package/workflows/review/edit.md +0 -7
  91. package/workflows/review/help.md +0 -14
  92. package/workflows/review/merge.md +0 -6
  93. package/workflows/review/new.md +0 -7
  94. package/workflows/review/publish.md +0 -6
package/dist/cli.js CHANGED
@@ -2383,7 +2383,8 @@ 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";
2386
+ import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
2387
+ import { mkdir as mkdir3, readdir, readFile as readFile3, realpath as realpath3, rename as rename2, rm as rm2 } from "node:fs/promises";
2387
2388
  import { basename as basename3, dirname as dirname4, join as join7, resolve as resolve3, sep } from "node:path";
2388
2389
 
2389
2390
  // src/extensions/fsx.ts
@@ -7276,11 +7277,13 @@ function validatePlanDocument(plan, options = {}) {
7276
7277
  if (!taskIds.has(dependency))
7277
7278
  add("dangling-dependency", "error", `Task ${task.id} depends on missing ${dependency}.`);
7278
7279
  }
7279
- if (options.strict && task.acceptanceCriteria.length === 0)
7280
- add("acceptance", "error", `Task ${task.id} has no acceptance criteria.`, task.id);
7280
+ if (options.strict && isPlaceholder(task.title))
7281
+ add("placeholder", "error", `Task ${task.id} has placeholder content.`, task.id);
7281
7282
  }
7282
7283
  if (options.strict && phase.tasks.length === 0)
7283
7284
  add("empty-phase", "error", `Phase ${phase.id} has no tasks.`, phase.id);
7285
+ if (options.strict && (isPlaceholder(phase.title) || isPlaceholder(phase.objective)))
7286
+ add("placeholder", "error", `Phase ${phase.id} has placeholder content.`, phase.id);
7284
7287
  }
7285
7288
  for (const cycle of dependencyCycles(plan))
7286
7289
  add("dependency-cycle", "error", `Dependency cycle: ${cycle.join(" -> ")}.`);
@@ -7289,10 +7292,131 @@ function validatePlanDocument(plan, options = {}) {
7289
7292
  add("design-length", "warning", `Design is ${designWords} words; prefer 300 or fewer.`);
7290
7293
  if (options.strict && designWords > 800)
7291
7294
  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.");
7295
+ if (options.strict) {
7296
+ if (plan.phases.length === 0)
7297
+ add("no-phases", "error", "Finalization requires at least one phase.");
7298
+ if (isPlaceholder(plan.intent))
7299
+ add("placeholder", "error", "Intent must contain concrete content.", "intent");
7300
+ if (!plan.requirements.length || plan.requirements.some(isPlaceholder))
7301
+ add("requirements", "error", "Requirements must contain concrete content.", "requirements");
7302
+ for (const [key, value] of Object.entries(plan.design)) {
7303
+ if (isPlaceholder(value))
7304
+ add("design", "error", `Design/${key} must contain concrete content.`, `design.${key}`);
7305
+ }
7306
+ }
7307
+ return issues;
7308
+ }
7309
+ function validatePlanBriefs(plan, briefs) {
7310
+ const issues = [];
7311
+ const phaseIds = plan.phases.map((phase) => phase.id);
7312
+ const briefIds = briefs.map((brief) => brief.phaseId);
7313
+ if (!sameIds(phaseIds, briefIds))
7314
+ issues.push({
7315
+ code: "brief-phase-ids",
7316
+ severity: "error",
7317
+ message: `Brief phase IDs must exactly match plan phases in order: ${phaseIds.join(", ")}.`
7318
+ });
7319
+ for (const [index, phase] of plan.phases.entries()) {
7320
+ const brief = briefs[index];
7321
+ if (!brief || brief.phaseId !== phase.id)
7322
+ continue;
7323
+ if (isPlaceholder(brief.summary))
7324
+ issues.push({ code: "brief-placeholder", severity: "error", message: `Brief ${phase.id} needs a summary.` });
7325
+ const taskIds = phase.tasks.map((task) => task.id);
7326
+ const briefTaskIds = brief.tasks.map((task) => task.taskId);
7327
+ if (!sameIds(taskIds, briefTaskIds))
7328
+ issues.push({
7329
+ code: "brief-task-ids",
7330
+ severity: "error",
7331
+ message: `Brief ${phase.id} task IDs must exactly match phase tasks in order: ${taskIds.join(", ")}.`
7332
+ });
7333
+ for (const task of brief.tasks) {
7334
+ for (const [field, values] of [
7335
+ ["ordered steps", task.steps],
7336
+ ["file scopes", task.fileScopes],
7337
+ ["acceptance criteria", task.acceptanceCriteria]
7338
+ ]) {
7339
+ if (!values.length || values.some(isPlaceholder))
7340
+ issues.push({
7341
+ code: "brief-incomplete",
7342
+ severity: "error",
7343
+ message: `Brief task ${task.taskId} needs concrete ${field}.`,
7344
+ path: task.taskId
7345
+ });
7346
+ }
7347
+ }
7348
+ for (const [field, values] of [
7349
+ ["API changes", brief.apiChanges],
7350
+ ["libraries", brief.libraries],
7351
+ ["constraints", brief.constraints]
7352
+ ]) {
7353
+ if (values.some(isPlaceholder))
7354
+ issues.push({
7355
+ code: "brief-placeholder",
7356
+ severity: "error",
7357
+ message: `Brief ${phase.id} has placeholder ${field}.`,
7358
+ path: phase.id
7359
+ });
7360
+ }
7361
+ }
7294
7362
  return issues;
7295
7363
  }
7364
+ function renderImplementationBrief(planRevision, ordinal, phase, brief) {
7365
+ const marker = { schemaVersion: 1, planRevision, ordinal, phaseId: phase.id };
7366
+ const tasks = phase.tasks.map((task, index) => {
7367
+ const details = brief.tasks[index];
7368
+ if (!details || details.taskId !== task.id)
7369
+ throw new Error(`Missing implementation details for ${task.id}.`);
7370
+ return `<!-- diffpi-brief-task: ${json({ id: task.id })} -->
7371
+ ### Task: ${task.title}
7372
+
7373
+ #### Ordered Steps
7374
+
7375
+ ${bulletList(details.steps)}
7376
+
7377
+ #### File Scopes
7378
+
7379
+ ${bulletList(details.fileScopes)}
7380
+
7381
+ #### Acceptance Criteria
7382
+
7383
+ ${bulletList(details.acceptanceCriteria)}
7384
+
7385
+ <!-- /diffpi-brief-task -->`;
7386
+ }).join(`
7387
+
7388
+ `);
7389
+ return `<!-- diffpi-implementation: ${json(marker)} -->
7390
+ # Phase ${ordinal}: ${phase.title}
7391
+
7392
+ - **Phase ID:** ${phase.id}
7393
+ - **Plan Revision:** ${planRevision}
7394
+
7395
+ ## Summary
7396
+
7397
+ ${brief.summary}
7398
+
7399
+ ## Objective
7400
+
7401
+ ${phase.objective}
7402
+
7403
+ ## Tasks
7404
+
7405
+ ${tasks}
7406
+
7407
+ ## API Changes
7408
+
7409
+ ${bulletList(brief.apiChanges, "None.")}
7410
+
7411
+ ## Libraries and Algorithms
7412
+
7413
+ ${bulletList(brief.libraries, "None.")}
7414
+
7415
+ ## Implementation Constraints
7416
+
7417
+ ${bulletList(brief.constraints, "None.")}
7418
+ `;
7419
+ }
7296
7420
  function renderPlanDocument(plan, previousSource) {
7297
7421
  if (previousSource && canPatch(previousSource, plan))
7298
7422
  return patchMarkers(previousSource, plan);
@@ -7301,6 +7425,8 @@ function renderPlanDocument(plan, previousSource) {
7301
7425
  id: plan.id,
7302
7426
  revision: plan.revision,
7303
7427
  branch: plan.branch,
7428
+ issueId: plan.issueId,
7429
+ issueUrl: plan.issueUrl,
7304
7430
  status: plan.status,
7305
7431
  execution: plan.execution,
7306
7432
  createdAt: plan.createdAt,
@@ -7309,6 +7435,8 @@ function renderPlanDocument(plan, previousSource) {
7309
7435
  const metadata = import_mdast_builder.list("unordered", [
7310
7436
  ["Plan ID", plan.id],
7311
7437
  ["Branch", plan.branch],
7438
+ ...plan.issueId ? [["Issue", plan.issueId]] : [],
7439
+ ...plan.issueUrl ? [["Issue URL", plan.issueUrl]] : [],
7312
7440
  ["Status", plan.status],
7313
7441
  ["Revision", String(plan.revision)]
7314
7442
  ].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 +7458,7 @@ function renderPlanDocument(plan, previousSource) {
7330
7458
  import_mdast_builder.heading(3, import_mdast_builder.text("Consequences")),
7331
7459
  import_mdast_builder.html(plan.design.consequences || "<!-- Describe trade-offs and limitations. -->"),
7332
7460
  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. -->")],
7461
+ ...plan.phases.length ? plan.phases.flatMap((phase) => [import_mdast_builder.html(renderPhase(phase))]) : [import_mdast_builder.html("<!-- Add phases with plan_apply_revision. -->")],
7334
7462
  import_mdast_builder.heading(2, import_mdast_builder.text("References")),
7335
7463
  references
7336
7464
  ]);
@@ -7404,10 +7532,7 @@ function renderTask(task) {
7404
7532
  const checked = task.status === "completed" || task.status === "skipped" ? "x" : " ";
7405
7533
  return `<!-- diffpi-task: ${json(marker)} -->
7406
7534
  - [${checked}] **${task.title}**
7407
- ${taskList("Steps", task.steps ?? [])}
7408
7535
  - Dependencies: ${list3(task.dependencies)}
7409
- ${taskList("File scopes", task.fileScopes)}
7410
- ${taskList("Acceptance criteria", task.acceptanceCriteria)}
7411
7536
  <!-- /diffpi-task -->`;
7412
7537
  }
7413
7538
  function parsePhases(input, ref) {
@@ -7457,10 +7582,7 @@ function parseTasks(input, ref) {
7457
7582
  return {
7458
7583
  ...marker,
7459
7584
  title,
7460
- steps: parseListValue(body, "Steps"),
7461
- dependencies: parseListValue(body, "Dependencies"),
7462
- fileScopes: parseListValue(body, "File scopes"),
7463
- acceptanceCriteria: parseListValue(body, "Acceptance criteria")
7585
+ dependencies: parseListValue(body, "Dependencies")
7464
7586
  };
7465
7587
  });
7466
7588
  assertUniqueIds(tasks.map((task) => task.id), "task id");
@@ -7487,6 +7609,8 @@ function patchMarkers(source, plan) {
7487
7609
  id: plan.id,
7488
7610
  revision: plan.revision,
7489
7611
  branch: plan.branch,
7612
+ issueId: plan.issueId,
7613
+ issueUrl: plan.issueUrl,
7490
7614
  status: plan.status,
7491
7615
  execution: plan.execution,
7492
7616
  createdAt: plan.createdAt,
@@ -7536,19 +7660,12 @@ function contentShape(plan) {
7536
7660
  requirements: plan.requirements,
7537
7661
  design: plan.design,
7538
7662
  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
- }))
7663
+ phases: plan.phases.map((phase) => ({
7664
+ id: phase.id,
7665
+ title: phase.title,
7666
+ objective: phase.objective,
7667
+ dependencies: phase.dependencies,
7668
+ tasks: phase.tasks.map(({ id, title, dependencies }) => ({ id, title, dependencies }))
7552
7669
  }))
7553
7670
  };
7554
7671
  }
@@ -7648,6 +7765,13 @@ function assertUniqueIds(ids, label) {
7648
7765
  function cleanPlaceholder(value) {
7649
7766
  return value.replace(/<!--[^]*?-->/g, "").trim();
7650
7767
  }
7768
+ function isPlaceholder(value) {
7769
+ const normalized = cleanPlaceholder(value).trim();
7770
+ return normalized.length === 0 || /^(?:todo|tbd|none|n\/a)$/i.test(normalized) || /define during implementation|describe the|add requirements|add implementation/i.test(normalized);
7771
+ }
7772
+ function sameIds(expected, actual) {
7773
+ return expected.length === actual.length && expected.every((value, index) => actual[index] === value);
7774
+ }
7651
7775
  function parseMarker(value, label) {
7652
7776
  try {
7653
7777
  return JSON.parse(value);
@@ -7655,15 +7779,14 @@ function parseMarker(value, label) {
7655
7779
  throw new Error(`Malformed ${label} marker JSON.`);
7656
7780
  }
7657
7781
  }
7658
- function taskList(label, values) {
7782
+ function bulletList(values, empty = "") {
7659
7783
  if (!values.length)
7660
- return ` - ${label}: none`;
7661
- const items = values.flatMap((value) => {
7784
+ return empty;
7785
+ return values.flatMap((value) => {
7662
7786
  const [first = "", ...continuations] = value.split(`
7663
7787
  `);
7664
- return [` - ${first}`, ...continuations.map((line) => ` ${line}`)];
7665
- });
7666
- return [` - ${label}:`, ...items].join(`
7788
+ return [`- ${first}`, ...continuations.map((line) => ` ${line}`)];
7789
+ }).join(`
7667
7790
  `);
7668
7791
  }
7669
7792
  function list3(values) {
@@ -7710,6 +7833,15 @@ async function resolvePlan(cwd, query, filters = {}, options = {}) {
7710
7833
  }
7711
7834
  function createPlanStore(options = {}) {
7712
7835
  const now = options.now ?? (() => new Date);
7836
+ const read = async (cwd, query, filters) => {
7837
+ const result = await resolvePlan(cwd, query, filters, options);
7838
+ if (!result.record) {
7839
+ if (result.ambiguous)
7840
+ throw new Error(`Plan query "${query}" is ambiguous: ${result.candidates.map((item) => item.id).join(", ")}.`);
7841
+ throw new Error(`Plan "${query}" was not found.`);
7842
+ }
7843
+ return result.record;
7844
+ };
7713
7845
  return {
7714
7846
  context(cwd, query, filters) {
7715
7847
  return resolvePlan(cwd, query, filters, options);
@@ -7718,57 +7850,77 @@ function createPlanStore(options = {}) {
7718
7850
  const root = await plansDir(input.cwd, options.homeDir);
7719
7851
  const id = planRecordName(input.shortSlug, now());
7720
7852
  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
- }
7853
+ await createPlanDirectory(root, dir, id);
7729
7854
  try {
7730
7855
  const timestamp = now().toISOString();
7731
7856
  const template = await loadTemplate("plan/PLAN", {
7732
7857
  homeDir: options.homeDir,
7733
7858
  bundledDir: options.bundledTemplatesDir
7734
7859
  });
7860
+ const title = input.title?.trim() || titleFromSlug(input.shortSlug);
7735
7861
  const source = renderTemplate(template.content, {
7736
7862
  id,
7737
7863
  branch: input.branch,
7738
- title: input.title?.trim() || titleFromSlug(input.shortSlug),
7864
+ title,
7739
7865
  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. -->",
7866
+ issue_id: input.issueId?.trim() || "",
7867
+ issue_url: input.issueUrl?.trim() || "",
7742
7868
  created_at: timestamp,
7743
7869
  updated_at: timestamp
7744
7870
  });
7745
7871
  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
7755
- };
7872
+ const authoringRequest = input.request ?? { kind: "user", text: input.intent?.trim() || `Initialize phase-less plan ${title}.` };
7873
+ await writeRevisionSnapshot(dir, document, source, authoringRequest, []);
7874
+ await writeLatestView(dir, document, source, []);
7875
+ return await readRecord(dir);
7756
7876
  } catch (error) {
7757
7877
  await rm2(dir, { recursive: true, force: true });
7758
7878
  throw error;
7759
7879
  }
7760
7880
  },
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;
7881
+ read,
7882
+ async applyRevision(cwd, request) {
7883
+ if (request.mode === "create") {
7884
+ const root = await plansDir(cwd, options.homeDir);
7885
+ const id = planRecordName(request.shortSlug, now());
7886
+ const dir = join7(root, id);
7887
+ await createPlanDirectory(root, dir, id);
7888
+ try {
7889
+ const timestamp = now().toISOString();
7890
+ const document = normalizeRevision(undefined, request, id, 0, timestamp);
7891
+ assertCompleteRevision(document, request.briefs);
7892
+ const source = renderPlanDocument(document);
7893
+ parsePlanDocument(source, join7(dir, "PLAN.md"));
7894
+ await writeRevisionSnapshot(dir, document, source, request.request, request.briefs);
7895
+ await writeLatestView(dir, document, source, request.briefs);
7896
+ return await readRecord(dir);
7897
+ } catch (error) {
7898
+ await rm2(dir, { recursive: true, force: true });
7899
+ throw error;
7900
+ }
7901
+ }
7902
+ const initial = await read(cwd, request.plan);
7903
+ return withDirectoryLock(join7(initial.dir, ".lock"), async () => {
7904
+ const current = await readRecord(initial.dir);
7905
+ if (current.document.revision !== request.expectedPlanRevision)
7906
+ throw new Error(`Expected plan revision ${request.expectedPlanRevision}, found ${current.document.revision}.`);
7907
+ const revision = current.document.revision + 1;
7908
+ const document = normalizeRevision(current.document, request, current.id, revision, now().toISOString());
7909
+ assertCompleteRevision(document, request.briefs);
7910
+ const source = renderPlanDocument(document);
7911
+ parsePlanDocument(source, current.planPath);
7912
+ await writeRevisionSnapshot(current.dir, document, source, request.request, request.briefs);
7913
+ try {
7914
+ await writeLatestView(current.dir, document, source, request.briefs);
7915
+ } catch (error) {
7916
+ await rm2(join7(current.dir, "revisions", String(revision)), { recursive: true, force: true });
7917
+ throw error;
7918
+ }
7919
+ return await readRecord(current.dir);
7920
+ }, { operation: "apply revision" });
7769
7921
  },
7770
7922
  async mutate(cwd, query, operation, update) {
7771
- const initial = await this.read(cwd, query);
7923
+ const initial = await read(cwd, query);
7772
7924
  return withDirectoryLock(join7(initial.dir, ".lock"), async () => {
7773
7925
  const current = await readRecord(initial.dir);
7774
7926
  const next = await update(structuredClone(current.document));
@@ -7776,18 +7928,17 @@ function createPlanStore(options = {}) {
7776
7928
  throw new Error("A plan mutation cannot change the plan ID.");
7777
7929
  const document = {
7778
7930
  ...next,
7779
- revision: current.document.revision + 1,
7931
+ revision: current.document.revision,
7780
7932
  updatedAt: now().toISOString()
7781
7933
  };
7782
7934
  const source = renderPlanDocument(document, current.source);
7783
7935
  parsePlanDocument(source, current.planPath);
7784
7936
  await atomicWrite(current.planPath, source);
7785
- await ensureImplementationFiles(current.dir, document, options);
7786
- return { ...current, document, source, implementationDir: join7(current.dir, "implementation") };
7937
+ return { ...current, document, source };
7787
7938
  }, { operation });
7788
7939
  },
7789
7940
  async log(cwd, query, event) {
7790
- const record = await this.read(cwd, query);
7941
+ const record = await read(cwd, query);
7791
7942
  return appendLogEntry(record.logPath, event);
7792
7943
  }
7793
7944
  };
@@ -7807,30 +7958,209 @@ async function readRecord(dir) {
7807
7958
  source
7808
7959
  };
7809
7960
  }
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`);
7961
+ async function createPlanDirectory(root, dir, id) {
7962
+ await assertContained(root, dirname4(dir));
7963
+ try {
7964
+ await mkdir3(dir);
7965
+ } catch (error) {
7966
+ if (error.code === "EEXIST")
7967
+ throw new Error(`Plan ${id} already exists.`);
7968
+ throw error;
7969
+ }
7970
+ }
7971
+ async function writeRevisionSnapshot(planDir, document, source, request, briefs) {
7972
+ if (!request.text.trim())
7973
+ throw new Error("An exact non-empty authoring request is required.");
7974
+ if (request.kind === "annotation" && !request.response?.trim())
7975
+ throw new Error("Annotation requests require a non-empty response.");
7976
+ const revisionsDir = join7(planDir, "revisions");
7977
+ await mkdir3(revisionsDir, { recursive: true });
7978
+ const target = join7(revisionsDir, String(document.revision));
7979
+ try {
7980
+ await readFile3(join7(target, "metadata.json"), "utf8");
7981
+ throw new Error(`Revision ${document.revision} already exists and is immutable.`);
7982
+ } catch (error) {
7983
+ if (error.code !== "ENOENT")
7984
+ throw error;
7985
+ }
7986
+ const staging = join7(revisionsDir, `.tmp-${document.revision}-${randomUUID2()}`);
7987
+ await mkdir3(staging);
7988
+ try {
7989
+ const requestSource = request.kind === "annotation" ? `## Original annotation
7990
+ ${request.text}
7991
+
7992
+ ## LLM response
7993
+ ${request.response}` : request.text;
7994
+ await atomicWrite(join7(staging, "request.md"), requestSource);
7995
+ await atomicWrite(join7(staging, "metadata.json"), `${JSON.stringify({
7996
+ schemaVersion: 1,
7997
+ id: document.id,
7998
+ revision: document.revision,
7999
+ requestKind: request.kind,
8000
+ requestSha256: sha256(request.text),
8001
+ ...request.kind === "annotation" ? { originalAnnotationSha256: sha256(request.text), responseSha256: sha256(request.response) } : {},
8002
+ createdAt: document.updatedAt
8003
+ }, null, 2)}
8004
+ `);
8005
+ await atomicWrite(join7(staging, "PLAN.md"), source);
8006
+ await writeImplementationDirectory(staging, document, briefs);
8007
+ await rename2(staging, target);
8008
+ } catch (error) {
8009
+ await rm2(staging, { recursive: true, force: true });
8010
+ throw error;
8011
+ }
8012
+ }
8013
+ async function writeLatestView(planDir, document, source, briefs) {
8014
+ const current = join7(planDir, "implementation");
8015
+ const stagingRoot = join7(planDir, `.latest-${randomUUID2()}`);
8016
+ const stagedImplementation = join7(stagingRoot, "implementation");
8017
+ const backup = join7(planDir, `.implementation-backup-${randomUUID2()}`);
8018
+ await mkdir3(stagingRoot);
8019
+ await writeImplementationDirectory(stagingRoot, document, briefs);
8020
+ let backedUp = false;
8021
+ try {
7819
8022
  try {
7820
- await readFile3(path, "utf8");
8023
+ await rename2(current, backup);
8024
+ backedUp = true;
7821
8025
  } catch (error) {
7822
8026
  if (error.code !== "ENOENT")
7823
8027
  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
8028
  }
8029
+ await rename2(stagedImplementation, current);
8030
+ await atomicWrite(join7(planDir, "PLAN.md"), source);
8031
+ if (backedUp)
8032
+ await rm2(backup, { recursive: true, force: true });
8033
+ } catch (error) {
8034
+ await rm2(current, { recursive: true, force: true });
8035
+ if (backedUp)
8036
+ await rename2(backup, current);
8037
+ throw error;
8038
+ } finally {
8039
+ await rm2(stagingRoot, { recursive: true, force: true });
7832
8040
  }
7833
8041
  }
8042
+ async function writeImplementationDirectory(baseDir, document, briefs) {
8043
+ const implementationDir = join7(baseDir, "implementation");
8044
+ await mkdir3(implementationDir, { recursive: true });
8045
+ for (const [index, phase] of document.phases.entries()) {
8046
+ const brief = briefs[index];
8047
+ if (!brief)
8048
+ throw new Error(`Missing implementation brief for phase ${phase.id}.`);
8049
+ await atomicWrite(join7(implementationDir, `phase-${index + 1}.md`), renderImplementationBrief(document.revision, index + 1, phase, brief));
8050
+ }
8051
+ }
8052
+ function normalizeRevision(current, request, id, revision, updatedAt) {
8053
+ if (current?.status === "completed")
8054
+ throw new Error("Completed plans cannot be amended.");
8055
+ if (current?.execution?.active && request.request.kind !== "blocker")
8056
+ throw new Error("An active execution accepts only a blocker amendment.");
8057
+ const currentPhases = new Map(current?.phases.map((phase) => [phase.id, phase]) ?? []);
8058
+ for (const phase of current?.phases ?? []) {
8059
+ if (!request.phases.some((candidate) => candidate.id === phase.id) && (phase.status === "completed" || phase.status === "in_progress"))
8060
+ throw new Error(`${phase.status === "completed" ? "Completed" : "Active"} phase ${phase.id} cannot be removed.`);
8061
+ }
8062
+ const phases = request.phases.map((draft) => reconcilePhase(currentPhases.get(draft.id), draft, current));
8063
+ const createdAt = current?.createdAt ?? updatedAt;
8064
+ return {
8065
+ schemaVersion: 1,
8066
+ id,
8067
+ revision,
8068
+ title: request.title,
8069
+ branch: current?.branch ?? (request.mode === "create" ? request.branch : ""),
8070
+ issueId: current?.issueId ?? (request.mode === "create" ? request.issueId : undefined),
8071
+ issueUrl: current?.issueUrl ?? (request.mode === "create" ? request.issueUrl : undefined),
8072
+ intent: request.intent,
8073
+ requirements: request.requirements,
8074
+ design: request.design,
8075
+ references: request.references,
8076
+ phases,
8077
+ status: nextPlanStatus(current),
8078
+ execution: current?.execution,
8079
+ createdAt,
8080
+ updatedAt
8081
+ };
8082
+ }
8083
+ function reconcilePhase(current, draft, plan) {
8084
+ if (!current)
8085
+ return {
8086
+ ...draft,
8087
+ revision: 0,
8088
+ status: "pending",
8089
+ gate: { phaseRevision: 0, status: "pending", results: [] },
8090
+ tasks: draft.tasks.map(newTask)
8091
+ };
8092
+ if (current.status === "completed") {
8093
+ if (JSON.stringify(phaseContent(current)) !== JSON.stringify(draft))
8094
+ throw new Error(`Completed phase ${current.id} cannot be changed.`);
8095
+ return current;
8096
+ }
8097
+ const byId = new Map(current.tasks.map((task) => [task.id, task]));
8098
+ for (const task of current.tasks) {
8099
+ if (!draft.tasks.some((candidate) => candidate.id === task.id) && (task.status === "completed" || task.status === "in_progress"))
8100
+ throw new Error(`${task.status === "completed" ? "Completed" : "Active"} task ${task.id} cannot be removed.`);
8101
+ }
8102
+ const tasks = draft.tasks.map((task) => reconcileTask(byId.get(task.id), task));
8103
+ const changed = JSON.stringify(phaseContent(current)) !== JSON.stringify(draft);
8104
+ const revision = changed ? current.revision + 1 : current.revision;
8105
+ const wasBlocked = current.status === "blocked";
8106
+ return {
8107
+ ...current,
8108
+ ...draft,
8109
+ revision,
8110
+ tasks,
8111
+ status: wasBlocked ? plan?.execution?.active ? "in_progress" : "pending" : current.status,
8112
+ gate: changed || wasBlocked ? { phaseRevision: revision, status: "stale", results: [] } : current.gate,
8113
+ blocker: wasBlocked ? undefined : current.blocker
8114
+ };
8115
+ }
8116
+ function reconcileTask(current, draft) {
8117
+ if (!current)
8118
+ return newTask(draft);
8119
+ const changed = JSON.stringify(taskContent(current)) !== JSON.stringify(draft);
8120
+ if ((current.status === "completed" || current.status === "in_progress") && changed)
8121
+ throw new Error(`${current.status === "completed" ? "Completed" : "Active"} task ${current.id} cannot be changed.`);
8122
+ if (current.status === "completed" || current.status === "in_progress")
8123
+ return current;
8124
+ return {
8125
+ ...current,
8126
+ ...draft,
8127
+ revision: changed ? current.revision + 1 : current.revision,
8128
+ status: current.status === "blocked" ? "pending" : current.status,
8129
+ owner: current.status === "blocked" ? undefined : current.owner,
8130
+ executionId: current.status === "blocked" ? undefined : current.executionId,
8131
+ blocker: current.status === "blocked" ? undefined : current.blocker
8132
+ };
8133
+ }
8134
+ function newTask(draft) {
8135
+ return { ...draft, revision: 0, status: "pending" };
8136
+ }
8137
+ function phaseContent(phase) {
8138
+ return {
8139
+ id: phase.id,
8140
+ title: phase.title,
8141
+ objective: phase.objective,
8142
+ dependencies: phase.dependencies,
8143
+ tasks: phase.tasks.map(taskContent)
8144
+ };
8145
+ }
8146
+ function taskContent(task) {
8147
+ return { id: task.id, title: task.title, dependencies: task.dependencies };
8148
+ }
8149
+ function nextPlanStatus(current) {
8150
+ if (!current)
8151
+ return "draft";
8152
+ if (current.execution?.active)
8153
+ return current.status === "blocked" ? "in_progress" : current.status;
8154
+ return current.status === "ready" || current.status === "blocked" ? "draft" : current.status;
8155
+ }
8156
+ function assertCompleteRevision(document, briefs) {
8157
+ const errors = [...validatePlanDocument(document, { strict: true }), ...validatePlanBriefs(document, briefs)].filter((issue) => issue.severity === "error");
8158
+ if (errors.length)
8159
+ throw new Error(`Invalid plan revision: ${errors.map((issue) => issue.message).join(" ")}`);
8160
+ }
8161
+ function sha256(value) {
8162
+ return createHash2("sha256").update(value).digest("hex");
8163
+ }
7834
8164
  async function assertContained(root, candidate) {
7835
8165
  const canonicalRoot = await realpath3(root);
7836
8166
  let canonicalCandidate;
@@ -7862,7 +8192,10 @@ function stripDate(value) {
7862
8192
  return value.replace(/^\d{6}-/, "");
7863
8193
  }
7864
8194
  function titleFromSlug(value) {
7865
- return normalizeSlug(value).replace(/^(?:[a-z]+-\d+-)/, "").split("-").map((part) => part[0].toUpperCase() + part.slice(1)).join(" ");
8195
+ const slug = normalizeSlug(value).replace(/^(?:[a-z]+-\d+-)/, "");
8196
+ return slug.replace(/(^|-)([a-z])/g, (_match, separator, letter) => {
8197
+ return `${separator ? " " : ""}${letter.toUpperCase()}`;
8198
+ });
7866
8199
  }
7867
8200
  // src/plan/reviews.ts
7868
8201
  import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile2 } from "node:fs/promises";
@@ -1,4 +1,4 @@
1
1
  import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
2
- import type { ModeController } from '../modes';
3
- export declare function registerPlanCommand(pi: ExtensionAPI, modes: ModeController): void;
2
+ /** Register the plan skill alias without interpreting its arguments. */
3
+ export declare function registerPlanCommand(pi: ExtensionAPI): void;
4
4
  //# sourceMappingURL=plan.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"plan.d.ts","sourceRoot":"","sources":["../../src/commands/plan.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAcpE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAI/C,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE,cAAc,GAAG,IAAI,CAKjF"}
1
+ {"version":3,"file":"plan.d.ts","sourceRoot":"","sources":["../../src/commands/plan.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AACpE,wEAAwE;AACxE,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAU1D"}