@difflab/pi 0.5.0 → 0.6.0-rc.202609270756.7689e22
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 +1147 -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 +879 -420
- package/dist/tools/plan.d.ts.map +1 -1
- package/dist/tools/review.d.ts +9 -1
- 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/auto.md +4 -4
- package/skills/review/references/workflows/complete.md +2 -2
- package/skills/review/references/workflows/edit.md +3 -3
- package/skills/review/references/workflows/help.md +8 -2
- package/skills/review/references/workflows/merge.md +2 -2
- package/skills/review/references/workflows/new.md +5 -5
- package/skills/review/references/workflows/open.md +1 -1
- package/skills/review/references/workflows/publish.md +2 -2
- 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/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/index.js
CHANGED
|
@@ -605,6 +605,16 @@ function diffpiLaunchName(cwd, workflow) {
|
|
|
605
605
|
}
|
|
606
606
|
async function openFileAdjacent(path, opts) {
|
|
607
607
|
const env = opts.env ?? process.env;
|
|
608
|
+
const mux = detectMux(env);
|
|
609
|
+
if (mux !== "none") {
|
|
610
|
+
const editor = await resolveEditor(env);
|
|
611
|
+
if (editor) {
|
|
612
|
+
const command = persistentEditorArgs(editor, path, opts.cwd, env.SHELL);
|
|
613
|
+
const opened = await openMuxTab(mux, command, opts.cwd, opts.name ?? "plan", [editor, path].join(" "), command);
|
|
614
|
+
if (opened)
|
|
615
|
+
return opened;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
608
618
|
const ide = detectIde(env);
|
|
609
619
|
if (ide !== "unknown") {
|
|
610
620
|
const ideCommand = { zed: "zed", vscode: "code", cursor: "cursor", windsurf: "windsurf", jetbrains: "idea" }[ide];
|
|
@@ -614,16 +624,6 @@ async function openFileAdjacent(path, opts) {
|
|
|
614
624
|
return { launched: true, via: ide, command: `${ideCommand} ${path}` };
|
|
615
625
|
}
|
|
616
626
|
}
|
|
617
|
-
const mux = detectMux(env);
|
|
618
|
-
if (mux !== "none") {
|
|
619
|
-
const editor = await resolveEditor(env);
|
|
620
|
-
if (editor) {
|
|
621
|
-
const command = [editor, path];
|
|
622
|
-
const opened = await openMuxTab(mux, command, opts.cwd, opts.name ?? "plan", command.join(" "));
|
|
623
|
-
if (opened)
|
|
624
|
-
return opened;
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
627
|
return { launched: false, via: "print", command: path, reason: "No supported IDE or multiplexer was available." };
|
|
628
628
|
}
|
|
629
629
|
async function openInNewTab(command, opts) {
|
|
@@ -658,10 +658,15 @@ async function openInNewTab(command, opts) {
|
|
|
658
658
|
function screenWindowArgs(command, cwd, name) {
|
|
659
659
|
return ["-X", "screen", "-t", name, "sh", "-lc", 'cd -- "$1" && shift && exec "$@"', "sh", cwd, ...command];
|
|
660
660
|
}
|
|
661
|
+
function persistentEditorArgs(editor, path, cwd, shell = "sh") {
|
|
662
|
+
return ["bash", "-lc", 'cd -- "$1" && shift && "$@"; exec "$0" -i', shell, cwd, editor, path];
|
|
663
|
+
}
|
|
661
664
|
async function resolveEditor(env) {
|
|
662
|
-
const configured
|
|
663
|
-
|
|
664
|
-
|
|
665
|
+
for (const configured of [env.EDITOR, env.VISUAL]) {
|
|
666
|
+
const editor = configured?.trim();
|
|
667
|
+
if (editor && await findExecutable(editor))
|
|
668
|
+
return editor;
|
|
669
|
+
}
|
|
665
670
|
for (const candidate of ["hx", "nvim", "vim"])
|
|
666
671
|
if (await findExecutable(candidate))
|
|
667
672
|
return candidate;
|
|
@@ -678,7 +683,7 @@ async function packageVersion() {
|
|
|
678
683
|
throw new Error("Cannot resolve the installed @difflab/pi version.");
|
|
679
684
|
return value.version;
|
|
680
685
|
}
|
|
681
|
-
async function openMuxTab(mux, command, cwd, name, printable) {
|
|
686
|
+
async function openMuxTab(mux, command, cwd, name, printable, tmuxCommand) {
|
|
682
687
|
if (mux === "zellij" && await findExecutable("zellij")) {
|
|
683
688
|
const result = await run("zellij", ["action", "new-tab", "--cwd", cwd, "--name", name, "--", ...command]);
|
|
684
689
|
if (result.code === 0)
|
|
@@ -688,7 +693,7 @@ async function openMuxTab(mux, command, cwd, name, printable) {
|
|
|
688
693
|
return { launched: true, via: "zellij-run", command: printable };
|
|
689
694
|
}
|
|
690
695
|
if (mux === "tmux" && await findExecutable("tmux")) {
|
|
691
|
-
const result = await run("tmux", ["new-window", "-c", cwd, "-n", name, printable]);
|
|
696
|
+
const result = tmuxCommand ? await run("tmux", ["new-window", "-c", cwd, "-n", name, ...tmuxCommand]) : await run("tmux", ["new-window", "-c", cwd, "-n", name, printable]);
|
|
692
697
|
if (result.code === 0)
|
|
693
698
|
return { launched: true, via: "tmux", command: printable };
|
|
694
699
|
}
|
|
@@ -1433,7 +1438,7 @@ function renderThreadArtifact(title, target, threads, options = {}) {
|
|
|
1433
1438
|
}
|
|
1434
1439
|
lines.push("## Source comments", "");
|
|
1435
1440
|
for (const thread of threads) {
|
|
1436
|
-
lines.push(`### ${thread.file ?? "review"}:${thread.line ?? "n/a"} — ${thread.author ?? "unknown"}`, "", `Thread: ${thread.id}`, `Status: ${thread.resolved ? "resolved" : thread.addressed ? "addressed" : "open"}`, "", thread.body, "", "---", "");
|
|
1441
|
+
lines.push(`### ${thread.file ?? "review"}:${thread.line ?? "n/a"} — ${thread.author ?? "unknown"}`, "", `Thread: ${thread.id}`, `Status: ${thread.resolved ? "resolved" : thread.addressed ? "addressed" : "open"}`, "", "**Source comment**", "", thread.body, "", "**Recorded response**", "", thread.reply?.trim() || "_No response recorded._", "", "---", "");
|
|
1437
1442
|
}
|
|
1438
1443
|
return `${lines.join(`
|
|
1439
1444
|
`).trimEnd()}
|
|
@@ -2629,7 +2634,8 @@ function parseJsonObject(content, path) {
|
|
|
2629
2634
|
throw new Error(`Expected valid JSON object in ${path}.`);
|
|
2630
2635
|
}
|
|
2631
2636
|
// src/plan/store.ts
|
|
2632
|
-
import {
|
|
2637
|
+
import { createHash as createHash3, randomUUID as randomUUID2 } from "node:crypto";
|
|
2638
|
+
import { mkdir as mkdir7, readdir as readdir2, readFile as readFile12, realpath as realpath4, rename as rename3, rm as rm2 } from "node:fs/promises";
|
|
2633
2639
|
import { basename as basename6, dirname as dirname8, join as join16, resolve as resolve4, sep } from "node:path";
|
|
2634
2640
|
|
|
2635
2641
|
// src/extensions/fsx.ts
|
|
@@ -7386,11 +7392,13 @@ function validatePlanDocument(plan, options = {}) {
|
|
|
7386
7392
|
if (!taskIds.has(dependency))
|
|
7387
7393
|
add("dangling-dependency", "error", `Task ${task.id} depends on missing ${dependency}.`);
|
|
7388
7394
|
}
|
|
7389
|
-
if (options.strict && task.
|
|
7390
|
-
add("
|
|
7395
|
+
if (options.strict && isPlaceholder(task.title))
|
|
7396
|
+
add("placeholder", "error", `Task ${task.id} has placeholder content.`, task.id);
|
|
7391
7397
|
}
|
|
7392
7398
|
if (options.strict && phase.tasks.length === 0)
|
|
7393
7399
|
add("empty-phase", "error", `Phase ${phase.id} has no tasks.`, phase.id);
|
|
7400
|
+
if (options.strict && (isPlaceholder(phase.title) || isPlaceholder(phase.objective)))
|
|
7401
|
+
add("placeholder", "error", `Phase ${phase.id} has placeholder content.`, phase.id);
|
|
7394
7402
|
}
|
|
7395
7403
|
for (const cycle of dependencyCycles(plan))
|
|
7396
7404
|
add("dependency-cycle", "error", `Dependency cycle: ${cycle.join(" -> ")}.`);
|
|
@@ -7399,10 +7407,131 @@ function validatePlanDocument(plan, options = {}) {
|
|
|
7399
7407
|
add("design-length", "warning", `Design is ${designWords} words; prefer 300 or fewer.`);
|
|
7400
7408
|
if (options.strict && designWords > 800)
|
|
7401
7409
|
add("design-too-long", "error", `Design is ${designWords} words; finalization allows at most 800.`);
|
|
7402
|
-
if (options.strict
|
|
7403
|
-
|
|
7410
|
+
if (options.strict) {
|
|
7411
|
+
if (plan.phases.length === 0)
|
|
7412
|
+
add("no-phases", "error", "Finalization requires at least one phase.");
|
|
7413
|
+
if (isPlaceholder(plan.intent))
|
|
7414
|
+
add("placeholder", "error", "Intent must contain concrete content.", "intent");
|
|
7415
|
+
if (!plan.requirements.length || plan.requirements.some(isPlaceholder))
|
|
7416
|
+
add("requirements", "error", "Requirements must contain concrete content.", "requirements");
|
|
7417
|
+
for (const [key, value] of Object.entries(plan.design)) {
|
|
7418
|
+
if (isPlaceholder(value))
|
|
7419
|
+
add("design", "error", `Design/${key} must contain concrete content.`, `design.${key}`);
|
|
7420
|
+
}
|
|
7421
|
+
}
|
|
7422
|
+
return issues;
|
|
7423
|
+
}
|
|
7424
|
+
function validatePlanBriefs(plan, briefs) {
|
|
7425
|
+
const issues = [];
|
|
7426
|
+
const phaseIds = plan.phases.map((phase) => phase.id);
|
|
7427
|
+
const briefIds = briefs.map((brief) => brief.phaseId);
|
|
7428
|
+
if (!sameIds(phaseIds, briefIds))
|
|
7429
|
+
issues.push({
|
|
7430
|
+
code: "brief-phase-ids",
|
|
7431
|
+
severity: "error",
|
|
7432
|
+
message: `Brief phase IDs must exactly match plan phases in order: ${phaseIds.join(", ")}.`
|
|
7433
|
+
});
|
|
7434
|
+
for (const [index, phase] of plan.phases.entries()) {
|
|
7435
|
+
const brief = briefs[index];
|
|
7436
|
+
if (!brief || brief.phaseId !== phase.id)
|
|
7437
|
+
continue;
|
|
7438
|
+
if (isPlaceholder(brief.summary))
|
|
7439
|
+
issues.push({ code: "brief-placeholder", severity: "error", message: `Brief ${phase.id} needs a summary.` });
|
|
7440
|
+
const taskIds = phase.tasks.map((task) => task.id);
|
|
7441
|
+
const briefTaskIds = brief.tasks.map((task) => task.taskId);
|
|
7442
|
+
if (!sameIds(taskIds, briefTaskIds))
|
|
7443
|
+
issues.push({
|
|
7444
|
+
code: "brief-task-ids",
|
|
7445
|
+
severity: "error",
|
|
7446
|
+
message: `Brief ${phase.id} task IDs must exactly match phase tasks in order: ${taskIds.join(", ")}.`
|
|
7447
|
+
});
|
|
7448
|
+
for (const task of brief.tasks) {
|
|
7449
|
+
for (const [field, values] of [
|
|
7450
|
+
["ordered steps", task.steps],
|
|
7451
|
+
["file scopes", task.fileScopes],
|
|
7452
|
+
["acceptance criteria", task.acceptanceCriteria]
|
|
7453
|
+
]) {
|
|
7454
|
+
if (!values.length || values.some(isPlaceholder))
|
|
7455
|
+
issues.push({
|
|
7456
|
+
code: "brief-incomplete",
|
|
7457
|
+
severity: "error",
|
|
7458
|
+
message: `Brief task ${task.taskId} needs concrete ${field}.`,
|
|
7459
|
+
path: task.taskId
|
|
7460
|
+
});
|
|
7461
|
+
}
|
|
7462
|
+
}
|
|
7463
|
+
for (const [field, values] of [
|
|
7464
|
+
["API changes", brief.apiChanges],
|
|
7465
|
+
["libraries", brief.libraries],
|
|
7466
|
+
["constraints", brief.constraints]
|
|
7467
|
+
]) {
|
|
7468
|
+
if (values.some(isPlaceholder))
|
|
7469
|
+
issues.push({
|
|
7470
|
+
code: "brief-placeholder",
|
|
7471
|
+
severity: "error",
|
|
7472
|
+
message: `Brief ${phase.id} has placeholder ${field}.`,
|
|
7473
|
+
path: phase.id
|
|
7474
|
+
});
|
|
7475
|
+
}
|
|
7476
|
+
}
|
|
7404
7477
|
return issues;
|
|
7405
7478
|
}
|
|
7479
|
+
function renderImplementationBrief(planRevision, ordinal, phase, brief) {
|
|
7480
|
+
const marker = { schemaVersion: 1, planRevision, ordinal, phaseId: phase.id };
|
|
7481
|
+
const tasks = phase.tasks.map((task, index) => {
|
|
7482
|
+
const details = brief.tasks[index];
|
|
7483
|
+
if (!details || details.taskId !== task.id)
|
|
7484
|
+
throw new Error(`Missing implementation details for ${task.id}.`);
|
|
7485
|
+
return `<!-- diffpi-brief-task: ${json({ id: task.id })} -->
|
|
7486
|
+
### Task: ${task.title}
|
|
7487
|
+
|
|
7488
|
+
#### Ordered Steps
|
|
7489
|
+
|
|
7490
|
+
${bulletList(details.steps)}
|
|
7491
|
+
|
|
7492
|
+
#### File Scopes
|
|
7493
|
+
|
|
7494
|
+
${bulletList(details.fileScopes)}
|
|
7495
|
+
|
|
7496
|
+
#### Acceptance Criteria
|
|
7497
|
+
|
|
7498
|
+
${bulletList(details.acceptanceCriteria)}
|
|
7499
|
+
|
|
7500
|
+
<!-- /diffpi-brief-task -->`;
|
|
7501
|
+
}).join(`
|
|
7502
|
+
|
|
7503
|
+
`);
|
|
7504
|
+
return `<!-- diffpi-implementation: ${json(marker)} -->
|
|
7505
|
+
# Phase ${ordinal}: ${phase.title}
|
|
7506
|
+
|
|
7507
|
+
- **Phase ID:** ${phase.id}
|
|
7508
|
+
- **Plan Revision:** ${planRevision}
|
|
7509
|
+
|
|
7510
|
+
## Summary
|
|
7511
|
+
|
|
7512
|
+
${brief.summary}
|
|
7513
|
+
|
|
7514
|
+
## Objective
|
|
7515
|
+
|
|
7516
|
+
${phase.objective}
|
|
7517
|
+
|
|
7518
|
+
## Tasks
|
|
7519
|
+
|
|
7520
|
+
${tasks}
|
|
7521
|
+
|
|
7522
|
+
## API Changes
|
|
7523
|
+
|
|
7524
|
+
${bulletList(brief.apiChanges, "None.")}
|
|
7525
|
+
|
|
7526
|
+
## Libraries and Algorithms
|
|
7527
|
+
|
|
7528
|
+
${bulletList(brief.libraries, "None.")}
|
|
7529
|
+
|
|
7530
|
+
## Implementation Constraints
|
|
7531
|
+
|
|
7532
|
+
${bulletList(brief.constraints, "None.")}
|
|
7533
|
+
`;
|
|
7534
|
+
}
|
|
7406
7535
|
function renderPlanDocument(plan, previousSource) {
|
|
7407
7536
|
if (previousSource && canPatch(previousSource, plan))
|
|
7408
7537
|
return patchMarkers(previousSource, plan);
|
|
@@ -7411,6 +7540,8 @@ function renderPlanDocument(plan, previousSource) {
|
|
|
7411
7540
|
id: plan.id,
|
|
7412
7541
|
revision: plan.revision,
|
|
7413
7542
|
branch: plan.branch,
|
|
7543
|
+
issueId: plan.issueId,
|
|
7544
|
+
issueUrl: plan.issueUrl,
|
|
7414
7545
|
status: plan.status,
|
|
7415
7546
|
execution: plan.execution,
|
|
7416
7547
|
createdAt: plan.createdAt,
|
|
@@ -7419,6 +7550,8 @@ function renderPlanDocument(plan, previousSource) {
|
|
|
7419
7550
|
const metadata = import_mdast_builder.list("unordered", [
|
|
7420
7551
|
["Plan ID", plan.id],
|
|
7421
7552
|
["Branch", plan.branch],
|
|
7553
|
+
...plan.issueId ? [["Issue", plan.issueId]] : [],
|
|
7554
|
+
...plan.issueUrl ? [["Issue URL", plan.issueUrl]] : [],
|
|
7422
7555
|
["Status", plan.status],
|
|
7423
7556
|
["Revision", String(plan.revision)]
|
|
7424
7557
|
].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}`)]))));
|
|
@@ -7440,7 +7573,7 @@ function renderPlanDocument(plan, previousSource) {
|
|
|
7440
7573
|
import_mdast_builder.heading(3, import_mdast_builder.text("Consequences")),
|
|
7441
7574
|
import_mdast_builder.html(plan.design.consequences || "<!-- Describe trade-offs and limitations. -->"),
|
|
7442
7575
|
import_mdast_builder.heading(2, import_mdast_builder.text("Implementation")),
|
|
7443
|
-
...plan.phases.length ? plan.phases.flatMap((phase) => [import_mdast_builder.html(renderPhase(phase))]) : [import_mdast_builder.html("<!-- Add phases with
|
|
7576
|
+
...plan.phases.length ? plan.phases.flatMap((phase) => [import_mdast_builder.html(renderPhase(phase))]) : [import_mdast_builder.html("<!-- Add phases with plan_apply_revision. -->")],
|
|
7444
7577
|
import_mdast_builder.heading(2, import_mdast_builder.text("References")),
|
|
7445
7578
|
references
|
|
7446
7579
|
]);
|
|
@@ -7514,10 +7647,7 @@ function renderTask(task) {
|
|
|
7514
7647
|
const checked = task.status === "completed" || task.status === "skipped" ? "x" : " ";
|
|
7515
7648
|
return `<!-- diffpi-task: ${json(marker)} -->
|
|
7516
7649
|
- [${checked}] **${task.title}**
|
|
7517
|
-
${taskList("Steps", task.steps ?? [])}
|
|
7518
7650
|
- Dependencies: ${list3(task.dependencies)}
|
|
7519
|
-
${taskList("File scopes", task.fileScopes)}
|
|
7520
|
-
${taskList("Acceptance criteria", task.acceptanceCriteria)}
|
|
7521
7651
|
<!-- /diffpi-task -->`;
|
|
7522
7652
|
}
|
|
7523
7653
|
function parsePhases(input, ref) {
|
|
@@ -7567,10 +7697,7 @@ function parseTasks(input, ref) {
|
|
|
7567
7697
|
return {
|
|
7568
7698
|
...marker,
|
|
7569
7699
|
title,
|
|
7570
|
-
|
|
7571
|
-
dependencies: parseListValue(body, "Dependencies"),
|
|
7572
|
-
fileScopes: parseListValue(body, "File scopes"),
|
|
7573
|
-
acceptanceCriteria: parseListValue(body, "Acceptance criteria")
|
|
7700
|
+
dependencies: parseListValue(body, "Dependencies")
|
|
7574
7701
|
};
|
|
7575
7702
|
});
|
|
7576
7703
|
assertUniqueIds(tasks.map((task) => task.id), "task id");
|
|
@@ -7597,6 +7724,8 @@ function patchMarkers(source, plan) {
|
|
|
7597
7724
|
id: plan.id,
|
|
7598
7725
|
revision: plan.revision,
|
|
7599
7726
|
branch: plan.branch,
|
|
7727
|
+
issueId: plan.issueId,
|
|
7728
|
+
issueUrl: plan.issueUrl,
|
|
7600
7729
|
status: plan.status,
|
|
7601
7730
|
execution: plan.execution,
|
|
7602
7731
|
createdAt: plan.createdAt,
|
|
@@ -7646,19 +7775,12 @@ function contentShape(plan) {
|
|
|
7646
7775
|
requirements: plan.requirements,
|
|
7647
7776
|
design: plan.design,
|
|
7648
7777
|
references: plan.references,
|
|
7649
|
-
phases: plan.phases.map((
|
|
7650
|
-
id,
|
|
7651
|
-
title,
|
|
7652
|
-
objective,
|
|
7653
|
-
dependencies,
|
|
7654
|
-
tasks: tasks.map(({ id
|
|
7655
|
-
id: taskId,
|
|
7656
|
-
title: taskTitle,
|
|
7657
|
-
steps,
|
|
7658
|
-
dependencies: taskDependencies,
|
|
7659
|
-
fileScopes,
|
|
7660
|
-
acceptanceCriteria
|
|
7661
|
-
}))
|
|
7778
|
+
phases: plan.phases.map((phase) => ({
|
|
7779
|
+
id: phase.id,
|
|
7780
|
+
title: phase.title,
|
|
7781
|
+
objective: phase.objective,
|
|
7782
|
+
dependencies: phase.dependencies,
|
|
7783
|
+
tasks: phase.tasks.map(({ id, title, dependencies }) => ({ id, title, dependencies }))
|
|
7662
7784
|
}))
|
|
7663
7785
|
};
|
|
7664
7786
|
}
|
|
@@ -7758,6 +7880,13 @@ function assertUniqueIds(ids, label) {
|
|
|
7758
7880
|
function cleanPlaceholder(value) {
|
|
7759
7881
|
return value.replace(/<!--[^]*?-->/g, "").trim();
|
|
7760
7882
|
}
|
|
7883
|
+
function isPlaceholder(value) {
|
|
7884
|
+
const normalized = cleanPlaceholder(value).trim();
|
|
7885
|
+
return normalized.length === 0 || /^(?:todo|tbd|none|n\/a)$/i.test(normalized) || /define during implementation|describe the|add requirements|add implementation/i.test(normalized);
|
|
7886
|
+
}
|
|
7887
|
+
function sameIds(expected, actual) {
|
|
7888
|
+
return expected.length === actual.length && expected.every((value, index) => actual[index] === value);
|
|
7889
|
+
}
|
|
7761
7890
|
function parseMarker(value, label) {
|
|
7762
7891
|
try {
|
|
7763
7892
|
return JSON.parse(value);
|
|
@@ -7765,15 +7894,14 @@ function parseMarker(value, label) {
|
|
|
7765
7894
|
throw new Error(`Malformed ${label} marker JSON.`);
|
|
7766
7895
|
}
|
|
7767
7896
|
}
|
|
7768
|
-
function
|
|
7897
|
+
function bulletList(values, empty = "") {
|
|
7769
7898
|
if (!values.length)
|
|
7770
|
-
return
|
|
7771
|
-
|
|
7899
|
+
return empty;
|
|
7900
|
+
return values.flatMap((value) => {
|
|
7772
7901
|
const [first = "", ...continuations] = value.split(`
|
|
7773
7902
|
`);
|
|
7774
|
-
return [
|
|
7775
|
-
})
|
|
7776
|
-
return [` - ${label}:`, ...items].join(`
|
|
7903
|
+
return [`- ${first}`, ...continuations.map((line) => ` ${line}`)];
|
|
7904
|
+
}).join(`
|
|
7777
7905
|
`);
|
|
7778
7906
|
}
|
|
7779
7907
|
function list3(values) {
|
|
@@ -7820,6 +7948,15 @@ async function resolvePlan(cwd, query, filters = {}, options = {}) {
|
|
|
7820
7948
|
}
|
|
7821
7949
|
function createPlanStore(options = {}) {
|
|
7822
7950
|
const now = options.now ?? (() => new Date);
|
|
7951
|
+
const read = async (cwd, query, filters) => {
|
|
7952
|
+
const result = await resolvePlan(cwd, query, filters, options);
|
|
7953
|
+
if (!result.record) {
|
|
7954
|
+
if (result.ambiguous)
|
|
7955
|
+
throw new Error(`Plan query "${query}" is ambiguous: ${result.candidates.map((item) => item.id).join(", ")}.`);
|
|
7956
|
+
throw new Error(`Plan "${query}" was not found.`);
|
|
7957
|
+
}
|
|
7958
|
+
return result.record;
|
|
7959
|
+
};
|
|
7823
7960
|
return {
|
|
7824
7961
|
context(cwd, query, filters) {
|
|
7825
7962
|
return resolvePlan(cwd, query, filters, options);
|
|
@@ -7828,57 +7965,77 @@ function createPlanStore(options = {}) {
|
|
|
7828
7965
|
const root = await plansDir(input.cwd, options.homeDir);
|
|
7829
7966
|
const id = planRecordName(input.shortSlug, now());
|
|
7830
7967
|
const dir = join16(root, id);
|
|
7831
|
-
await
|
|
7832
|
-
try {
|
|
7833
|
-
await mkdir7(dir);
|
|
7834
|
-
} catch (error) {
|
|
7835
|
-
if (error.code === "EEXIST")
|
|
7836
|
-
throw new Error(`Plan ${id} already exists.`);
|
|
7837
|
-
throw error;
|
|
7838
|
-
}
|
|
7968
|
+
await createPlanDirectory(root, dir, id);
|
|
7839
7969
|
try {
|
|
7840
7970
|
const timestamp = now().toISOString();
|
|
7841
7971
|
const template = await loadTemplate("plan/PLAN", {
|
|
7842
7972
|
homeDir: options.homeDir,
|
|
7843
7973
|
bundledDir: options.bundledTemplatesDir
|
|
7844
7974
|
});
|
|
7975
|
+
const title = input.title?.trim() || titleFromSlug(input.shortSlug);
|
|
7845
7976
|
const source = renderTemplate(template.content, {
|
|
7846
7977
|
id,
|
|
7847
7978
|
branch: input.branch,
|
|
7848
|
-
title
|
|
7979
|
+
title,
|
|
7849
7980
|
intent: input.intent?.trim() || "<!-- Describe the intended outcome. -->",
|
|
7850
|
-
issue_id: input.issueId?.trim() || "
|
|
7851
|
-
issue_url: input.issueUrl?.trim() || "
|
|
7981
|
+
issue_id: input.issueId?.trim() || "",
|
|
7982
|
+
issue_url: input.issueUrl?.trim() || "",
|
|
7852
7983
|
created_at: timestamp,
|
|
7853
7984
|
updated_at: timestamp
|
|
7854
7985
|
});
|
|
7855
7986
|
const document = parsePlanDocument(source, join16(dir, "PLAN.md"));
|
|
7856
|
-
|
|
7857
|
-
|
|
7858
|
-
|
|
7859
|
-
|
|
7860
|
-
planPath: join16(dir, "PLAN.md"),
|
|
7861
|
-
logPath: join16(dir, "logs.txt"),
|
|
7862
|
-
implementationDir: join16(dir, "implementation"),
|
|
7863
|
-
document,
|
|
7864
|
-
source
|
|
7865
|
-
};
|
|
7987
|
+
const authoringRequest = input.request ?? { kind: "user", text: input.intent?.trim() || `Initialize phase-less plan ${title}.` };
|
|
7988
|
+
await writeRevisionSnapshot(dir, document, source, authoringRequest, []);
|
|
7989
|
+
await writeLatestView(dir, document, source, []);
|
|
7990
|
+
return await readRecord(dir);
|
|
7866
7991
|
} catch (error) {
|
|
7867
7992
|
await rm2(dir, { recursive: true, force: true });
|
|
7868
7993
|
throw error;
|
|
7869
7994
|
}
|
|
7870
7995
|
},
|
|
7871
|
-
|
|
7872
|
-
|
|
7873
|
-
if (
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
|
|
7996
|
+
read,
|
|
7997
|
+
async applyRevision(cwd, request) {
|
|
7998
|
+
if (request.mode === "create") {
|
|
7999
|
+
const root = await plansDir(cwd, options.homeDir);
|
|
8000
|
+
const id = planRecordName(request.shortSlug, now());
|
|
8001
|
+
const dir = join16(root, id);
|
|
8002
|
+
await createPlanDirectory(root, dir, id);
|
|
8003
|
+
try {
|
|
8004
|
+
const timestamp = now().toISOString();
|
|
8005
|
+
const document = normalizeRevision(undefined, request, id, 0, timestamp);
|
|
8006
|
+
assertCompleteRevision(document, request.briefs);
|
|
8007
|
+
const source = renderPlanDocument(document);
|
|
8008
|
+
parsePlanDocument(source, join16(dir, "PLAN.md"));
|
|
8009
|
+
await writeRevisionSnapshot(dir, document, source, request.request, request.briefs);
|
|
8010
|
+
await writeLatestView(dir, document, source, request.briefs);
|
|
8011
|
+
return await readRecord(dir);
|
|
8012
|
+
} catch (error) {
|
|
8013
|
+
await rm2(dir, { recursive: true, force: true });
|
|
8014
|
+
throw error;
|
|
8015
|
+
}
|
|
7877
8016
|
}
|
|
7878
|
-
|
|
8017
|
+
const initial = await read(cwd, request.plan);
|
|
8018
|
+
return withDirectoryLock(join16(initial.dir, ".lock"), async () => {
|
|
8019
|
+
const current = await readRecord(initial.dir);
|
|
8020
|
+
if (current.document.revision !== request.expectedPlanRevision)
|
|
8021
|
+
throw new Error(`Expected plan revision ${request.expectedPlanRevision}, found ${current.document.revision}.`);
|
|
8022
|
+
const revision = current.document.revision + 1;
|
|
8023
|
+
const document = normalizeRevision(current.document, request, current.id, revision, now().toISOString());
|
|
8024
|
+
assertCompleteRevision(document, request.briefs);
|
|
8025
|
+
const source = renderPlanDocument(document);
|
|
8026
|
+
parsePlanDocument(source, current.planPath);
|
|
8027
|
+
await writeRevisionSnapshot(current.dir, document, source, request.request, request.briefs);
|
|
8028
|
+
try {
|
|
8029
|
+
await writeLatestView(current.dir, document, source, request.briefs);
|
|
8030
|
+
} catch (error) {
|
|
8031
|
+
await rm2(join16(current.dir, "revisions", String(revision)), { recursive: true, force: true });
|
|
8032
|
+
throw error;
|
|
8033
|
+
}
|
|
8034
|
+
return await readRecord(current.dir);
|
|
8035
|
+
}, { operation: "apply revision" });
|
|
7879
8036
|
},
|
|
7880
8037
|
async mutate(cwd, query, operation, update) {
|
|
7881
|
-
const initial = await
|
|
8038
|
+
const initial = await read(cwd, query);
|
|
7882
8039
|
return withDirectoryLock(join16(initial.dir, ".lock"), async () => {
|
|
7883
8040
|
const current = await readRecord(initial.dir);
|
|
7884
8041
|
const next = await update(structuredClone(current.document));
|
|
@@ -7886,18 +8043,17 @@ function createPlanStore(options = {}) {
|
|
|
7886
8043
|
throw new Error("A plan mutation cannot change the plan ID.");
|
|
7887
8044
|
const document = {
|
|
7888
8045
|
...next,
|
|
7889
|
-
revision: current.document.revision
|
|
8046
|
+
revision: current.document.revision,
|
|
7890
8047
|
updatedAt: now().toISOString()
|
|
7891
8048
|
};
|
|
7892
8049
|
const source = renderPlanDocument(document, current.source);
|
|
7893
8050
|
parsePlanDocument(source, current.planPath);
|
|
7894
8051
|
await atomicWrite(current.planPath, source);
|
|
7895
|
-
|
|
7896
|
-
return { ...current, document, source, implementationDir: join16(current.dir, "implementation") };
|
|
8052
|
+
return { ...current, document, source };
|
|
7897
8053
|
}, { operation });
|
|
7898
8054
|
},
|
|
7899
8055
|
async log(cwd, query, event) {
|
|
7900
|
-
const record = await
|
|
8056
|
+
const record = await read(cwd, query);
|
|
7901
8057
|
return appendLogEntry(record.logPath, event);
|
|
7902
8058
|
}
|
|
7903
8059
|
};
|
|
@@ -7917,30 +8073,209 @@ async function readRecord(dir) {
|
|
|
7917
8073
|
source
|
|
7918
8074
|
};
|
|
7919
8075
|
}
|
|
7920
|
-
async function
|
|
7921
|
-
|
|
7922
|
-
|
|
7923
|
-
|
|
7924
|
-
|
|
7925
|
-
|
|
7926
|
-
|
|
7927
|
-
|
|
7928
|
-
|
|
8076
|
+
async function createPlanDirectory(root, dir, id) {
|
|
8077
|
+
await assertContained(root, dirname8(dir));
|
|
8078
|
+
try {
|
|
8079
|
+
await mkdir7(dir);
|
|
8080
|
+
} catch (error) {
|
|
8081
|
+
if (error.code === "EEXIST")
|
|
8082
|
+
throw new Error(`Plan ${id} already exists.`);
|
|
8083
|
+
throw error;
|
|
8084
|
+
}
|
|
8085
|
+
}
|
|
8086
|
+
async function writeRevisionSnapshot(planDir, document, source, request, briefs) {
|
|
8087
|
+
if (!request.text.trim())
|
|
8088
|
+
throw new Error("An exact non-empty authoring request is required.");
|
|
8089
|
+
if (request.kind === "annotation" && !request.response?.trim())
|
|
8090
|
+
throw new Error("Annotation requests require a non-empty response.");
|
|
8091
|
+
const revisionsDir = join16(planDir, "revisions");
|
|
8092
|
+
await mkdir7(revisionsDir, { recursive: true });
|
|
8093
|
+
const target = join16(revisionsDir, String(document.revision));
|
|
8094
|
+
try {
|
|
8095
|
+
await readFile12(join16(target, "metadata.json"), "utf8");
|
|
8096
|
+
throw new Error(`Revision ${document.revision} already exists and is immutable.`);
|
|
8097
|
+
} catch (error) {
|
|
8098
|
+
if (error.code !== "ENOENT")
|
|
8099
|
+
throw error;
|
|
8100
|
+
}
|
|
8101
|
+
const staging = join16(revisionsDir, `.tmp-${document.revision}-${randomUUID2()}`);
|
|
8102
|
+
await mkdir7(staging);
|
|
8103
|
+
try {
|
|
8104
|
+
const requestSource = request.kind === "annotation" ? `## Original annotation
|
|
8105
|
+
${request.text}
|
|
8106
|
+
|
|
8107
|
+
## LLM response
|
|
8108
|
+
${request.response}` : request.text;
|
|
8109
|
+
await atomicWrite(join16(staging, "request.md"), requestSource);
|
|
8110
|
+
await atomicWrite(join16(staging, "metadata.json"), `${JSON.stringify({
|
|
8111
|
+
schemaVersion: 1,
|
|
8112
|
+
id: document.id,
|
|
8113
|
+
revision: document.revision,
|
|
8114
|
+
requestKind: request.kind,
|
|
8115
|
+
requestSha256: sha256(request.text),
|
|
8116
|
+
...request.kind === "annotation" ? { originalAnnotationSha256: sha256(request.text), responseSha256: sha256(request.response) } : {},
|
|
8117
|
+
createdAt: document.updatedAt
|
|
8118
|
+
}, null, 2)}
|
|
8119
|
+
`);
|
|
8120
|
+
await atomicWrite(join16(staging, "PLAN.md"), source);
|
|
8121
|
+
await writeImplementationDirectory(staging, document, briefs);
|
|
8122
|
+
await rename3(staging, target);
|
|
8123
|
+
} catch (error) {
|
|
8124
|
+
await rm2(staging, { recursive: true, force: true });
|
|
8125
|
+
throw error;
|
|
8126
|
+
}
|
|
8127
|
+
}
|
|
8128
|
+
async function writeLatestView(planDir, document, source, briefs) {
|
|
8129
|
+
const current = join16(planDir, "implementation");
|
|
8130
|
+
const stagingRoot = join16(planDir, `.latest-${randomUUID2()}`);
|
|
8131
|
+
const stagedImplementation = join16(stagingRoot, "implementation");
|
|
8132
|
+
const backup = join16(planDir, `.implementation-backup-${randomUUID2()}`);
|
|
8133
|
+
await mkdir7(stagingRoot);
|
|
8134
|
+
await writeImplementationDirectory(stagingRoot, document, briefs);
|
|
8135
|
+
let backedUp = false;
|
|
8136
|
+
try {
|
|
7929
8137
|
try {
|
|
7930
|
-
await
|
|
8138
|
+
await rename3(current, backup);
|
|
8139
|
+
backedUp = true;
|
|
7931
8140
|
} catch (error) {
|
|
7932
8141
|
if (error.code !== "ENOENT")
|
|
7933
8142
|
throw error;
|
|
7934
|
-
const tasks = phase.tasks.map((task) => `- ${task.id}: ${task.title}`).join("\\n") || "- Add implementation tasks.";
|
|
7935
|
-
await atomicWrite(path, renderTemplate(template.content, {
|
|
7936
|
-
phase_id: phase.id,
|
|
7937
|
-
phase_title: phase.title,
|
|
7938
|
-
phase_objective: phase.objective,
|
|
7939
|
-
phase_tasks: tasks
|
|
7940
|
-
}));
|
|
7941
8143
|
}
|
|
8144
|
+
await rename3(stagedImplementation, current);
|
|
8145
|
+
await atomicWrite(join16(planDir, "PLAN.md"), source);
|
|
8146
|
+
if (backedUp)
|
|
8147
|
+
await rm2(backup, { recursive: true, force: true });
|
|
8148
|
+
} catch (error) {
|
|
8149
|
+
await rm2(current, { recursive: true, force: true });
|
|
8150
|
+
if (backedUp)
|
|
8151
|
+
await rename3(backup, current);
|
|
8152
|
+
throw error;
|
|
8153
|
+
} finally {
|
|
8154
|
+
await rm2(stagingRoot, { recursive: true, force: true });
|
|
7942
8155
|
}
|
|
7943
8156
|
}
|
|
8157
|
+
async function writeImplementationDirectory(baseDir, document, briefs) {
|
|
8158
|
+
const implementationDir = join16(baseDir, "implementation");
|
|
8159
|
+
await mkdir7(implementationDir, { recursive: true });
|
|
8160
|
+
for (const [index, phase] of document.phases.entries()) {
|
|
8161
|
+
const brief = briefs[index];
|
|
8162
|
+
if (!brief)
|
|
8163
|
+
throw new Error(`Missing implementation brief for phase ${phase.id}.`);
|
|
8164
|
+
await atomicWrite(join16(implementationDir, `phase-${index + 1}.md`), renderImplementationBrief(document.revision, index + 1, phase, brief));
|
|
8165
|
+
}
|
|
8166
|
+
}
|
|
8167
|
+
function normalizeRevision(current, request, id, revision, updatedAt) {
|
|
8168
|
+
if (current?.status === "completed")
|
|
8169
|
+
throw new Error("Completed plans cannot be amended.");
|
|
8170
|
+
if (current?.execution?.active && request.request.kind !== "blocker")
|
|
8171
|
+
throw new Error("An active execution accepts only a blocker amendment.");
|
|
8172
|
+
const currentPhases = new Map(current?.phases.map((phase) => [phase.id, phase]) ?? []);
|
|
8173
|
+
for (const phase of current?.phases ?? []) {
|
|
8174
|
+
if (!request.phases.some((candidate) => candidate.id === phase.id) && (phase.status === "completed" || phase.status === "in_progress"))
|
|
8175
|
+
throw new Error(`${phase.status === "completed" ? "Completed" : "Active"} phase ${phase.id} cannot be removed.`);
|
|
8176
|
+
}
|
|
8177
|
+
const phases = request.phases.map((draft) => reconcilePhase(currentPhases.get(draft.id), draft, current));
|
|
8178
|
+
const createdAt = current?.createdAt ?? updatedAt;
|
|
8179
|
+
return {
|
|
8180
|
+
schemaVersion: 1,
|
|
8181
|
+
id,
|
|
8182
|
+
revision,
|
|
8183
|
+
title: request.title,
|
|
8184
|
+
branch: current?.branch ?? (request.mode === "create" ? request.branch : ""),
|
|
8185
|
+
issueId: current?.issueId ?? (request.mode === "create" ? request.issueId : undefined),
|
|
8186
|
+
issueUrl: current?.issueUrl ?? (request.mode === "create" ? request.issueUrl : undefined),
|
|
8187
|
+
intent: request.intent,
|
|
8188
|
+
requirements: request.requirements,
|
|
8189
|
+
design: request.design,
|
|
8190
|
+
references: request.references,
|
|
8191
|
+
phases,
|
|
8192
|
+
status: nextPlanStatus(current),
|
|
8193
|
+
execution: current?.execution,
|
|
8194
|
+
createdAt,
|
|
8195
|
+
updatedAt
|
|
8196
|
+
};
|
|
8197
|
+
}
|
|
8198
|
+
function reconcilePhase(current, draft, plan) {
|
|
8199
|
+
if (!current)
|
|
8200
|
+
return {
|
|
8201
|
+
...draft,
|
|
8202
|
+
revision: 0,
|
|
8203
|
+
status: "pending",
|
|
8204
|
+
gate: { phaseRevision: 0, status: "pending", results: [] },
|
|
8205
|
+
tasks: draft.tasks.map(newTask)
|
|
8206
|
+
};
|
|
8207
|
+
if (current.status === "completed") {
|
|
8208
|
+
if (JSON.stringify(phaseContent(current)) !== JSON.stringify(draft))
|
|
8209
|
+
throw new Error(`Completed phase ${current.id} cannot be changed.`);
|
|
8210
|
+
return current;
|
|
8211
|
+
}
|
|
8212
|
+
const byId = new Map(current.tasks.map((task) => [task.id, task]));
|
|
8213
|
+
for (const task of current.tasks) {
|
|
8214
|
+
if (!draft.tasks.some((candidate) => candidate.id === task.id) && (task.status === "completed" || task.status === "in_progress"))
|
|
8215
|
+
throw new Error(`${task.status === "completed" ? "Completed" : "Active"} task ${task.id} cannot be removed.`);
|
|
8216
|
+
}
|
|
8217
|
+
const tasks = draft.tasks.map((task) => reconcileTask(byId.get(task.id), task));
|
|
8218
|
+
const changed = JSON.stringify(phaseContent(current)) !== JSON.stringify(draft);
|
|
8219
|
+
const revision = changed ? current.revision + 1 : current.revision;
|
|
8220
|
+
const wasBlocked = current.status === "blocked";
|
|
8221
|
+
return {
|
|
8222
|
+
...current,
|
|
8223
|
+
...draft,
|
|
8224
|
+
revision,
|
|
8225
|
+
tasks,
|
|
8226
|
+
status: wasBlocked ? plan?.execution?.active ? "in_progress" : "pending" : current.status,
|
|
8227
|
+
gate: changed || wasBlocked ? { phaseRevision: revision, status: "stale", results: [] } : current.gate,
|
|
8228
|
+
blocker: wasBlocked ? undefined : current.blocker
|
|
8229
|
+
};
|
|
8230
|
+
}
|
|
8231
|
+
function reconcileTask(current, draft) {
|
|
8232
|
+
if (!current)
|
|
8233
|
+
return newTask(draft);
|
|
8234
|
+
const changed = JSON.stringify(taskContent(current)) !== JSON.stringify(draft);
|
|
8235
|
+
if ((current.status === "completed" || current.status === "in_progress") && changed)
|
|
8236
|
+
throw new Error(`${current.status === "completed" ? "Completed" : "Active"} task ${current.id} cannot be changed.`);
|
|
8237
|
+
if (current.status === "completed" || current.status === "in_progress")
|
|
8238
|
+
return current;
|
|
8239
|
+
return {
|
|
8240
|
+
...current,
|
|
8241
|
+
...draft,
|
|
8242
|
+
revision: changed ? current.revision + 1 : current.revision,
|
|
8243
|
+
status: current.status === "blocked" ? "pending" : current.status,
|
|
8244
|
+
owner: current.status === "blocked" ? undefined : current.owner,
|
|
8245
|
+
executionId: current.status === "blocked" ? undefined : current.executionId,
|
|
8246
|
+
blocker: current.status === "blocked" ? undefined : current.blocker
|
|
8247
|
+
};
|
|
8248
|
+
}
|
|
8249
|
+
function newTask(draft) {
|
|
8250
|
+
return { ...draft, revision: 0, status: "pending" };
|
|
8251
|
+
}
|
|
8252
|
+
function phaseContent(phase) {
|
|
8253
|
+
return {
|
|
8254
|
+
id: phase.id,
|
|
8255
|
+
title: phase.title,
|
|
8256
|
+
objective: phase.objective,
|
|
8257
|
+
dependencies: phase.dependencies,
|
|
8258
|
+
tasks: phase.tasks.map(taskContent)
|
|
8259
|
+
};
|
|
8260
|
+
}
|
|
8261
|
+
function taskContent(task) {
|
|
8262
|
+
return { id: task.id, title: task.title, dependencies: task.dependencies };
|
|
8263
|
+
}
|
|
8264
|
+
function nextPlanStatus(current) {
|
|
8265
|
+
if (!current)
|
|
8266
|
+
return "draft";
|
|
8267
|
+
if (current.execution?.active)
|
|
8268
|
+
return current.status === "blocked" ? "in_progress" : current.status;
|
|
8269
|
+
return current.status === "ready" || current.status === "blocked" ? "draft" : current.status;
|
|
8270
|
+
}
|
|
8271
|
+
function assertCompleteRevision(document, briefs) {
|
|
8272
|
+
const errors = [...validatePlanDocument(document, { strict: true }), ...validatePlanBriefs(document, briefs)].filter((issue) => issue.severity === "error");
|
|
8273
|
+
if (errors.length)
|
|
8274
|
+
throw new Error(`Invalid plan revision: ${errors.map((issue) => issue.message).join(" ")}`);
|
|
8275
|
+
}
|
|
8276
|
+
function sha256(value) {
|
|
8277
|
+
return createHash3("sha256").update(value).digest("hex");
|
|
8278
|
+
}
|
|
7944
8279
|
async function assertContained(root, candidate) {
|
|
7945
8280
|
const canonicalRoot = await realpath4(root);
|
|
7946
8281
|
let canonicalCandidate;
|
|
@@ -7972,7 +8307,10 @@ function stripDate(value) {
|
|
|
7972
8307
|
return value.replace(/^\d{6}-/, "");
|
|
7973
8308
|
}
|
|
7974
8309
|
function titleFromSlug(value) {
|
|
7975
|
-
|
|
8310
|
+
const slug = normalizeSlug(value).replace(/^(?:[a-z]+-\d+-)/, "");
|
|
8311
|
+
return slug.replace(/(^|-)([a-z])/g, (_match, separator, letter) => {
|
|
8312
|
+
return `${separator ? " " : ""}${letter.toUpperCase()}`;
|
|
8313
|
+
});
|
|
7976
8314
|
}
|
|
7977
8315
|
// src/setup.ts
|
|
7978
8316
|
import { parseFrontmatter as parseFrontmatter3 } from "@earendil-works/pi-coding-agent";
|