taskplane 0.23.13 → 0.23.15
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.
|
@@ -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
|
}
|
|
@@ -2938,6 +2938,83 @@ export function buildAgentIdFromLane(
|
|
|
2938
2938
|
*
|
|
2939
2939
|
* @since TP-102
|
|
2940
2940
|
*/
|
|
2941
|
+
/**
|
|
2942
|
+
* Parse an agent .md file: extract frontmatter and body.
|
|
2943
|
+
* Returns null if file doesn't exist or is malformed.
|
|
2944
|
+
* @since TP-117
|
|
2945
|
+
*/
|
|
2946
|
+
function parseAgentFile(filePath: string): { fm: Record<string, string>; body: string } | null {
|
|
2947
|
+
try {
|
|
2948
|
+
if (!existsSync(filePath)) return null;
|
|
2949
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
2950
|
+
const fmEnd = raw.indexOf("---", 4);
|
|
2951
|
+
if (fmEnd < 0) return { fm: {}, body: raw.trim() };
|
|
2952
|
+
const fmBlock = raw.slice(4, fmEnd).trim();
|
|
2953
|
+
const fm: Record<string, string> = {};
|
|
2954
|
+
for (const line of fmBlock.split("\n")) {
|
|
2955
|
+
const m = line.match(/^([\w-]+)\s*:\s*(.+)/);
|
|
2956
|
+
if (m) fm[m[1]] = m[2].trim();
|
|
2957
|
+
}
|
|
2958
|
+
return { fm, body: raw.slice(fmEnd + 3).trim() };
|
|
2959
|
+
} catch { return null; }
|
|
2960
|
+
}
|
|
2961
|
+
|
|
2962
|
+
/**
|
|
2963
|
+
* Load the base agent prompt from the taskplane package's templates/ directory.
|
|
2964
|
+
* Resolves the package root via well-known npm global paths.
|
|
2965
|
+
* @since TP-117
|
|
2966
|
+
*/
|
|
2967
|
+
function loadBaseAgentPrompt(agentName: string): string {
|
|
2968
|
+
const relPath = join("node_modules", "taskplane", "templates", "agents", `${agentName}.md`);
|
|
2969
|
+
const candidates: string[] = [];
|
|
2970
|
+
|
|
2971
|
+
// Global npm paths
|
|
2972
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
2973
|
+
if (process.env.APPDATA) candidates.push(join(process.env.APPDATA, "npm", relPath));
|
|
2974
|
+
if (home) {
|
|
2975
|
+
candidates.push(join(home, "AppData", "Roaming", "npm", relPath));
|
|
2976
|
+
candidates.push(join(home, ".npm-global", "lib", relPath));
|
|
2977
|
+
}
|
|
2978
|
+
candidates.push(join("/usr", "local", "lib", relPath));
|
|
2979
|
+
candidates.push(join("/opt", "homebrew", "lib", relPath));
|
|
2980
|
+
|
|
2981
|
+
// Dynamic: npm root -g
|
|
2982
|
+
try {
|
|
2983
|
+
const result = spawnSync("npm", ["root", "-g"], { encoding: "utf-8", timeout: 5000, shell: true });
|
|
2984
|
+
if (result.stdout?.trim()) {
|
|
2985
|
+
candidates.push(join(result.stdout.trim(), "taskplane", "templates", "agents", `${agentName}.md`));
|
|
2986
|
+
}
|
|
2987
|
+
} catch { /* ignore */ }
|
|
2988
|
+
|
|
2989
|
+
for (const p of candidates) {
|
|
2990
|
+
const def = parseAgentFile(p);
|
|
2991
|
+
if (def?.body) return def.body;
|
|
2992
|
+
}
|
|
2993
|
+
return "";
|
|
2994
|
+
}
|
|
2995
|
+
|
|
2996
|
+
/**
|
|
2997
|
+
* Load local project agent prompt from .pi/agents/ or agents/ directory.
|
|
2998
|
+
* Supports standalone mode (local replaces base entirely).
|
|
2999
|
+
* @since TP-117
|
|
3000
|
+
*/
|
|
3001
|
+
function loadLocalAgentPrompt(stateRoot: string, agentName: string): string {
|
|
3002
|
+
const paths = [
|
|
3003
|
+
join(stateRoot, ".pi", "agents", `${agentName}.md`),
|
|
3004
|
+
join(stateRoot, "agents", `${agentName}.md`),
|
|
3005
|
+
];
|
|
3006
|
+
for (const p of paths) {
|
|
3007
|
+
const def = parseAgentFile(p);
|
|
3008
|
+
if (def) {
|
|
3009
|
+
// standalone: true → use local as-is (body only, replaces base)
|
|
3010
|
+
if (def.fm.standalone === "true") return def.body;
|
|
3011
|
+
// Otherwise return body as project-specific guidance to append
|
|
3012
|
+
if (def.body) return def.body;
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
return "";
|
|
3016
|
+
}
|
|
3017
|
+
|
|
2941
3018
|
export function resolveRuntimeStateRoot(
|
|
2942
3019
|
repoRoot: string,
|
|
2943
3020
|
workspaceRoot?: string,
|
|
@@ -2987,16 +3064,20 @@ export async function executeLaneV2(
|
|
|
2987
3064
|
const opId = resolveOperatorId(config);
|
|
2988
3065
|
const agentIdPrefix = `${tmuxPrefix}-${opId}`;
|
|
2989
3066
|
|
|
2990
|
-
// Load worker agent definition
|
|
3067
|
+
// Load worker agent definition: compose base template + local project guidance.
|
|
3068
|
+
// The base template (templates/agents/task-worker.md) contains critical behavioral
|
|
3069
|
+
// rules: checkpoint discipline, STATUS.md resume algorithm, review_step instructions.
|
|
3070
|
+
// The local file (.pi/agents/task-worker.md) adds project-specific guidance.
|
|
2991
3071
|
let workerSystemPrompt = "You are a task execution agent. Read STATUS.md first, find unchecked items, work on them, checkpoint after each.";
|
|
2992
3072
|
try {
|
|
2993
|
-
const
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3073
|
+
const basePrompt = loadBaseAgentPrompt("task-worker");
|
|
3074
|
+
const localPrompt = loadLocalAgentPrompt(stateRoot, "task-worker");
|
|
3075
|
+
if (basePrompt && localPrompt) {
|
|
3076
|
+
workerSystemPrompt = basePrompt + "\n\n---\n\n## Project-Specific Guidance\n\n" + localPrompt;
|
|
3077
|
+
} else if (basePrompt) {
|
|
3078
|
+
workerSystemPrompt = basePrompt;
|
|
3079
|
+
} else if (localPrompt) {
|
|
3080
|
+
workerSystemPrompt = localPrompt;
|
|
3000
3081
|
}
|
|
3001
3082
|
} catch { /* use default */ }
|
|
3002
3083
|
|
|
@@ -3036,6 +3117,7 @@ export async function executeLaneV2(
|
|
|
3036
3117
|
workerTools: "read,write,edit,bash,grep,find,ls",
|
|
3037
3118
|
workerThinking: "",
|
|
3038
3119
|
workerSystemPrompt,
|
|
3120
|
+
projectName: config.project?.name || "project",
|
|
3039
3121
|
maxIterations: 20,
|
|
3040
3122
|
noProgressLimit: 3,
|
|
3041
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
|
};
|