taskplane 0.23.14 → 0.23.16
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/extensions/taskplane/agent-bridge-extension.ts +264 -2
- package/extensions/taskplane/execution.ts +1 -0
- package/extensions/taskplane/lane-runner.ts +4 -0
- package/package.json +1 -1
- package/skills/create-taskplane-task/SKILL.md +8 -1
- package/skills/create-taskplane-task/references/prompt-template.md +3 -0
|
@@ -24,8 +24,9 @@
|
|
|
24
24
|
|
|
25
25
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
26
26
|
import { Type } from "@mariozechner/pi-ai";
|
|
27
|
-
import { writeFileSync, mkdirSync, renameSync } from "fs";
|
|
28
|
-
import { join } from "path";
|
|
27
|
+
import { writeFileSync, readFileSync, existsSync, mkdirSync, renameSync } from "fs";
|
|
28
|
+
import { join, dirname } from "path";
|
|
29
|
+
import { spawn as nodeSpawn } from "child_process";
|
|
29
30
|
import { randomBytes } from "crypto";
|
|
30
31
|
|
|
31
32
|
/**
|
|
@@ -156,4 +157,265 @@ export default function (pi: ExtensionAPI) {
|
|
|
156
157
|
}
|
|
157
158
|
},
|
|
158
159
|
});
|
|
160
|
+
|
|
161
|
+
// ── review_step Tool (TP-117) ─────────────────────────────────────
|
|
162
|
+
// Spawns a reviewer subprocess to evaluate work at step boundaries.
|
|
163
|
+
// The reviewer runs as a separate Pi process, writes feedback to
|
|
164
|
+
// .reviews/, and this tool returns the verdict to the worker.
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Resolve the Pi CLI entrypoint path (same logic as agent-host.ts).
|
|
168
|
+
*/
|
|
169
|
+
function resolvePiCli(): string {
|
|
170
|
+
const relPath = join("node_modules", "@mariozechner", "pi-coding-agent", "dist", "cli.js");
|
|
171
|
+
const candidates: string[] = [];
|
|
172
|
+
if (process.env.APPDATA) candidates.push(join(process.env.APPDATA, "npm", relPath));
|
|
173
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
174
|
+
if (home) {
|
|
175
|
+
candidates.push(join(home, "AppData", "Roaming", "npm", relPath));
|
|
176
|
+
candidates.push(join(home, ".npm-global", "lib", relPath));
|
|
177
|
+
}
|
|
178
|
+
candidates.push(join("/usr", "local", "lib", relPath));
|
|
179
|
+
for (const c of candidates) {
|
|
180
|
+
if (existsSync(c)) return c;
|
|
181
|
+
}
|
|
182
|
+
throw new Error("Cannot find Pi CLI entrypoint");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Load the reviewer system prompt from base template + local override.
|
|
187
|
+
*/
|
|
188
|
+
function loadReviewerPrompt(): string {
|
|
189
|
+
const basePaths = [
|
|
190
|
+
process.env.APPDATA ? join(process.env.APPDATA, "npm", "node_modules", "taskplane", "templates", "agents", "task-reviewer.md") : "",
|
|
191
|
+
(process.env.HOME || process.env.USERPROFILE || "") ? join(process.env.HOME || process.env.USERPROFILE || "", "AppData", "Roaming", "npm", "node_modules", "taskplane", "templates", "agents", "task-reviewer.md") : "",
|
|
192
|
+
].filter(Boolean);
|
|
193
|
+
let basePrompt = "You are a code reviewer. Read the request and write your review to the specified output file.";
|
|
194
|
+
for (const p of basePaths) {
|
|
195
|
+
try {
|
|
196
|
+
if (!existsSync(p)) continue;
|
|
197
|
+
const raw = readFileSync(p, "utf-8");
|
|
198
|
+
const fmEnd = raw.indexOf("---", 4);
|
|
199
|
+
if (fmEnd > 0) { basePrompt = raw.slice(fmEnd + 3).trim(); break; }
|
|
200
|
+
} catch { continue; }
|
|
201
|
+
}
|
|
202
|
+
// Local override
|
|
203
|
+
const localPaths = [join(process.cwd(), ".pi", "agents", "task-reviewer.md"), join(process.cwd(), "agents", "task-reviewer.md")];
|
|
204
|
+
for (const p of localPaths) {
|
|
205
|
+
try {
|
|
206
|
+
if (!existsSync(p)) continue;
|
|
207
|
+
const raw = readFileSync(p, "utf-8");
|
|
208
|
+
const fmEnd = raw.indexOf("---", 4);
|
|
209
|
+
if (fmEnd > 0) {
|
|
210
|
+
const localBody = raw.slice(fmEnd + 3).trim();
|
|
211
|
+
if (localBody) basePrompt += "\n\n---\n\n## Project-Specific Guidance\n\n" + localBody;
|
|
212
|
+
}
|
|
213
|
+
break;
|
|
214
|
+
} catch { continue; }
|
|
215
|
+
}
|
|
216
|
+
return basePrompt;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Spawn a reviewer Pi subprocess and wait for it to complete.
|
|
221
|
+
* Returns the process exit code.
|
|
222
|
+
*/
|
|
223
|
+
function spawnReviewer(prompt: string, systemPrompt: string, cwd: string): Promise<number> {
|
|
224
|
+
return new Promise((resolve) => {
|
|
225
|
+
const cliPath = resolvePiCli();
|
|
226
|
+
const args = [
|
|
227
|
+
cliPath, "--mode", "rpc", "--no-session", "--no-extensions", "--no-skills",
|
|
228
|
+
"--tools", "read,write,edit,bash,grep,find,ls",
|
|
229
|
+
"--system-prompt", systemPrompt,
|
|
230
|
+
];
|
|
231
|
+
const proc = nodeSpawn(process.execPath, args, {
|
|
232
|
+
shell: false,
|
|
233
|
+
cwd,
|
|
234
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
235
|
+
env: { ...process.env },
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Send prompt and close stdin after delay
|
|
239
|
+
proc.stdin?.write(JSON.stringify({ type: "prompt", message: prompt }) + "\n");
|
|
240
|
+
|
|
241
|
+
// Watch for agent_end event to close stdin
|
|
242
|
+
let buf = "";
|
|
243
|
+
proc.stdout?.on("data", (chunk: Buffer) => {
|
|
244
|
+
buf += chunk.toString();
|
|
245
|
+
if (buf.includes('"agent_end"')) {
|
|
246
|
+
setTimeout(() => { try { proc.stdin?.end(); } catch {} }, 100);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
proc.on("close", (code) => resolve(code ?? 1));
|
|
251
|
+
proc.on("error", () => resolve(1));
|
|
252
|
+
|
|
253
|
+
// Timeout: 10 minutes
|
|
254
|
+
setTimeout(() => { try { proc.kill("SIGTERM"); } catch {} }, 10 * 60 * 1000);
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
pi.registerTool({
|
|
259
|
+
name: "review_step",
|
|
260
|
+
label: "Review Step",
|
|
261
|
+
description:
|
|
262
|
+
"Spawn a reviewer agent to evaluate your work on a step. " +
|
|
263
|
+
"Returns APPROVE, REVISE, RETHINK, or UNAVAILABLE. " +
|
|
264
|
+
"Use at step boundaries based on the task's review level.",
|
|
265
|
+
promptSnippet: "review_step(step, type, baseline?) — spawn reviewer for a step",
|
|
266
|
+
promptGuidelines: [
|
|
267
|
+
"Call review_step at step boundaries based on the task's Review Level (from STATUS.md header).",
|
|
268
|
+
"Review Level 0: skip all reviews. Level 1: plan review. Level 2: plan + code review. Level 3: plan + code + test.",
|
|
269
|
+
"Skip reviews for Step 0 (Preflight) and the final documentation step.",
|
|
270
|
+
"For code reviews: capture HEAD commit before starting a step with `git rev-parse HEAD` and pass as baseline.",
|
|
271
|
+
"On REVISE: read the review file in .reviews/ for feedback, fix issues, then proceed.",
|
|
272
|
+
"On RETHINK: reconsider your approach.",
|
|
273
|
+
],
|
|
274
|
+
parameters: Type.Object({
|
|
275
|
+
step: Type.Number({ description: "Step number to review" }),
|
|
276
|
+
type: Type.Union(
|
|
277
|
+
[Type.Literal("plan"), Type.Literal("code")],
|
|
278
|
+
{ description: 'Review type: "plan" or "code"' },
|
|
279
|
+
),
|
|
280
|
+
baseline: Type.Optional(Type.String({
|
|
281
|
+
description: "Git commit SHA for code review diff baseline",
|
|
282
|
+
})),
|
|
283
|
+
}),
|
|
284
|
+
async execute(_toolCallId, params) {
|
|
285
|
+
const { step: stepNum, type: reviewType, baseline } = params;
|
|
286
|
+
const cwd = process.cwd();
|
|
287
|
+
|
|
288
|
+
// Find task folder and paths
|
|
289
|
+
const taskFolder = process.env.TASKPLANE_TASK_FOLDER || cwd;
|
|
290
|
+
const statusPath = join(taskFolder, "STATUS.md");
|
|
291
|
+
const reviewsDir = join(taskFolder, ".reviews");
|
|
292
|
+
if (!existsSync(reviewsDir)) mkdirSync(reviewsDir, { recursive: true });
|
|
293
|
+
|
|
294
|
+
// Read review counter from STATUS.md
|
|
295
|
+
let reviewCounter = 0;
|
|
296
|
+
try {
|
|
297
|
+
const statusContent = readFileSync(statusPath, "utf-8");
|
|
298
|
+
const rcMatch = statusContent.match(/\*\*Review Counter:\*\*\s*(\d+)/);
|
|
299
|
+
if (rcMatch) reviewCounter = parseInt(rcMatch[1]);
|
|
300
|
+
} catch { /* default 0 */ }
|
|
301
|
+
|
|
302
|
+
reviewCounter++;
|
|
303
|
+
const num = String(reviewCounter).padStart(3, "0");
|
|
304
|
+
const outputPath = join(reviewsDir, `R${num}-${reviewType}-step${stepNum}.md`);
|
|
305
|
+
|
|
306
|
+
// Find step name from PROMPT.md
|
|
307
|
+
let stepName = `Step ${stepNum}`;
|
|
308
|
+
try {
|
|
309
|
+
const promptFiles = [join(taskFolder, "PROMPT.md")];
|
|
310
|
+
for (const pf of promptFiles) {
|
|
311
|
+
if (!existsSync(pf)) continue;
|
|
312
|
+
const content = readFileSync(pf, "utf-8");
|
|
313
|
+
const stepMatch = content.match(new RegExp(`###\\s+Step\\s+${stepNum}[:\\s]+(.+)`));
|
|
314
|
+
if (stepMatch) { stepName = stepMatch[1].trim(); break; }
|
|
315
|
+
}
|
|
316
|
+
} catch { /* use default */ }
|
|
317
|
+
|
|
318
|
+
// Generate review request prompt
|
|
319
|
+
const promptPath = join(taskFolder, "PROMPT.md");
|
|
320
|
+
const projectName = process.env.TASKPLANE_PROJECT_NAME || "project";
|
|
321
|
+
const diffCmd = baseline ? `git diff ${baseline}..HEAD` : `git diff`;
|
|
322
|
+
const diffNamesCmd = baseline ? `git diff ${baseline}..HEAD --name-only` : `git diff --name-only`;
|
|
323
|
+
|
|
324
|
+
let reviewPrompt: string;
|
|
325
|
+
if (reviewType === "plan") {
|
|
326
|
+
reviewPrompt = [
|
|
327
|
+
`# Review Request: Plan Review`,
|
|
328
|
+
``,
|
|
329
|
+
`You are reviewing an implementation plan for a ${projectName} task.`,
|
|
330
|
+
`You have full tool access — use \`read\` to examine files and \`bash\` to run commands.`,
|
|
331
|
+
``,
|
|
332
|
+
`## Task Context`,
|
|
333
|
+
`- **Task PROMPT:** ${promptPath}`,
|
|
334
|
+
`- **Task STATUS:** ${statusPath}`,
|
|
335
|
+
`- **Step being planned:** Step ${stepNum}: ${stepName}`,
|
|
336
|
+
``,
|
|
337
|
+
`## Instructions`,
|
|
338
|
+
`1. Read the PROMPT.md for full requirements`,
|
|
339
|
+
`2. Read STATUS.md for progress so far`,
|
|
340
|
+
`3. Evaluate the plan for this step`,
|
|
341
|
+
``,
|
|
342
|
+
`## Output`,
|
|
343
|
+
`Write your review to: \`${outputPath}\``,
|
|
344
|
+
].join("\n");
|
|
345
|
+
} else {
|
|
346
|
+
reviewPrompt = [
|
|
347
|
+
`# Review Request: Code Review`,
|
|
348
|
+
``,
|
|
349
|
+
`You are reviewing code changes for a ${projectName} task.`,
|
|
350
|
+
`You have full tool access — use \`read\` to examine files and \`bash\` to run commands.`,
|
|
351
|
+
``,
|
|
352
|
+
`## Task Context`,
|
|
353
|
+
`- **Task PROMPT:** ${promptPath}`,
|
|
354
|
+
`- **Task STATUS:** ${statusPath}`,
|
|
355
|
+
`- **Step reviewed:** Step ${stepNum}: ${stepName}`,
|
|
356
|
+
...(baseline ? [`- **Baseline commit:** ${baseline}`] : []),
|
|
357
|
+
``,
|
|
358
|
+
`## Instructions`,
|
|
359
|
+
`1. Run \`${diffNamesCmd}\` to see changed files`,
|
|
360
|
+
`2. Run \`${diffCmd}\` for the full diff`,
|
|
361
|
+
`3. Read changed files for context`,
|
|
362
|
+
``,
|
|
363
|
+
`## Output`,
|
|
364
|
+
`Write your review to: \`${outputPath}\``,
|
|
365
|
+
].join("\n");
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
const systemPrompt = loadReviewerPrompt();
|
|
370
|
+
const exitCode = await spawnReviewer(reviewPrompt, systemPrompt, cwd);
|
|
371
|
+
|
|
372
|
+
// Update review counter in STATUS.md
|
|
373
|
+
try {
|
|
374
|
+
const status = readFileSync(statusPath, "utf-8");
|
|
375
|
+
const updated = status.replace(/\*\*Review Counter:\*\*\s*\d+/, `**Review Counter:** ${reviewCounter}`);
|
|
376
|
+
writeFileSync(statusPath, updated);
|
|
377
|
+
} catch { /* best effort */ }
|
|
378
|
+
|
|
379
|
+
// Read review output and extract verdict
|
|
380
|
+
if (existsSync(outputPath)) {
|
|
381
|
+
const reviewContent = readFileSync(outputPath, "utf-8");
|
|
382
|
+
const verdictMatch = reviewContent.match(/###?\s*Verdict[:\s]*(APPROVE|REVISE|RETHINK)/i);
|
|
383
|
+
let verdict = verdictMatch ? verdictMatch[1].toUpperCase() : "UNKNOWN";
|
|
384
|
+
if (verdict === "UNKNOWN") {
|
|
385
|
+
const lower = reviewContent.toLowerCase();
|
|
386
|
+
if (lower.includes("approve") && !lower.includes("do not approve")) verdict = "APPROVE";
|
|
387
|
+
else if (lower.includes("revise") || lower.includes("changes requested")) verdict = "REVISE";
|
|
388
|
+
else if (lower.includes("rethink")) verdict = "RETHINK";
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Log review in STATUS.md execution log
|
|
392
|
+
try {
|
|
393
|
+
const status = readFileSync(statusPath, "utf-8");
|
|
394
|
+
const logEntry = `| ${new Date().toISOString().slice(0, 16).replace("T", " ")} | Review R${num} | ${reviewType} Step ${stepNum}: ${verdict} |\n`;
|
|
395
|
+
writeFileSync(statusPath, status.trimEnd() + "\n" + logEntry);
|
|
396
|
+
} catch { /* best effort */ }
|
|
397
|
+
|
|
398
|
+
const reviewFile = `.reviews/R${num}-${reviewType}-step${stepNum}.md`;
|
|
399
|
+
if (verdict === "APPROVE") {
|
|
400
|
+
return { content: [{ type: "text" as const, text: `APPROVE` }], details: undefined };
|
|
401
|
+
} else if (verdict === "REVISE") {
|
|
402
|
+
const summaryMatch = reviewContent.match(/###?\s*Summary[:\s]*([\s\S]*?)(?=###|$)/i);
|
|
403
|
+
const details = summaryMatch ? summaryMatch[1].trim().slice(0, 500) : "See review file.";
|
|
404
|
+
return { content: [{ type: "text" as const, text: `REVISE: ${details}\n\nFull review: ${reviewFile}` }], details: undefined };
|
|
405
|
+
} else if (verdict === "RETHINK") {
|
|
406
|
+
return { content: [{ type: "text" as const, text: `RETHINK — reconsider approach. See ${reviewFile}` }], details: undefined };
|
|
407
|
+
} else {
|
|
408
|
+
return { content: [{ type: "text" as const, text: `Review complete (verdict unclear). See ${reviewFile}` }], details: undefined };
|
|
409
|
+
}
|
|
410
|
+
} else {
|
|
411
|
+
return { content: [{ type: "text" as const, text: `UNAVAILABLE — reviewer exited (code ${exitCode}) but produced no output.` }], details: undefined };
|
|
412
|
+
}
|
|
413
|
+
} catch (err) {
|
|
414
|
+
return {
|
|
415
|
+
content: [{ type: "text" as const, text: `UNAVAILABLE — reviewer failed: ${err instanceof Error ? err.message : String(err)}` }],
|
|
416
|
+
details: undefined,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
},
|
|
420
|
+
});
|
|
159
421
|
}
|
|
@@ -3117,6 +3117,7 @@ export async function executeLaneV2(
|
|
|
3117
3117
|
workerTools: "read,write,edit,bash,grep,find,ls",
|
|
3118
3118
|
workerThinking: "",
|
|
3119
3119
|
workerSystemPrompt,
|
|
3120
|
+
projectName: config.project?.name || "project",
|
|
3120
3121
|
maxIterations: 20,
|
|
3121
3122
|
noProgressLimit: 3,
|
|
3122
3123
|
maxWorkerMinutes: config.failure?.maxWorkerMinutes || 30,
|
|
@@ -92,6 +92,8 @@ export interface LaneRunnerConfig {
|
|
|
92
92
|
workerThinking: string;
|
|
93
93
|
/** Worker system prompt */
|
|
94
94
|
workerSystemPrompt: string;
|
|
95
|
+
/** Project name (for review request context) */
|
|
96
|
+
projectName?: string;
|
|
95
97
|
/** Max worker iterations before giving up */
|
|
96
98
|
maxIterations: number;
|
|
97
99
|
/** No-progress stall limit */
|
|
@@ -273,6 +275,8 @@ export async function executeTaskV2(
|
|
|
273
275
|
env: {
|
|
274
276
|
TASKPLANE_OUTBOX_DIR: outboxDir,
|
|
275
277
|
TASKPLANE_AGENT_ID: workerAgentId,
|
|
278
|
+
TASKPLANE_TASK_FOLDER: taskFolder,
|
|
279
|
+
TASKPLANE_PROJECT_NAME: config.projectName || "project",
|
|
276
280
|
ORCH_BATCH_ID: config.batchId,
|
|
277
281
|
},
|
|
278
282
|
};
|
package/package.json
CHANGED
|
@@ -82,7 +82,14 @@ reads the config to discover what areas exist rather than assuming a layout.
|
|
|
82
82
|
|
|
83
83
|
### Step 2: Assess Complexity & Size
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
**You MUST explicitly score and assign review level before creating PROMPT.md.**
|
|
86
|
+
|
|
87
|
+
Quick reference (full rubric in [Complexity Assessment](#complexity-assessment)):
|
|
88
|
+
- Score each dimension 0-2: Blast radius, Pattern novelty, Security, Reversibility
|
|
89
|
+
- Sum → Level: 0-1→L0 (None), 2-3→L1 (Plan), 4-5→L2 (Plan+Code), 6-8→L3 (Full)
|
|
90
|
+
- Size: S (<2h), M (2-4h), L (4-8h), XL (8h+ → must split)
|
|
91
|
+
|
|
92
|
+
**Do not default to Review Level 0.** Level 0 is only appropriate for trivial changes (doc updates, config, boilerplate). Most M-sized tasks score ≥2 and require at least Level 1.
|
|
86
93
|
|
|
87
94
|
### Step 3: Create Task Folder
|
|
88
95
|
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Copy this template when creating a new task. Replace all `[bracketed]` fields.
|
|
4
4
|
|
|
5
|
+
**Before creating:** Verify you have scored complexity and assigned Review Level.
|
|
6
|
+
Review Level 0 is ONLY for trivial changes. Most M+ tasks need Level ≥1.
|
|
7
|
+
|
|
5
8
|
---
|
|
6
9
|
|
|
7
10
|
````markdown
|