taskplane 0.27.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/public/app.js +162 -13
- package/extensions/taskplane/discovery.ts +1818 -1508
- package/extensions/taskplane/execution.ts +33 -2
- package/extensions/taskplane/lane-runner.ts +375 -44
- package/extensions/taskplane/types.ts +25 -1
- package/package.json +1 -1
- package/skills/create-taskplane-task/SKILL.md +58 -0
- package/skills/create-taskplane-task/references/prompt-template.md +39 -0
- package/templates/agents/task-worker-segment.md +44 -0
- package/templates/agents/task-worker.md +429 -429
|
@@ -662,6 +662,27 @@ export function parseWorktreeStatusMd(
|
|
|
662
662
|
*
|
|
663
663
|
* @since TP-070
|
|
664
664
|
*/
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Parse STATUS.md directly from a known absolute path.
|
|
668
|
+
* Unlike parseWorktreeStatusMdAsync, this does NOT re-resolve the path —
|
|
669
|
+
* it reads exactly the file you point it to. Use this when the caller
|
|
670
|
+
* already has the authoritative statusPath (e.g., from buildExecutionUnit).
|
|
671
|
+
*
|
|
672
|
+
* @since TP-501
|
|
673
|
+
*/
|
|
674
|
+
export async function parseStatusMdAtPath(
|
|
675
|
+
statusPath: string,
|
|
676
|
+
): Promise<{ parsed: ParsedWorktreeStatus | null; error: string | null }> {
|
|
677
|
+
return parseStatusMdContent(statusPath);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Parse STATUS.md by resolving the path from taskFolder + worktree context.
|
|
682
|
+
* Use parseStatusMdAtPath instead when the caller already has the authoritative path.
|
|
683
|
+
*
|
|
684
|
+
* @since TP-070
|
|
685
|
+
*/
|
|
665
686
|
export async function parseWorktreeStatusMdAsync(
|
|
666
687
|
taskFolder: string,
|
|
667
688
|
worktreePath: string,
|
|
@@ -669,8 +690,13 @@ export async function parseWorktreeStatusMdAsync(
|
|
|
669
690
|
isWorkspaceMode?: boolean,
|
|
670
691
|
): Promise<{ parsed: ParsedWorktreeStatus | null; error: string | null }> {
|
|
671
692
|
const resolved = resolveCanonicalTaskPaths(taskFolder, worktreePath, repoRoot, isWorkspaceMode);
|
|
672
|
-
|
|
693
|
+
return parseStatusMdContent(resolved.statusPath);
|
|
694
|
+
}
|
|
673
695
|
|
|
696
|
+
/** Shared STATUS.md content parser — reads and parses from a known path. Handles file-not-found. */
|
|
697
|
+
async function parseStatusMdContent(
|
|
698
|
+
statusPath: string,
|
|
699
|
+
): Promise<{ parsed: ParsedWorktreeStatus | null; error: string | null }> {
|
|
674
700
|
if (!(await fileExistsAsync(statusPath))) {
|
|
675
701
|
return { parsed: null, error: `STATUS.md not found at ${statusPath}` };
|
|
676
702
|
}
|
|
@@ -1188,7 +1214,7 @@ export async function monitorLanes(
|
|
|
1188
1214
|
const unit = buildExecutionUnit(lane, task, repoRoot, isWorkspaceMode);
|
|
1189
1215
|
const donePath = unit.packet.donePath;
|
|
1190
1216
|
const statusPath = unit.packet.statusPath;
|
|
1191
|
-
const statusResult = await
|
|
1217
|
+
const statusResult = await parseStatusMdAtPath(statusPath);
|
|
1192
1218
|
|
|
1193
1219
|
const snapshot = await resolveTaskMonitorState(
|
|
1194
1220
|
task.taskId,
|
|
@@ -2516,6 +2542,7 @@ export async function executeLaneV2(
|
|
|
2516
2542
|
// rules: checkpoint discipline, STATUS.md resume algorithm, review_step instructions.
|
|
2517
2543
|
// The local file (.pi/agents/task-worker.md) adds project-specific guidance.
|
|
2518
2544
|
let workerSystemPrompt = "You are a task execution agent. Read STATUS.md first, find unchecked items, work on them, checkpoint after each.";
|
|
2545
|
+
let workerSegmentPrompt = "";
|
|
2519
2546
|
try {
|
|
2520
2547
|
const basePrompt = loadBaseAgentPrompt("task-worker");
|
|
2521
2548
|
const localPrompt = loadLocalAgentPrompt(stateRoot, "task-worker");
|
|
@@ -2526,6 +2553,9 @@ export async function executeLaneV2(
|
|
|
2526
2553
|
} else if (localPrompt) {
|
|
2527
2554
|
workerSystemPrompt = localPrompt;
|
|
2528
2555
|
}
|
|
2556
|
+
// Load segment-scoped prompt overlay (appended when isSegmentScoped)
|
|
2557
|
+
const segPrompt = loadBaseAgentPrompt("task-worker-segment");
|
|
2558
|
+
if (segPrompt) workerSegmentPrompt = segPrompt;
|
|
2529
2559
|
} catch { /* use default */ }
|
|
2530
2560
|
|
|
2531
2561
|
execLog(laneId, "LANE", `starting Runtime V2 execution of ${lane.tasks.length} task(s)`, {
|
|
@@ -2572,6 +2602,7 @@ export async function executeLaneV2(
|
|
|
2572
2602
|
workerTools: "read,write,edit,bash,grep,find,ls",
|
|
2573
2603
|
workerThinking: "",
|
|
2574
2604
|
workerSystemPrompt,
|
|
2605
|
+
workerSegmentPrompt,
|
|
2575
2606
|
reviewerModel: extraEnvVars?.TASKPLANE_REVIEWER_MODEL || "",
|
|
2576
2607
|
reviewerThinking: extraEnvVars?.TASKPLANE_REVIEWER_THINKING || "",
|
|
2577
2608
|
reviewerTools: extraEnvVars?.TASKPLANE_REVIEWER_TOOLS || "",
|
|
@@ -62,10 +62,114 @@ import {
|
|
|
62
62
|
type LaneTaskOutcome,
|
|
63
63
|
type LaneTaskStatus,
|
|
64
64
|
type SupervisorAlertCallback,
|
|
65
|
+
type StepSegmentMapping,
|
|
65
66
|
} from "./types.ts";
|
|
66
67
|
|
|
67
68
|
const LANE_RUNNER_DIR = dirname(fileURLToPath(import.meta.url));
|
|
68
69
|
|
|
70
|
+
// ── Segment Scoping Helpers (Phase A, TP-174) ────────────────────────
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get the set of step numbers that have segments for a given repoId.
|
|
74
|
+
*
|
|
75
|
+
* Used to filter the "remaining steps" view so the worker only sees steps
|
|
76
|
+
* that contain work for its repo.
|
|
77
|
+
*
|
|
78
|
+
* @param stepSegmentMap - Parsed step-segment mapping from PROMPT.md
|
|
79
|
+
* @param repoId - Repo ID to filter by
|
|
80
|
+
* @returns Set of step numbers that have at least one segment for this repoId
|
|
81
|
+
* @since TP-174
|
|
82
|
+
*/
|
|
83
|
+
export function getStepsForRepoId(
|
|
84
|
+
stepSegmentMap: StepSegmentMapping[],
|
|
85
|
+
repoId: string,
|
|
86
|
+
): Set<number> {
|
|
87
|
+
const stepNumbers = new Set<number>();
|
|
88
|
+
for (const step of stepSegmentMap) {
|
|
89
|
+
if (step.segments.some(seg => seg.repoId === repoId)) {
|
|
90
|
+
stepNumbers.add(step.stepNumber);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return stepNumbers;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Extract a segment's checkbox block from STATUS.md content for a given step and repoId.
|
|
98
|
+
*
|
|
99
|
+
* Looks for `#### Segment: <repoId>` headers within `### Step N:` sections,
|
|
100
|
+
* then returns the checkbox lines belonging to that segment block.
|
|
101
|
+
*
|
|
102
|
+
* @param statusContent - Raw STATUS.md content
|
|
103
|
+
* @param stepNumber - Step number to look in
|
|
104
|
+
* @param repoId - Repo ID of the segment
|
|
105
|
+
* @returns Object with checked/unchecked counts, or null if no segment block found
|
|
106
|
+
* @since TP-174
|
|
107
|
+
*/
|
|
108
|
+
export function getSegmentCheckboxes(
|
|
109
|
+
statusContent: string,
|
|
110
|
+
stepNumber: number,
|
|
111
|
+
repoId: string,
|
|
112
|
+
): { checked: number; unchecked: number; total: number; uncheckedTexts: string[] } | null {
|
|
113
|
+
const text = statusContent.replace(/\r\n/g, "\n");
|
|
114
|
+
|
|
115
|
+
// Find the step section
|
|
116
|
+
const stepHeaderPattern = new RegExp(`^###\\s+Step\\s+${stepNumber}:`, "m");
|
|
117
|
+
const stepMatch = text.match(stepHeaderPattern);
|
|
118
|
+
if (!stepMatch || stepMatch.index === undefined) return null;
|
|
119
|
+
|
|
120
|
+
// Find the end of this step section (next ### or end of file)
|
|
121
|
+
const afterStep = text.slice(stepMatch.index + stepMatch[0].length);
|
|
122
|
+
const nextStepMatch = afterStep.search(/^###\s+Step\s+\d+:/m);
|
|
123
|
+
const stepContent = nextStepMatch !== -1 ? afterStep.slice(0, nextStepMatch) : afterStep;
|
|
124
|
+
|
|
125
|
+
// Find the segment header within this step
|
|
126
|
+
const segHeaderPattern = new RegExp(`^####\\s+Segment:\\s*${repoId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*$`, "m");
|
|
127
|
+
const segMatch = stepContent.match(segHeaderPattern);
|
|
128
|
+
if (!segMatch || segMatch.index === undefined) return null;
|
|
129
|
+
|
|
130
|
+
// Extract content from segment header to next #### header or ### header or ---
|
|
131
|
+
const afterSeg = stepContent.slice(segMatch.index + segMatch[0].length);
|
|
132
|
+
const nextSectionMatch = afterSeg.search(/^(?:####\s|###\s|---)/m);
|
|
133
|
+
const segContent = nextSectionMatch !== -1 ? afterSeg.slice(0, nextSectionMatch) : afterSeg;
|
|
134
|
+
|
|
135
|
+
// Count checkboxes
|
|
136
|
+
let checked = 0;
|
|
137
|
+
let unchecked = 0;
|
|
138
|
+
const uncheckedTexts: string[] = [];
|
|
139
|
+
const cbRegex = /^\s*-\s*\[([ xX])\]\s*(.*)/gm;
|
|
140
|
+
let m;
|
|
141
|
+
while ((m = cbRegex.exec(segContent)) !== null) {
|
|
142
|
+
if (m[1].toLowerCase() === "x") {
|
|
143
|
+
checked++;
|
|
144
|
+
} else {
|
|
145
|
+
unchecked++;
|
|
146
|
+
uncheckedTexts.push(m[2].trim());
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return { checked, unchecked, total: checked + unchecked, uncheckedTexts };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Check if all checkboxes in a segment block are checked.
|
|
155
|
+
*
|
|
156
|
+
* @param statusContent - Raw STATUS.md content
|
|
157
|
+
* @param stepNumber - Step number to check
|
|
158
|
+
* @param repoId - Repo ID of the segment
|
|
159
|
+
* @returns true when all checkboxes in the segment block are checked
|
|
160
|
+
* @since TP-174
|
|
161
|
+
*/
|
|
162
|
+
export function isSegmentComplete(
|
|
163
|
+
statusContent: string,
|
|
164
|
+
stepNumber: number,
|
|
165
|
+
repoId: string,
|
|
166
|
+
): boolean {
|
|
167
|
+
const result = getSegmentCheckboxes(statusContent, stepNumber, repoId);
|
|
168
|
+
if (!result) return false;
|
|
169
|
+
if (result.total === 0) return false;
|
|
170
|
+
return result.unchecked === 0;
|
|
171
|
+
}
|
|
172
|
+
|
|
69
173
|
// ── Types ────────────────────────────────────────────────────────────
|
|
70
174
|
|
|
71
175
|
/**
|
|
@@ -94,8 +198,10 @@ export interface LaneRunnerConfig {
|
|
|
94
198
|
workerTools: string;
|
|
95
199
|
/** Worker thinking mode */
|
|
96
200
|
workerThinking: string;
|
|
97
|
-
/** Worker system prompt */
|
|
201
|
+
/** Worker system prompt (full-task mode) */
|
|
98
202
|
workerSystemPrompt: string;
|
|
203
|
+
/** Worker system prompt for segment-scoped mode (appended to base) */
|
|
204
|
+
workerSegmentPrompt: string;
|
|
99
205
|
/**
|
|
100
206
|
* Reviewer model (empty string = inherit session default).
|
|
101
207
|
* Set from TASKPLANE_REVIEWER_MODEL env var, sourced from runnerConfig.reviewer.model.
|
|
@@ -217,17 +323,53 @@ export async function executeTaskV2(
|
|
|
217
323
|
// TP-115: carry latest worker telemetry across iterations and into post-loop terminal snapshots
|
|
218
324
|
let lastTelemetry: Partial<AgentHostResult> = {};
|
|
219
325
|
|
|
326
|
+
// TP-174: Build segment context once for emitSnapshot calls.
|
|
327
|
+
// Available outside the loop so it can be passed to makeResult too.
|
|
328
|
+
const snapshotSegmentCtx: { stepSegmentMap: StepSegmentMapping[]; repoId: string } | null =
|
|
329
|
+
(segmentId && unit.task.stepSegmentMap && config.repoId)
|
|
330
|
+
? (() => {
|
|
331
|
+
const repoSteps = getStepsForRepoId(unit.task.stepSegmentMap!, config.repoId);
|
|
332
|
+
return repoSteps.size > 0
|
|
333
|
+
? { stepSegmentMap: unit.task.stepSegmentMap!, repoId: config.repoId }
|
|
334
|
+
: null;
|
|
335
|
+
})()
|
|
336
|
+
: null;
|
|
337
|
+
|
|
220
338
|
for (let iter = 0; iter < config.maxIterations; iter++) {
|
|
221
339
|
if (pauseSignal.paused) {
|
|
222
340
|
logExecution(statusPath, "Paused", `User paused at iteration ${totalIterations}`);
|
|
223
341
|
return makeResult(taskId, segmentId, workerAgentId, "skipped", startTime,
|
|
224
|
-
"Paused by user", false, totalIterations, cumulativeCostUsd, cumulativeTokens, config, statusPath, reviewerStatePath);
|
|
342
|
+
"Paused by user", false, totalIterations, cumulativeCostUsd, cumulativeTokens, config, statusPath, reviewerStatePath, undefined, snapshotSegmentCtx);
|
|
225
343
|
}
|
|
226
344
|
|
|
227
345
|
// Determine remaining steps
|
|
228
346
|
const currentStatus = parseStatusMd(readFileSync(statusPath, "utf-8"));
|
|
229
347
|
const parsed = parsePromptMd(readFileSync(promptPath, "utf-8"), promptPath);
|
|
348
|
+
|
|
349
|
+
// TP-174: Resolve segment-scoped step filtering.
|
|
350
|
+
// Use config.repoId (structured identity) instead of parsing opaque segmentId.
|
|
351
|
+
const stepSegmentMap = unit.task.stepSegmentMap;
|
|
352
|
+
const currentRepoId = segmentId ? config.repoId : null;
|
|
353
|
+
const rawRepoStepNumbers = (stepSegmentMap && currentRepoId)
|
|
354
|
+
? getStepsForRepoId(stepSegmentMap, currentRepoId)
|
|
355
|
+
: null;
|
|
356
|
+
// TP-174 legacy fallback: If no steps have segments for this repoId
|
|
357
|
+
// (multi-segment task without explicit markers, where all checkboxes
|
|
358
|
+
// are assigned to the fallback/packet repo), disable segment filtering.
|
|
359
|
+
const repoStepNumbers = (rawRepoStepNumbers && rawRepoStepNumbers.size > 0)
|
|
360
|
+
? rawRepoStepNumbers
|
|
361
|
+
: null;
|
|
362
|
+
|
|
363
|
+
// TP-174: Read STATUS.md content once for segment-scoped checks
|
|
364
|
+
const iterStatusContent = readFileSync(statusPath, "utf-8");
|
|
365
|
+
|
|
230
366
|
const remainingSteps = parsed.steps.filter(step => {
|
|
367
|
+
// TP-174: When segment-scoped, only show steps that have work for this repoId
|
|
368
|
+
if (repoStepNumbers && !repoStepNumbers.has(step.number)) return false;
|
|
369
|
+
// TP-174: Use segment-scoped completion check in segment mode
|
|
370
|
+
if (repoStepNumbers && currentRepoId) {
|
|
371
|
+
return !isSegmentComplete(iterStatusContent, step.number, currentRepoId);
|
|
372
|
+
}
|
|
231
373
|
const ss = currentStatus.steps.find(s => s.number === step.number);
|
|
232
374
|
return !isStepComplete(ss);
|
|
233
375
|
});
|
|
@@ -247,12 +389,26 @@ export async function executeTaskV2(
|
|
|
247
389
|
}
|
|
248
390
|
|
|
249
391
|
// Count checkboxes before worker runs
|
|
250
|
-
|
|
392
|
+
// TP-174: When segment-scoped, count only this segment's checkboxes
|
|
393
|
+
let prevTotalChecked: number;
|
|
394
|
+
if (repoStepNumbers && currentRepoId) {
|
|
395
|
+
const preStatusContent = readFileSync(statusPath, "utf-8");
|
|
396
|
+
const segCbs = getSegmentCheckboxes(preStatusContent, firstStep.number, currentRepoId);
|
|
397
|
+
prevTotalChecked = segCbs ? segCbs.checked : 0;
|
|
398
|
+
} else {
|
|
399
|
+
prevTotalChecked = currentStatus.steps.reduce((sum, s) => sum + s.totalChecked, 0);
|
|
400
|
+
}
|
|
251
401
|
|
|
252
402
|
// ── Build worker prompt ─────────────────────────────────────
|
|
253
403
|
const wrapUpFile = join(taskFolder, ".task-wrap-up");
|
|
254
404
|
if (existsSync(wrapUpFile)) try { unlinkSync(wrapUpFile); } catch { /* ignore */ }
|
|
255
405
|
|
|
406
|
+
// TP-174/TP-501: Compute segment scope mode BEFORE building prompt.
|
|
407
|
+
const isSegmentScoped = !!(stepSegmentMap && currentRepoId && repoStepNumbers
|
|
408
|
+
&& remainingSteps.length > 0
|
|
409
|
+
&& stepSegmentMap.find(s => s.stepNumber === remainingSteps[0].number)
|
|
410
|
+
?.segments.find(seg => seg.repoId === currentRepoId));
|
|
411
|
+
|
|
256
412
|
const promptLines = [
|
|
257
413
|
`Read your task instructions at: ${promptPath}`,
|
|
258
414
|
`Read your execution state at: ${statusPath}`,
|
|
@@ -266,7 +422,11 @@ export async function executeTaskV2(
|
|
|
266
422
|
`- Execution repo ID: ${unit.executionRepoId}`,
|
|
267
423
|
`- Execution worktree (worker cwd): ${unit.worktreePath}`,
|
|
268
424
|
`- Lane repo ID: ${config.repoId}`,
|
|
269
|
-
|
|
425
|
+
// Only show segment ID when segment-scoped. For FULL_TASK, omit to avoid
|
|
426
|
+
// workers incorrectly self-scoping based on segment metadata.
|
|
427
|
+
...(isSegmentScoped
|
|
428
|
+
? [`- Active segment ID: ${segmentId}`]
|
|
429
|
+
: []),
|
|
270
430
|
``,
|
|
271
431
|
`Packet home context:`,
|
|
272
432
|
`- Packet home repo ID: ${unit.packetHomeRepoId}`,
|
|
@@ -281,7 +441,8 @@ export async function executeTaskV2(
|
|
|
281
441
|
`⚠️ CHECKPOINT RULE: After completing EACH checkbox item, immediately edit STATUS.md to check it off (- [ ] → - [x]) BEFORE starting the next item. Do NOT batch checkbox updates at the end of a step.`,
|
|
282
442
|
];
|
|
283
443
|
|
|
284
|
-
|
|
444
|
+
// Only show segment DAG in segment-scoped mode
|
|
445
|
+
const segmentDag = isSegmentScoped ? unit.task.explicitSegmentDag : null;
|
|
285
446
|
if (segmentDag && segmentDag.repoIds.length > 0) {
|
|
286
447
|
const edgeSummary = segmentDag.edges.length > 0
|
|
287
448
|
? segmentDag.edges.map(edge => `${edge.fromRepoId}->${edge.toRepoId}`).join(", ")
|
|
@@ -294,6 +455,68 @@ export async function executeTaskV2(
|
|
|
294
455
|
);
|
|
295
456
|
}
|
|
296
457
|
|
|
458
|
+
// Segment scope mode is determined by which system prompt was loaded.
|
|
459
|
+
// No SegmentScopeMode line needed — the prompt IS the mode.
|
|
460
|
+
|
|
461
|
+
// TP-174: Segment-scoped prompt — show only this segment's checkboxes
|
|
462
|
+
if (stepSegmentMap && currentRepoId && repoStepNumbers && remainingSteps.length > 0) {
|
|
463
|
+
const currentStepNum = remainingSteps[0].number;
|
|
464
|
+
const currentStepMapping = stepSegmentMap.find(s => s.stepNumber === currentStepNum);
|
|
465
|
+
const mySegment = currentStepMapping?.segments.find(seg => seg.repoId === currentRepoId);
|
|
466
|
+
|
|
467
|
+
// Only inject segment-scoped prompt when the current step has an explicit
|
|
468
|
+
// segment for this repoId. If mySegment is missing (legacy task without
|
|
469
|
+
// markers, or step has no work for this repo), skip and preserve legacy behavior.
|
|
470
|
+
if (currentStepMapping && mySegment) {
|
|
471
|
+
const otherSegments = currentStepMapping.segments.filter(seg => seg.repoId !== currentRepoId);
|
|
472
|
+
|
|
473
|
+
// Count total segments for this repo across all steps
|
|
474
|
+
const totalStepsForRepo = repoStepNumbers ? repoStepNumbers.size : 0;
|
|
475
|
+
const segmentIndexInStep = currentStepMapping.segments.findIndex(seg => seg.repoId === currentRepoId) + 1;
|
|
476
|
+
const totalSegmentsInStep = currentStepMapping.segments.length;
|
|
477
|
+
|
|
478
|
+
promptLines.push(
|
|
479
|
+
``,
|
|
480
|
+
`Segment-scoped context (Phase A):`,
|
|
481
|
+
`Active segment: ${segmentId} (Step ${currentStepNum}, segment ${segmentIndexInStep} of ${totalSegmentsInStep})`,
|
|
482
|
+
`Your repo: ${currentRepoId}`,
|
|
483
|
+
``,
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
if (mySegment && mySegment.checkboxes.length > 0) {
|
|
487
|
+
promptLines.push(`Your checkboxes for this step:`);
|
|
488
|
+
for (const cb of mySegment.checkboxes) {
|
|
489
|
+
promptLines.push(` ${cb}`);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (otherSegments.length > 0) {
|
|
494
|
+
promptLines.push(``);
|
|
495
|
+
promptLines.push(`Other segments in this step (NOT yours — do not attempt):`);
|
|
496
|
+
for (const seg of otherSegments) {
|
|
497
|
+
promptLines.push(` - ${seg.repoId}: ${seg.checkboxes.length} checkbox(es) (will run in a separate segment)`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// List completed steps for this repo
|
|
502
|
+
const completedForRepo = parsed.steps.filter(step => {
|
|
503
|
+
if (!repoStepNumbers || !repoStepNumbers.has(step.number)) return false;
|
|
504
|
+
const ss = currentStatus.steps.find(s => s.number === step.number);
|
|
505
|
+
return isStepComplete(ss);
|
|
506
|
+
});
|
|
507
|
+
if (completedForRepo.length > 0) {
|
|
508
|
+
promptLines.push(``);
|
|
509
|
+
promptLines.push(`Prior steps completed: ${completedForRepo.map(s => `Step ${s.number} (${s.name})`).join(", ")}`);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
promptLines.push(
|
|
513
|
+
``,
|
|
514
|
+
`When all YOUR checkboxes are checked, your segment is done — exit successfully.`,
|
|
515
|
+
`Do NOT attempt work in other repos.`,
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
297
520
|
if (totalIterations > 1 && remainingSteps.length > 0) {
|
|
298
521
|
const remainingSet = new Set(remainingSteps.map(s => s.number));
|
|
299
522
|
const completedSteps = parsed.steps.filter(s => !remainingSet.has(s.number));
|
|
@@ -341,7 +564,9 @@ export async function executeTaskV2(
|
|
|
341
564
|
repoId: config.repoId,
|
|
342
565
|
cwd: unit.worktreePath,
|
|
343
566
|
prompt: promptLines.join("\n"),
|
|
344
|
-
systemPrompt: config.
|
|
567
|
+
systemPrompt: (isSegmentScoped && config.workerSegmentPrompt
|
|
568
|
+
? config.workerSystemPrompt + "\n\n---\n\n" + config.workerSegmentPrompt
|
|
569
|
+
: config.workerSystemPrompt) || undefined,
|
|
345
570
|
model: config.workerModel || undefined,
|
|
346
571
|
tools: config.workerTools || "read,write,edit,bash,grep,find,ls",
|
|
347
572
|
thinking: config.workerThinking || undefined,
|
|
@@ -363,7 +588,10 @@ export async function executeTaskV2(
|
|
|
363
588
|
TASKPLANE_REVIEWER_STATE_PATH: reviewerStatePath,
|
|
364
589
|
TASKPLANE_PROJECT_NAME: config.projectName || "project",
|
|
365
590
|
TASKPLANE_TASK_ID: taskId,
|
|
366
|
-
|
|
591
|
+
// Hard-set segment env vars based on mode. In FULL_TASK mode,
|
|
592
|
+
// explicitly clear them to prevent env inheritance leaking segment cues.
|
|
593
|
+
TASKPLANE_ACTIVE_SEGMENT_ID: isSegmentScoped ? (segmentId ?? "") : "",
|
|
594
|
+
TASKPLANE_SEGMENT_ID: isSegmentScoped ? (segmentId ?? "") : "",
|
|
367
595
|
TASKPLANE_SUPERVISOR_AUTONOMY: config.supervisorAutonomy || "autonomous",
|
|
368
596
|
ORCH_BATCH_ID: config.batchId,
|
|
369
597
|
...(config.reviewerModel ? { TASKPLANE_REVIEWER_MODEL: config.reviewerModel } : {}),
|
|
@@ -379,8 +607,15 @@ export async function executeTaskV2(
|
|
|
379
607
|
// 2. Blocker logged (non-empty Blockers section)
|
|
380
608
|
try {
|
|
381
609
|
const statusContent = readFileSync(statusPath, "utf-8");
|
|
382
|
-
|
|
383
|
-
|
|
610
|
+
// TP-174: Use same scope as prevTotalChecked (segment or global)
|
|
611
|
+
let midTotalChecked: number;
|
|
612
|
+
if (repoStepNumbers && currentRepoId) {
|
|
613
|
+
const segCbs = getSegmentCheckboxes(statusContent, firstStep.number, currentRepoId);
|
|
614
|
+
midTotalChecked = segCbs ? segCbs.checked : 0;
|
|
615
|
+
} else {
|
|
616
|
+
const midStatus = parseStatusMd(statusContent);
|
|
617
|
+
midTotalChecked = midStatus.steps.reduce((sum, s) => sum + s.totalChecked, 0);
|
|
618
|
+
}
|
|
384
619
|
if (midTotalChecked > prevTotalChecked) {
|
|
385
620
|
// Worker checked off checkboxes — let it exit normally
|
|
386
621
|
return null;
|
|
@@ -402,10 +637,20 @@ export async function executeTaskV2(
|
|
|
402
637
|
const uncheckedItems: string[] = [];
|
|
403
638
|
try {
|
|
404
639
|
const statusContent = readFileSync(statusPath, "utf-8");
|
|
405
|
-
|
|
406
|
-
if (
|
|
407
|
-
|
|
408
|
-
|
|
640
|
+
// TP-174: When segment-scoped, report only this segment's unchecked items
|
|
641
|
+
if (repoStepNumbers && currentRepoId) {
|
|
642
|
+
const segCbs = getSegmentCheckboxes(statusContent, firstStep.number, currentRepoId);
|
|
643
|
+
if (segCbs) {
|
|
644
|
+
for (const text of segCbs.uncheckedTexts.slice(0, 5)) {
|
|
645
|
+
uncheckedItems.push(text);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
} else {
|
|
649
|
+
const uncheckedMatches = statusContent.match(/^- \[ \] .+$/gm);
|
|
650
|
+
if (uncheckedMatches) {
|
|
651
|
+
for (const item of uncheckedMatches.slice(0, 5)) {
|
|
652
|
+
uncheckedItems.push(item.replace(/^- \[ \] /, "").trim());
|
|
653
|
+
}
|
|
409
654
|
}
|
|
410
655
|
}
|
|
411
656
|
} catch { /* best effort */ }
|
|
@@ -523,7 +768,7 @@ export async function executeTaskV2(
|
|
|
523
768
|
iterationTelemetry = telemetry;
|
|
524
769
|
lastTelemetry = telemetry;
|
|
525
770
|
// Emit lane snapshot
|
|
526
|
-
emitSnapshot(config, taskId, segmentId, "running", telemetry, statusPath, reviewerStatePath);
|
|
771
|
+
emitSnapshot(config, taskId, segmentId, "running", telemetry, statusPath, reviewerStatePath, snapshotSegmentCtx);
|
|
527
772
|
} catch { /* non-fatal: telemetry callback must never crash the engine */ }
|
|
528
773
|
});
|
|
529
774
|
|
|
@@ -533,7 +778,7 @@ export async function executeTaskV2(
|
|
|
533
778
|
let reviewerSnapshotFailures = 0;
|
|
534
779
|
const reviewerRefreshFailureThreshold = 5;
|
|
535
780
|
const reviewerRefresh = setInterval(() => {
|
|
536
|
-
const ok = emitSnapshot(config, taskId, segmentId, "running", iterationTelemetry, statusPath, reviewerStatePath);
|
|
781
|
+
const ok = emitSnapshot(config, taskId, segmentId, "running", iterationTelemetry, statusPath, reviewerStatePath, snapshotSegmentCtx);
|
|
537
782
|
if (ok) {
|
|
538
783
|
reviewerSnapshotFailures = 0;
|
|
539
784
|
return;
|
|
@@ -653,8 +898,16 @@ export async function executeTaskV2(
|
|
|
653
898
|
`${statusMsg} in ${Math.round(workerResult.durationMs / 1000)}s, tools: ${workerResult.toolCalls}`);
|
|
654
899
|
|
|
655
900
|
// ── Check progress ──────────────────────────────────────────
|
|
656
|
-
const
|
|
657
|
-
const
|
|
901
|
+
const afterStatusContent = readFileSync(statusPath, "utf-8");
|
|
902
|
+
const afterStatus = parseStatusMd(afterStatusContent);
|
|
903
|
+
// TP-174: Segment-scoped progress delta
|
|
904
|
+
let afterTotalChecked: number;
|
|
905
|
+
if (repoStepNumbers && currentRepoId) {
|
|
906
|
+
const segCbs = getSegmentCheckboxes(afterStatusContent, firstStep.number, currentRepoId);
|
|
907
|
+
afterTotalChecked = segCbs ? segCbs.checked : 0;
|
|
908
|
+
} else {
|
|
909
|
+
afterTotalChecked = afterStatus.steps.reduce((sum, s) => sum + s.totalChecked, 0);
|
|
910
|
+
}
|
|
658
911
|
const progressDelta = afterTotalChecked - prevTotalChecked;
|
|
659
912
|
|
|
660
913
|
if (progressDelta <= 0) {
|
|
@@ -689,7 +942,7 @@ export async function executeTaskV2(
|
|
|
689
942
|
if (noProgressCount >= config.noProgressLimit) {
|
|
690
943
|
logExecution(statusPath, "Task blocked", `No progress after ${noProgressCount} iterations`);
|
|
691
944
|
return makeResult(taskId, segmentId, workerAgentId, "failed", startTime,
|
|
692
|
-
`No progress after ${noProgressCount} iterations`, false, totalIterations, cumulativeCostUsd, cumulativeTokens, config, statusPath, reviewerStatePath, lastTelemetry);
|
|
945
|
+
`No progress after ${noProgressCount} iterations`, false, totalIterations, cumulativeCostUsd, cumulativeTokens, config, statusPath, reviewerStatePath, lastTelemetry, snapshotSegmentCtx);
|
|
693
946
|
}
|
|
694
947
|
}
|
|
695
948
|
} else {
|
|
@@ -697,41 +950,93 @@ export async function executeTaskV2(
|
|
|
697
950
|
}
|
|
698
951
|
|
|
699
952
|
// Mark completed steps
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
953
|
+
// TP-174: When segment-scoped, mark step complete when the segment's
|
|
954
|
+
// checkboxes are all checked (not the full step which may have other segments).
|
|
955
|
+
if (repoStepNumbers && currentRepoId) {
|
|
956
|
+
for (const stepNum of repoStepNumbers) {
|
|
957
|
+
if (isSegmentComplete(afterStatusContent, stepNum, currentRepoId)) {
|
|
958
|
+
// Only mark step complete in STATUS.md if ALL segments in that step
|
|
959
|
+
// are complete (not just ours). But for loop exit, we only care about ours.
|
|
960
|
+
const ss = afterStatus.steps.find(s => s.number === stepNum);
|
|
961
|
+
if (isStepComplete(ss)) {
|
|
962
|
+
updateStepStatus(statusPath, stepNum, "complete");
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
} else {
|
|
967
|
+
for (const step of parsed.steps) {
|
|
968
|
+
const ss = afterStatus.steps.find(s => s.number === step.number);
|
|
969
|
+
if (isStepComplete(ss)) {
|
|
970
|
+
updateStepStatus(statusPath, step.number, "complete");
|
|
971
|
+
}
|
|
704
972
|
}
|
|
705
973
|
}
|
|
706
974
|
|
|
707
975
|
// Check if all steps are now complete
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
976
|
+
// TP-174: When segment-scoped, exit when all steps for this repoId
|
|
977
|
+
// have their segment checkboxes complete.
|
|
978
|
+
let allComplete: boolean;
|
|
979
|
+
if (repoStepNumbers && currentRepoId) {
|
|
980
|
+
allComplete = [...repoStepNumbers].every(stepNum =>
|
|
981
|
+
isSegmentComplete(afterStatusContent, stepNum, currentRepoId),
|
|
982
|
+
);
|
|
983
|
+
} else {
|
|
984
|
+
allComplete = parsed.steps.every(step => {
|
|
985
|
+
const ss = afterStatus.steps.find(s => s.number === step.number);
|
|
986
|
+
return isStepComplete(ss);
|
|
987
|
+
});
|
|
988
|
+
}
|
|
712
989
|
if (allComplete) break;
|
|
713
990
|
}
|
|
714
991
|
|
|
715
992
|
// ── 3. Post-loop completion check ───────────────────────────────
|
|
716
|
-
const
|
|
993
|
+
const finalStatusContent = readFileSync(statusPath, "utf-8");
|
|
994
|
+
const finalStatus = parseStatusMd(finalStatusContent);
|
|
717
995
|
const parsed = parsePromptMd(readFileSync(promptPath, "utf-8"), promptPath);
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
996
|
+
|
|
997
|
+
// TP-174: Segment-scoped post-loop check. Re-derive repo scoping since
|
|
998
|
+
// the iteration loop variables are out of scope here.
|
|
999
|
+
const postLoopRepoId = segmentId ? config.repoId : null;
|
|
1000
|
+
const postLoopStepSegMap = unit.task.stepSegmentMap;
|
|
1001
|
+
const postLoopRepoSteps = (postLoopStepSegMap && postLoopRepoId)
|
|
1002
|
+
? getStepsForRepoId(postLoopStepSegMap, postLoopRepoId)
|
|
1003
|
+
: null;
|
|
1004
|
+
const effectivePostLoopRepoSteps = (postLoopRepoSteps && postLoopRepoSteps.size > 0)
|
|
1005
|
+
? postLoopRepoSteps
|
|
1006
|
+
: null;
|
|
1007
|
+
|
|
1008
|
+
let allStepsComplete: boolean;
|
|
1009
|
+
if (effectivePostLoopRepoSteps && postLoopRepoId) {
|
|
1010
|
+
allStepsComplete = [...effectivePostLoopRepoSteps].every(stepNum =>
|
|
1011
|
+
isSegmentComplete(finalStatusContent, stepNum, postLoopRepoId),
|
|
1012
|
+
);
|
|
1013
|
+
} else {
|
|
1014
|
+
allStepsComplete = parsed.steps.every(step => {
|
|
1015
|
+
const ss = finalStatus.steps.find(s => s.number === step.number);
|
|
1016
|
+
return isStepComplete(ss);
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
722
1019
|
|
|
723
1020
|
if (!allStepsComplete) {
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
1021
|
+
let incomplete: string;
|
|
1022
|
+
if (effectivePostLoopRepoSteps && postLoopRepoId) {
|
|
1023
|
+
incomplete = [...effectivePostLoopRepoSteps]
|
|
1024
|
+
.filter(stepNum => !isSegmentComplete(finalStatusContent, stepNum, postLoopRepoId))
|
|
1025
|
+
.map(n => `Step ${n}`)
|
|
1026
|
+
.join(", ");
|
|
1027
|
+
} else {
|
|
1028
|
+
incomplete = parsed.steps
|
|
1029
|
+
.filter(step => {
|
|
1030
|
+
const ss = finalStatus.steps.find(s => s.number === step.number);
|
|
1031
|
+
return !isStepComplete(ss);
|
|
1032
|
+
})
|
|
1033
|
+
.map(s => `Step ${s.number}`)
|
|
1034
|
+
.join(", ");
|
|
1035
|
+
}
|
|
731
1036
|
logExecution(statusPath, "Task incomplete", `Max iterations reached. Incomplete: ${incomplete}`);
|
|
732
1037
|
return makeResult(taskId, segmentId, workerAgentId, "failed", startTime,
|
|
733
1038
|
`Max iterations (${config.maxIterations}) reached with incomplete steps: ${incomplete}`,
|
|
734
|
-
false, totalIterations, cumulativeCostUsd, cumulativeTokens, config, statusPath, reviewerStatePath, lastTelemetry);
|
|
1039
|
+
false, totalIterations, cumulativeCostUsd, cumulativeTokens, config, statusPath, reviewerStatePath, lastTelemetry, snapshotSegmentCtx);
|
|
735
1040
|
}
|
|
736
1041
|
|
|
737
1042
|
// TP-145: Determine if this is a non-final segment of a multi-segment task.
|
|
@@ -774,7 +1079,7 @@ export async function executeTaskV2(
|
|
|
774
1079
|
? "non-final"
|
|
775
1080
|
: "pending expansion requests";
|
|
776
1081
|
return makeResult(taskId, segmentId, workerAgentId, "succeeded", startTime,
|
|
777
|
-
`Segment completed (${suppressionReason} — .DONE suppressed)`, false, totalIterations, cumulativeCostUsd, cumulativeTokens, config, statusPath, reviewerStatePath, lastTelemetry);
|
|
1082
|
+
`Segment completed (${suppressionReason} — .DONE suppressed)`, false, totalIterations, cumulativeCostUsd, cumulativeTokens, config, statusPath, reviewerStatePath, lastTelemetry, snapshotSegmentCtx);
|
|
778
1083
|
}
|
|
779
1084
|
|
|
780
1085
|
// Create .DONE if not already present (final segment or single-segment/whole-task execution)
|
|
@@ -785,7 +1090,7 @@ export async function executeTaskV2(
|
|
|
785
1090
|
logExecution(statusPath, "Task complete", ".DONE created");
|
|
786
1091
|
|
|
787
1092
|
return makeResult(taskId, segmentId, workerAgentId, "succeeded", startTime,
|
|
788
|
-
".DONE file created by lane-runner", true, totalIterations, cumulativeCostUsd, cumulativeTokens, config, statusPath, reviewerStatePath, lastTelemetry);
|
|
1093
|
+
".DONE file created by lane-runner", true, totalIterations, cumulativeCostUsd, cumulativeTokens, config, statusPath, reviewerStatePath, lastTelemetry, snapshotSegmentCtx);
|
|
789
1094
|
}
|
|
790
1095
|
|
|
791
1096
|
// ── Helpers ──────────────────────────────────────────────────────────
|
|
@@ -846,6 +1151,8 @@ function makeResult(
|
|
|
846
1151
|
statusPath?: string,
|
|
847
1152
|
reviewerStatePath?: string,
|
|
848
1153
|
finalTelemetry?: Partial<AgentHostResult>,
|
|
1154
|
+
/** TP-174: Segment context for segment-scoped snapshot progress */
|
|
1155
|
+
segmentCtx?: { stepSegmentMap: StepSegmentMapping[]; repoId: string } | null,
|
|
849
1156
|
): LaneRunnerTaskResult {
|
|
850
1157
|
const telemetry = status === "skipped"
|
|
851
1158
|
? undefined
|
|
@@ -880,7 +1187,7 @@ function makeResult(
|
|
|
880
1187
|
// TP-115: Emit terminal snapshot with real telemetry from agent-host result
|
|
881
1188
|
if (config && statusPath && reviewerStatePath) {
|
|
882
1189
|
const terminalStatus = mapLaneTaskStatusToTerminalSnapshotStatus(status);
|
|
883
|
-
emitSnapshot(config, taskId, segmentId, terminalStatus, finalTelemetry ?? {}, statusPath, reviewerStatePath);
|
|
1190
|
+
emitSnapshot(config, taskId, segmentId, terminalStatus, finalTelemetry ?? {}, statusPath, reviewerStatePath, segmentCtx);
|
|
884
1191
|
}
|
|
885
1192
|
|
|
886
1193
|
return result;
|
|
@@ -957,6 +1264,8 @@ function emitSnapshot(
|
|
|
957
1264
|
telemetry: Partial<AgentHostResult>,
|
|
958
1265
|
statusPath: string,
|
|
959
1266
|
reviewerStatePath: string,
|
|
1267
|
+
/** TP-174: Optional segment context for segment-scoped progress reporting */
|
|
1268
|
+
segmentContext?: { stepSegmentMap: StepSegmentMapping[]; repoId: string } | null,
|
|
960
1269
|
): boolean {
|
|
961
1270
|
try {
|
|
962
1271
|
// Parse progress from STATUS.md
|
|
@@ -965,8 +1274,30 @@ function emitSnapshot(
|
|
|
965
1274
|
const content = readFileSync(statusPath, "utf-8");
|
|
966
1275
|
const parsed = parseStatusMd(content);
|
|
967
1276
|
const currentStepMatch = content.match(/\*\*Current Step:\*\*\s*(.+)/);
|
|
968
|
-
|
|
969
|
-
|
|
1277
|
+
|
|
1278
|
+
// TP-174: Segment-scoped progress when segment markers are present.
|
|
1279
|
+
// Only count checkboxes from steps that belong to this segment's repoId.
|
|
1280
|
+
let checked: number;
|
|
1281
|
+
let total: number;
|
|
1282
|
+
if (segmentContext) {
|
|
1283
|
+
const { stepSegmentMap, repoId } = segmentContext;
|
|
1284
|
+
const repoSteps = getStepsForRepoId(stepSegmentMap, repoId);
|
|
1285
|
+
let segChecked = 0;
|
|
1286
|
+
let segTotal = 0;
|
|
1287
|
+
for (const stepNum of repoSteps) {
|
|
1288
|
+
const segCbs = getSegmentCheckboxes(content, stepNum, repoId);
|
|
1289
|
+
if (segCbs) {
|
|
1290
|
+
segChecked += segCbs.checked;
|
|
1291
|
+
segTotal += segCbs.total;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
checked = segChecked;
|
|
1295
|
+
total = segTotal;
|
|
1296
|
+
} else {
|
|
1297
|
+
checked = parsed.steps.reduce((sum, s) => sum + s.totalChecked, 0);
|
|
1298
|
+
total = parsed.steps.reduce((sum, s) => sum + s.totalItems, 0);
|
|
1299
|
+
}
|
|
1300
|
+
|
|
970
1301
|
progress = {
|
|
971
1302
|
currentStep: currentStepMatch?.[1]?.trim() || "Unknown",
|
|
972
1303
|
checked,
|