@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.
- package/README.md +8 -6
- package/agents/diffpi-copilot.md +6 -3
- package/agents/diffpi-orchestrator.md +6 -4
- package/agents/diffpi-planner.md +3 -3
- package/agents/diffpi-reviewer.md +3 -3
- package/agents/diffpi-worker.md +1 -1
- package/dist/assets.d.ts +0 -1
- package/dist/assets.d.ts.map +1 -1
- package/dist/cli.js +417 -84
- package/dist/commands/plan.d.ts +2 -2
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/commands/review.d.ts +2 -3
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/environment.d.ts +2 -0
- package/dist/environment.d.ts.map +1 -1
- package/dist/extensions/index.js +1114 -3634
- package/dist/index.js +437 -99
- package/dist/plan/markdown.d.ts +4 -1
- package/dist/plan/markdown.d.ts.map +1 -1
- package/dist/{tools/plan-schema.d.ts → plan/schema.d.ts} +113 -75
- package/dist/plan/schema.d.ts.map +1 -0
- package/dist/plan/store.d.ts +39 -1
- package/dist/plan/store.d.ts.map +1 -1
- package/dist/plan/types.d.ts +27 -3
- package/dist/plan/types.d.ts.map +1 -1
- package/dist/review/local-reviews.d.ts +31 -0
- package/dist/review/local-reviews.d.ts.map +1 -0
- package/dist/review/review-markdown.d.ts.map +1 -1
- package/dist/tools/index.js +836 -410
- package/dist/tools/plan.d.ts.map +1 -1
- package/dist/tools/review.d.ts +6 -0
- package/dist/tools/review.d.ts.map +1 -1
- package/package.json +2 -4
- package/skills/plan/SKILL.md +8 -14
- package/skills/plan/references/design-quality.md +41 -0
- package/skills/plan/references/workflows/annotate.md +11 -0
- package/skills/plan/references/workflows/finalize.md +7 -0
- package/skills/plan/references/workflows/go.md +8 -0
- package/skills/plan/references/workflows/help.md +10 -0
- package/skills/plan/references/workflows/init.md +5 -0
- package/skills/plan/references/workflows/new.md +7 -0
- package/skills/plan/references/workflows/update.md +7 -0
- package/skills/review/SKILL.md +7 -9
- package/skills/review/references/review-standards.md +8 -1
- package/skills/review/references/workflows/address.md +4 -4
- package/skills/review/references/workflows/help.md +8 -2
- package/skills/review/references/workflows/open.md +1 -1
- package/skills/review/references/workflows/status.md +3 -0
- package/templates/plan/PLAN.md +2 -2
- package/templates/plan/implementation.md +16 -10
- package/dist/commands.d.ts +0 -22
- package/dist/commands.d.ts.map +0 -1
- package/dist/extensions/cmdtsx.d.ts +0 -2
- package/dist/extensions/cmdtsx.d.ts.map +0 -1
- package/dist/tools/plan-schema.d.ts.map +0 -1
- package/dist/workflow.d.ts +0 -33
- package/dist/workflow.d.ts.map +0 -1
- package/skills/review/references/workflows/auto.md +0 -18
- package/skills/review/references/workflows/complete.md +0 -7
- package/skills/review/references/workflows/edit.md +0 -7
- package/skills/review/references/workflows/merge.md +0 -6
- package/skills/review/references/workflows/new.md +0 -19
- package/skills/review/references/workflows/publish.md +0 -8
- package/workflows/plan/annotate.md +0 -13
- package/workflows/plan/finalize.md +0 -15
- package/workflows/plan/go.md +0 -20
- package/workflows/plan/help.md +0 -20
- package/workflows/plan/init.md +0 -13
- package/workflows/plan/new.md +0 -20
- package/workflows/plan/update.md +0 -18
package/dist/cli.js
CHANGED
|
@@ -2383,7 +2383,8 @@ function appendBounded(current, next) {
|
|
|
2383
2383
|
}
|
|
2384
2384
|
|
|
2385
2385
|
// src/plan/store.ts
|
|
2386
|
-
import {
|
|
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.
|
|
7280
|
-
add("
|
|
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
|
|
7293
|
-
|
|
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
|
|
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
|
-
|
|
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((
|
|
7540
|
-
id,
|
|
7541
|
-
title,
|
|
7542
|
-
objective,
|
|
7543
|
-
dependencies,
|
|
7544
|
-
tasks: tasks.map(({ id
|
|
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
|
|
7782
|
+
function bulletList(values, empty = "") {
|
|
7659
7783
|
if (!values.length)
|
|
7660
|
-
return
|
|
7661
|
-
|
|
7784
|
+
return empty;
|
|
7785
|
+
return values.flatMap((value) => {
|
|
7662
7786
|
const [first = "", ...continuations] = value.split(`
|
|
7663
7787
|
`);
|
|
7664
|
-
return [
|
|
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
|
|
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
|
|
7864
|
+
title,
|
|
7739
7865
|
intent: input.intent?.trim() || "<!-- Describe the intended outcome. -->",
|
|
7740
|
-
issue_id: input.issueId?.trim() || "
|
|
7741
|
-
issue_url: input.issueUrl?.trim() || "
|
|
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
|
-
|
|
7747
|
-
|
|
7748
|
-
|
|
7749
|
-
|
|
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
|
-
|
|
7762
|
-
|
|
7763
|
-
if (
|
|
7764
|
-
|
|
7765
|
-
|
|
7766
|
-
|
|
7767
|
-
|
|
7768
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
7811
|
-
|
|
7812
|
-
|
|
7813
|
-
|
|
7814
|
-
|
|
7815
|
-
|
|
7816
|
-
|
|
7817
|
-
|
|
7818
|
-
|
|
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
|
|
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
|
-
|
|
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";
|
package/dist/commands/plan.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
2
|
-
|
|
3
|
-
export declare function registerPlanCommand(pi: ExtensionAPI
|
|
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;
|
|
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"}
|