@difflab/pi 0.5.0 → 0.6.0-rc.202609262054.4a8e556
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -6
- package/agents/diffpi-copilot.md +6 -3
- package/agents/diffpi-orchestrator.md +6 -4
- package/agents/diffpi-planner.md +3 -3
- package/agents/diffpi-reviewer.md +3 -3
- package/agents/diffpi-worker.md +1 -1
- package/dist/assets.d.ts +0 -1
- package/dist/assets.d.ts.map +1 -1
- package/dist/cli.js +417 -84
- package/dist/commands/plan.d.ts +2 -2
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/commands/review.d.ts +2 -3
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/environment.d.ts +2 -0
- package/dist/environment.d.ts.map +1 -1
- package/dist/extensions/index.js +1114 -3634
- package/dist/index.js +437 -99
- package/dist/plan/markdown.d.ts +4 -1
- package/dist/plan/markdown.d.ts.map +1 -1
- package/dist/{tools/plan-schema.d.ts → plan/schema.d.ts} +113 -75
- package/dist/plan/schema.d.ts.map +1 -0
- package/dist/plan/store.d.ts +39 -1
- package/dist/plan/store.d.ts.map +1 -1
- package/dist/plan/types.d.ts +27 -3
- package/dist/plan/types.d.ts.map +1 -1
- package/dist/review/local-reviews.d.ts +31 -0
- package/dist/review/local-reviews.d.ts.map +1 -0
- package/dist/review/review-markdown.d.ts.map +1 -1
- package/dist/tools/index.js +836 -410
- package/dist/tools/plan.d.ts.map +1 -1
- package/dist/tools/review.d.ts +6 -0
- package/dist/tools/review.d.ts.map +1 -1
- package/package.json +2 -4
- package/skills/plan/SKILL.md +8 -14
- package/skills/plan/references/design-quality.md +41 -0
- package/skills/plan/references/workflows/annotate.md +11 -0
- package/skills/plan/references/workflows/finalize.md +7 -0
- package/skills/plan/references/workflows/go.md +8 -0
- package/skills/plan/references/workflows/help.md +10 -0
- package/skills/plan/references/workflows/init.md +5 -0
- package/skills/plan/references/workflows/new.md +7 -0
- package/skills/plan/references/workflows/update.md +7 -0
- package/skills/review/SKILL.md +7 -9
- package/skills/review/references/review-standards.md +8 -1
- package/skills/review/references/workflows/address.md +4 -4
- package/skills/review/references/workflows/help.md +8 -2
- package/skills/review/references/workflows/open.md +1 -1
- package/skills/review/references/workflows/status.md +3 -0
- package/templates/plan/PLAN.md +2 -2
- package/templates/plan/implementation.md +16 -10
- package/dist/commands.d.ts +0 -22
- package/dist/commands.d.ts.map +0 -1
- package/dist/extensions/cmdtsx.d.ts +0 -2
- package/dist/extensions/cmdtsx.d.ts.map +0 -1
- package/dist/tools/plan-schema.d.ts.map +0 -1
- package/dist/workflow.d.ts +0 -33
- package/dist/workflow.d.ts.map +0 -1
- package/skills/review/references/workflows/auto.md +0 -18
- package/skills/review/references/workflows/complete.md +0 -7
- package/skills/review/references/workflows/edit.md +0 -7
- package/skills/review/references/workflows/merge.md +0 -6
- package/skills/review/references/workflows/new.md +0 -19
- package/skills/review/references/workflows/publish.md +0 -8
- package/workflows/plan/annotate.md +0 -13
- package/workflows/plan/finalize.md +0 -15
- package/workflows/plan/go.md +0 -20
- package/workflows/plan/help.md +0 -20
- package/workflows/plan/init.md +0 -13
- package/workflows/plan/new.md +0 -20
- package/workflows/plan/update.md +0 -18
package/dist/tools/index.js
CHANGED
|
@@ -503,6 +503,16 @@ function diffpiLaunchName(cwd, workflow) {
|
|
|
503
503
|
}
|
|
504
504
|
async function openFileAdjacent(path, opts) {
|
|
505
505
|
const env = opts.env ?? process.env;
|
|
506
|
+
const mux = detectMux(env);
|
|
507
|
+
if (mux !== "none") {
|
|
508
|
+
const editor = await resolveEditor(env);
|
|
509
|
+
if (editor) {
|
|
510
|
+
const command = persistentEditorArgs(editor, path, opts.cwd, env.SHELL);
|
|
511
|
+
const opened = await openMuxTab(mux, command, opts.cwd, opts.name ?? "plan", [editor, path].join(" "), command);
|
|
512
|
+
if (opened)
|
|
513
|
+
return opened;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
506
516
|
const ide = detectIde(env);
|
|
507
517
|
if (ide !== "unknown") {
|
|
508
518
|
const ideCommand = { zed: "zed", vscode: "code", cursor: "cursor", windsurf: "windsurf", jetbrains: "idea" }[ide];
|
|
@@ -512,16 +522,6 @@ async function openFileAdjacent(path, opts) {
|
|
|
512
522
|
return { launched: true, via: ide, command: `${ideCommand} ${path}` };
|
|
513
523
|
}
|
|
514
524
|
}
|
|
515
|
-
const mux = detectMux(env);
|
|
516
|
-
if (mux !== "none") {
|
|
517
|
-
const editor = await resolveEditor(env);
|
|
518
|
-
if (editor) {
|
|
519
|
-
const command = [editor, path];
|
|
520
|
-
const opened = await openMuxTab(mux, command, opts.cwd, opts.name ?? "plan", command.join(" "));
|
|
521
|
-
if (opened)
|
|
522
|
-
return opened;
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
525
|
return { launched: false, via: "print", command: path, reason: "No supported IDE or multiplexer was available." };
|
|
526
526
|
}
|
|
527
527
|
async function openInNewTab(command, opts) {
|
|
@@ -556,10 +556,15 @@ async function openInNewTab(command, opts) {
|
|
|
556
556
|
function screenWindowArgs(command, cwd, name) {
|
|
557
557
|
return ["-X", "screen", "-t", name, "sh", "-lc", 'cd -- "$1" && shift && exec "$@"', "sh", cwd, ...command];
|
|
558
558
|
}
|
|
559
|
+
function persistentEditorArgs(editor, path, cwd, shell = "sh") {
|
|
560
|
+
return ["bash", "-lc", 'cd -- "$1" && shift && "$@"; exec "$0" -i', shell, cwd, editor, path];
|
|
561
|
+
}
|
|
559
562
|
async function resolveEditor(env) {
|
|
560
|
-
const configured
|
|
561
|
-
|
|
562
|
-
|
|
563
|
+
for (const configured of [env.EDITOR, env.VISUAL]) {
|
|
564
|
+
const editor = configured?.trim();
|
|
565
|
+
if (editor && await findExecutable(editor))
|
|
566
|
+
return editor;
|
|
567
|
+
}
|
|
563
568
|
for (const candidate of ["hx", "nvim", "vim"])
|
|
564
569
|
if (await findExecutable(candidate))
|
|
565
570
|
return candidate;
|
|
@@ -576,7 +581,7 @@ async function packageVersion() {
|
|
|
576
581
|
throw new Error("Cannot resolve the installed @difflab/pi version.");
|
|
577
582
|
return value.version;
|
|
578
583
|
}
|
|
579
|
-
async function openMuxTab(mux, command, cwd, name, printable) {
|
|
584
|
+
async function openMuxTab(mux, command, cwd, name, printable, tmuxCommand) {
|
|
580
585
|
if (mux === "zellij" && await findExecutable("zellij")) {
|
|
581
586
|
const result = await run("zellij", ["action", "new-tab", "--cwd", cwd, "--name", name, "--", ...command]);
|
|
582
587
|
if (result.code === 0)
|
|
@@ -586,7 +591,7 @@ async function openMuxTab(mux, command, cwd, name, printable) {
|
|
|
586
591
|
return { launched: true, via: "zellij-run", command: printable };
|
|
587
592
|
}
|
|
588
593
|
if (mux === "tmux" && await findExecutable("tmux")) {
|
|
589
|
-
const result = await run("tmux", ["new-window", "-c", cwd, "-n", name, printable]);
|
|
594
|
+
const result = tmuxCommand ? await run("tmux", ["new-window", "-c", cwd, "-n", name, ...tmuxCommand]) : await run("tmux", ["new-window", "-c", cwd, "-n", name, printable]);
|
|
590
595
|
if (result.code === 0)
|
|
591
596
|
return { launched: true, via: "tmux", command: printable };
|
|
592
597
|
}
|
|
@@ -1285,7 +1290,7 @@ function sanitize(value) {
|
|
|
1285
1290
|
}
|
|
1286
1291
|
|
|
1287
1292
|
// src/tools/plan.ts
|
|
1288
|
-
import { join as
|
|
1293
|
+
import { join as join15 } from "node:path";
|
|
1289
1294
|
import { pathToFileURL } from "node:url";
|
|
1290
1295
|
import { defineTool as defineTool5 } from "@earendil-works/pi-coding-agent";
|
|
1291
1296
|
import { z as z7 } from "zod";
|
|
@@ -1522,6 +1527,9 @@ async function discoverMiseTasks(cwd) {
|
|
|
1522
1527
|
|
|
1523
1528
|
// src/plan/markdown.ts
|
|
1524
1529
|
var import_mdast_builder = __toESM(require_src(), 1);
|
|
1530
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1531
|
+
import { readFile as readFile5, readdir } from "node:fs/promises";
|
|
1532
|
+
import { join as join10 } from "node:path";
|
|
1525
1533
|
|
|
1526
1534
|
// ../../node_modules/zwitch/index.js
|
|
1527
1535
|
var own = {}.hasOwnProperty;
|
|
@@ -6161,6 +6169,8 @@ function isUint8Array2(value) {
|
|
|
6161
6169
|
var PLAN_MARKER = /<!-- diffpi-plan: (\{[^\n]+\}) -->/;
|
|
6162
6170
|
var PHASE_MARKER = /<!-- diffpi-phase: (\{[^\n]+\}) -->/g;
|
|
6163
6171
|
var TASK_MARKER = /<!-- diffpi-task: (\{[^\n]+\}) -->/g;
|
|
6172
|
+
var BRIEF_MARKER = /<!-- diffpi-implementation: (\{[^\n]+\}) -->/;
|
|
6173
|
+
var BRIEF_TASK_MARKER = /<!-- diffpi-brief-task: (\{[^\n]+\}) -->/g;
|
|
6164
6174
|
var SUPPORTED_SCHEMA = 1;
|
|
6165
6175
|
function countDesignWords(plan) {
|
|
6166
6176
|
return [plan.design.bigIdeas, plan.design.keyApiUpdates, plan.design.consequences].join(" ").trim().split(/\s+/).filter(Boolean).length;
|
|
@@ -6189,11 +6199,13 @@ function validatePlanDocument(plan, options = {}) {
|
|
|
6189
6199
|
if (!taskIds.has(dependency))
|
|
6190
6200
|
add("dangling-dependency", "error", `Task ${task.id} depends on missing ${dependency}.`);
|
|
6191
6201
|
}
|
|
6192
|
-
if (options.strict && task.
|
|
6193
|
-
add("
|
|
6202
|
+
if (options.strict && isPlaceholder(task.title))
|
|
6203
|
+
add("placeholder", "error", `Task ${task.id} has placeholder content.`, task.id);
|
|
6194
6204
|
}
|
|
6195
6205
|
if (options.strict && phase.tasks.length === 0)
|
|
6196
6206
|
add("empty-phase", "error", `Phase ${phase.id} has no tasks.`, phase.id);
|
|
6207
|
+
if (options.strict && (isPlaceholder(phase.title) || isPlaceholder(phase.objective)))
|
|
6208
|
+
add("placeholder", "error", `Phase ${phase.id} has placeholder content.`, phase.id);
|
|
6197
6209
|
}
|
|
6198
6210
|
for (const cycle of dependencyCycles(plan))
|
|
6199
6211
|
add("dependency-cycle", "error", `Dependency cycle: ${cycle.join(" -> ")}.`);
|
|
@@ -6202,10 +6214,140 @@ function validatePlanDocument(plan, options = {}) {
|
|
|
6202
6214
|
add("design-length", "warning", `Design is ${designWords} words; prefer 300 or fewer.`);
|
|
6203
6215
|
if (options.strict && designWords > 800)
|
|
6204
6216
|
add("design-too-long", "error", `Design is ${designWords} words; finalization allows at most 800.`);
|
|
6205
|
-
if (options.strict
|
|
6206
|
-
|
|
6217
|
+
if (options.strict) {
|
|
6218
|
+
if (plan.phases.length === 0)
|
|
6219
|
+
add("no-phases", "error", "Finalization requires at least one phase.");
|
|
6220
|
+
if (isPlaceholder(plan.intent))
|
|
6221
|
+
add("placeholder", "error", "Intent must contain concrete content.", "intent");
|
|
6222
|
+
if (!plan.requirements.length || plan.requirements.some(isPlaceholder))
|
|
6223
|
+
add("requirements", "error", "Requirements must contain concrete content.", "requirements");
|
|
6224
|
+
for (const [key, value] of Object.entries(plan.design)) {
|
|
6225
|
+
if (isPlaceholder(value))
|
|
6226
|
+
add("design", "error", `Design/${key} must contain concrete content.`, `design.${key}`);
|
|
6227
|
+
}
|
|
6228
|
+
}
|
|
6229
|
+
return issues;
|
|
6230
|
+
}
|
|
6231
|
+
function validatePlanBriefs(plan, briefs) {
|
|
6232
|
+
const issues = [];
|
|
6233
|
+
const phaseIds = plan.phases.map((phase) => phase.id);
|
|
6234
|
+
const briefIds = briefs.map((brief) => brief.phaseId);
|
|
6235
|
+
if (!sameIds(phaseIds, briefIds))
|
|
6236
|
+
issues.push({
|
|
6237
|
+
code: "brief-phase-ids",
|
|
6238
|
+
severity: "error",
|
|
6239
|
+
message: `Brief phase IDs must exactly match plan phases in order: ${phaseIds.join(", ")}.`
|
|
6240
|
+
});
|
|
6241
|
+
for (const [index, phase] of plan.phases.entries()) {
|
|
6242
|
+
const brief = briefs[index];
|
|
6243
|
+
if (!brief || brief.phaseId !== phase.id)
|
|
6244
|
+
continue;
|
|
6245
|
+
if (isPlaceholder(brief.summary))
|
|
6246
|
+
issues.push({ code: "brief-placeholder", severity: "error", message: `Brief ${phase.id} needs a summary.` });
|
|
6247
|
+
const taskIds = phase.tasks.map((task) => task.id);
|
|
6248
|
+
const briefTaskIds = brief.tasks.map((task) => task.taskId);
|
|
6249
|
+
if (!sameIds(taskIds, briefTaskIds))
|
|
6250
|
+
issues.push({
|
|
6251
|
+
code: "brief-task-ids",
|
|
6252
|
+
severity: "error",
|
|
6253
|
+
message: `Brief ${phase.id} task IDs must exactly match phase tasks in order: ${taskIds.join(", ")}.`
|
|
6254
|
+
});
|
|
6255
|
+
for (const task of brief.tasks) {
|
|
6256
|
+
for (const [field, values] of [
|
|
6257
|
+
["ordered steps", task.steps],
|
|
6258
|
+
["file scopes", task.fileScopes],
|
|
6259
|
+
["acceptance criteria", task.acceptanceCriteria]
|
|
6260
|
+
]) {
|
|
6261
|
+
if (!values.length || values.some(isPlaceholder))
|
|
6262
|
+
issues.push({
|
|
6263
|
+
code: "brief-incomplete",
|
|
6264
|
+
severity: "error",
|
|
6265
|
+
message: `Brief task ${task.taskId} needs concrete ${field}.`,
|
|
6266
|
+
path: task.taskId
|
|
6267
|
+
});
|
|
6268
|
+
}
|
|
6269
|
+
}
|
|
6270
|
+
for (const [field, values] of [
|
|
6271
|
+
["API changes", brief.apiChanges],
|
|
6272
|
+
["libraries", brief.libraries],
|
|
6273
|
+
["constraints", brief.constraints]
|
|
6274
|
+
]) {
|
|
6275
|
+
if (values.some(isPlaceholder))
|
|
6276
|
+
issues.push({
|
|
6277
|
+
code: "brief-placeholder",
|
|
6278
|
+
severity: "error",
|
|
6279
|
+
message: `Brief ${phase.id} has placeholder ${field}.`,
|
|
6280
|
+
path: phase.id
|
|
6281
|
+
});
|
|
6282
|
+
}
|
|
6283
|
+
}
|
|
6284
|
+
return issues;
|
|
6285
|
+
}
|
|
6286
|
+
async function validatePlanRecord(record, options = {}) {
|
|
6287
|
+
const issues = validatePlanDocument(record.document, options);
|
|
6288
|
+
if (!options.strict)
|
|
6289
|
+
return issues;
|
|
6290
|
+
await validateImplementationDirectory(record.dir, record.document, issues, "latest");
|
|
6291
|
+
if (options.checkSnapshots !== false)
|
|
6292
|
+
await validateRevisionSnapshots(record, issues);
|
|
6207
6293
|
return issues;
|
|
6208
6294
|
}
|
|
6295
|
+
function renderImplementationBrief(planRevision, ordinal, phase, brief) {
|
|
6296
|
+
const marker = { schemaVersion: 1, planRevision, ordinal, phaseId: phase.id };
|
|
6297
|
+
const tasks = phase.tasks.map((task, index) => {
|
|
6298
|
+
const details = brief.tasks[index];
|
|
6299
|
+
if (!details || details.taskId !== task.id)
|
|
6300
|
+
throw new Error(`Missing implementation details for ${task.id}.`);
|
|
6301
|
+
return `<!-- diffpi-brief-task: ${json({ id: task.id })} -->
|
|
6302
|
+
### Task: ${task.title}
|
|
6303
|
+
|
|
6304
|
+
#### Ordered Steps
|
|
6305
|
+
|
|
6306
|
+
${bulletList(details.steps)}
|
|
6307
|
+
|
|
6308
|
+
#### File Scopes
|
|
6309
|
+
|
|
6310
|
+
${bulletList(details.fileScopes)}
|
|
6311
|
+
|
|
6312
|
+
#### Acceptance Criteria
|
|
6313
|
+
|
|
6314
|
+
${bulletList(details.acceptanceCriteria)}
|
|
6315
|
+
|
|
6316
|
+
<!-- /diffpi-brief-task -->`;
|
|
6317
|
+
}).join(`
|
|
6318
|
+
|
|
6319
|
+
`);
|
|
6320
|
+
return `<!-- diffpi-implementation: ${json(marker)} -->
|
|
6321
|
+
# Phase ${ordinal}: ${phase.title}
|
|
6322
|
+
|
|
6323
|
+
- **Phase ID:** ${phase.id}
|
|
6324
|
+
- **Plan Revision:** ${planRevision}
|
|
6325
|
+
|
|
6326
|
+
## Summary
|
|
6327
|
+
|
|
6328
|
+
${brief.summary}
|
|
6329
|
+
|
|
6330
|
+
## Objective
|
|
6331
|
+
|
|
6332
|
+
${phase.objective}
|
|
6333
|
+
|
|
6334
|
+
## Tasks
|
|
6335
|
+
|
|
6336
|
+
${tasks}
|
|
6337
|
+
|
|
6338
|
+
## API Changes
|
|
6339
|
+
|
|
6340
|
+
${bulletList(brief.apiChanges, "None.")}
|
|
6341
|
+
|
|
6342
|
+
## Libraries and Algorithms
|
|
6343
|
+
|
|
6344
|
+
${bulletList(brief.libraries, "None.")}
|
|
6345
|
+
|
|
6346
|
+
## Implementation Constraints
|
|
6347
|
+
|
|
6348
|
+
${bulletList(brief.constraints, "None.")}
|
|
6349
|
+
`;
|
|
6350
|
+
}
|
|
6209
6351
|
function renderPlanDocument(plan, previousSource) {
|
|
6210
6352
|
if (previousSource && canPatch(previousSource, plan))
|
|
6211
6353
|
return patchMarkers(previousSource, plan);
|
|
@@ -6214,6 +6356,8 @@ function renderPlanDocument(plan, previousSource) {
|
|
|
6214
6356
|
id: plan.id,
|
|
6215
6357
|
revision: plan.revision,
|
|
6216
6358
|
branch: plan.branch,
|
|
6359
|
+
issueId: plan.issueId,
|
|
6360
|
+
issueUrl: plan.issueUrl,
|
|
6217
6361
|
status: plan.status,
|
|
6218
6362
|
execution: plan.execution,
|
|
6219
6363
|
createdAt: plan.createdAt,
|
|
@@ -6222,6 +6366,8 @@ function renderPlanDocument(plan, previousSource) {
|
|
|
6222
6366
|
const metadata = import_mdast_builder.list("unordered", [
|
|
6223
6367
|
["Plan ID", plan.id],
|
|
6224
6368
|
["Branch", plan.branch],
|
|
6369
|
+
...plan.issueId ? [["Issue", plan.issueId]] : [],
|
|
6370
|
+
...plan.issueUrl ? [["Issue URL", plan.issueUrl]] : [],
|
|
6225
6371
|
["Status", plan.status],
|
|
6226
6372
|
["Revision", String(plan.revision)]
|
|
6227
6373
|
].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}`)]))));
|
|
@@ -6243,7 +6389,7 @@ function renderPlanDocument(plan, previousSource) {
|
|
|
6243
6389
|
import_mdast_builder.heading(3, import_mdast_builder.text("Consequences")),
|
|
6244
6390
|
import_mdast_builder.html(plan.design.consequences || "<!-- Describe trade-offs and limitations. -->"),
|
|
6245
6391
|
import_mdast_builder.heading(2, import_mdast_builder.text("Implementation")),
|
|
6246
|
-
...plan.phases.length ? plan.phases.flatMap((phase) => [import_mdast_builder.html(renderPhase(phase))]) : [import_mdast_builder.html("<!-- Add phases with
|
|
6392
|
+
...plan.phases.length ? plan.phases.flatMap((phase) => [import_mdast_builder.html(renderPhase(phase))]) : [import_mdast_builder.html("<!-- Add phases with plan_apply_revision. -->")],
|
|
6247
6393
|
import_mdast_builder.heading(2, import_mdast_builder.text("References")),
|
|
6248
6394
|
references
|
|
6249
6395
|
]);
|
|
@@ -6317,10 +6463,7 @@ function renderTask(task) {
|
|
|
6317
6463
|
const checked = task.status === "completed" || task.status === "skipped" ? "x" : " ";
|
|
6318
6464
|
return `<!-- diffpi-task: ${json(marker)} -->
|
|
6319
6465
|
- [${checked}] **${task.title}**
|
|
6320
|
-
${taskList("Steps", task.steps ?? [])}
|
|
6321
6466
|
- Dependencies: ${list3(task.dependencies)}
|
|
6322
|
-
${taskList("File scopes", task.fileScopes)}
|
|
6323
|
-
${taskList("Acceptance criteria", task.acceptanceCriteria)}
|
|
6324
6467
|
<!-- /diffpi-task -->`;
|
|
6325
6468
|
}
|
|
6326
6469
|
function parsePhases(input, ref) {
|
|
@@ -6370,10 +6513,7 @@ function parseTasks(input, ref) {
|
|
|
6370
6513
|
return {
|
|
6371
6514
|
...marker,
|
|
6372
6515
|
title,
|
|
6373
|
-
|
|
6374
|
-
dependencies: parseListValue(body, "Dependencies"),
|
|
6375
|
-
fileScopes: parseListValue(body, "File scopes"),
|
|
6376
|
-
acceptanceCriteria: parseListValue(body, "Acceptance criteria")
|
|
6516
|
+
dependencies: parseListValue(body, "Dependencies")
|
|
6377
6517
|
};
|
|
6378
6518
|
});
|
|
6379
6519
|
assertUniqueIds(tasks.map((task) => task.id), "task id");
|
|
@@ -6400,6 +6540,8 @@ function patchMarkers(source, plan) {
|
|
|
6400
6540
|
id: plan.id,
|
|
6401
6541
|
revision: plan.revision,
|
|
6402
6542
|
branch: plan.branch,
|
|
6543
|
+
issueId: plan.issueId,
|
|
6544
|
+
issueUrl: plan.issueUrl,
|
|
6403
6545
|
status: plan.status,
|
|
6404
6546
|
execution: plan.execution,
|
|
6405
6547
|
createdAt: plan.createdAt,
|
|
@@ -6449,19 +6591,12 @@ function contentShape(plan) {
|
|
|
6449
6591
|
requirements: plan.requirements,
|
|
6450
6592
|
design: plan.design,
|
|
6451
6593
|
references: plan.references,
|
|
6452
|
-
phases: plan.phases.map((
|
|
6453
|
-
id,
|
|
6454
|
-
title,
|
|
6455
|
-
objective,
|
|
6456
|
-
dependencies,
|
|
6457
|
-
tasks: tasks.map(({ id
|
|
6458
|
-
id: taskId,
|
|
6459
|
-
title: taskTitle,
|
|
6460
|
-
steps,
|
|
6461
|
-
dependencies: taskDependencies,
|
|
6462
|
-
fileScopes,
|
|
6463
|
-
acceptanceCriteria
|
|
6464
|
-
}))
|
|
6594
|
+
phases: plan.phases.map((phase) => ({
|
|
6595
|
+
id: phase.id,
|
|
6596
|
+
title: phase.title,
|
|
6597
|
+
objective: phase.objective,
|
|
6598
|
+
dependencies: phase.dependencies,
|
|
6599
|
+
tasks: phase.tasks.map(({ id, title, dependencies }) => ({ id, title, dependencies }))
|
|
6465
6600
|
}))
|
|
6466
6601
|
};
|
|
6467
6602
|
}
|
|
@@ -6491,6 +6626,152 @@ function dependencyCycles(plan) {
|
|
|
6491
6626
|
walk(id, []);
|
|
6492
6627
|
return cycles;
|
|
6493
6628
|
}
|
|
6629
|
+
async function validateRevisionSnapshots(record, issues) {
|
|
6630
|
+
const revisionsDir = join10(record.dir, "revisions");
|
|
6631
|
+
let entries;
|
|
6632
|
+
try {
|
|
6633
|
+
entries = await readdir(revisionsDir);
|
|
6634
|
+
} catch {
|
|
6635
|
+
issues.push({ code: "snapshot-missing", severity: "error", message: "Missing revisions directory." });
|
|
6636
|
+
return;
|
|
6637
|
+
}
|
|
6638
|
+
const expected = Array.from({ length: record.document.revision + 1 }, (_, index) => String(index));
|
|
6639
|
+
if (!sameIds(expected, entries.filter((entry) => /^\d+$/.test(entry)).sort((a, b) => Number(a) - Number(b))))
|
|
6640
|
+
issues.push({
|
|
6641
|
+
code: "snapshot-sequence",
|
|
6642
|
+
severity: "error",
|
|
6643
|
+
message: `Revision snapshots must be exactly ${expected.join(", ")}.`,
|
|
6644
|
+
path: revisionsDir
|
|
6645
|
+
});
|
|
6646
|
+
for (const entry of entries) {
|
|
6647
|
+
if (!/^\d+$/.test(entry))
|
|
6648
|
+
issues.push({ code: "snapshot-entry", severity: "error", message: `Unexpected revisions entry: ${entry}.` });
|
|
6649
|
+
}
|
|
6650
|
+
for (const revision of expected) {
|
|
6651
|
+
const dir = join10(revisionsDir, revision);
|
|
6652
|
+
try {
|
|
6653
|
+
const [request, metadataSource, planSource] = await Promise.all([
|
|
6654
|
+
readFile5(join10(dir, "request.md"), "utf8"),
|
|
6655
|
+
readFile5(join10(dir, "metadata.json"), "utf8"),
|
|
6656
|
+
readFile5(join10(dir, "PLAN.md"), "utf8")
|
|
6657
|
+
]);
|
|
6658
|
+
if (isPlaceholder(request))
|
|
6659
|
+
issues.push({ code: "snapshot-request", severity: "error", message: `Revision ${revision} request is empty.` });
|
|
6660
|
+
const metadata = JSON.parse(metadataSource);
|
|
6661
|
+
const annotation = metadata.requestKind === "annotation" ? /^## Original annotation\n([\s\S]*?)\n\n## LLM response\n([\s\S]*)$/.exec(request) : undefined;
|
|
6662
|
+
const requestText = annotation?.[1];
|
|
6663
|
+
const responseText = annotation?.[2];
|
|
6664
|
+
if (metadata.schemaVersion !== 1 || metadata.id !== record.id || metadata.revision !== Number(revision) || !["user", "annotation", "blocker"].includes(metadata.requestKind) || (metadata.requestKind === "annotation" ? !annotation || !requestText?.trim() || !responseText?.trim() || metadata.requestSha256 !== sha256(requestText) || metadata.originalAnnotationSha256 !== sha256(requestText) || metadata.responseSha256 !== sha256(responseText) : metadata.requestSha256 !== sha256(request)))
|
|
6665
|
+
issues.push({
|
|
6666
|
+
code: "snapshot-metadata",
|
|
6667
|
+
severity: "error",
|
|
6668
|
+
message: `Revision ${revision} metadata does not match its immutable request.`
|
|
6669
|
+
});
|
|
6670
|
+
const snapshotPlan = parsePlanDocument(planSource, join10(dir, "PLAN.md"));
|
|
6671
|
+
if (snapshotPlan.id !== record.id || snapshotPlan.revision !== Number(revision))
|
|
6672
|
+
issues.push({
|
|
6673
|
+
code: "snapshot-plan",
|
|
6674
|
+
severity: "error",
|
|
6675
|
+
message: `Revision ${revision} PLAN.md has mismatched identity or revision.`
|
|
6676
|
+
});
|
|
6677
|
+
await validateImplementationDirectory(dir, snapshotPlan, issues, `revision ${revision}`);
|
|
6678
|
+
} catch (error) {
|
|
6679
|
+
issues.push({
|
|
6680
|
+
code: "snapshot-missing",
|
|
6681
|
+
severity: "error",
|
|
6682
|
+
message: `Revision ${revision} is incomplete: ${error.message}`,
|
|
6683
|
+
path: dir
|
|
6684
|
+
});
|
|
6685
|
+
}
|
|
6686
|
+
}
|
|
6687
|
+
}
|
|
6688
|
+
async function validateImplementationDirectory(baseDir, plan, issues, label) {
|
|
6689
|
+
const dir = join10(baseDir, "implementation");
|
|
6690
|
+
let entries;
|
|
6691
|
+
try {
|
|
6692
|
+
entries = await readdir(dir);
|
|
6693
|
+
} catch {
|
|
6694
|
+
issues.push({
|
|
6695
|
+
code: "brief-directory",
|
|
6696
|
+
severity: "error",
|
|
6697
|
+
message: `${label} is missing implementation/.`,
|
|
6698
|
+
path: dir
|
|
6699
|
+
});
|
|
6700
|
+
return;
|
|
6701
|
+
}
|
|
6702
|
+
const expected = plan.phases.map((_phase, index) => `phase-${index + 1}.md`);
|
|
6703
|
+
const actual = entries.filter((entry) => entry.endsWith(".md")).sort((left, right) => left.localeCompare(right));
|
|
6704
|
+
if (!sameIds([...expected].sort((left, right) => left.localeCompare(right)), actual))
|
|
6705
|
+
issues.push({
|
|
6706
|
+
code: "brief-filenames",
|
|
6707
|
+
severity: "error",
|
|
6708
|
+
message: `${label} briefs must use exactly: ${expected.join(", ") || "(none)"}.`,
|
|
6709
|
+
path: dir
|
|
6710
|
+
});
|
|
6711
|
+
for (const [index, phase] of plan.phases.entries()) {
|
|
6712
|
+
const path = join10(dir, `phase-${index + 1}.md`);
|
|
6713
|
+
try {
|
|
6714
|
+
validateBriefSource(await readFile5(path, "utf8"), plan.revision, index + 1, phase, path, issues);
|
|
6715
|
+
} catch (error) {
|
|
6716
|
+
issues.push({
|
|
6717
|
+
code: "brief-missing",
|
|
6718
|
+
severity: "error",
|
|
6719
|
+
message: `${label} brief phase-${index + 1}.md is missing: ${error.message}`,
|
|
6720
|
+
path
|
|
6721
|
+
});
|
|
6722
|
+
}
|
|
6723
|
+
}
|
|
6724
|
+
}
|
|
6725
|
+
function validateBriefSource(source, planRevision, ordinal, phase, path, issues) {
|
|
6726
|
+
const matches = source.match(new RegExp(BRIEF_MARKER.source, "g")) ?? [];
|
|
6727
|
+
const markerMatch = source.match(BRIEF_MARKER);
|
|
6728
|
+
if (matches.length !== 1 || !markerMatch) {
|
|
6729
|
+
issues.push({ code: "brief-marker", severity: "error", message: `${path} needs one implementation marker.`, path });
|
|
6730
|
+
return;
|
|
6731
|
+
}
|
|
6732
|
+
const marker = parseMarker(markerMatch[1], `${path} implementation`);
|
|
6733
|
+
if (marker.schemaVersion !== 1 || marker.planRevision !== planRevision || marker.ordinal !== ordinal || marker.phaseId !== phase.id)
|
|
6734
|
+
issues.push({
|
|
6735
|
+
code: "brief-marker",
|
|
6736
|
+
severity: "error",
|
|
6737
|
+
message: `${path} marker must identify phase ${ordinal} (${phase.id}) at revision ${planRevision}.`,
|
|
6738
|
+
path
|
|
6739
|
+
});
|
|
6740
|
+
const taskIds = [...source.matchAll(BRIEF_TASK_MARKER)].map((match) => parseMarker(match[1], `${path} task`).id);
|
|
6741
|
+
if (!sameIds(phase.tasks.map((task) => task.id), taskIds))
|
|
6742
|
+
issues.push({
|
|
6743
|
+
code: "brief-task-ids",
|
|
6744
|
+
severity: "error",
|
|
6745
|
+
message: `${path} task markers must exactly match phase task IDs in order.`,
|
|
6746
|
+
path
|
|
6747
|
+
});
|
|
6748
|
+
const prose = source.replace(BRIEF_MARKER, "").replace(BRIEF_TASK_MARKER, "").replace(/<!-- \/diffpi-brief-task -->/g, "");
|
|
6749
|
+
if (/<!--[^]*?-->|\b(?:TODO|TBD)\b|define during implementation/i.test(prose))
|
|
6750
|
+
issues.push({
|
|
6751
|
+
code: "brief-placeholder",
|
|
6752
|
+
severity: "error",
|
|
6753
|
+
message: `${path} contains placeholder content.`,
|
|
6754
|
+
path
|
|
6755
|
+
});
|
|
6756
|
+
try {
|
|
6757
|
+
if (isPlaceholder(section(source, "Summary")))
|
|
6758
|
+
throw new Error("Summary is empty.");
|
|
6759
|
+
for (const task of phase.tasks) {
|
|
6760
|
+
const taskStart = source.indexOf(`<!-- diffpi-brief-task: ${json({ id: task.id })} -->`);
|
|
6761
|
+
const taskEnd = source.indexOf("<!-- /diffpi-brief-task -->", taskStart);
|
|
6762
|
+
if (taskStart < 0 || taskEnd < 0)
|
|
6763
|
+
throw new Error(`Task ${task.id} body is missing.`);
|
|
6764
|
+
const body = source.slice(taskStart, taskEnd);
|
|
6765
|
+
for (const heading of ["Ordered Steps", "File Scopes", "Acceptance Criteria"]) {
|
|
6766
|
+
const content = briefSubsection(body, heading);
|
|
6767
|
+
if (!parseBullets(content).length || parseBullets(content).some(isPlaceholder))
|
|
6768
|
+
throw new Error(`Task ${task.id} ${heading} is incomplete.`);
|
|
6769
|
+
}
|
|
6770
|
+
}
|
|
6771
|
+
} catch (error) {
|
|
6772
|
+
issues.push({ code: "brief-incomplete", severity: "error", message: `${path}: ${error.message}`, path });
|
|
6773
|
+
}
|
|
6774
|
+
}
|
|
6494
6775
|
function section(source, heading) {
|
|
6495
6776
|
const match = source.match(new RegExp(`^## ${escapeRegExp(heading)}\\s*$`, "m"));
|
|
6496
6777
|
if (!match?.index) {
|
|
@@ -6515,6 +6796,14 @@ function subsection(source, parent, heading) {
|
|
|
6515
6796
|
function parseBullets(input) {
|
|
6516
6797
|
return [...input.matchAll(/^- (?!<!--)(.+)$/gm)].map((match) => match[1].trim());
|
|
6517
6798
|
}
|
|
6799
|
+
function briefSubsection(source, heading) {
|
|
6800
|
+
const match = source.match(new RegExp(`^#### ${escapeRegExp(heading)}\\s*$`, "m"));
|
|
6801
|
+
if (match?.index === undefined)
|
|
6802
|
+
throw new Error(`Missing required brief heading: ${heading}.`);
|
|
6803
|
+
const rest = source.slice(match.index + match[0].length);
|
|
6804
|
+
const end = rest.search(/^#### |^<!-- \/diffpi-brief-task -->/m);
|
|
6805
|
+
return (end < 0 ? rest : rest.slice(0, end)).trim();
|
|
6806
|
+
}
|
|
6518
6807
|
function parseListValue(body, label) {
|
|
6519
6808
|
const match = new RegExp(`^ {2}- ${escapeRegExp(label)}:[ \\t]*(.*)$`, "m").exec(body);
|
|
6520
6809
|
if (!match)
|
|
@@ -6561,6 +6850,16 @@ function assertUniqueIds(ids, label) {
|
|
|
6561
6850
|
function cleanPlaceholder(value) {
|
|
6562
6851
|
return value.replace(/<!--[^]*?-->/g, "").trim();
|
|
6563
6852
|
}
|
|
6853
|
+
function isPlaceholder(value) {
|
|
6854
|
+
const normalized = cleanPlaceholder(value).trim();
|
|
6855
|
+
return normalized.length === 0 || /^(?:todo|tbd|none|n\/a)$/i.test(normalized) || /define during implementation|describe the|add requirements|add implementation/i.test(normalized);
|
|
6856
|
+
}
|
|
6857
|
+
function sameIds(expected, actual) {
|
|
6858
|
+
return expected.length === actual.length && expected.every((value, index) => actual[index] === value);
|
|
6859
|
+
}
|
|
6860
|
+
function sha256(value) {
|
|
6861
|
+
return createHash2("sha256").update(value).digest("hex");
|
|
6862
|
+
}
|
|
6564
6863
|
function parseMarker(value, label) {
|
|
6565
6864
|
try {
|
|
6566
6865
|
return JSON.parse(value);
|
|
@@ -6568,15 +6867,14 @@ function parseMarker(value, label) {
|
|
|
6568
6867
|
throw new Error(`Malformed ${label} marker JSON.`);
|
|
6569
6868
|
}
|
|
6570
6869
|
}
|
|
6571
|
-
function
|
|
6870
|
+
function bulletList(values, empty = "") {
|
|
6572
6871
|
if (!values.length)
|
|
6573
|
-
return
|
|
6574
|
-
|
|
6872
|
+
return empty;
|
|
6873
|
+
return values.flatMap((value) => {
|
|
6575
6874
|
const [first = "", ...continuations] = value.split(`
|
|
6576
6875
|
`);
|
|
6577
|
-
return [
|
|
6578
|
-
})
|
|
6579
|
-
return [` - ${label}:`, ...items].join(`
|
|
6876
|
+
return [`- ${first}`, ...continuations.map((line) => ` ${line}`)];
|
|
6877
|
+
}).join(`
|
|
6580
6878
|
`);
|
|
6581
6879
|
}
|
|
6582
6880
|
function list3(values) {
|
|
@@ -6902,13 +7200,13 @@ function heartbeat(plan) {
|
|
|
6902
7200
|
}
|
|
6903
7201
|
|
|
6904
7202
|
// src/plan/reviews.ts
|
|
6905
|
-
import { mkdir as mkdir4, readFile as
|
|
6906
|
-
import { dirname as dirname5, join as
|
|
7203
|
+
import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile3 } from "node:fs/promises";
|
|
7204
|
+
import { dirname as dirname5, join as join11 } from "node:path";
|
|
6907
7205
|
async function readPlanReview(record) {
|
|
6908
7206
|
const path = reviewPath(record);
|
|
6909
7207
|
let source;
|
|
6910
7208
|
try {
|
|
6911
|
-
source = await
|
|
7209
|
+
source = await readFile6(path, "utf8");
|
|
6912
7210
|
} catch (error) {
|
|
6913
7211
|
if (error.code === "ENOENT")
|
|
6914
7212
|
return;
|
|
@@ -6925,7 +7223,7 @@ async function readPlanReview(record) {
|
|
|
6925
7223
|
return { path, dump };
|
|
6926
7224
|
}
|
|
6927
7225
|
function reviewPath(record) {
|
|
6928
|
-
return
|
|
7226
|
+
return join11(record.dir, "reviews", `${record.document.revision}.json`);
|
|
6929
7227
|
}
|
|
6930
7228
|
function isPlanReviewDump(value) {
|
|
6931
7229
|
if (!value || typeof value !== "object")
|
|
@@ -6935,14 +7233,15 @@ function isPlanReviewDump(value) {
|
|
|
6935
7233
|
}
|
|
6936
7234
|
|
|
6937
7235
|
// src/plan/store.ts
|
|
6938
|
-
import {
|
|
6939
|
-
import {
|
|
7236
|
+
import { createHash as createHash3, randomUUID as randomUUID2 } from "node:crypto";
|
|
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";
|
|
6940
7239
|
|
|
6941
7240
|
// src/extensions/fsx.ts
|
|
6942
7241
|
import { mkdir as mkdir5, rename, rm, writeFile as writeFile4 } from "node:fs/promises";
|
|
6943
|
-
import { basename as basename4, dirname as dirname6, join as
|
|
7242
|
+
import { basename as basename4, dirname as dirname6, join as join12 } from "node:path";
|
|
6944
7243
|
async function atomicWrite(path, content) {
|
|
6945
|
-
const temp =
|
|
7244
|
+
const temp = join12(dirname6(path), `.${basename4(path)}.${crypto.randomUUID()}.tmp`);
|
|
6946
7245
|
await writeFile4(temp, content, { encoding: "utf8", mode: 384, flag: "wx" });
|
|
6947
7246
|
await rename(temp, path);
|
|
6948
7247
|
}
|
|
@@ -6973,13 +7272,13 @@ function sleep(ms) {
|
|
|
6973
7272
|
}
|
|
6974
7273
|
|
|
6975
7274
|
// src/templates.ts
|
|
6976
|
-
import { readFile as
|
|
7275
|
+
import { readFile as readFile7 } from "node:fs/promises";
|
|
6977
7276
|
import { homedir as homedir4 } from "node:os";
|
|
6978
|
-
import { join as
|
|
7277
|
+
import { join as join13, normalize } from "node:path";
|
|
6979
7278
|
async function loadTemplate(name, options = {}) {
|
|
6980
7279
|
const relative = templateRelativePath(name);
|
|
6981
|
-
const userPath =
|
|
6982
|
-
const bundledPath =
|
|
7280
|
+
const userPath = join13(options.homeDir ?? homedir4(), ".difflab", "diffpi", "templates", relative);
|
|
7281
|
+
const bundledPath = join13(options.bundledDir ?? resolveBundledTemplatesDir(), relative);
|
|
6983
7282
|
const user = await readOptionalFile(userPath);
|
|
6984
7283
|
if (user !== undefined)
|
|
6985
7284
|
return { name, path: userPath, source: "user", content: user };
|
|
@@ -7000,7 +7299,7 @@ function templateRelativePath(name) {
|
|
|
7000
7299
|
}
|
|
7001
7300
|
async function readOptionalFile(path) {
|
|
7002
7301
|
try {
|
|
7003
|
-
return await
|
|
7302
|
+
return await readFile7(path, "utf8");
|
|
7004
7303
|
} catch (error) {
|
|
7005
7304
|
if (error.code === "ENOENT")
|
|
7006
7305
|
return;
|
|
@@ -7015,7 +7314,7 @@ function planRecordName(shortSlug, date = new Date) {
|
|
|
7015
7314
|
}
|
|
7016
7315
|
async function resolvePlan(cwd, query, filters = {}, options = {}) {
|
|
7017
7316
|
const root = await plansDir(cwd, options.homeDir);
|
|
7018
|
-
const entries = await
|
|
7317
|
+
const entries = await readdir2(root, { withFileTypes: true });
|
|
7019
7318
|
const normalizedQuery = query ? normalizeQuery(query) : undefined;
|
|
7020
7319
|
const records = [];
|
|
7021
7320
|
for (const entry of entries) {
|
|
@@ -7023,7 +7322,7 @@ async function resolvePlan(cwd, query, filters = {}, options = {}) {
|
|
|
7023
7322
|
continue;
|
|
7024
7323
|
if (normalizedQuery && entry.name !== normalizedQuery && stripDate(entry.name) !== normalizedQuery)
|
|
7025
7324
|
continue;
|
|
7026
|
-
const dir =
|
|
7325
|
+
const dir = join14(root, entry.name);
|
|
7027
7326
|
await assertContained(root, dir);
|
|
7028
7327
|
try {
|
|
7029
7328
|
const record = await readRecord(dir);
|
|
@@ -7042,6 +7341,15 @@ async function resolvePlan(cwd, query, filters = {}, options = {}) {
|
|
|
7042
7341
|
}
|
|
7043
7342
|
function createPlanStore(options = {}) {
|
|
7044
7343
|
const now = options.now ?? (() => new Date);
|
|
7344
|
+
const read = async (cwd, query, filters) => {
|
|
7345
|
+
const result = await resolvePlan(cwd, query, filters, options);
|
|
7346
|
+
if (!result.record) {
|
|
7347
|
+
if (result.ambiguous)
|
|
7348
|
+
throw new Error(`Plan query "${query}" is ambiguous: ${result.candidates.map((item) => item.id).join(", ")}.`);
|
|
7349
|
+
throw new Error(`Plan "${query}" was not found.`);
|
|
7350
|
+
}
|
|
7351
|
+
return result.record;
|
|
7352
|
+
};
|
|
7045
7353
|
return {
|
|
7046
7354
|
context(cwd, query, filters) {
|
|
7047
7355
|
return resolvePlan(cwd, query, filters, options);
|
|
@@ -7049,120 +7357,318 @@ function createPlanStore(options = {}) {
|
|
|
7049
7357
|
async init(input) {
|
|
7050
7358
|
const root = await plansDir(input.cwd, options.homeDir);
|
|
7051
7359
|
const id = planRecordName(input.shortSlug, now());
|
|
7052
|
-
const dir =
|
|
7053
|
-
await
|
|
7054
|
-
try {
|
|
7055
|
-
await mkdir6(dir);
|
|
7056
|
-
} catch (error) {
|
|
7057
|
-
if (error.code === "EEXIST")
|
|
7058
|
-
throw new Error(`Plan ${id} already exists.`);
|
|
7059
|
-
throw error;
|
|
7060
|
-
}
|
|
7360
|
+
const dir = join14(root, id);
|
|
7361
|
+
await createPlanDirectory(root, dir, id);
|
|
7061
7362
|
try {
|
|
7062
7363
|
const timestamp = now().toISOString();
|
|
7063
7364
|
const template = await loadTemplate("plan/PLAN", {
|
|
7064
7365
|
homeDir: options.homeDir,
|
|
7065
7366
|
bundledDir: options.bundledTemplatesDir
|
|
7066
7367
|
});
|
|
7368
|
+
const title = input.title?.trim() || titleFromSlug(input.shortSlug);
|
|
7067
7369
|
const source = renderTemplate(template.content, {
|
|
7068
7370
|
id,
|
|
7069
7371
|
branch: input.branch,
|
|
7070
|
-
title
|
|
7372
|
+
title,
|
|
7071
7373
|
intent: input.intent?.trim() || "<!-- Describe the intended outcome. -->",
|
|
7072
|
-
issue_id: input.issueId?.trim() || "
|
|
7073
|
-
issue_url: input.issueUrl?.trim() || "
|
|
7374
|
+
issue_id: input.issueId?.trim() || "",
|
|
7375
|
+
issue_url: input.issueUrl?.trim() || "",
|
|
7074
7376
|
created_at: timestamp,
|
|
7075
7377
|
updated_at: timestamp
|
|
7076
7378
|
});
|
|
7077
|
-
const document = parsePlanDocument(source,
|
|
7078
|
-
|
|
7079
|
-
|
|
7080
|
-
|
|
7081
|
-
|
|
7082
|
-
planPath: join13(dir, "PLAN.md"),
|
|
7083
|
-
logPath: join13(dir, "logs.txt"),
|
|
7084
|
-
implementationDir: join13(dir, "implementation"),
|
|
7085
|
-
document,
|
|
7086
|
-
source
|
|
7087
|
-
};
|
|
7379
|
+
const document = parsePlanDocument(source, join14(dir, "PLAN.md"));
|
|
7380
|
+
const authoringRequest = input.request ?? { kind: "user", text: input.intent?.trim() || `Initialize phase-less plan ${title}.` };
|
|
7381
|
+
await writeRevisionSnapshot(dir, document, source, authoringRequest, []);
|
|
7382
|
+
await writeLatestView(dir, document, source, []);
|
|
7383
|
+
return await readRecord(dir);
|
|
7088
7384
|
} catch (error) {
|
|
7089
7385
|
await rm2(dir, { recursive: true, force: true });
|
|
7090
7386
|
throw error;
|
|
7091
7387
|
}
|
|
7092
7388
|
},
|
|
7093
|
-
|
|
7094
|
-
|
|
7095
|
-
if (
|
|
7096
|
-
|
|
7097
|
-
|
|
7098
|
-
|
|
7389
|
+
read,
|
|
7390
|
+
async applyRevision(cwd, request) {
|
|
7391
|
+
if (request.mode === "create") {
|
|
7392
|
+
const root = await plansDir(cwd, options.homeDir);
|
|
7393
|
+
const id = planRecordName(request.shortSlug, now());
|
|
7394
|
+
const dir = join14(root, id);
|
|
7395
|
+
await createPlanDirectory(root, dir, id);
|
|
7396
|
+
try {
|
|
7397
|
+
const timestamp = now().toISOString();
|
|
7398
|
+
const document = normalizeRevision(undefined, request, id, 0, timestamp);
|
|
7399
|
+
assertCompleteRevision(document, request.briefs);
|
|
7400
|
+
const source = renderPlanDocument(document);
|
|
7401
|
+
parsePlanDocument(source, join14(dir, "PLAN.md"));
|
|
7402
|
+
await writeRevisionSnapshot(dir, document, source, request.request, request.briefs);
|
|
7403
|
+
await writeLatestView(dir, document, source, request.briefs);
|
|
7404
|
+
return await readRecord(dir);
|
|
7405
|
+
} catch (error) {
|
|
7406
|
+
await rm2(dir, { recursive: true, force: true });
|
|
7407
|
+
throw error;
|
|
7408
|
+
}
|
|
7099
7409
|
}
|
|
7100
|
-
|
|
7410
|
+
const initial = await read(cwd, request.plan);
|
|
7411
|
+
return withDirectoryLock(join14(initial.dir, ".lock"), async () => {
|
|
7412
|
+
const current = await readRecord(initial.dir);
|
|
7413
|
+
if (current.document.revision !== request.expectedPlanRevision)
|
|
7414
|
+
throw new Error(`Expected plan revision ${request.expectedPlanRevision}, found ${current.document.revision}.`);
|
|
7415
|
+
const revision = current.document.revision + 1;
|
|
7416
|
+
const document = normalizeRevision(current.document, request, current.id, revision, now().toISOString());
|
|
7417
|
+
assertCompleteRevision(document, request.briefs);
|
|
7418
|
+
const source = renderPlanDocument(document);
|
|
7419
|
+
parsePlanDocument(source, current.planPath);
|
|
7420
|
+
await writeRevisionSnapshot(current.dir, document, source, request.request, request.briefs);
|
|
7421
|
+
try {
|
|
7422
|
+
await writeLatestView(current.dir, document, source, request.briefs);
|
|
7423
|
+
} catch (error) {
|
|
7424
|
+
await rm2(join14(current.dir, "revisions", String(revision)), { recursive: true, force: true });
|
|
7425
|
+
throw error;
|
|
7426
|
+
}
|
|
7427
|
+
return await readRecord(current.dir);
|
|
7428
|
+
}, { operation: "apply revision" });
|
|
7101
7429
|
},
|
|
7102
7430
|
async mutate(cwd, query, operation, update) {
|
|
7103
|
-
const initial = await
|
|
7104
|
-
return withDirectoryLock(
|
|
7431
|
+
const initial = await read(cwd, query);
|
|
7432
|
+
return withDirectoryLock(join14(initial.dir, ".lock"), async () => {
|
|
7105
7433
|
const current = await readRecord(initial.dir);
|
|
7106
7434
|
const next = await update(structuredClone(current.document));
|
|
7107
7435
|
if (next.id !== current.id)
|
|
7108
7436
|
throw new Error("A plan mutation cannot change the plan ID.");
|
|
7109
7437
|
const document = {
|
|
7110
7438
|
...next,
|
|
7111
|
-
revision: current.document.revision
|
|
7439
|
+
revision: current.document.revision,
|
|
7112
7440
|
updatedAt: now().toISOString()
|
|
7113
7441
|
};
|
|
7114
7442
|
const source = renderPlanDocument(document, current.source);
|
|
7115
7443
|
parsePlanDocument(source, current.planPath);
|
|
7116
7444
|
await atomicWrite(current.planPath, source);
|
|
7117
|
-
|
|
7118
|
-
return { ...current, document, source, implementationDir: join13(current.dir, "implementation") };
|
|
7445
|
+
return { ...current, document, source };
|
|
7119
7446
|
}, { operation });
|
|
7120
7447
|
},
|
|
7121
7448
|
async log(cwd, query, event) {
|
|
7122
|
-
const record = await
|
|
7449
|
+
const record = await read(cwd, query);
|
|
7123
7450
|
return appendLogEntry(record.logPath, event);
|
|
7124
7451
|
}
|
|
7125
7452
|
};
|
|
7126
7453
|
}
|
|
7127
7454
|
async function readRecord(dir) {
|
|
7128
|
-
const planPath =
|
|
7129
|
-
const logPath =
|
|
7130
|
-
const source = await
|
|
7455
|
+
const planPath = join14(dir, "PLAN.md");
|
|
7456
|
+
const logPath = join14(dir, "logs.txt");
|
|
7457
|
+
const source = await readFile8(planPath, "utf8");
|
|
7131
7458
|
const document = parsePlanDocument(source, planPath);
|
|
7132
7459
|
return {
|
|
7133
7460
|
id: basename5(dir),
|
|
7134
7461
|
dir,
|
|
7135
7462
|
planPath,
|
|
7136
7463
|
logPath,
|
|
7137
|
-
implementationDir:
|
|
7464
|
+
implementationDir: join14(dir, "implementation"),
|
|
7138
7465
|
document,
|
|
7139
7466
|
source
|
|
7140
7467
|
};
|
|
7141
7468
|
}
|
|
7142
|
-
async function
|
|
7143
|
-
|
|
7144
|
-
|
|
7145
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
|
|
7469
|
+
async function createPlanDirectory(root, dir, id) {
|
|
7470
|
+
await assertContained(root, dirname7(dir));
|
|
7471
|
+
try {
|
|
7472
|
+
await mkdir6(dir);
|
|
7473
|
+
} catch (error) {
|
|
7474
|
+
if (error.code === "EEXIST")
|
|
7475
|
+
throw new Error(`Plan ${id} already exists.`);
|
|
7476
|
+
throw error;
|
|
7477
|
+
}
|
|
7478
|
+
}
|
|
7479
|
+
async function writeRevisionSnapshot(planDir, document, source, request, briefs) {
|
|
7480
|
+
if (!request.text.trim())
|
|
7481
|
+
throw new Error("An exact non-empty authoring request is required.");
|
|
7482
|
+
if (request.kind === "annotation" && !request.response?.trim())
|
|
7483
|
+
throw new Error("Annotation requests require a non-empty response.");
|
|
7484
|
+
const revisionsDir = join14(planDir, "revisions");
|
|
7485
|
+
await mkdir6(revisionsDir, { recursive: true });
|
|
7486
|
+
const target = join14(revisionsDir, String(document.revision));
|
|
7487
|
+
try {
|
|
7488
|
+
await readFile8(join14(target, "metadata.json"), "utf8");
|
|
7489
|
+
throw new Error(`Revision ${document.revision} already exists and is immutable.`);
|
|
7490
|
+
} catch (error) {
|
|
7491
|
+
if (error.code !== "ENOENT")
|
|
7492
|
+
throw error;
|
|
7493
|
+
}
|
|
7494
|
+
const staging = join14(revisionsDir, `.tmp-${document.revision}-${randomUUID2()}`);
|
|
7495
|
+
await mkdir6(staging);
|
|
7496
|
+
try {
|
|
7497
|
+
const requestSource = request.kind === "annotation" ? `## Original annotation
|
|
7498
|
+
${request.text}
|
|
7499
|
+
|
|
7500
|
+
## LLM response
|
|
7501
|
+
${request.response}` : request.text;
|
|
7502
|
+
await atomicWrite(join14(staging, "request.md"), requestSource);
|
|
7503
|
+
await atomicWrite(join14(staging, "metadata.json"), `${JSON.stringify({
|
|
7504
|
+
schemaVersion: 1,
|
|
7505
|
+
id: document.id,
|
|
7506
|
+
revision: document.revision,
|
|
7507
|
+
requestKind: request.kind,
|
|
7508
|
+
requestSha256: sha2562(request.text),
|
|
7509
|
+
...request.kind === "annotation" ? { originalAnnotationSha256: sha2562(request.text), responseSha256: sha2562(request.response) } : {},
|
|
7510
|
+
createdAt: document.updatedAt
|
|
7511
|
+
}, null, 2)}
|
|
7512
|
+
`);
|
|
7513
|
+
await atomicWrite(join14(staging, "PLAN.md"), source);
|
|
7514
|
+
await writeImplementationDirectory(staging, document, briefs);
|
|
7515
|
+
await rename2(staging, target);
|
|
7516
|
+
} catch (error) {
|
|
7517
|
+
await rm2(staging, { recursive: true, force: true });
|
|
7518
|
+
throw error;
|
|
7519
|
+
}
|
|
7520
|
+
}
|
|
7521
|
+
async function writeLatestView(planDir, document, source, briefs) {
|
|
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()}`);
|
|
7526
|
+
await mkdir6(stagingRoot);
|
|
7527
|
+
await writeImplementationDirectory(stagingRoot, document, briefs);
|
|
7528
|
+
let backedUp = false;
|
|
7529
|
+
try {
|
|
7151
7530
|
try {
|
|
7152
|
-
await
|
|
7531
|
+
await rename2(current, backup);
|
|
7532
|
+
backedUp = true;
|
|
7153
7533
|
} catch (error) {
|
|
7154
7534
|
if (error.code !== "ENOENT")
|
|
7155
7535
|
throw error;
|
|
7156
|
-
const tasks = phase.tasks.map((task) => `- ${task.id}: ${task.title}`).join("\\n") || "- Add implementation tasks.";
|
|
7157
|
-
await atomicWrite(path, renderTemplate(template.content, {
|
|
7158
|
-
phase_id: phase.id,
|
|
7159
|
-
phase_title: phase.title,
|
|
7160
|
-
phase_objective: phase.objective,
|
|
7161
|
-
phase_tasks: tasks
|
|
7162
|
-
}));
|
|
7163
7536
|
}
|
|
7537
|
+
await rename2(stagedImplementation, current);
|
|
7538
|
+
await atomicWrite(join14(planDir, "PLAN.md"), source);
|
|
7539
|
+
if (backedUp)
|
|
7540
|
+
await rm2(backup, { recursive: true, force: true });
|
|
7541
|
+
} catch (error) {
|
|
7542
|
+
await rm2(current, { recursive: true, force: true });
|
|
7543
|
+
if (backedUp)
|
|
7544
|
+
await rename2(backup, current);
|
|
7545
|
+
throw error;
|
|
7546
|
+
} finally {
|
|
7547
|
+
await rm2(stagingRoot, { recursive: true, force: true });
|
|
7164
7548
|
}
|
|
7165
7549
|
}
|
|
7550
|
+
async function writeImplementationDirectory(baseDir, document, briefs) {
|
|
7551
|
+
const implementationDir = join14(baseDir, "implementation");
|
|
7552
|
+
await mkdir6(implementationDir, { recursive: true });
|
|
7553
|
+
for (const [index, phase] of document.phases.entries()) {
|
|
7554
|
+
const brief = briefs[index];
|
|
7555
|
+
if (!brief)
|
|
7556
|
+
throw new Error(`Missing implementation brief for phase ${phase.id}.`);
|
|
7557
|
+
await atomicWrite(join14(implementationDir, `phase-${index + 1}.md`), renderImplementationBrief(document.revision, index + 1, phase, brief));
|
|
7558
|
+
}
|
|
7559
|
+
}
|
|
7560
|
+
function normalizeRevision(current, request, id, revision, updatedAt) {
|
|
7561
|
+
if (current?.status === "completed")
|
|
7562
|
+
throw new Error("Completed plans cannot be amended.");
|
|
7563
|
+
if (current?.execution?.active && request.request.kind !== "blocker")
|
|
7564
|
+
throw new Error("An active execution accepts only a blocker amendment.");
|
|
7565
|
+
const currentPhases = new Map(current?.phases.map((phase) => [phase.id, phase]) ?? []);
|
|
7566
|
+
for (const phase of current?.phases ?? []) {
|
|
7567
|
+
if (!request.phases.some((candidate) => candidate.id === phase.id) && (phase.status === "completed" || phase.status === "in_progress"))
|
|
7568
|
+
throw new Error(`${phase.status === "completed" ? "Completed" : "Active"} phase ${phase.id} cannot be removed.`);
|
|
7569
|
+
}
|
|
7570
|
+
const phases = request.phases.map((draft) => reconcilePhase(currentPhases.get(draft.id), draft, current));
|
|
7571
|
+
const createdAt = current?.createdAt ?? updatedAt;
|
|
7572
|
+
return {
|
|
7573
|
+
schemaVersion: 1,
|
|
7574
|
+
id,
|
|
7575
|
+
revision,
|
|
7576
|
+
title: request.title,
|
|
7577
|
+
branch: current?.branch ?? (request.mode === "create" ? request.branch : ""),
|
|
7578
|
+
issueId: current?.issueId ?? (request.mode === "create" ? request.issueId : undefined),
|
|
7579
|
+
issueUrl: current?.issueUrl ?? (request.mode === "create" ? request.issueUrl : undefined),
|
|
7580
|
+
intent: request.intent,
|
|
7581
|
+
requirements: request.requirements,
|
|
7582
|
+
design: request.design,
|
|
7583
|
+
references: request.references,
|
|
7584
|
+
phases,
|
|
7585
|
+
status: nextPlanStatus(current),
|
|
7586
|
+
execution: current?.execution,
|
|
7587
|
+
createdAt,
|
|
7588
|
+
updatedAt
|
|
7589
|
+
};
|
|
7590
|
+
}
|
|
7591
|
+
function reconcilePhase(current, draft, plan) {
|
|
7592
|
+
if (!current)
|
|
7593
|
+
return {
|
|
7594
|
+
...draft,
|
|
7595
|
+
revision: 0,
|
|
7596
|
+
status: "pending",
|
|
7597
|
+
gate: { phaseRevision: 0, status: "pending", results: [] },
|
|
7598
|
+
tasks: draft.tasks.map(newTask)
|
|
7599
|
+
};
|
|
7600
|
+
if (current.status === "completed") {
|
|
7601
|
+
if (JSON.stringify(phaseContent(current)) !== JSON.stringify(draft))
|
|
7602
|
+
throw new Error(`Completed phase ${current.id} cannot be changed.`);
|
|
7603
|
+
return current;
|
|
7604
|
+
}
|
|
7605
|
+
const byId = new Map(current.tasks.map((task) => [task.id, task]));
|
|
7606
|
+
for (const task of current.tasks) {
|
|
7607
|
+
if (!draft.tasks.some((candidate) => candidate.id === task.id) && (task.status === "completed" || task.status === "in_progress"))
|
|
7608
|
+
throw new Error(`${task.status === "completed" ? "Completed" : "Active"} task ${task.id} cannot be removed.`);
|
|
7609
|
+
}
|
|
7610
|
+
const tasks = draft.tasks.map((task) => reconcileTask(byId.get(task.id), task));
|
|
7611
|
+
const changed = JSON.stringify(phaseContent(current)) !== JSON.stringify(draft);
|
|
7612
|
+
const revision = changed ? current.revision + 1 : current.revision;
|
|
7613
|
+
const wasBlocked = current.status === "blocked";
|
|
7614
|
+
return {
|
|
7615
|
+
...current,
|
|
7616
|
+
...draft,
|
|
7617
|
+
revision,
|
|
7618
|
+
tasks,
|
|
7619
|
+
status: wasBlocked ? plan?.execution?.active ? "in_progress" : "pending" : current.status,
|
|
7620
|
+
gate: changed || wasBlocked ? { phaseRevision: revision, status: "stale", results: [] } : current.gate,
|
|
7621
|
+
blocker: wasBlocked ? undefined : current.blocker
|
|
7622
|
+
};
|
|
7623
|
+
}
|
|
7624
|
+
function reconcileTask(current, draft) {
|
|
7625
|
+
if (!current)
|
|
7626
|
+
return newTask(draft);
|
|
7627
|
+
const changed = JSON.stringify(taskContent(current)) !== JSON.stringify(draft);
|
|
7628
|
+
if ((current.status === "completed" || current.status === "in_progress") && changed)
|
|
7629
|
+
throw new Error(`${current.status === "completed" ? "Completed" : "Active"} task ${current.id} cannot be changed.`);
|
|
7630
|
+
if (current.status === "completed" || current.status === "in_progress")
|
|
7631
|
+
return current;
|
|
7632
|
+
return {
|
|
7633
|
+
...current,
|
|
7634
|
+
...draft,
|
|
7635
|
+
revision: changed ? current.revision + 1 : current.revision,
|
|
7636
|
+
status: current.status === "blocked" ? "pending" : current.status,
|
|
7637
|
+
owner: current.status === "blocked" ? undefined : current.owner,
|
|
7638
|
+
executionId: current.status === "blocked" ? undefined : current.executionId,
|
|
7639
|
+
blocker: current.status === "blocked" ? undefined : current.blocker
|
|
7640
|
+
};
|
|
7641
|
+
}
|
|
7642
|
+
function newTask(draft) {
|
|
7643
|
+
return { ...draft, revision: 0, status: "pending" };
|
|
7644
|
+
}
|
|
7645
|
+
function phaseContent(phase) {
|
|
7646
|
+
return {
|
|
7647
|
+
id: phase.id,
|
|
7648
|
+
title: phase.title,
|
|
7649
|
+
objective: phase.objective,
|
|
7650
|
+
dependencies: phase.dependencies,
|
|
7651
|
+
tasks: phase.tasks.map(taskContent)
|
|
7652
|
+
};
|
|
7653
|
+
}
|
|
7654
|
+
function taskContent(task) {
|
|
7655
|
+
return { id: task.id, title: task.title, dependencies: task.dependencies };
|
|
7656
|
+
}
|
|
7657
|
+
function nextPlanStatus(current) {
|
|
7658
|
+
if (!current)
|
|
7659
|
+
return "draft";
|
|
7660
|
+
if (current.execution?.active)
|
|
7661
|
+
return current.status === "blocked" ? "in_progress" : current.status;
|
|
7662
|
+
return current.status === "ready" || current.status === "blocked" ? "draft" : current.status;
|
|
7663
|
+
}
|
|
7664
|
+
function assertCompleteRevision(document, briefs) {
|
|
7665
|
+
const errors = [...validatePlanDocument(document, { strict: true }), ...validatePlanBriefs(document, briefs)].filter((issue) => issue.severity === "error");
|
|
7666
|
+
if (errors.length)
|
|
7667
|
+
throw new Error(`Invalid plan revision: ${errors.map((issue) => issue.message).join(" ")}`);
|
|
7668
|
+
}
|
|
7669
|
+
function sha2562(value) {
|
|
7670
|
+
return createHash3("sha256").update(value).digest("hex");
|
|
7671
|
+
}
|
|
7166
7672
|
async function assertContained(root, candidate) {
|
|
7167
7673
|
const canonicalRoot = await realpath3(root);
|
|
7168
7674
|
let canonicalCandidate;
|
|
@@ -7194,34 +7700,54 @@ function stripDate(value) {
|
|
|
7194
7700
|
return value.replace(/^\d{6}-/, "");
|
|
7195
7701
|
}
|
|
7196
7702
|
function titleFromSlug(value) {
|
|
7197
|
-
|
|
7703
|
+
const slug = normalizeSlug(value).replace(/^(?:[a-z]+-\d+-)/, "");
|
|
7704
|
+
return slug.replace(/(^|-)([a-z])/g, (_match, separator, letter) => {
|
|
7705
|
+
return `${separator ? " " : ""}${letter.toUpperCase()}`;
|
|
7706
|
+
});
|
|
7198
7707
|
}
|
|
7199
7708
|
|
|
7200
|
-
// src/
|
|
7709
|
+
// src/plan/schema.ts
|
|
7201
7710
|
import { z as z6 } from "zod";
|
|
7202
7711
|
var planStatusSchema = z6.enum(["draft", "ready", "in_progress", "blocked", "completed"]);
|
|
7203
7712
|
var planUnitStatusSchema = z6.enum(["draft", "ready", "in_progress", "blocked", "completed", "pending", "skipped"]);
|
|
7204
|
-
var planTaskDraftSchema = z6.object({
|
|
7205
|
-
id: zx.id,
|
|
7206
|
-
title: zx.text,
|
|
7207
|
-
steps: z6.array(zx.text).optional(),
|
|
7208
|
-
dependencies: z6.array(zx.id).optional(),
|
|
7209
|
-
fileScopes: z6.array(zx.text).optional(),
|
|
7210
|
-
acceptanceCriteria: z6.array(zx.text).optional()
|
|
7211
|
-
}).strict();
|
|
7713
|
+
var planTaskDraftSchema = z6.object({ id: zx.id, title: zx.text, dependencies: z6.array(zx.id) }).strict();
|
|
7212
7714
|
var planPhaseDraftSchema = z6.object({
|
|
7213
7715
|
id: zx.id,
|
|
7214
7716
|
title: zx.text,
|
|
7215
7717
|
objective: zx.text,
|
|
7216
|
-
dependencies: z6.array(zx.id)
|
|
7217
|
-
tasks: z6.array(planTaskDraftSchema).
|
|
7718
|
+
dependencies: z6.array(zx.id),
|
|
7719
|
+
tasks: z6.array(planTaskDraftSchema).min(1)
|
|
7218
7720
|
}).strict();
|
|
7219
|
-
var
|
|
7220
|
-
|
|
7221
|
-
|
|
7222
|
-
|
|
7223
|
-
|
|
7721
|
+
var planBriefSchema = z6.object({
|
|
7722
|
+
phaseId: zx.id,
|
|
7723
|
+
summary: zx.text,
|
|
7724
|
+
apiChanges: z6.array(zx.text),
|
|
7725
|
+
libraries: z6.array(zx.text),
|
|
7726
|
+
constraints: z6.array(zx.text),
|
|
7727
|
+
tasks: z6.array(z6.object({
|
|
7728
|
+
taskId: zx.id,
|
|
7729
|
+
steps: z6.array(zx.text).min(1),
|
|
7730
|
+
fileScopes: z6.array(zx.text).min(1),
|
|
7731
|
+
acceptanceCriteria: z6.array(zx.text).min(1)
|
|
7732
|
+
}).strict()).min(1)
|
|
7224
7733
|
}).strict();
|
|
7734
|
+
var planReferenceSchema = z6.object({ id: zx.id, value: zx.text }).strict();
|
|
7735
|
+
var planDesignSchema = z6.object({ bigIdeas: zx.text, keyApiUpdates: zx.text, consequences: zx.text }).strict();
|
|
7736
|
+
var planRequestSchema = z6.discriminatedUnion("kind", [
|
|
7737
|
+
z6.object({ kind: z6.literal("annotation"), text: zx.text, response: zx.text }).strict(),
|
|
7738
|
+
z6.object({ kind: z6.literal("user"), text: zx.text, response: zx.text.optional() }).strict(),
|
|
7739
|
+
z6.object({ kind: z6.literal("blocker"), text: zx.text, response: zx.text.optional() }).strict()
|
|
7740
|
+
]);
|
|
7741
|
+
var revisionContentFields = {
|
|
7742
|
+
request: planRequestSchema,
|
|
7743
|
+
title: zx.text,
|
|
7744
|
+
intent: zx.text,
|
|
7745
|
+
requirements: z6.array(zx.text).min(1),
|
|
7746
|
+
design: planDesignSchema,
|
|
7747
|
+
references: z6.array(planReferenceSchema),
|
|
7748
|
+
phases: z6.array(planPhaseDraftSchema).min(1),
|
|
7749
|
+
briefs: z6.array(planBriefSchema).min(1)
|
|
7750
|
+
};
|
|
7225
7751
|
var planContextParametersSchema = z6.object({
|
|
7226
7752
|
cwd: zx.cwd,
|
|
7227
7753
|
plan: z6.string().optional(),
|
|
@@ -7235,46 +7761,28 @@ var planInitParametersSchema = z6.object({
|
|
|
7235
7761
|
title: zx.text.optional(),
|
|
7236
7762
|
intent: zx.text.optional(),
|
|
7237
7763
|
issueId: zx.text.optional(),
|
|
7238
|
-
issueUrl: z6.
|
|
7764
|
+
issueUrl: z6.url().optional(),
|
|
7765
|
+
request: planRequestSchema.optional(),
|
|
7239
7766
|
open: z6.boolean().optional()
|
|
7240
7767
|
}).strict();
|
|
7241
|
-
var
|
|
7242
|
-
|
|
7243
|
-
|
|
7244
|
-
|
|
7245
|
-
|
|
7246
|
-
|
|
7247
|
-
|
|
7248
|
-
|
|
7249
|
-
|
|
7250
|
-
|
|
7251
|
-
|
|
7252
|
-
|
|
7253
|
-
|
|
7254
|
-
|
|
7255
|
-
|
|
7256
|
-
|
|
7257
|
-
phase: planPhaseDraftSchema
|
|
7258
|
-
}).strict();
|
|
7259
|
-
var planRemovePhaseParametersSchema = z6.object({
|
|
7260
|
-
cwd: zx.cwd,
|
|
7261
|
-
plan: zx.text,
|
|
7262
|
-
expectedPlanRevision: zx.revision,
|
|
7263
|
-
phaseId: zx.id,
|
|
7264
|
-
reason: zx.text
|
|
7265
|
-
}).strict();
|
|
7266
|
-
var planUpdatePhaseParametersSchema = z6.object({
|
|
7267
|
-
cwd: zx.cwd,
|
|
7268
|
-
plan: zx.text,
|
|
7269
|
-
expectedPlanRevision: zx.revision,
|
|
7270
|
-
phaseId: zx.id,
|
|
7271
|
-
patch: z6.object({
|
|
7272
|
-
title: zx.text.optional(),
|
|
7273
|
-
objective: zx.text.optional(),
|
|
7274
|
-
dependencies: z6.array(zx.id).optional(),
|
|
7275
|
-
tasks: z6.array(planTaskDraftSchema).optional()
|
|
7768
|
+
var planApplyRevisionParametersSchema = z6.discriminatedUnion("mode", [
|
|
7769
|
+
z6.object({
|
|
7770
|
+
mode: z6.literal("create"),
|
|
7771
|
+
cwd: zx.cwd,
|
|
7772
|
+
shortSlug: zx.id,
|
|
7773
|
+
branch: z6.string().min(1).optional(),
|
|
7774
|
+
issueId: zx.text.optional(),
|
|
7775
|
+
issueUrl: z6.url().optional(),
|
|
7776
|
+
...revisionContentFields
|
|
7777
|
+
}).strict(),
|
|
7778
|
+
z6.object({
|
|
7779
|
+
mode: z6.literal("amend"),
|
|
7780
|
+
cwd: zx.cwd,
|
|
7781
|
+
plan: zx.text,
|
|
7782
|
+
expectedPlanRevision: zx.revision,
|
|
7783
|
+
...revisionContentFields
|
|
7276
7784
|
}).strict()
|
|
7277
|
-
|
|
7785
|
+
]);
|
|
7278
7786
|
var planValidateParametersSchema = z6.object({ cwd: zx.cwd, plan: zx.text, strict: z6.boolean().optional() }).strict();
|
|
7279
7787
|
var planLogProgressParametersSchema = z6.object({
|
|
7280
7788
|
cwd: zx.cwd,
|
|
@@ -7374,116 +7882,17 @@ function createPlanTools(_pi, _modes, store = createPlanStore()) {
|
|
|
7374
7882
|
}
|
|
7375
7883
|
}),
|
|
7376
7884
|
defineTool5({
|
|
7377
|
-
name: "
|
|
7378
|
-
label: "plan
|
|
7379
|
-
description: "
|
|
7380
|
-
parameters: parameters(
|
|
7381
|
-
executionMode: "sequential",
|
|
7382
|
-
async execute(_id, input) {
|
|
7383
|
-
const params = planUpdateOverviewParametersSchema.parse(input);
|
|
7384
|
-
const record = await store.mutate(params.cwd ?? process.cwd(), params.plan, "update overview", (plan) => {
|
|
7385
|
-
assertAuthoringRevision(plan, params.expectedPlanRevision);
|
|
7386
|
-
assertAuthorable(plan);
|
|
7387
|
-
return resetDraft({
|
|
7388
|
-
...plan,
|
|
7389
|
-
intent: params.intent ?? plan.intent,
|
|
7390
|
-
requirements: params.requirements ?? plan.requirements,
|
|
7391
|
-
design: params.design ?? plan.design,
|
|
7392
|
-
references: params.references ?? plan.references
|
|
7393
|
-
});
|
|
7394
|
-
});
|
|
7395
|
-
return result2(`Updated ${record.id} to revision ${record.document.revision}.`, { record });
|
|
7396
|
-
}
|
|
7397
|
-
}),
|
|
7398
|
-
defineTool5({
|
|
7399
|
-
name: "plan_add_phase",
|
|
7400
|
-
label: "plan add phase",
|
|
7401
|
-
description: "Add an ordered phase with stable IDs.",
|
|
7402
|
-
parameters: parameters(planAddPhaseParametersSchema),
|
|
7403
|
-
executionMode: "sequential",
|
|
7404
|
-
async execute(_id, input) {
|
|
7405
|
-
const params = planAddPhaseParametersSchema.parse(input);
|
|
7406
|
-
const record = await store.mutate(params.cwd ?? process.cwd(), params.plan, "add phase", (plan) => {
|
|
7407
|
-
assertAuthoringRevision(plan, params.expectedPlanRevision);
|
|
7408
|
-
assertAuthorable(plan);
|
|
7409
|
-
if (allIds(plan).has(params.phase.id))
|
|
7410
|
-
throw new Error(`Duplicate stable ID: ${params.phase.id}.`);
|
|
7411
|
-
const phase = newPhase(params.phase);
|
|
7412
|
-
const phases = [...plan.phases];
|
|
7413
|
-
if (params.afterPhaseId) {
|
|
7414
|
-
const index = phases.findIndex((item) => item.id === params.afterPhaseId);
|
|
7415
|
-
if (index < 0)
|
|
7416
|
-
throw new Error(`Unknown phase: ${params.afterPhaseId}.`);
|
|
7417
|
-
phases.splice(index + 1, 0, phase);
|
|
7418
|
-
} else
|
|
7419
|
-
phases.push(phase);
|
|
7420
|
-
assertNewIds(plan, phase);
|
|
7421
|
-
return resetDraft({ ...plan, phases });
|
|
7422
|
-
});
|
|
7423
|
-
return result2(`Added phase ${params.phase.id} to ${record.id}.`, { record });
|
|
7424
|
-
}
|
|
7425
|
-
}),
|
|
7426
|
-
defineTool5({
|
|
7427
|
-
name: "plan_remove_phase",
|
|
7428
|
-
label: "plan remove phase",
|
|
7429
|
-
description: "Remove an unfinished phase that has no dependents.",
|
|
7430
|
-
parameters: parameters(planRemovePhaseParametersSchema),
|
|
7885
|
+
name: "plan_apply_revision",
|
|
7886
|
+
label: "plan apply revision",
|
|
7887
|
+
description: "Create mode writes a complete revision 0 without plan_init; amend mode atomically writes the next complete authoring revision.",
|
|
7888
|
+
parameters: parameters(planApplyRevisionParametersSchema),
|
|
7431
7889
|
executionMode: "sequential",
|
|
7432
7890
|
async execute(_id, input) {
|
|
7433
|
-
const params =
|
|
7434
|
-
const
|
|
7435
|
-
|
|
7436
|
-
|
|
7437
|
-
|
|
7438
|
-
if (phase.status === "completed" || phase.tasks.some((task) => task.status === "completed"))
|
|
7439
|
-
throw new Error(`Completed phase ${phase.id} cannot be removed.`);
|
|
7440
|
-
if (plan.phases.some((item) => item.dependencies.includes(phase.id)))
|
|
7441
|
-
throw new Error(`Phase ${phase.id} has dependents and cannot be removed.`);
|
|
7442
|
-
return resetDraft({ ...plan, phases: plan.phases.filter((item) => item.id !== phase.id) });
|
|
7443
|
-
});
|
|
7444
|
-
await store.log(params.cwd ?? process.cwd(), record.id, {
|
|
7445
|
-
planRevision: record.document.revision,
|
|
7446
|
-
kind: "updated",
|
|
7447
|
-
actor: "planner",
|
|
7448
|
-
message: `Removed phase ${params.phaseId}: ${params.reason}`,
|
|
7449
|
-
phaseId: params.phaseId
|
|
7450
|
-
});
|
|
7451
|
-
return result2(`Removed phase ${params.phaseId}.`, { record });
|
|
7452
|
-
}
|
|
7453
|
-
}),
|
|
7454
|
-
defineTool5({
|
|
7455
|
-
name: "plan_update_phase",
|
|
7456
|
-
label: "plan update phase",
|
|
7457
|
-
description: "Update an unfinished phase and its ordered tasks.",
|
|
7458
|
-
parameters: parameters(planUpdatePhaseParametersSchema),
|
|
7459
|
-
executionMode: "sequential",
|
|
7460
|
-
async execute(_id, input) {
|
|
7461
|
-
const params = planUpdatePhaseParametersSchema.parse(input);
|
|
7462
|
-
const record = await store.mutate(params.cwd ?? process.cwd(), params.plan, "update phase", (plan) => {
|
|
7463
|
-
assertAuthoringRevision(plan, params.expectedPlanRevision);
|
|
7464
|
-
assertAuthorable(plan, params.phaseId);
|
|
7465
|
-
const existing = getPhase2(plan, params.phaseId);
|
|
7466
|
-
if (existing.status === "completed")
|
|
7467
|
-
throw new Error(`Completed phase ${existing.id} cannot be changed.`);
|
|
7468
|
-
const tasks = params.patch.tasks ? reconcileTasks(existing.tasks, params.patch.tasks) : existing.tasks;
|
|
7469
|
-
const updated = {
|
|
7470
|
-
...existing,
|
|
7471
|
-
title: params.patch.title ?? existing.title,
|
|
7472
|
-
objective: params.patch.objective ?? existing.objective,
|
|
7473
|
-
dependencies: params.patch.dependencies ?? existing.dependencies,
|
|
7474
|
-
tasks,
|
|
7475
|
-
revision: existing.revision + 1,
|
|
7476
|
-
status: existing.status === "blocked" ? "in_progress" : existing.status,
|
|
7477
|
-
gate: { ...existing.gate, status: "stale" },
|
|
7478
|
-
blocker: existing.status === "blocked" ? undefined : existing.blocker
|
|
7479
|
-
};
|
|
7480
|
-
assertNewIds({ ...plan, phases: plan.phases.filter((phase) => phase.id !== existing.id) }, updated);
|
|
7481
|
-
return resetDraft({
|
|
7482
|
-
...plan,
|
|
7483
|
-
phases: plan.phases.map((phase) => phase.id === existing.id ? updated : phase)
|
|
7484
|
-
});
|
|
7485
|
-
});
|
|
7486
|
-
return result2(`Updated phase ${params.phaseId}.`, { record });
|
|
7891
|
+
const params = planApplyRevisionParametersSchema.parse(input);
|
|
7892
|
+
const workingDirectory = params.cwd ?? process.cwd();
|
|
7893
|
+
const request = params.mode === "create" ? { ...params, branch: params.branch ?? await currentBranch(workingDirectory) } : params;
|
|
7894
|
+
const record = await store.applyRevision(workingDirectory, request);
|
|
7895
|
+
return result2(`Applied revision ${record.document.revision} to ${record.id}.`, { record });
|
|
7487
7896
|
}
|
|
7488
7897
|
}),
|
|
7489
7898
|
defineTool5({
|
|
@@ -7495,7 +7904,7 @@ function createPlanTools(_pi, _modes, store = createPlanStore()) {
|
|
|
7495
7904
|
async execute(_id, input) {
|
|
7496
7905
|
const params = planValidateParametersSchema.parse(input);
|
|
7497
7906
|
const record = await store.read(params.cwd ?? process.cwd(), params.plan);
|
|
7498
|
-
const issues =
|
|
7907
|
+
const issues = await validatePlanRecord(record, { strict: params.strict });
|
|
7499
7908
|
const words = countDesignWords(record.document);
|
|
7500
7909
|
return result2(issues.length ? issues.map((issue) => `- ${issue.severity}: ${issue.message}`).join(`
|
|
7501
7910
|
`) : "Plan is valid.", { record, issues, designWordCount: words, ready: !issues.some((issue) => issue.severity === "error") });
|
|
@@ -7631,7 +8040,7 @@ function createPlanTools(_pi, _modes, store = createPlanStore()) {
|
|
|
7631
8040
|
const resolution = await store.context(workingDirectory, params.plan);
|
|
7632
8041
|
if (!resolution.record)
|
|
7633
8042
|
throw new Error(resolution.ambiguous ? `Plan is ambiguous: ${resolution.candidates.map((item) => item.id).join(", ")}.` : "No matching plan.");
|
|
7634
|
-
const cliPath =
|
|
8043
|
+
const cliPath = join15(resolveBundledAgentsDir(), "..", "dist", "cli.js");
|
|
7635
8044
|
const command = [
|
|
7636
8045
|
process.execPath,
|
|
7637
8046
|
cliPath,
|
|
@@ -7726,8 +8135,8 @@ function renderExecutionPrompt(packet) {
|
|
|
7726
8135
|
return [
|
|
7727
8136
|
`Execute the durable Diffpi plan using this packet: ${JSON.stringify(packet)}.`,
|
|
7728
8137
|
"Call plan_context first and keep plan tools as the source of truth for eligibility, ownership, status, gates, and commits.",
|
|
7729
|
-
"Use
|
|
7730
|
-
"
|
|
8138
|
+
"Use bounded Agent delegation for eligible tasks. Run dependent tasks in order; parallelize only tasks with non-overlapping declared file scopes. Collect and verify worker results before changing plan state.",
|
|
8139
|
+
"Use a multi-agent SubagentWorkflow only when the user explicitly opts into that orchestration.",
|
|
7731
8140
|
"Persist every transition, progress event, issue, and deviation. Do not edit PLAN.md directly. Delegated implementation workers never commit or restructure the plan.",
|
|
7732
8141
|
"In commit mode, create one local commit after each phase passes its gates and do not push. In push mode, commit and push each phase, record pending CI, and launch a bounded background Worker to call watch_ci for the exact SHA and then plan_record_ci for the phase while the next phase executes. Collect that monitor before the next push and collect all monitors before plan completion; failed or timed-out CI blocks execution."
|
|
7733
8142
|
].join(" ");
|
|
@@ -7791,81 +8200,6 @@ ${dirty.stdout.trim()}`);
|
|
|
7791
8200
|
prompt: renderExecutionPrompt(packet)
|
|
7792
8201
|
});
|
|
7793
8202
|
}
|
|
7794
|
-
function newPhase(input) {
|
|
7795
|
-
return {
|
|
7796
|
-
id: input.id,
|
|
7797
|
-
revision: 0,
|
|
7798
|
-
title: input.title,
|
|
7799
|
-
objective: input.objective,
|
|
7800
|
-
dependencies: input.dependencies ?? [],
|
|
7801
|
-
tasks: (input.tasks ?? []).map(newTask),
|
|
7802
|
-
status: "pending",
|
|
7803
|
-
gate: { phaseRevision: 0, status: "pending", results: [] }
|
|
7804
|
-
};
|
|
7805
|
-
}
|
|
7806
|
-
function newTask(input) {
|
|
7807
|
-
return {
|
|
7808
|
-
id: input.id,
|
|
7809
|
-
revision: 0,
|
|
7810
|
-
title: input.title,
|
|
7811
|
-
steps: input.steps,
|
|
7812
|
-
dependencies: input.dependencies ?? [],
|
|
7813
|
-
fileScopes: input.fileScopes ?? [],
|
|
7814
|
-
acceptanceCriteria: input.acceptanceCriteria ?? [],
|
|
7815
|
-
status: "pending"
|
|
7816
|
-
};
|
|
7817
|
-
}
|
|
7818
|
-
function reconcileTasks(existing, drafts) {
|
|
7819
|
-
const byId = new Map(existing.map((task) => [task.id, task]));
|
|
7820
|
-
const requested = new Set(drafts.map((task) => task.id));
|
|
7821
|
-
const protectedTask = existing.find((task) => (task.status === "completed" || task.status === "in_progress") && !requested.has(task.id));
|
|
7822
|
-
if (protectedTask)
|
|
7823
|
-
throw new Error(`${protectedTask.status === "completed" ? "Completed" : "Active"} task ${protectedTask.id} cannot be removed.`);
|
|
7824
|
-
return drafts.map((draft) => {
|
|
7825
|
-
const current = byId.get(draft.id);
|
|
7826
|
-
if (current?.status === "completed" || current?.status === "in_progress")
|
|
7827
|
-
return current;
|
|
7828
|
-
return current ? {
|
|
7829
|
-
...newTask(draft),
|
|
7830
|
-
revision: current.revision + 1,
|
|
7831
|
-
status: current.status === "blocked" ? "pending" : current.status,
|
|
7832
|
-
owner: current.status === "blocked" ? undefined : current.owner,
|
|
7833
|
-
executionId: current.status === "blocked" ? undefined : current.executionId,
|
|
7834
|
-
blocker: current.status === "blocked" ? undefined : current.blocker
|
|
7835
|
-
} : newTask(draft);
|
|
7836
|
-
});
|
|
7837
|
-
}
|
|
7838
|
-
function assertAuthoringRevision(plan, expected) {
|
|
7839
|
-
if (plan.revision !== expected)
|
|
7840
|
-
throw new Error(`Stale plan revision: expected ${expected}, found ${plan.revision}.`);
|
|
7841
|
-
}
|
|
7842
|
-
function assertAuthorable(plan, blockedPhaseId) {
|
|
7843
|
-
if (plan.status === "completed")
|
|
7844
|
-
throw new Error("Completed plans cannot be structurally changed.");
|
|
7845
|
-
if (!plan.execution?.active)
|
|
7846
|
-
return;
|
|
7847
|
-
if (!blockedPhaseId)
|
|
7848
|
-
throw new Error("An active execution owns this plan.");
|
|
7849
|
-
const phase = getPhase2(plan, blockedPhaseId);
|
|
7850
|
-
if (phase.status !== "blocked" && !phase.tasks.some((task) => task.status === "blocked"))
|
|
7851
|
-
throw new Error("Planner may amend only blocked work during an active execution.");
|
|
7852
|
-
}
|
|
7853
|
-
function resetDraft(plan) {
|
|
7854
|
-
if (plan.status === "blocked" && plan.execution?.active)
|
|
7855
|
-
return { ...plan, status: "in_progress" };
|
|
7856
|
-
return plan.status === "ready" || plan.status === "blocked" ? { ...plan, status: "draft" } : plan;
|
|
7857
|
-
}
|
|
7858
|
-
function allIds(plan) {
|
|
7859
|
-
return new Set([plan.id, ...plan.phases.flatMap((phase) => [phase.id, ...phase.tasks.map((task) => task.id)])]);
|
|
7860
|
-
}
|
|
7861
|
-
function assertNewIds(plan, phase) {
|
|
7862
|
-
const known = allIds(plan);
|
|
7863
|
-
for (const candidate of [phase.id, ...phase.tasks.map((task) => task.id)]) {
|
|
7864
|
-
if (known.has(candidate))
|
|
7865
|
-
throw new Error(`Duplicate stable ID: ${candidate}.`);
|
|
7866
|
-
known.add(candidate);
|
|
7867
|
-
}
|
|
7868
|
-
}
|
|
7869
8203
|
function getPhase2(plan, phaseId) {
|
|
7870
8204
|
const phase = plan.phases.find((item) => item.id === phaseId);
|
|
7871
8205
|
if (!phase)
|
|
@@ -7903,8 +8237,8 @@ function result2(text, details = {}) {
|
|
|
7903
8237
|
|
|
7904
8238
|
// src/tools/review.ts
|
|
7905
8239
|
import { existsSync as existsSync2 } from "node:fs";
|
|
7906
|
-
import { mkdir as mkdir7, readdir as
|
|
7907
|
-
import { join as
|
|
8240
|
+
import { mkdir as mkdir7, readdir as readdir3, readFile as readFile12, rename as rename4, stat, unlink as unlink2, writeFile as writeFile7 } from "node:fs/promises";
|
|
8241
|
+
import { join as join17 } from "node:path";
|
|
7908
8242
|
import { defineTool as defineTool6 } from "@earendil-works/pi-coding-agent";
|
|
7909
8243
|
import { z as z11 } from "zod";
|
|
7910
8244
|
|
|
@@ -8075,7 +8409,7 @@ function renderThreadArtifact(title, target, threads, options = {}) {
|
|
|
8075
8409
|
}
|
|
8076
8410
|
lines.push("## Source comments", "");
|
|
8077
8411
|
for (const thread of threads) {
|
|
8078
|
-
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, "", "---", "");
|
|
8412
|
+
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._", "", "---", "");
|
|
8079
8413
|
}
|
|
8080
8414
|
return `${lines.join(`
|
|
8081
8415
|
`).trimEnd()}
|
|
@@ -8506,10 +8840,10 @@ function hasGitlabDraftNotes(input) {
|
|
|
8506
8840
|
}
|
|
8507
8841
|
|
|
8508
8842
|
// src/review/local-review-backend.ts
|
|
8509
|
-
import { readFile as
|
|
8843
|
+
import { readFile as readFile10, writeFile as writeFile5 } from "node:fs/promises";
|
|
8510
8844
|
|
|
8511
8845
|
// src/extensions/tuicrx.ts
|
|
8512
|
-
import { readFile as
|
|
8846
|
+
import { readFile as readFile9, realpath as realpath4 } from "node:fs/promises";
|
|
8513
8847
|
import { resolve as resolve4 } from "node:path";
|
|
8514
8848
|
async function tuicrAvailable() {
|
|
8515
8849
|
return Boolean(await findExecutable("tuicr"));
|
|
@@ -8606,7 +8940,7 @@ async function findMatchingSession(sessions, cwd, branch) {
|
|
|
8606
8940
|
return;
|
|
8607
8941
|
}
|
|
8608
8942
|
async function readSession(path) {
|
|
8609
|
-
const content = await
|
|
8943
|
+
const content = await readFile9(path, "utf8");
|
|
8610
8944
|
try {
|
|
8611
8945
|
return JSON.parse(content);
|
|
8612
8946
|
} catch {
|
|
@@ -8727,7 +9061,7 @@ function LocalReviewBackend(options) {
|
|
|
8727
9061
|
},
|
|
8728
9062
|
async listThreads() {
|
|
8729
9063
|
try {
|
|
8730
|
-
return parseThreadArtifact(await
|
|
9064
|
+
return parseThreadArtifact(await readFile10(options.artifactPath, "utf8"));
|
|
8731
9065
|
} catch (error) {
|
|
8732
9066
|
if (error.code === "ENOENT")
|
|
8733
9067
|
return [];
|
|
@@ -8735,7 +9069,7 @@ function LocalReviewBackend(options) {
|
|
|
8735
9069
|
}
|
|
8736
9070
|
},
|
|
8737
9071
|
async reply(input) {
|
|
8738
|
-
const content = await
|
|
9072
|
+
const content = await readFile10(options.artifactPath, "utf8");
|
|
8739
9073
|
await writeFile5(options.artifactPath, upsertThreadReply(content, input.threadId, input.body, input.question, input.resolve), "utf8");
|
|
8740
9074
|
},
|
|
8741
9075
|
async publish() {
|
|
@@ -8756,9 +9090,9 @@ function createLocalReviewBackend(options) {
|
|
|
8756
9090
|
return LocalReviewBackend(options);
|
|
8757
9091
|
}
|
|
8758
9092
|
// src/review/review-state.ts
|
|
8759
|
-
import { createHash as
|
|
8760
|
-
import { readFile as
|
|
8761
|
-
import { join as
|
|
9093
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
9094
|
+
import { readFile as readFile11, rename as rename3, writeFile as writeFile6 } from "node:fs/promises";
|
|
9095
|
+
import { join as join16 } from "node:path";
|
|
8762
9096
|
import { z as z10 } from "zod";
|
|
8763
9097
|
var reviewPublicationStateSchema = z10.object({
|
|
8764
9098
|
target: z10.string().optional(),
|
|
@@ -8782,9 +9116,9 @@ function unpublishedReviewComments(comments, knownFingerprints) {
|
|
|
8782
9116
|
}
|
|
8783
9117
|
async function loadReviewPublicationState(cwd, vcs, number, homeDir) {
|
|
8784
9118
|
const target = `${vcs.provider}:${vcs.owner}/${vcs.repo}#${number}`;
|
|
8785
|
-
const path =
|
|
9119
|
+
const path = join16(await sessionsDir(cwd, homeDir), `review-publish-${digest(target).slice(0, 16)}.json`);
|
|
8786
9120
|
try {
|
|
8787
|
-
const parsed = reviewPublicationStateSchema.parse(JSON.parse(await
|
|
9121
|
+
const parsed = reviewPublicationStateSchema.parse(JSON.parse(await readFile11(path, "utf8")));
|
|
8788
9122
|
return { path, state: { ...parsed, target } };
|
|
8789
9123
|
} catch (error) {
|
|
8790
9124
|
if (error.code === "ENOENT") {
|
|
@@ -8800,10 +9134,10 @@ async function saveReviewPublicationState(path, state) {
|
|
|
8800
9134
|
const temp = `${path}.${process.pid}.tmp`;
|
|
8801
9135
|
await writeFile6(temp, `${JSON.stringify(state, null, 2)}
|
|
8802
9136
|
`, "utf8");
|
|
8803
|
-
await
|
|
9137
|
+
await rename3(temp, path);
|
|
8804
9138
|
}
|
|
8805
9139
|
function digest(value) {
|
|
8806
|
-
return
|
|
9140
|
+
return createHash4("sha256").update(value).digest("hex");
|
|
8807
9141
|
}
|
|
8808
9142
|
// src/tools/review.ts
|
|
8809
9143
|
var contextSchema = z11.object({
|
|
@@ -8845,6 +9179,17 @@ var completeSchema = localSchema.extend({
|
|
|
8845
9179
|
function hasReviewDraft(comments, body) {
|
|
8846
9180
|
return comments.length > 0 || body.trim().length > 0;
|
|
8847
9181
|
}
|
|
9182
|
+
function reviewBrowserCommand(url, platform = process.platform) {
|
|
9183
|
+
return platform === "darwin" ? { command: "open", args: [url] } : platform === "win32" ? { command: "cmd", args: ["/c", "start", "", url] } : { command: "xdg-open", args: [url] };
|
|
9184
|
+
}
|
|
9185
|
+
async function openReviewUrl(url, execute = run, platform = process.platform) {
|
|
9186
|
+
const launch = reviewBrowserCommand(url, platform);
|
|
9187
|
+
try {
|
|
9188
|
+
return (await execute(launch.command, launch.args)).code === 0;
|
|
9189
|
+
} catch {
|
|
9190
|
+
return false;
|
|
9191
|
+
}
|
|
9192
|
+
}
|
|
8848
9193
|
function partitionReviewComments(comments, knownFingerprints, model) {
|
|
8849
9194
|
const normalized = comments.map((comment) => {
|
|
8850
9195
|
const published = { ...comment, body: withCommentProvenance(comment, model) };
|
|
@@ -8899,7 +9244,9 @@ function createReviewTools() {
|
|
|
8899
9244
|
const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
|
|
8900
9245
|
const store = await ensureStore(review.cwd);
|
|
8901
9246
|
const env = { ide: detectIde(), mux: detectMux(), shell: detectShell() };
|
|
8902
|
-
const session = await resolveTuicrSession(review, Boolean(params.local || params.workingTree))
|
|
9247
|
+
const session = await resolveTuicrSession(review, Boolean(params.local || params.workingTree)).catch(() => {
|
|
9248
|
+
return;
|
|
9249
|
+
});
|
|
8903
9250
|
const baseRef = review.pr?.baseRef ?? (review.forge ? await review.forge.defaultBranch() : "local");
|
|
8904
9251
|
const backend = params.local || params.workingTree ? "tuicr" : review.forge?.provider ?? "unsupported";
|
|
8905
9252
|
return result3([
|
|
@@ -8923,6 +9270,21 @@ function createReviewTools() {
|
|
|
8923
9270
|
});
|
|
8924
9271
|
}
|
|
8925
9272
|
}),
|
|
9273
|
+
defineTool6({
|
|
9274
|
+
name: "review_status",
|
|
9275
|
+
label: "review status",
|
|
9276
|
+
description: "Report local and remote review lifecycle status without requiring a forge or tuicr.",
|
|
9277
|
+
promptSnippet: "Call review_status for review lifecycle state",
|
|
9278
|
+
promptGuidelines: ["Use this for status even when no forge or tuicr binary is available."],
|
|
9279
|
+
parameters: parameters2(localSchema),
|
|
9280
|
+
executionMode: "parallel",
|
|
9281
|
+
async execute(_id, input) {
|
|
9282
|
+
const params = localSchema.parse(input);
|
|
9283
|
+
const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
|
|
9284
|
+
const status = await collectReviewStatus(review);
|
|
9285
|
+
return result3(status.text, status.details);
|
|
9286
|
+
}
|
|
9287
|
+
}),
|
|
8926
9288
|
defineTool6({
|
|
8927
9289
|
name: "review_new",
|
|
8928
9290
|
label: "review new",
|
|
@@ -8966,6 +9328,31 @@ ${openMessage}`, {
|
|
|
8966
9328
|
});
|
|
8967
9329
|
}
|
|
8968
9330
|
}),
|
|
9331
|
+
defineTool6({
|
|
9332
|
+
name: "review_open",
|
|
9333
|
+
label: "review open",
|
|
9334
|
+
description: "Open an existing remote PR/MR in the system browser; never creates one.",
|
|
9335
|
+
promptSnippet: "Call review_open to open an existing PR/MR",
|
|
9336
|
+
promptGuidelines: ["Direct users to review_new when no remote review exists."],
|
|
9337
|
+
parameters: parameters2(localSchema),
|
|
9338
|
+
executionMode: "sequential",
|
|
9339
|
+
async execute(_id, input) {
|
|
9340
|
+
const params = localSchema.parse(input);
|
|
9341
|
+
const review = await resolveReviewContext(resolveWorkingDirectory(params), params.target);
|
|
9342
|
+
if (params.local || params.workingTree)
|
|
9343
|
+
return launchLocalReview(review, true);
|
|
9344
|
+
if (!review.forge)
|
|
9345
|
+
return result3(`${unsupportedForgeMessage()} Use review_new with local=true for an offline review.`);
|
|
9346
|
+
if (!review.pr)
|
|
9347
|
+
return result3("No remote PR/MR exists. Use review_new to create a draft review first.");
|
|
9348
|
+
const opened = await openReviewUrl(review.pr.url);
|
|
9349
|
+
return result3(`${opened ? "Opened" : "Open this URL"} PR/MR #${review.pr.number}: ${review.pr.url}`, {
|
|
9350
|
+
pr: review.pr,
|
|
9351
|
+
opened,
|
|
9352
|
+
url: review.pr.url
|
|
9353
|
+
});
|
|
9354
|
+
}
|
|
9355
|
+
}),
|
|
8969
9356
|
defineTool6({
|
|
8970
9357
|
name: "review_edit",
|
|
8971
9358
|
label: "review edit",
|
|
@@ -9272,7 +9659,7 @@ ${params.body}`, {
|
|
|
9272
9659
|
if (!existsSync2(artifact))
|
|
9273
9660
|
return result3("No local review artifact. Run review_comments with local=true first.");
|
|
9274
9661
|
const archived = await uniqueRecordPath(await completedReviewsDir(review.cwd), reviewSlug(session.slug));
|
|
9275
|
-
await
|
|
9662
|
+
await rename4(artifact, archived);
|
|
9276
9663
|
await unlink2(session.path);
|
|
9277
9664
|
return result3(`Archived ${archived} and deleted the tuicr session ${session.slug}.`, {
|
|
9278
9665
|
artifact: archived,
|
|
@@ -9332,7 +9719,7 @@ ${params.body}`, {
|
|
|
9332
9719
|
async function syncLocalThreadArtifact(path, title, sessionSlug, threads) {
|
|
9333
9720
|
let previous = [];
|
|
9334
9721
|
try {
|
|
9335
|
-
previous = parseThreadArtifact(await
|
|
9722
|
+
previous = parseThreadArtifact(await readFile12(path, "utf8"));
|
|
9336
9723
|
} catch (error) {
|
|
9337
9724
|
if (error.code !== "ENOENT")
|
|
9338
9725
|
throw error;
|
|
@@ -9365,6 +9752,45 @@ function result3(text, details = {}) {
|
|
|
9365
9752
|
function unsupportedForgeMessage() {
|
|
9366
9753
|
return "No supported GitHub or GitLab remote was detected. Use local=true for an offline tuicr review.";
|
|
9367
9754
|
}
|
|
9755
|
+
async function collectReviewStatus(review) {
|
|
9756
|
+
const [porcelain, branch, localSession, remoteSession] = await Promise.all([
|
|
9757
|
+
runChecked("git", ["-C", review.cwd, "status", "--porcelain"]),
|
|
9758
|
+
runChecked("git", ["-C", review.cwd, "branch", "--show-current"]),
|
|
9759
|
+
resolveTuicrSession(review, true).catch(() => {
|
|
9760
|
+
return;
|
|
9761
|
+
}),
|
|
9762
|
+
resolveTuicrSession(review, false).catch(() => {
|
|
9763
|
+
return;
|
|
9764
|
+
})
|
|
9765
|
+
]);
|
|
9766
|
+
const entries = porcelain.stdout.split("\\n").filter(Boolean);
|
|
9767
|
+
const staged = entries.filter((line) => !line.startsWith("??") && line[0] !== " ").length;
|
|
9768
|
+
const unstaged = entries.filter((line) => !line.startsWith("??") && line[1] !== " ").length;
|
|
9769
|
+
const untracked = entries.filter((line) => line.startsWith("??")).length;
|
|
9770
|
+
const remote = review.pr ? `yes (#${review.pr.number})` : "no";
|
|
9771
|
+
const url = review.pr?.url ?? "none";
|
|
9772
|
+
const text = [
|
|
9773
|
+
`Branch: ${branch.stdout.trim() || review.vcs.branch}`,
|
|
9774
|
+
`Worktree: ${entries.length ? "dirty" : "clean"} (staged=${staged}, unstaged=${unstaged}, untracked=${untracked})`,
|
|
9775
|
+
`Local working-tree tuicr review: ${localSession ? `yes (${localSession.slug}, ${localSession.commentCount} comments)` : "no"}`,
|
|
9776
|
+
`Remote PR/MR: ${remote}`,
|
|
9777
|
+
`Remote URL: ${url}`,
|
|
9778
|
+
`Remote tuicr session: ${remoteSession ? `yes (${remoteSession.slug}, ${remoteSession.commentCount} comments)` : "no"}`
|
|
9779
|
+
].join("\\n");
|
|
9780
|
+
return {
|
|
9781
|
+
text,
|
|
9782
|
+
details: {
|
|
9783
|
+
branch: branch.stdout.trim(),
|
|
9784
|
+
staged,
|
|
9785
|
+
unstaged,
|
|
9786
|
+
untracked,
|
|
9787
|
+
localSession,
|
|
9788
|
+
remoteSession,
|
|
9789
|
+
pr: review.pr,
|
|
9790
|
+
url
|
|
9791
|
+
}
|
|
9792
|
+
};
|
|
9793
|
+
}
|
|
9368
9794
|
function modelRoute(ctx) {
|
|
9369
9795
|
if (!ctx.model)
|
|
9370
9796
|
throw new Error("Cannot record review provenance because Pi did not provide an active model route.");
|
|
@@ -9588,11 +10014,11 @@ async function newReviewArtifactPath(cwd, target) {
|
|
|
9588
10014
|
}
|
|
9589
10015
|
async function localThreadArtifactPath(cwd, sessionSlug) {
|
|
9590
10016
|
const dir = await reviewsDir(cwd);
|
|
9591
|
-
return
|
|
10017
|
+
return join17(dir, `${reviewSlug(sessionSlug) || "local-review"}.md`);
|
|
9592
10018
|
}
|
|
9593
10019
|
async function readThreadArtifact(path) {
|
|
9594
10020
|
try {
|
|
9595
|
-
return parseThreadArtifact(await
|
|
10021
|
+
return parseThreadArtifact(await readFile12(path, "utf8"));
|
|
9596
10022
|
} catch (error) {
|
|
9597
10023
|
if (error.code === "ENOENT") {
|
|
9598
10024
|
throw new Error(`Expected local reply overlay is missing: ${path}`);
|
|
@@ -9616,9 +10042,9 @@ async function findLatestThreadArtifact(cwd, review, target) {
|
|
|
9616
10042
|
return [];
|
|
9617
10043
|
return [
|
|
9618
10044
|
(async () => {
|
|
9619
|
-
const path =
|
|
10045
|
+
const path = join17(dir, name);
|
|
9620
10046
|
const info = await stat(path);
|
|
9621
|
-
return { path, name, content: await
|
|
10047
|
+
return { path, name, content: await readFile12(path, "utf8"), modified: info.mtimeMs };
|
|
9622
10048
|
})()
|
|
9623
10049
|
];
|
|
9624
10050
|
}));
|
|
@@ -9632,7 +10058,7 @@ async function findLatestThreadArtifact(cwd, review, target) {
|
|
|
9632
10058
|
}
|
|
9633
10059
|
async function listReviewArtifactNames(dir) {
|
|
9634
10060
|
try {
|
|
9635
|
-
return await
|
|
10061
|
+
return await readdir3(dir);
|
|
9636
10062
|
} catch (error) {
|
|
9637
10063
|
throw new Error(`Cannot read review artifacts in ${dir}.`, { cause: error });
|
|
9638
10064
|
}
|
|
@@ -9641,10 +10067,10 @@ function deriveTitle(branch) {
|
|
|
9641
10067
|
return branch.replace(/^(feature|feat|fix|bug|chore)\//, "").replace(/^eng-\d+-/i, "").replace(/[-_]+/g, " ").replace(/^\w/, (char) => char.toUpperCase());
|
|
9642
10068
|
}
|
|
9643
10069
|
function uniqueRecordPath(dir, base) {
|
|
9644
|
-
let path =
|
|
10070
|
+
let path = join17(dir, `${base}.md`);
|
|
9645
10071
|
let count = 2;
|
|
9646
10072
|
while (existsSync2(path))
|
|
9647
|
-
path =
|
|
10073
|
+
path = join17(dir, `${base}-${count++}.md`);
|
|
9648
10074
|
return path;
|
|
9649
10075
|
}
|
|
9650
10076
|
function artifactNameMatches(name, suffix) {
|
|
@@ -9665,21 +10091,21 @@ import { z as z13 } from "zod";
|
|
|
9665
10091
|
|
|
9666
10092
|
// src/setup.ts
|
|
9667
10093
|
import { parseFrontmatter as parseFrontmatter2 } from "@earendil-works/pi-coding-agent";
|
|
9668
|
-
import { readdir as
|
|
10094
|
+
import { readdir as readdir5, readFile as readFile15 } from "node:fs/promises";
|
|
9669
10095
|
import { homedir as homedir8 } from "node:os";
|
|
9670
|
-
import { basename as basename6, join as
|
|
10096
|
+
import { basename as basename6, join as join21 } from "node:path";
|
|
9671
10097
|
|
|
9672
10098
|
// src/config.ts
|
|
9673
10099
|
import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
9674
10100
|
import { z as z12 } from "zod";
|
|
9675
10101
|
import { homedir as homedir5 } from "node:os";
|
|
9676
|
-
import { join as
|
|
10102
|
+
import { join as join18 } from "node:path";
|
|
9677
10103
|
|
|
9678
10104
|
// src/fsx.ts
|
|
9679
|
-
import { readdir as
|
|
10105
|
+
import { readdir as readdir4, readFile as readFile13 } from "node:fs/promises";
|
|
9680
10106
|
async function readTextIfExists(path) {
|
|
9681
10107
|
try {
|
|
9682
|
-
return await
|
|
10108
|
+
return await readFile13(path, "utf8");
|
|
9683
10109
|
} catch (error) {
|
|
9684
10110
|
if (isMissingPath(error))
|
|
9685
10111
|
return;
|
|
@@ -9699,10 +10125,10 @@ var diffpiConfigSchema = z12.object({
|
|
|
9699
10125
|
agents: z12.record(z12.string(), agentConfigSchema).optional()
|
|
9700
10126
|
}).strict();
|
|
9701
10127
|
function diffpiConfigPaths(homeDir = homedir5()) {
|
|
9702
|
-
const directory =
|
|
10128
|
+
const directory = join18(homeDir, ".difflab", "diffpi");
|
|
9703
10129
|
return {
|
|
9704
|
-
yaml:
|
|
9705
|
-
json:
|
|
10130
|
+
yaml: join18(directory, "config.yaml"),
|
|
10131
|
+
json: join18(directory, "config.json")
|
|
9706
10132
|
};
|
|
9707
10133
|
}
|
|
9708
10134
|
async function loadDiffpiConfig(options = {}) {
|
|
@@ -9758,12 +10184,12 @@ function normalizeModelReference(value) {
|
|
|
9758
10184
|
}
|
|
9759
10185
|
|
|
9760
10186
|
// src/mcp.ts
|
|
9761
|
-
import { mkdir as mkdir8, readFile as
|
|
10187
|
+
import { mkdir as mkdir8, readFile as readFile14, writeFile as writeFile8 } from "node:fs/promises";
|
|
9762
10188
|
import { homedir as homedir6 } from "node:os";
|
|
9763
|
-
import { dirname as dirname8, join as
|
|
10189
|
+
import { dirname as dirname8, join as join19 } from "node:path";
|
|
9764
10190
|
var mcp = {
|
|
9765
10191
|
globalConfigPath(homeDir = homedir6()) {
|
|
9766
|
-
return
|
|
10192
|
+
return join19(homeDir, ".config", "mcp", "mcp.json");
|
|
9767
10193
|
},
|
|
9768
10194
|
async serversEnsure(servers, options = {}) {
|
|
9769
10195
|
const path = options.path ?? mcp.globalConfigPath();
|
|
@@ -9806,7 +10232,7 @@ function getParsedConfig(content, path) {
|
|
|
9806
10232
|
}
|
|
9807
10233
|
async function getOptionalFile2(path) {
|
|
9808
10234
|
try {
|
|
9809
|
-
return await
|
|
10235
|
+
return await readFile14(path, "utf8");
|
|
9810
10236
|
} catch (error) {
|
|
9811
10237
|
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
9812
10238
|
return;
|
|
@@ -9820,7 +10246,7 @@ function isRecord(value) {
|
|
|
9820
10246
|
// src/pi.ts
|
|
9821
10247
|
import { mkdir as mkdir9, writeFile as writeFile9 } from "node:fs/promises";
|
|
9822
10248
|
import { homedir as homedir7 } from "node:os";
|
|
9823
|
-
import { dirname as dirname9, join as
|
|
10249
|
+
import { dirname as dirname9, join as join20 } from "node:path";
|
|
9824
10250
|
var pi = {
|
|
9825
10251
|
executableCheck: findPiExecutable,
|
|
9826
10252
|
packageList: listPiPackages,
|
|
@@ -9833,7 +10259,7 @@ var pi = {
|
|
|
9833
10259
|
configEnsure: ensurePiConfig
|
|
9834
10260
|
};
|
|
9835
10261
|
async function ensurePiAgent(filename, content, agentDir = resolvePiAgentDir(), dryRun = false) {
|
|
9836
|
-
const path =
|
|
10262
|
+
const path = join20(agentDir, "agents", filename);
|
|
9837
10263
|
const currentText = await readTextIfExists(path);
|
|
9838
10264
|
const changed = currentText !== content;
|
|
9839
10265
|
if (changed && !dryRun) {
|
|
@@ -9842,10 +10268,10 @@ async function ensurePiAgent(filename, content, agentDir = resolvePiAgentDir(),
|
|
|
9842
10268
|
}
|
|
9843
10269
|
return { path, changed, existed: currentText !== undefined, planned: changed && dryRun };
|
|
9844
10270
|
}
|
|
9845
|
-
async function checkGlobalPiSkill(name, agentDir = resolvePiAgentDir(), sharedSkillsDir =
|
|
9846
|
-
const roots = [
|
|
10271
|
+
async function checkGlobalPiSkill(name, agentDir = resolvePiAgentDir(), sharedSkillsDir = join20(homedir7(), ".agents", "skills")) {
|
|
10272
|
+
const roots = [join20(agentDir, "skills"), sharedSkillsDir];
|
|
9847
10273
|
for (const root of roots) {
|
|
9848
|
-
if (await readTextIfExists(
|
|
10274
|
+
if (await readTextIfExists(join20(root, name, "SKILL.md")) !== undefined)
|
|
9849
10275
|
return true;
|
|
9850
10276
|
}
|
|
9851
10277
|
return false;
|
|
@@ -9895,7 +10321,7 @@ async function installPiPackage(executable, source) {
|
|
|
9895
10321
|
await runChecked(executable, ["install", source]);
|
|
9896
10322
|
}
|
|
9897
10323
|
function resolvePiAgentDir(homeDir = homedir7()) {
|
|
9898
|
-
return process.env.PI_CODING_AGENT_DIR ?? (process.env.XDG_CONFIG_HOME ?
|
|
10324
|
+
return process.env.PI_CODING_AGENT_DIR ?? (process.env.XDG_CONFIG_HOME ? join20(process.env.XDG_CONFIG_HOME, "pi") : join20(homeDir, ".pi", "agent"));
|
|
9899
10325
|
}
|
|
9900
10326
|
function parseJsonObject(content, path) {
|
|
9901
10327
|
if (!content?.trim())
|
|
@@ -9947,7 +10373,7 @@ var FORGE_DEPENDENCIES = {
|
|
|
9947
10373
|
};
|
|
9948
10374
|
async function ensureMise(options = {}) {
|
|
9949
10375
|
const homeDir = options.homeDir ?? homedir8();
|
|
9950
|
-
const current = await mise.executableCheck() ?? await mise.executableCheck(
|
|
10376
|
+
const current = await mise.executableCheck() ?? await mise.executableCheck(join21(homeDir, ".local", "bin", "mise"));
|
|
9951
10377
|
if (current)
|
|
9952
10378
|
return { executable: current, action: createSetupAction("mise", "ready", current) };
|
|
9953
10379
|
reportProgress(options, "Installing mise");
|
|
@@ -9996,9 +10422,9 @@ async function ensureMiseDeps(miseExecutable, options = {}) {
|
|
|
9996
10422
|
async function ensurePiPlugins(options = {}) {
|
|
9997
10423
|
const actions = await ensurePiPackages(PI_PACKAGES, options);
|
|
9998
10424
|
const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
|
|
9999
|
-
const webSearch = await pi.configEnsure(
|
|
10425
|
+
const webSearch = await pi.configEnsure(join21(agentDir, "web-search.json"), (config) => ({ ...config, workflow: "auto-summary" }), options.dryRun);
|
|
10000
10426
|
actions.push(getConfigSetupAction("web search settings", webSearch));
|
|
10001
|
-
const lsp = await pi.configEnsure(
|
|
10427
|
+
const lsp = await pi.configEnsure(join21(agentDir, "pi-lsp.json"), (config) => ({
|
|
10002
10428
|
...config,
|
|
10003
10429
|
progressive: { ...getRecord(config.progressive), enabled: true, inject: "none" }
|
|
10004
10430
|
}), options.dryRun);
|
|
@@ -10009,11 +10435,11 @@ async function ensurePiAgents(options = {}) {
|
|
|
10009
10435
|
const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
|
|
10010
10436
|
const bundledAgentsDir = options.bundledAgentsDir ?? BUNDLED_AGENTS_DIR;
|
|
10011
10437
|
const userConfig = await loadDiffpiConfig({ homeDir: options.homeDir });
|
|
10012
|
-
const entries = (await
|
|
10438
|
+
const entries = (await readdir5(bundledAgentsDir, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.startsWith("diffpi-") && entry.name.endsWith(".md")).sort((left, right) => left.name.localeCompare(right.name));
|
|
10013
10439
|
const actions = [];
|
|
10014
10440
|
for (const entry of entries) {
|
|
10015
10441
|
const id = basename6(entry.name, ".md").replace(/^diffpi-/, "");
|
|
10016
|
-
const source = await
|
|
10442
|
+
const source = await readFile15(join21(bundledAgentsDir, entry.name), "utf8");
|
|
10017
10443
|
const content = materializeAgentModels(source, id, userConfig.config, options.availableModels);
|
|
10018
10444
|
const result = await pi.agentEnsure(entry.name, content, agentDir, options.dryRun);
|
|
10019
10445
|
actions.push(getConfigSetupAction(`pi agent ${id}`, result));
|
|
@@ -10022,7 +10448,7 @@ async function ensurePiAgents(options = {}) {
|
|
|
10022
10448
|
}
|
|
10023
10449
|
async function ensurePiSkills(miseExecutable, options = {}) {
|
|
10024
10450
|
const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
|
|
10025
|
-
const sharedSkillsDir =
|
|
10451
|
+
const sharedSkillsDir = join21(options.homeDir ?? homedir8(), ".agents", "skills");
|
|
10026
10452
|
const actions = [];
|
|
10027
10453
|
for (const source of PI_SKILL_SOURCES) {
|
|
10028
10454
|
const missing = [];
|
|
@@ -10218,7 +10644,7 @@ function reportProgress(options, message) {
|
|
|
10218
10644
|
options.onProgress?.(message);
|
|
10219
10645
|
}
|
|
10220
10646
|
async function packageVersion2() {
|
|
10221
|
-
const source = await
|
|
10647
|
+
const source = await readFile15(join21(resolveBundledAgentsDir(), "..", "package.json"), "utf8");
|
|
10222
10648
|
let value;
|
|
10223
10649
|
try {
|
|
10224
10650
|
value = JSON.parse(source);
|