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