@difflab/pi 0.7.0-rc.202609271854.6af8600 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents/diffpi-planner.md +5 -5
- package/dist/cli.js +100 -53
- package/dist/extensions/index.js +86 -90
- package/dist/index.js +87 -90
- package/dist/plan/store.d.ts.map +1 -1
- package/dist/tools/index.js +86 -90
- package/package.json +1 -1
- package/skills/plan/SKILL.md +5 -5
- package/skills/plan/references/design-quality.md +29 -18
- package/skills/plan/references/workflows/new.md +5 -5
- package/skills/plan/references/workflows/update.md +5 -5
- package/templates/plan/PLAN.md +11 -17
- package/templates/plan/implementation.md +12 -28
- package/agents/diffpi-plan-reviewer.md +0 -27
package/dist/tools/index.js
CHANGED
|
@@ -1290,7 +1290,7 @@ function sanitize(value) {
|
|
|
1290
1290
|
}
|
|
1291
1291
|
|
|
1292
1292
|
// src/tools/plan.ts
|
|
1293
|
-
import { join as
|
|
1293
|
+
import { join as join15 } from "node:path";
|
|
1294
1294
|
import { pathToFileURL } from "node:url";
|
|
1295
1295
|
import { defineTool as defineTool5 } from "@earendil-works/pi-coding-agent";
|
|
1296
1296
|
import { z as z7 } from "zod";
|
|
@@ -7234,8 +7234,8 @@ function isPlanReviewDump(value) {
|
|
|
7234
7234
|
|
|
7235
7235
|
// src/plan/store.ts
|
|
7236
7236
|
import { createHash as createHash3, randomUUID as randomUUID2 } from "node:crypto";
|
|
7237
|
-
import { mkdir as mkdir6, readdir as readdir2, readFile as
|
|
7238
|
-
import { basename as basename5, dirname as dirname7, join as
|
|
7237
|
+
import { mkdir as mkdir6, readdir as readdir2, readFile as readFile8, realpath as realpath3, rename as rename2, rm as rm2 } from "node:fs/promises";
|
|
7238
|
+
import { basename as basename5, dirname as dirname7, join as join14, resolve as resolve3, sep } from "node:path";
|
|
7239
7239
|
|
|
7240
7240
|
// src/extensions/fsx.ts
|
|
7241
7241
|
import { mkdir as mkdir5, rename, rm, writeFile as writeFile4 } from "node:fs/promises";
|
|
@@ -7271,6 +7271,42 @@ function sleep(ms) {
|
|
|
7271
7271
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
7272
7272
|
}
|
|
7273
7273
|
|
|
7274
|
+
// src/templates.ts
|
|
7275
|
+
import { readFile as readFile7 } from "node:fs/promises";
|
|
7276
|
+
import { homedir as homedir4 } from "node:os";
|
|
7277
|
+
import { join as join13, normalize } from "node:path";
|
|
7278
|
+
async function loadTemplate(name, options = {}) {
|
|
7279
|
+
const relative = templateRelativePath(name);
|
|
7280
|
+
const userPath = join13(options.homeDir ?? homedir4(), ".difflab", "diffpi", "templates", relative);
|
|
7281
|
+
const bundledPath = join13(options.bundledDir ?? resolveBundledTemplatesDir(), relative);
|
|
7282
|
+
const user = await readOptionalFile(userPath);
|
|
7283
|
+
if (user !== undefined)
|
|
7284
|
+
return { name, path: userPath, source: "user", content: user };
|
|
7285
|
+
const bundled = await readOptionalFile(bundledPath);
|
|
7286
|
+
if (bundled !== undefined)
|
|
7287
|
+
return { name, path: bundledPath, source: "bundled", content: bundled };
|
|
7288
|
+
throw new Error(`Template "${name}" was not found at ${userPath} or ${bundledPath}.`);
|
|
7289
|
+
}
|
|
7290
|
+
function renderTemplate(content, variables) {
|
|
7291
|
+
return content.replace(/\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g, (match, key) => variables[key] ?? match);
|
|
7292
|
+
}
|
|
7293
|
+
function templateRelativePath(name) {
|
|
7294
|
+
const normalized = normalize(name.replaceAll("\\", "/")).replace(/^\.\//, "");
|
|
7295
|
+
if (!normalized || normalized.startsWith("../") || normalized.includes("/../") || normalized.startsWith("/")) {
|
|
7296
|
+
throw new Error(`Invalid template name: ${name}`);
|
|
7297
|
+
}
|
|
7298
|
+
return normalized.endsWith(".md") ? normalized : `${normalized}.md`;
|
|
7299
|
+
}
|
|
7300
|
+
async function readOptionalFile(path) {
|
|
7301
|
+
try {
|
|
7302
|
+
return await readFile7(path, "utf8");
|
|
7303
|
+
} catch (error) {
|
|
7304
|
+
if (error.code === "ENOENT")
|
|
7305
|
+
return;
|
|
7306
|
+
throw error;
|
|
7307
|
+
}
|
|
7308
|
+
}
|
|
7309
|
+
|
|
7274
7310
|
// src/plan/store.ts
|
|
7275
7311
|
function planRecordName(shortSlug, date = new Date) {
|
|
7276
7312
|
const slug = normalizeSlug(shortSlug);
|
|
@@ -7286,7 +7322,7 @@ async function resolvePlan(cwd, query, filters = {}, options = {}) {
|
|
|
7286
7322
|
continue;
|
|
7287
7323
|
if (normalizedQuery && entry.name !== normalizedQuery && stripDate(entry.name) !== normalizedQuery)
|
|
7288
7324
|
continue;
|
|
7289
|
-
const dir =
|
|
7325
|
+
const dir = join14(root, entry.name);
|
|
7290
7326
|
await assertContained(root, dir);
|
|
7291
7327
|
try {
|
|
7292
7328
|
const record = await readRecord(dir);
|
|
@@ -7321,30 +7357,26 @@ function createPlanStore(options = {}) {
|
|
|
7321
7357
|
async init(input) {
|
|
7322
7358
|
const root = await plansDir(input.cwd, options.homeDir);
|
|
7323
7359
|
const id = planRecordName(input.shortSlug, now());
|
|
7324
|
-
const dir =
|
|
7360
|
+
const dir = join14(root, id);
|
|
7325
7361
|
await createPlanDirectory(root, dir, id);
|
|
7326
7362
|
try {
|
|
7327
7363
|
const timestamp = now().toISOString();
|
|
7364
|
+
const template = await loadTemplate("plan/PLAN", {
|
|
7365
|
+
homeDir: options.homeDir,
|
|
7366
|
+
bundledDir: options.bundledTemplatesDir
|
|
7367
|
+
});
|
|
7328
7368
|
const title = input.title?.trim() || titleFromSlug(input.shortSlug);
|
|
7329
|
-
const
|
|
7330
|
-
schemaVersion: 1,
|
|
7369
|
+
const source = renderTemplate(template.content, {
|
|
7331
7370
|
id,
|
|
7332
|
-
revision: 0,
|
|
7333
|
-
title,
|
|
7334
7371
|
branch: input.branch,
|
|
7335
|
-
|
|
7336
|
-
...input.issueUrl?.trim() ? { issueUrl: input.issueUrl.trim() } : {},
|
|
7372
|
+
title,
|
|
7337
7373
|
intent: input.intent?.trim() || "<!-- Describe the intended outcome. -->",
|
|
7338
|
-
|
|
7339
|
-
|
|
7340
|
-
|
|
7341
|
-
|
|
7342
|
-
|
|
7343
|
-
|
|
7344
|
-
updatedAt: timestamp
|
|
7345
|
-
};
|
|
7346
|
-
const source = renderPlanDocument(document);
|
|
7347
|
-
parsePlanDocument(source, join13(dir, "PLAN.md"));
|
|
7374
|
+
issue_id: input.issueId?.trim() || "",
|
|
7375
|
+
issue_url: input.issueUrl?.trim() || "",
|
|
7376
|
+
created_at: timestamp,
|
|
7377
|
+
updated_at: timestamp
|
|
7378
|
+
});
|
|
7379
|
+
const document = parsePlanDocument(source, join14(dir, "PLAN.md"));
|
|
7348
7380
|
const authoringRequest = input.request ?? { kind: "user", text: input.intent?.trim() || `Initialize phase-less plan ${title}.` };
|
|
7349
7381
|
await writeRevisionSnapshot(dir, document, source, authoringRequest, []);
|
|
7350
7382
|
await writeLatestView(dir, document, source, []);
|
|
@@ -7359,14 +7391,14 @@ function createPlanStore(options = {}) {
|
|
|
7359
7391
|
if (request.mode === "create") {
|
|
7360
7392
|
const root = await plansDir(cwd, options.homeDir);
|
|
7361
7393
|
const id = planRecordName(request.shortSlug, now());
|
|
7362
|
-
const dir =
|
|
7394
|
+
const dir = join14(root, id);
|
|
7363
7395
|
await createPlanDirectory(root, dir, id);
|
|
7364
7396
|
try {
|
|
7365
7397
|
const timestamp = now().toISOString();
|
|
7366
7398
|
const document = normalizeRevision(undefined, request, id, 0, timestamp);
|
|
7367
7399
|
assertCompleteRevision(document, request.briefs);
|
|
7368
7400
|
const source = renderPlanDocument(document);
|
|
7369
|
-
parsePlanDocument(source,
|
|
7401
|
+
parsePlanDocument(source, join14(dir, "PLAN.md"));
|
|
7370
7402
|
await writeRevisionSnapshot(dir, document, source, request.request, request.briefs);
|
|
7371
7403
|
await writeLatestView(dir, document, source, request.briefs);
|
|
7372
7404
|
return await readRecord(dir);
|
|
@@ -7376,7 +7408,7 @@ function createPlanStore(options = {}) {
|
|
|
7376
7408
|
}
|
|
7377
7409
|
}
|
|
7378
7410
|
const initial = await read(cwd, request.plan);
|
|
7379
|
-
return withDirectoryLock(
|
|
7411
|
+
return withDirectoryLock(join14(initial.dir, ".lock"), async () => {
|
|
7380
7412
|
const current = await readRecord(initial.dir);
|
|
7381
7413
|
if (current.document.revision !== request.expectedPlanRevision)
|
|
7382
7414
|
throw new Error(`Expected plan revision ${request.expectedPlanRevision}, found ${current.document.revision}.`);
|
|
@@ -7389,7 +7421,7 @@ function createPlanStore(options = {}) {
|
|
|
7389
7421
|
try {
|
|
7390
7422
|
await writeLatestView(current.dir, document, source, request.briefs);
|
|
7391
7423
|
} catch (error) {
|
|
7392
|
-
await rm2(
|
|
7424
|
+
await rm2(join14(current.dir, "revisions", String(revision)), { recursive: true, force: true });
|
|
7393
7425
|
throw error;
|
|
7394
7426
|
}
|
|
7395
7427
|
return await readRecord(current.dir);
|
|
@@ -7397,7 +7429,7 @@ function createPlanStore(options = {}) {
|
|
|
7397
7429
|
},
|
|
7398
7430
|
async mutate(cwd, query, operation, update) {
|
|
7399
7431
|
const initial = await read(cwd, query);
|
|
7400
|
-
return withDirectoryLock(
|
|
7432
|
+
return withDirectoryLock(join14(initial.dir, ".lock"), async () => {
|
|
7401
7433
|
const current = await readRecord(initial.dir);
|
|
7402
7434
|
const next = await update(structuredClone(current.document));
|
|
7403
7435
|
if (next.id !== current.id)
|
|
@@ -7420,16 +7452,16 @@ function createPlanStore(options = {}) {
|
|
|
7420
7452
|
};
|
|
7421
7453
|
}
|
|
7422
7454
|
async function readRecord(dir) {
|
|
7423
|
-
const planPath =
|
|
7424
|
-
const logPath =
|
|
7425
|
-
const source = await
|
|
7455
|
+
const planPath = join14(dir, "PLAN.md");
|
|
7456
|
+
const logPath = join14(dir, "logs.txt");
|
|
7457
|
+
const source = await readFile8(planPath, "utf8");
|
|
7426
7458
|
const document = parsePlanDocument(source, planPath);
|
|
7427
7459
|
return {
|
|
7428
7460
|
id: basename5(dir),
|
|
7429
7461
|
dir,
|
|
7430
7462
|
planPath,
|
|
7431
7463
|
logPath,
|
|
7432
|
-
implementationDir:
|
|
7464
|
+
implementationDir: join14(dir, "implementation"),
|
|
7433
7465
|
document,
|
|
7434
7466
|
source
|
|
7435
7467
|
};
|
|
@@ -7449,17 +7481,17 @@ async function writeRevisionSnapshot(planDir, document, source, request, briefs)
|
|
|
7449
7481
|
throw new Error("An exact non-empty authoring request is required.");
|
|
7450
7482
|
if (request.kind === "annotation" && !request.response?.trim())
|
|
7451
7483
|
throw new Error("Annotation requests require a non-empty response.");
|
|
7452
|
-
const revisionsDir =
|
|
7484
|
+
const revisionsDir = join14(planDir, "revisions");
|
|
7453
7485
|
await mkdir6(revisionsDir, { recursive: true });
|
|
7454
|
-
const target =
|
|
7486
|
+
const target = join14(revisionsDir, String(document.revision));
|
|
7455
7487
|
try {
|
|
7456
|
-
await
|
|
7488
|
+
await readFile8(join14(target, "metadata.json"), "utf8");
|
|
7457
7489
|
throw new Error(`Revision ${document.revision} already exists and is immutable.`);
|
|
7458
7490
|
} catch (error) {
|
|
7459
7491
|
if (error.code !== "ENOENT")
|
|
7460
7492
|
throw error;
|
|
7461
7493
|
}
|
|
7462
|
-
const staging =
|
|
7494
|
+
const staging = join14(revisionsDir, `.tmp-${document.revision}-${randomUUID2()}`);
|
|
7463
7495
|
await mkdir6(staging);
|
|
7464
7496
|
try {
|
|
7465
7497
|
const requestSource = request.kind === "annotation" ? `## Original annotation
|
|
@@ -7467,8 +7499,8 @@ ${request.text}
|
|
|
7467
7499
|
|
|
7468
7500
|
## LLM response
|
|
7469
7501
|
${request.response}` : request.text;
|
|
7470
|
-
await atomicWrite(
|
|
7471
|
-
await atomicWrite(
|
|
7502
|
+
await atomicWrite(join14(staging, "request.md"), requestSource);
|
|
7503
|
+
await atomicWrite(join14(staging, "metadata.json"), `${JSON.stringify({
|
|
7472
7504
|
schemaVersion: 1,
|
|
7473
7505
|
id: document.id,
|
|
7474
7506
|
revision: document.revision,
|
|
@@ -7478,7 +7510,7 @@ ${request.response}` : request.text;
|
|
|
7478
7510
|
createdAt: document.updatedAt
|
|
7479
7511
|
}, null, 2)}
|
|
7480
7512
|
`);
|
|
7481
|
-
await atomicWrite(
|
|
7513
|
+
await atomicWrite(join14(staging, "PLAN.md"), source);
|
|
7482
7514
|
await writeImplementationDirectory(staging, document, briefs);
|
|
7483
7515
|
await rename2(staging, target);
|
|
7484
7516
|
} catch (error) {
|
|
@@ -7487,10 +7519,10 @@ ${request.response}` : request.text;
|
|
|
7487
7519
|
}
|
|
7488
7520
|
}
|
|
7489
7521
|
async function writeLatestView(planDir, document, source, briefs) {
|
|
7490
|
-
const current =
|
|
7491
|
-
const stagingRoot =
|
|
7492
|
-
const stagedImplementation =
|
|
7493
|
-
const backup =
|
|
7522
|
+
const current = join14(planDir, "implementation");
|
|
7523
|
+
const stagingRoot = join14(planDir, `.latest-${randomUUID2()}`);
|
|
7524
|
+
const stagedImplementation = join14(stagingRoot, "implementation");
|
|
7525
|
+
const backup = join14(planDir, `.implementation-backup-${randomUUID2()}`);
|
|
7494
7526
|
await mkdir6(stagingRoot);
|
|
7495
7527
|
await writeImplementationDirectory(stagingRoot, document, briefs);
|
|
7496
7528
|
let backedUp = false;
|
|
@@ -7503,7 +7535,7 @@ async function writeLatestView(planDir, document, source, briefs) {
|
|
|
7503
7535
|
throw error;
|
|
7504
7536
|
}
|
|
7505
7537
|
await rename2(stagedImplementation, current);
|
|
7506
|
-
await atomicWrite(
|
|
7538
|
+
await atomicWrite(join14(planDir, "PLAN.md"), source);
|
|
7507
7539
|
if (backedUp)
|
|
7508
7540
|
await rm2(backup, { recursive: true, force: true });
|
|
7509
7541
|
} catch (error) {
|
|
@@ -7516,13 +7548,13 @@ async function writeLatestView(planDir, document, source, briefs) {
|
|
|
7516
7548
|
}
|
|
7517
7549
|
}
|
|
7518
7550
|
async function writeImplementationDirectory(baseDir, document, briefs) {
|
|
7519
|
-
const implementationDir =
|
|
7551
|
+
const implementationDir = join14(baseDir, "implementation");
|
|
7520
7552
|
await mkdir6(implementationDir, { recursive: true });
|
|
7521
7553
|
for (const [index, phase] of document.phases.entries()) {
|
|
7522
7554
|
const brief = briefs[index];
|
|
7523
7555
|
if (!brief)
|
|
7524
7556
|
throw new Error(`Missing implementation brief for phase ${phase.id}.`);
|
|
7525
|
-
await atomicWrite(
|
|
7557
|
+
await atomicWrite(join14(implementationDir, `phase-${index + 1}.md`), renderImplementationBrief(document.revision, index + 1, phase, brief));
|
|
7526
7558
|
}
|
|
7527
7559
|
}
|
|
7528
7560
|
function normalizeRevision(current, request, id, revision, updatedAt) {
|
|
@@ -8008,7 +8040,7 @@ function createPlanTools(_pi, _modes, store = createPlanStore()) {
|
|
|
8008
8040
|
const resolution = await store.context(workingDirectory, params.plan);
|
|
8009
8041
|
if (!resolution.record)
|
|
8010
8042
|
throw new Error(resolution.ambiguous ? `Plan is ambiguous: ${resolution.candidates.map((item) => item.id).join(", ")}.` : "No matching plan.");
|
|
8011
|
-
const cliPath =
|
|
8043
|
+
const cliPath = join15(resolveBundledAgentsDir(), "..", "dist", "cli.js");
|
|
8012
8044
|
const command = [
|
|
8013
8045
|
process.execPath,
|
|
8014
8046
|
cliPath,
|
|
@@ -8808,10 +8840,10 @@ function hasGitlabDraftNotes(input) {
|
|
|
8808
8840
|
}
|
|
8809
8841
|
|
|
8810
8842
|
// src/review/local-review-backend.ts
|
|
8811
|
-
import { readFile as
|
|
8843
|
+
import { readFile as readFile10, writeFile as writeFile5 } from "node:fs/promises";
|
|
8812
8844
|
|
|
8813
8845
|
// src/extensions/tuicrx.ts
|
|
8814
|
-
import { readFile as
|
|
8846
|
+
import { readFile as readFile9, realpath as realpath4 } from "node:fs/promises";
|
|
8815
8847
|
import { resolve as resolve4 } from "node:path";
|
|
8816
8848
|
async function tuicrAvailable() {
|
|
8817
8849
|
return Boolean(await findExecutable("tuicr"));
|
|
@@ -8908,7 +8940,7 @@ async function findMatchingSession(sessions, cwd, branch) {
|
|
|
8908
8940
|
return;
|
|
8909
8941
|
}
|
|
8910
8942
|
async function readSession(path) {
|
|
8911
|
-
const content = await
|
|
8943
|
+
const content = await readFile9(path, "utf8");
|
|
8912
8944
|
try {
|
|
8913
8945
|
return JSON.parse(content);
|
|
8914
8946
|
} catch {
|
|
@@ -9029,7 +9061,7 @@ function LocalReviewBackend(options) {
|
|
|
9029
9061
|
},
|
|
9030
9062
|
async listThreads() {
|
|
9031
9063
|
try {
|
|
9032
|
-
return parseThreadArtifact(await
|
|
9064
|
+
return parseThreadArtifact(await readFile10(options.artifactPath, "utf8"));
|
|
9033
9065
|
} catch (error) {
|
|
9034
9066
|
if (error.code === "ENOENT")
|
|
9035
9067
|
return [];
|
|
@@ -9037,7 +9069,7 @@ function LocalReviewBackend(options) {
|
|
|
9037
9069
|
}
|
|
9038
9070
|
},
|
|
9039
9071
|
async reply(input) {
|
|
9040
|
-
const content = await
|
|
9072
|
+
const content = await readFile10(options.artifactPath, "utf8");
|
|
9041
9073
|
await writeFile5(options.artifactPath, upsertThreadReply(content, input.threadId, input.body, input.question, input.resolve), "utf8");
|
|
9042
9074
|
},
|
|
9043
9075
|
async publish() {
|
|
@@ -9059,8 +9091,8 @@ function createLocalReviewBackend(options) {
|
|
|
9059
9091
|
}
|
|
9060
9092
|
// src/review/review-state.ts
|
|
9061
9093
|
import { createHash as createHash4 } from "node:crypto";
|
|
9062
|
-
import { readFile as
|
|
9063
|
-
import { join as
|
|
9094
|
+
import { readFile as readFile11, rename as rename3, writeFile as writeFile6 } from "node:fs/promises";
|
|
9095
|
+
import { join as join16 } from "node:path";
|
|
9064
9096
|
import { z as z10 } from "zod";
|
|
9065
9097
|
var reviewPublicationStateSchema = z10.object({
|
|
9066
9098
|
target: z10.string().optional(),
|
|
@@ -9084,9 +9116,9 @@ function unpublishedReviewComments(comments, knownFingerprints) {
|
|
|
9084
9116
|
}
|
|
9085
9117
|
async function loadReviewPublicationState(cwd, vcs, number, homeDir) {
|
|
9086
9118
|
const target = `${vcs.provider}:${vcs.owner}/${vcs.repo}#${number}`;
|
|
9087
|
-
const path =
|
|
9119
|
+
const path = join16(await sessionsDir(cwd, homeDir), `review-publish-${digest(target).slice(0, 16)}.json`);
|
|
9088
9120
|
try {
|
|
9089
|
-
const parsed = reviewPublicationStateSchema.parse(JSON.parse(await
|
|
9121
|
+
const parsed = reviewPublicationStateSchema.parse(JSON.parse(await readFile11(path, "utf8")));
|
|
9090
9122
|
return { path, state: { ...parsed, target } };
|
|
9091
9123
|
} catch (error) {
|
|
9092
9124
|
if (error.code === "ENOENT") {
|
|
@@ -9107,42 +9139,6 @@ async function saveReviewPublicationState(path, state) {
|
|
|
9107
9139
|
function digest(value) {
|
|
9108
9140
|
return createHash4("sha256").update(value).digest("hex");
|
|
9109
9141
|
}
|
|
9110
|
-
// src/templates.ts
|
|
9111
|
-
import { readFile as readFile11 } from "node:fs/promises";
|
|
9112
|
-
import { homedir as homedir4 } from "node:os";
|
|
9113
|
-
import { join as join16, normalize } from "node:path";
|
|
9114
|
-
async function loadTemplate(name, options = {}) {
|
|
9115
|
-
const relative = templateRelativePath(name);
|
|
9116
|
-
const userPath = join16(options.homeDir ?? homedir4(), ".difflab", "diffpi", "templates", relative);
|
|
9117
|
-
const bundledPath = join16(options.bundledDir ?? resolveBundledTemplatesDir(), relative);
|
|
9118
|
-
const user = await readOptionalFile(userPath);
|
|
9119
|
-
if (user !== undefined)
|
|
9120
|
-
return { name, path: userPath, source: "user", content: user };
|
|
9121
|
-
const bundled = await readOptionalFile(bundledPath);
|
|
9122
|
-
if (bundled !== undefined)
|
|
9123
|
-
return { name, path: bundledPath, source: "bundled", content: bundled };
|
|
9124
|
-
throw new Error(`Template "${name}" was not found at ${userPath} or ${bundledPath}.`);
|
|
9125
|
-
}
|
|
9126
|
-
function renderTemplate(content, variables) {
|
|
9127
|
-
return content.replace(/\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g, (match, key) => variables[key] ?? match);
|
|
9128
|
-
}
|
|
9129
|
-
function templateRelativePath(name) {
|
|
9130
|
-
const normalized = normalize(name.replaceAll("\\", "/")).replace(/^\.\//, "");
|
|
9131
|
-
if (!normalized || normalized.startsWith("../") || normalized.includes("/../") || normalized.startsWith("/")) {
|
|
9132
|
-
throw new Error(`Invalid template name: ${name}`);
|
|
9133
|
-
}
|
|
9134
|
-
return normalized.endsWith(".md") ? normalized : `${normalized}.md`;
|
|
9135
|
-
}
|
|
9136
|
-
async function readOptionalFile(path) {
|
|
9137
|
-
try {
|
|
9138
|
-
return await readFile11(path, "utf8");
|
|
9139
|
-
} catch (error) {
|
|
9140
|
-
if (error.code === "ENOENT")
|
|
9141
|
-
return;
|
|
9142
|
-
throw error;
|
|
9143
|
-
}
|
|
9144
|
-
}
|
|
9145
|
-
|
|
9146
9142
|
// src/tools/review.ts
|
|
9147
9143
|
var contextSchema = z11.object({
|
|
9148
9144
|
cwd: z11.string().optional(),
|
package/package.json
CHANGED
package/skills/plan/SKILL.md
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: plan
|
|
3
3
|
description: Create, revise, annotate, finalize, and execute durable Diffpi plans. Use for /skill:plan, /plan, or natural-language planning requests.
|
|
4
|
-
allowed-tools: read grep find
|
|
4
|
+
allowed-tools: read grep find symbol_search module_report read_symbol ask_user_question Agent get_subagent_result steer_subagent plan_context plan_init plan_apply_revision plan_validate plan_review plan_annotate plan_start_execution plan_update_status plan_log_progress plan_run_gates plan_record_ci watch_ci
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Plan
|
|
8
8
|
|
|
9
|
-
This skill owns the workflow. `/plan` only forwards its raw arguments to `/skill:plan`. Do not emit another command to paste or rely on a command handler to select an agent, parse flags, or inject a workflow.
|
|
9
|
+
This skill owns the workflow. `/plan` only forwards its raw arguments to `/skill:plan`. Do not emit another command to paste or rely on a command handler to select an agent, parse flags, or inject a workflow. Call plan tools directly; never edit managed PLAN.md, implementation briefs, revisions, reviews, or logs with file tools.
|
|
10
10
|
|
|
11
|
-
Identify the first explicit verb (`init`, `new`, `update`, `annotate`, `finalize`, `go`, `help`) or infer one from the user's request.
|
|
11
|
+
Identify the first explicit verb (`init`, `new`, `update`, `annotate`, `finalize`, `go`, `help`) or infer one from the user's request. If the verb or required plan is ambiguous, call `plan_context` and ask only for a material decision using `ask_user_question`. Read exactly the corresponding [workflow reference](references/workflows/) before acting. Preserve the exact incoming user request (and applied review feedback) as revision input; do not paraphrase it.
|
|
12
12
|
|
|
13
|
-
`new` creates a complete plan without launching an editor; `init` creates a phase-less draft and opens it. For
|
|
13
|
+
`new` creates a complete plan without launching an editor; `init` creates a phase-less draft and opens it. For content changes, build a complete candidate and apply it **once per user request** with `plan_apply_revision`. Phase/task status and gates use their dedicated tools and do not constitute a new content revision. Strict `plan_validate` checks document and brief structure, not whether source code implements the plan. `go` calls `plan_start_execution` before any code change; if the plan is draft or on the wrong branch, stop.
|
|
14
14
|
|
|
15
15
|
`--bg` requests one named background agent through `Agent` (Planner for authoring, Orchestrator for execution); pass the verb, exact arguments, cwd, and a `background: true` instruction. Remove `--bg` from the child's request and explicitly forbid redispatch. Return after launching it. A background child must not ask user questions. If `Agent` is unavailable, stop and report that background execution cannot be started; never silently execute inline. Without `--bg`, execute in the current turn: author as Planner, coordinate go as Orchestrator and delegate bounded Worker tasks; do not assume an inline mode switch occurred. Do not launch a multi-agent workflow unless the user explicitly opts into one.
|
|
16
16
|
|
|
17
|
-
Operational references: [init](references/workflows/init.md), [new](references/workflows/new.md), [update](references/workflows/update.md), [annotate](references/workflows/annotate.md), [finalize](references/workflows/finalize.md), [go](references/workflows/go.md), [help](references/workflows/help.md).
|
|
17
|
+
Operational references: [init](references/workflows/init.md), [new](references/workflows/new.md), [update](references/workflows/update.md), [annotate](references/workflows/annotate.md), [finalize](references/workflows/finalize.md), [go](references/workflows/go.md), [help](references/workflows/help.md).
|
|
@@ -1,30 +1,41 @@
|
|
|
1
1
|
# Plan authoring quality
|
|
2
2
|
|
|
3
|
-
Build from repository evidence. Keep
|
|
3
|
+
Build the design from repository evidence, not generic slogans. Keep it concise enough to scan, but never shrink a section into an uninformative one-line paragraph just to reach a word count.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Big Ideas
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
- Give each phase a stable ID, title, objective, and prerequisites. Prerequisites belong to phases only.
|
|
9
|
-
- Put each discrete implementation action in a flat task checkbox with a stable task ID and title. Do not put dependency metadata, steps, file scopes, or acceptance criteria under the checkbox.
|
|
10
|
-
- Preserve phase/task ID and order parity in every implementation brief.
|
|
7
|
+
Use outcome-oriented bullets. Each bullet says what will become different and why it belongs in this plan; do not repeat the phase titles. For example:
|
|
11
8
|
|
|
12
|
-
|
|
9
|
+
- Keep task status and phase ordering in `PLAN.md` so readers can see progress without reading implementation instructions.
|
|
10
|
+
- Write one numbered brief per phase so Workers receive concrete steps and file/API contracts.
|
|
11
|
+
- Reject incomplete briefs at finalization instead of handing placeholders to execution agents.
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
- List exact file scopes and acceptance criteria for every task.
|
|
16
|
-
- Put libraries and algorithms under **Implementation Constraints**.
|
|
17
|
-
- Include exactly one fenced `text` phase-level directory tree. Label every file leaf with one of `[ADD]`, `[MODIFY]`, `[REMOVE]`, `[MOVE from: path]`, or `[VERIFY]`.
|
|
18
|
-
- Read back each file after writing it. Normal successive write/edit calls are intentional: incomplete drafts remain visible until the plan is ready.
|
|
13
|
+
## Key API Addition/Updates
|
|
19
14
|
|
|
20
|
-
|
|
15
|
+
Use explanatory paragraphs and illustrative fenced examples showing the actual public call and data contract. Explain which previous call is removed, who calls the new API, what arguments/return value mean, and how errors are surfaced. Do not invent details that the repository has not established; call out a genuine unresolved choice instead. For example:
|
|
21
16
|
|
|
22
|
-
|
|
17
|
+
```ts
|
|
18
|
+
await plan_apply_revision({
|
|
19
|
+
mode: 'amend', // use 'create' with shortSlug instead for revision 0
|
|
20
|
+
plan,
|
|
21
|
+
expectedPlanRevision,
|
|
22
|
+
request: { kind: 'user', text: exactRequest },
|
|
23
|
+
title,
|
|
24
|
+
intent,
|
|
25
|
+
requirements,
|
|
26
|
+
design: { bigIdeas, keyApiUpdates, consequences },
|
|
27
|
+
references,
|
|
28
|
+
phases,
|
|
29
|
+
briefs,
|
|
30
|
+
});
|
|
31
|
+
```
|
|
23
32
|
|
|
24
|
-
|
|
33
|
+
Follow the example with a paragraph describing revision creation and validation, then another covering old mutators or callers being replaced. `mode: 'create'` instead takes `shortSlug` (and optional branch/issue fields) and writes revision 0 directly; `mode: 'amend'` takes `plan` and `expectedPlanRevision` and writes the next revision. Both modes require the same `request`, overview fields, ordered `phases`, and matching `briefs`; there is no `inputs` field and no separate `complete` field. If no public API changes, say so explicitly and show the relevant internal interface or data flow instead.
|
|
25
34
|
|
|
26
|
-
|
|
35
|
+
## Consequences
|
|
27
36
|
|
|
28
|
-
|
|
37
|
+
Write distinct before/after paragraphs: how a user invokes the workflow, what a client or Worker reads/calls, and how failure looks. Describe concrete behavior: editor launch, status transitions, validation, snapshots, and review visibility as applicable. Include at least one trade-off, such as storage cost or compatibility break; do not substitute a generic maintainability claim.
|
|
29
38
|
|
|
30
|
-
|
|
39
|
+
## PLAN.md versus briefs
|
|
40
|
+
|
|
41
|
+
Under each PLAN.md phase, make **each discrete implementation action a separate stable task checkbox**. Do not embed Steps, File scopes, or Acceptance criteria below its checkbox. In `implementation/phase-<ordinal>.md`, relist every stable task ID and title and provide ordered implementation steps, exact affected files, API/data-contract changes, algorithms/libraries, constraints, and acceptance criteria. A lightweight Worker must be able to implement the task from the brief without guessing. Before finalization, inspect each rendered brief and require strict validation; no scaffold comments or “define during implementation” placeholders are acceptable.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# New
|
|
2
2
|
|
|
3
|
-
1.
|
|
4
|
-
2. Research repository files, APIs, tests, and constraints. Follow [plan authoring quality](../design-quality.md)
|
|
5
|
-
3.
|
|
6
|
-
4.
|
|
7
|
-
5.
|
|
3
|
+
1. Call `plan_context` for the supplied slug and branch. If a matching plan already exists, stop rather than silently overwriting it. If the goal is unspecified in foreground mode, use `ask_user_question` for the material scope decision; in background mode, state a blocker if no safe assumption exists.
|
|
4
|
+
2. Research the relevant repository files, APIs, tests, and constraints before drafting. Follow [plan authoring quality](../design-quality.md): substantive Big Ideas bullets, illustrated API contracts, explicit before/after consequences, one stable PLAN.md task per implementation action, and a fully specified numbered brief per phase. Each brief must use numbered phase files, preserve exact phase/task IDs and order parity with PLAN.md, give ordered steps, exact file scopes, API/data contracts, algorithms/libraries, constraints, and acceptance criteria; inspect every rendered brief strictly for accuracy and completeness.
|
|
5
|
+
3. Preserve the exact user request as a typed revision input. Build the **complete** candidate overview, ordered phases and task IDs, and matching briefs in memory. Call `plan_apply_revision` **once** with `mode: "create"`, the short slug, `request: {kind: "user", text: <exact incoming request>}`, a full overview and phase list, and one complete brief per phase; `create` writes revision 0 directly, so `/plan new` works without `plan_init`; do not create a phase-less shell first or open an editor.
|
|
6
|
+
4. Validate the result strictly and inspect the generated brief paths. If any placeholders, missing task steps, mismatched IDs, or incomplete sections remain, report the failure rather than claiming readiness. A validation result is structural, not a check that source code already implements the plan.
|
|
7
|
+
5. Report the plan ID and paths. Continue discussing changes in the same conversation; a later user request is a separate revision. Offer `annotate` for human review and `finalize` when ready, without making the user paste a command merely to continue this request.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Update
|
|
2
2
|
|
|
3
|
-
1.
|
|
4
|
-
2. Research affected
|
|
5
|
-
3.
|
|
6
|
-
4.
|
|
7
|
-
5.
|
|
3
|
+
1. Call `plan_context` for the slug/branch; never guess among multiple candidates. Call `plan_review` once for the selected current revision before interpreting the request. A review of an older revision is stale; do not reapply it.
|
|
4
|
+
2. Preserve the exact incoming request in `request.text` and select `request.kind` (`user`, `annotation`, or `blocker`) according to the trigger. When applying a plan review, read its full feedback; when unblocking execution, preserve the worker's blocker in the request. For annotation requests, include the exact LLM response/outcome in `request.response` and classify every comment as applied, answered, or unresolved with a concrete reason. Do not use vague “later” or “follow-up” deferrals unless the user explicitly requested that deferral. Research affected source files. Follow [plan authoring quality](../design-quality.md), including substantive Design sections and complete per-task implementation briefs. For both new and amended content, briefs must use numbered phase files, exact phase/task ID and order parity, ordered steps, exact file scopes, API/data contracts, algorithms/libraries, constraints, and acceptance criteria; strictly inspect every rendered brief for accuracy and completeness.
|
|
5
|
+
3. Build the **entire resulting plan and numbered briefs in memory**, preserving completed task IDs, ownership, evidence, and dependencies. For a single authoring request, call `plan_apply_revision` with `mode: "amend"` exactly once, passing `plan`, `expectedPlanRevision`, `request`, the entire overview, ordered phases, and matching briefs. Do not mutate overview/phase/task content with separate tools, edit managed files by hand, or create a revision per comment.
|
|
6
|
+
4. In foreground mode, ask only for missing decisions that materially affect scope; background work must not ask. Strictly validate the saved plan and briefs; stop with precise errors if they are incomplete. Do not treat structural validation as evidence that implementation already meets requirements.
|
|
7
|
+
5. Report the one new revision and its snapshot path, what changed, and any remaining blocker. Further chat instructions form a future separate revision; `annotate` reviews the plan and `finalize` marks it ready.
|
package/templates/plan/PLAN.md
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
<!-- diffpi-plan: {"schemaVersion":1,"id":"{{id}}","revision":0,"branch":"{{branch}}","issueId":"{{issue_id}}","issueUrl":"{{issue_url}}","status":"draft","createdAt":"{{created_at}}","updatedAt":"{{updated_at}}"} -->
|
|
2
|
+
|
|
1
3
|
# {{title}}
|
|
2
4
|
|
|
3
5
|
- **Plan ID:** {{id}}
|
|
4
6
|
- **Branch:** {{branch}}
|
|
5
|
-
- **Status:** draft
|
|
6
7
|
- **Issue:** {{issue_id}}
|
|
7
8
|
- **Issue URL:** {{issue_url}}
|
|
9
|
+
- **Status:** draft
|
|
10
|
+
- **Revision:** 0
|
|
8
11
|
|
|
9
12
|
## Intent
|
|
10
13
|
|
|
@@ -12,35 +15,26 @@
|
|
|
12
15
|
|
|
13
16
|
## Requirements
|
|
14
17
|
|
|
15
|
-
<!-- Add
|
|
18
|
+
<!-- Add requirements. -->
|
|
16
19
|
|
|
17
20
|
## Design
|
|
18
21
|
|
|
19
22
|
### Big Ideas
|
|
20
23
|
|
|
21
|
-
<!-- Describe the
|
|
24
|
+
<!-- Describe the main approach. -->
|
|
22
25
|
|
|
23
26
|
### Key API Addition/Updates
|
|
24
27
|
|
|
25
|
-
<!-- Describe public
|
|
28
|
+
<!-- Describe public API changes. -->
|
|
26
29
|
|
|
27
30
|
### Consequences
|
|
28
31
|
|
|
29
|
-
<!-- Describe
|
|
30
|
-
|
|
31
|
-
## Phases
|
|
32
|
-
|
|
33
|
-
<!-- Use numbered phases. State prerequisites on the phase only; do not add task dependencies here. -->
|
|
34
|
-
|
|
35
|
-
### Phase 1: <!-- phase title -->
|
|
32
|
+
<!-- Describe trade-offs and limitations. -->
|
|
36
33
|
|
|
37
|
-
|
|
38
|
-
- **Prerequisites:** <!-- list phase IDs, or “None” -->
|
|
39
|
-
- **Objective:** <!-- state the outcome -->
|
|
34
|
+
## Implementation
|
|
40
35
|
|
|
41
|
-
|
|
42
|
-
- [ ] <!-- assign a stable task ID --> Another task title
|
|
36
|
+
<!-- Add phases with one complete plan_apply_revision request. -->
|
|
43
37
|
|
|
44
38
|
## References
|
|
45
39
|
|
|
46
|
-
<!-- Add
|
|
40
|
+
<!-- Add references. -->
|