taskplane 0.7.2 → 0.8.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 +26 -13
- package/extensions/task-runner.ts +346 -127
- package/extensions/taskplane/config-loader.ts +1 -1
- package/extensions/taskplane/config-schema.ts +7 -5
- package/extensions/taskplane/extension.ts +16 -3
- package/extensions/taskplane/merge.ts +1 -1
- package/extensions/taskplane/supervisor.ts +46 -3
- package/extensions/taskplane/types.ts +2 -2
- package/package.json +1 -1
- package/templates/agents/local/task-worker.md +3 -2
- package/templates/agents/task-worker.md +17 -13
- package/templates/config/task-orchestrator.yaml +1 -1
- package/templates/config/task-runner.yaml +3 -3
package/dashboard/public/app.js
CHANGED
|
@@ -17,9 +17,11 @@ function formatDuration(ms) {
|
|
|
17
17
|
return `${m}m ${String(s).padStart(2, "0")}s`;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
function relativeTime(
|
|
21
|
-
if (!
|
|
22
|
-
const
|
|
20
|
+
function relativeTime(epochOrIso) {
|
|
21
|
+
if (!epochOrIso) return "";
|
|
22
|
+
const ts = typeof epochOrIso === "string" ? new Date(epochOrIso).getTime() : epochOrIso;
|
|
23
|
+
if (isNaN(ts)) return "";
|
|
24
|
+
const diff = Date.now() - ts;
|
|
23
25
|
if (diff < 60000) return `${Math.floor(diff / 1000)}s ago`;
|
|
24
26
|
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
|
|
25
27
|
return `${Math.floor(diff / 3600000)}h ago`;
|
|
@@ -711,17 +713,16 @@ function renderNoBatch() {
|
|
|
711
713
|
if ($mergePanel) $mergePanel.style.display = "none";
|
|
712
714
|
if ($errorsPanel) $errorsPanel.style.display = "none";
|
|
713
715
|
|
|
714
|
-
//
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
// No history yet — show placeholder in the history panel
|
|
716
|
+
// Show a placeholder while history loads. loadHistoryList() (called in
|
|
717
|
+
// render() just before this) is async — the fresh list may not be
|
|
718
|
+
// available yet. The loadHistoryList callback will replace this with
|
|
719
|
+
// the actual latest entry once it resolves.
|
|
720
|
+
if (!viewingHistoryId) {
|
|
720
721
|
$historyBody.innerHTML = `
|
|
721
722
|
<div class="no-batch">
|
|
722
723
|
<div class="no-batch-icon">⏳</div>
|
|
723
|
-
<div class="no-batch-title">
|
|
724
|
-
<div class="no-batch-hint"
|
|
724
|
+
<div class="no-batch-title">Batch complete</div>
|
|
725
|
+
<div class="no-batch-hint">Loading history…</div>
|
|
725
726
|
</div>`;
|
|
726
727
|
$historyPanel.style.display = "";
|
|
727
728
|
}
|
|
@@ -1405,10 +1406,22 @@ function loadHistoryList() {
|
|
|
1405
1406
|
.then(list => {
|
|
1406
1407
|
historyList = list || [];
|
|
1407
1408
|
renderHistoryDropdown();
|
|
1408
|
-
//
|
|
1409
|
-
|
|
1409
|
+
// Auto-select the latest history entry when no live batch is running.
|
|
1410
|
+
// Always update the view here — renderNoBatch() shows a placeholder
|
|
1411
|
+
// while this async fetch completes, so we need to replace it with
|
|
1412
|
+
// the actual latest entry. This fixes #20 where the stale cached
|
|
1413
|
+
// historyList caused the previous batch to be shown.
|
|
1414
|
+
if (noBatchRendered && historyList.length > 0) {
|
|
1410
1415
|
viewHistoryEntry(historyList[0].batchId);
|
|
1411
1416
|
$historySelect.value = historyList[0].batchId;
|
|
1417
|
+
} else if (noBatchRendered && historyList.length === 0) {
|
|
1418
|
+
$historyBody.innerHTML = `
|
|
1419
|
+
<div class="no-batch">
|
|
1420
|
+
<div class="no-batch-icon">⏳</div>
|
|
1421
|
+
<div class="no-batch-title">No batch running</div>
|
|
1422
|
+
<div class="no-batch-hint">.pi/batch-state.json not found<br>Start an orchestrator batch to see the dashboard.</div>
|
|
1423
|
+
</div>`;
|
|
1424
|
+
$historyPanel.style.display = "";
|
|
1412
1425
|
}
|
|
1413
1426
|
})
|
|
1414
1427
|
.catch(() => {});
|
|
@@ -164,7 +164,7 @@ const DEFAULT_CONFIG: TaskConfig = {
|
|
|
164
164
|
worker: { model: "", tools: "read,write,edit,bash,grep,find,ls", thinking: "off" },
|
|
165
165
|
reviewer: { model: "openai/gpt-5.3-codex", tools: "read,bash,grep,find,ls", thinking: "on" },
|
|
166
166
|
context: {
|
|
167
|
-
worker_context_window:
|
|
167
|
+
worker_context_window: 0, warn_percent: 85, kill_percent: 95,
|
|
168
168
|
max_worker_iterations: 20, max_review_cycles: 2, no_progress_limit: 3,
|
|
169
169
|
},
|
|
170
170
|
quality_gate: {
|
|
@@ -309,6 +309,46 @@ function getMaxWorkerMinutes(config: TaskConfig): number {
|
|
|
309
309
|
return 30;
|
|
310
310
|
}
|
|
311
311
|
|
|
312
|
+
// ── Context Window Resolution ─────────────────────────────────────────
|
|
313
|
+
|
|
314
|
+
/** Default fallback context window when neither config nor model provides a value. */
|
|
315
|
+
const FALLBACK_CONTEXT_WINDOW = 200_000;
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Resolve the effective context window size for worker spawning.
|
|
319
|
+
*
|
|
320
|
+
* Resolution order (first non-zero value wins):
|
|
321
|
+
* 1. Explicit user config (worker_context_window > 0 in config)
|
|
322
|
+
* 2. Auto-detect from pi model registry (ctx.model.contextWindow)
|
|
323
|
+
* 3. Fallback to 200K tokens
|
|
324
|
+
*
|
|
325
|
+
* A config value of 0 signals "auto-detect" — the default when no explicit
|
|
326
|
+
* value is configured. This allows pi's model registry to provide the real
|
|
327
|
+
* context window for the active model.
|
|
328
|
+
*
|
|
329
|
+
* @returns Object with `contextWindow` (resolved size) and `source` (diagnostic label)
|
|
330
|
+
*/
|
|
331
|
+
function resolveContextWindow(
|
|
332
|
+
config: TaskConfig,
|
|
333
|
+
ctx: ExtensionContext,
|
|
334
|
+
): { contextWindow: number; source: string } {
|
|
335
|
+
// 1. Explicit user config — non-zero means the user set it deliberately
|
|
336
|
+
const configVal = config.context.worker_context_window;
|
|
337
|
+
if (configVal > 0) {
|
|
338
|
+
return { contextWindow: configVal, source: "explicit config" };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// 2. Auto-detect from pi model registry
|
|
342
|
+
const modelWindow = ctx.model?.contextWindow;
|
|
343
|
+
if (modelWindow && modelWindow > 0) {
|
|
344
|
+
const modelId = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "unknown";
|
|
345
|
+
return { contextWindow: modelWindow, source: `auto-detected from ${modelId}` };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// 3. Fallback
|
|
349
|
+
return { contextWindow: FALLBACK_CONTEXT_WINDOW, source: `fallback ${FALLBACK_CONTEXT_WINDOW}` };
|
|
350
|
+
}
|
|
351
|
+
|
|
312
352
|
// ── Orchestrator Sidecar Files ────────────────────────────────────────
|
|
313
353
|
|
|
314
354
|
/**
|
|
@@ -876,6 +916,27 @@ function getHeadCommitSha(): string {
|
|
|
876
916
|
}
|
|
877
917
|
}
|
|
878
918
|
|
|
919
|
+
/**
|
|
920
|
+
* Find the git commit SHA where a specific step was completed.
|
|
921
|
+
* Workers commit at step boundaries with messages like:
|
|
922
|
+
* feat(TP-048): complete Step N — description
|
|
923
|
+
* Returns the commit SHA if found, or empty string.
|
|
924
|
+
*/
|
|
925
|
+
function findStepBoundaryCommit(stepNumber: number, taskId: string, since?: string): string {
|
|
926
|
+
try {
|
|
927
|
+
// Search git log for the step completion commit
|
|
928
|
+
const args = ["log", "--oneline", "--grep", `complete Step ${stepNumber}`, "--grep", taskId, "--all-match", "-1", "--format=%H"];
|
|
929
|
+
if (since) args.push(`${since}..HEAD`);
|
|
930
|
+
const result = spawnSync("git", args, {
|
|
931
|
+
encoding: "utf-8",
|
|
932
|
+
timeout: 5000,
|
|
933
|
+
});
|
|
934
|
+
return result.status === 0 ? (result.stdout || "").trim() : "";
|
|
935
|
+
} catch {
|
|
936
|
+
return "";
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
879
940
|
// ── Standards Resolution ─────────────────────────────────────────────
|
|
880
941
|
|
|
881
942
|
/**
|
|
@@ -1296,6 +1357,8 @@ function tailSidecarJsonl(filePath: string, tailState: SidecarTailState): Sideca
|
|
|
1296
1357
|
export const _tailSidecarJsonl = tailSidecarJsonl;
|
|
1297
1358
|
export const _createSidecarTailState = createSidecarTailState;
|
|
1298
1359
|
export const _getSidecarDir = getSidecarDir;
|
|
1360
|
+
export const _resolveContextWindow = resolveContextWindow;
|
|
1361
|
+
export const _FALLBACK_CONTEXT_WINDOW = FALLBACK_CONTEXT_WINDOW;
|
|
1299
1362
|
export type { SidecarTailState, SidecarTelemetryDelta };
|
|
1300
1363
|
|
|
1301
1364
|
// ── Exit Summary & Diagnostic ────────────────────────────────────────
|
|
@@ -1907,29 +1970,266 @@ export default function (pi: ExtensionAPI) {
|
|
|
1907
1970
|
updateStatusField(statusPath, "Last Updated", new Date().toISOString().slice(0, 10));
|
|
1908
1971
|
logExecution(statusPath, "Task started", "Extension-driven execution");
|
|
1909
1972
|
|
|
1910
|
-
//
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1973
|
+
// ── Per-task worker loop ─────────────────────────────────────
|
|
1974
|
+
// Spawn one worker per iteration; each worker handles ALL remaining
|
|
1975
|
+
// steps. Reviews run after the worker exits, per newly-completed step.
|
|
1976
|
+
// If context limit is hit mid-task, the next iteration picks up from
|
|
1977
|
+
// the first incomplete step via STATUS.md — same recovery mechanism.
|
|
1978
|
+
|
|
1979
|
+
// Collect baseline commits per step (for code review diffs).
|
|
1980
|
+
// Baselines are updated at step boundaries so each code review
|
|
1981
|
+
// sees only that step's changes, not cumulative diffs.
|
|
1982
|
+
const stepBaselineCommits = new Map<number, string>();
|
|
1983
|
+
|
|
1984
|
+
// Track steps that received a REVISE verdict and need rework.
|
|
1985
|
+
// This prevents the checkbox-count heuristic from re-completing them
|
|
1986
|
+
// before the worker has had a chance to address reviewer feedback.
|
|
1987
|
+
const needsRework = new Set<number>();
|
|
1988
|
+
|
|
1989
|
+
// Track which steps have already received a plan review so we don't
|
|
1990
|
+
// re-run plan review on rework cycles (only code review reruns).
|
|
1991
|
+
const planReviewedSteps = new Set<number>();
|
|
1992
|
+
|
|
1993
|
+
// Mark all incomplete steps as in-progress and capture the baseline
|
|
1994
|
+
// commit for the first one. Reviews are transition-based: they run
|
|
1995
|
+
// when a step newly completes after the worker exits, not up-front.
|
|
1996
|
+
{
|
|
1997
|
+
const currentStatus = parseStatusMd(readFileSync(statusPath, "utf-8"));
|
|
1998
|
+
for (const step of task.steps) {
|
|
1999
|
+
const ss = currentStatus.steps.find(s => s.number === step.number);
|
|
2000
|
+
if (ss?.status === "complete") continue;
|
|
2001
|
+
|
|
2002
|
+
// Mark step as in-progress and log its start
|
|
2003
|
+
updateStepStatus(statusPath, step.number, "in-progress");
|
|
2004
|
+
logExecution(statusPath, `Step ${step.number} started`, step.name);
|
|
2005
|
+
|
|
2006
|
+
// Capture baseline commit for the FIRST incomplete step only.
|
|
2007
|
+
// Later steps get their baselines when the prior step completes
|
|
2008
|
+
// (see the newlyCompleted handler in the iteration loop).
|
|
2009
|
+
// This prevents cross-step diff bleeding in code reviews.
|
|
2010
|
+
if (!stepBaselineCommits.size) {
|
|
2011
|
+
stepBaselineCommits.set(step.number, getHeadCommitSha());
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
1916
2014
|
}
|
|
1917
2015
|
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
2016
|
+
// Helper: determine if a parsed step is complete.
|
|
2017
|
+
// A step in needsRework (set after REVISE) is never complete, even if
|
|
2018
|
+
// all checkboxes are checked — the worker must address the feedback first.
|
|
2019
|
+
function isStepComplete(ss: StepInfo | undefined): boolean {
|
|
2020
|
+
if (!ss) return false;
|
|
2021
|
+
if (needsRework.has(ss.number)) return false;
|
|
2022
|
+
if (ss.status === "complete") return true;
|
|
2023
|
+
// Fallback: infer from checkboxes (covers "in-progress" and "not-started")
|
|
2024
|
+
return ss.totalChecked === ss.totalItems && ss.totalItems > 0;
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
let noProgressCount = 0;
|
|
2028
|
+
for (let iter = 0; iter < config.context.max_worker_iterations; iter++) {
|
|
1921
2029
|
if (state.phase === "paused") {
|
|
1922
|
-
logExecution(statusPath, "Paused", `User paused at
|
|
1923
|
-
ctx.ui.notify(`Task paused at
|
|
2030
|
+
logExecution(statusPath, "Paused", `User paused at iteration ${iter + 1}`);
|
|
2031
|
+
ctx.ui.notify(`Task paused at iteration ${iter + 1}`, "info");
|
|
1924
2032
|
return;
|
|
1925
2033
|
}
|
|
1926
2034
|
|
|
1927
|
-
|
|
2035
|
+
// Determine remaining (incomplete) steps
|
|
2036
|
+
const currentStatus = parseStatusMd(readFileSync(statusPath, "utf-8"));
|
|
2037
|
+
const remainingSteps: StepInfo[] = [];
|
|
2038
|
+
for (const step of task.steps) {
|
|
2039
|
+
const ss = currentStatus.steps.find(s => s.number === step.number);
|
|
2040
|
+
if (!isStepComplete(ss)) remainingSteps.push(step);
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
if (remainingSteps.length === 0) break; // All steps done
|
|
2044
|
+
|
|
2045
|
+
state.currentStep = remainingSteps[0].number;
|
|
2046
|
+
updateStatusField(statusPath, "Current Step", `Step ${remainingSteps[0].number}: ${remainingSteps[0].name}`);
|
|
2047
|
+
state.workerIteration = iter + 1;
|
|
2048
|
+
state.totalIterations++;
|
|
2049
|
+
updateStatusField(statusPath, "Iteration", `${state.totalIterations}`);
|
|
1928
2050
|
updateWidgets();
|
|
1929
2051
|
|
|
1930
|
-
|
|
2052
|
+
// Count total checked checkboxes across all steps BEFORE worker runs
|
|
2053
|
+
const prevTotalChecked = currentStatus.steps.reduce((sum, s) => sum + s.totalChecked, 0);
|
|
2054
|
+
|
|
2055
|
+
// Track which steps are complete before the worker runs
|
|
2056
|
+
const completedBefore = new Set<number>();
|
|
2057
|
+
for (const ss of currentStatus.steps) {
|
|
2058
|
+
if (isStepComplete(ss)) completedBefore.add(ss.number);
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
// Ensure the first remaining step has a baseline. On subsequent
|
|
2062
|
+
// iterations (after context-limit recovery), the first remaining step
|
|
2063
|
+
// may not have a baseline yet if it wasn't the first step originally.
|
|
2064
|
+
if (remainingSteps.length > 0 && !stepBaselineCommits.has(remainingSteps[0].number)) {
|
|
2065
|
+
stepBaselineCommits.set(remainingSteps[0].number, getHeadCommitSha());
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
await runWorker(remainingSteps, ctx);
|
|
2069
|
+
|
|
2070
|
+
if (state.phase === "error") return;
|
|
1931
2071
|
|
|
1932
|
-
|
|
2072
|
+
// ── Post-worker: determine which steps were newly completed ──
|
|
2073
|
+
const afterStatus = parseStatusMd(readFileSync(statusPath, "utf-8"));
|
|
2074
|
+
const afterTotalChecked = afterStatus.steps.reduce((sum, s) => sum + s.totalChecked, 0);
|
|
2075
|
+
|
|
2076
|
+
// Progress tracking: compare total checked across ALL steps
|
|
2077
|
+
const progressDelta = afterTotalChecked - prevTotalChecked;
|
|
2078
|
+
if (progressDelta <= 0) {
|
|
2079
|
+
noProgressCount++;
|
|
2080
|
+
logExecution(statusPath, "No progress", `Iteration ${iter + 1}: 0 new checkboxes (${noProgressCount}/${config.context.no_progress_limit} stall limit)`);
|
|
2081
|
+
ctx.ui.notify(`⚠️ No progress in iteration ${iter + 1} (${noProgressCount}/${config.context.no_progress_limit})`, "warning");
|
|
2082
|
+
if (noProgressCount >= config.context.no_progress_limit) {
|
|
2083
|
+
logExecution(statusPath, "Task blocked", `No progress after ${noProgressCount} iterations`);
|
|
2084
|
+
ctx.ui.notify(`⚠️ Task blocked — no progress after ${noProgressCount} iterations`, "error");
|
|
2085
|
+
state.phase = "error";
|
|
2086
|
+
return;
|
|
2087
|
+
}
|
|
2088
|
+
} else {
|
|
2089
|
+
noProgressCount = 0;
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
// Find newly completed steps.
|
|
2093
|
+
// For steps in needsRework, the worker must have addressed the
|
|
2094
|
+
// reviewer feedback — we detect this by checking if STATUS.md was
|
|
2095
|
+
// updated (the worker adds/checks revision items). We temporarily
|
|
2096
|
+
// remove the step from needsRework to let isStepComplete() evaluate
|
|
2097
|
+
// the checkbox state, then re-add if it's still not complete.
|
|
2098
|
+
const newlyCompleted: StepInfo[] = [];
|
|
2099
|
+
for (const step of task.steps) {
|
|
2100
|
+
if (completedBefore.has(step.number)) continue;
|
|
2101
|
+
const ss = afterStatus.steps.find(s => s.number === step.number);
|
|
2102
|
+
if (needsRework.has(step.number)) {
|
|
2103
|
+
// For rework steps, check if the worker set status to "complete"
|
|
2104
|
+
// or if the step now has all checkboxes checked (worker addressed feedback)
|
|
2105
|
+
if (ss?.status === "complete" ||
|
|
2106
|
+
(ss && ss.totalChecked === ss.totalItems && ss.totalItems > 0)) {
|
|
2107
|
+
needsRework.delete(step.number);
|
|
2108
|
+
updateStepStatus(statusPath, step.number, "complete");
|
|
2109
|
+
logExecution(statusPath, `Step ${step.number} complete`, `${step.name} (rework)`);
|
|
2110
|
+
newlyCompleted.push(step);
|
|
2111
|
+
}
|
|
2112
|
+
} else if (isStepComplete(ss)) {
|
|
2113
|
+
updateStepStatus(statusPath, step.number, "complete");
|
|
2114
|
+
logExecution(statusPath, `Step ${step.number} complete`, step.name);
|
|
2115
|
+
newlyCompleted.push(step);
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
// Update baselines for subsequent steps using step boundary commits.
|
|
2120
|
+
// When the worker completes multiple steps in one iteration, each step's
|
|
2121
|
+
// commit becomes the baseline for the next step's code review.
|
|
2122
|
+
if (newlyCompleted.length > 1) {
|
|
2123
|
+
for (let i = 0; i < newlyCompleted.length; i++) {
|
|
2124
|
+
const completedStep = newlyCompleted[i];
|
|
2125
|
+
const boundaryCommit = findStepBoundaryCommit(
|
|
2126
|
+
completedStep.number, task.taskId,
|
|
2127
|
+
stepBaselineCommits.get(completedStep.number)
|
|
2128
|
+
);
|
|
2129
|
+
if (boundaryCommit && i + 1 < newlyCompleted.length) {
|
|
2130
|
+
// Use this step's completion commit as the next step's baseline
|
|
2131
|
+
stepBaselineCommits.set(newlyCompleted[i + 1].number, boundaryCommit);
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
// Log iteration summary with progress delta and completed steps
|
|
2137
|
+
const completedNames = newlyCompleted.map(s => `Step ${s.number}`).join(", ");
|
|
2138
|
+
if (newlyCompleted.length > 0) {
|
|
2139
|
+
logExecution(statusPath, `Iteration ${iter + 1} summary`, `+${progressDelta} checkboxes, completed: ${completedNames}`);
|
|
2140
|
+
ctx.ui.notify(`Iteration ${iter + 1}: completed ${completedNames} (+${progressDelta} checkboxes)`, "info");
|
|
2141
|
+
} else if (progressDelta > 0) {
|
|
2142
|
+
logExecution(statusPath, `Iteration ${iter + 1} summary`, `+${progressDelta} checkboxes, no steps fully completed`);
|
|
2143
|
+
ctx.ui.notify(`Iteration ${iter + 1}: +${progressDelta} checkboxes (no steps fully completed)`, "info");
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
// ── Run reviews for newly completed steps (transition-based) ──
|
|
2147
|
+
// Plan reviews run once per step (first completion); code reviews
|
|
2148
|
+
// run on every completion (including rework). Both respect review
|
|
2149
|
+
// level and low-risk skip logic.
|
|
2150
|
+
// Gate on phase !== "error" so reviews still run when paused
|
|
2151
|
+
// (pause is honored after reviews, before next iteration).
|
|
2152
|
+
if (state.phase !== "error") {
|
|
2153
|
+
for (const step of newlyCompleted) {
|
|
2154
|
+
const lowRisk = isLowRiskStep(step.number, task.steps.length);
|
|
2155
|
+
|
|
2156
|
+
// ── Plan review (level ≥ 1, first completion only) ──
|
|
2157
|
+
if (task.reviewLevel >= 1 && !planReviewedSteps.has(step.number)) {
|
|
2158
|
+
if (lowRisk) {
|
|
2159
|
+
const label = step.number === 0 ? "Preflight" : "final step";
|
|
2160
|
+
logExecution(statusPath, `Skip plan review`, `Step ${step.number} (${label}) — low-risk`);
|
|
2161
|
+
ctx.ui.notify(`⏭️ Skipping plan review for Step ${step.number} (${label})`, "info");
|
|
2162
|
+
} else {
|
|
2163
|
+
const verdict = await doReview("plan", step, ctx);
|
|
2164
|
+
if (verdict === "RETHINK") {
|
|
2165
|
+
ctx.ui.notify(`Reviewer: RETHINK on Step ${step.number} plan. Proceeding with caution.`, "warning");
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
planReviewedSteps.add(step.number);
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
// ── Code review (level ≥ 2) ──
|
|
2172
|
+
if (task.reviewLevel >= 2) {
|
|
2173
|
+
if (lowRisk) {
|
|
2174
|
+
const label = step.number === 0 ? "Preflight" : "final step";
|
|
2175
|
+
logExecution(statusPath, `Skip code review`, `Step ${step.number} (${label}) — low-risk`);
|
|
2176
|
+
ctx.ui.notify(`⏭️ Skipping code review for Step ${step.number} (${label})`, "info");
|
|
2177
|
+
} else {
|
|
2178
|
+
const baseline = stepBaselineCommits.get(step.number);
|
|
2179
|
+
const verdict = await doReview("code", step, ctx, baseline);
|
|
2180
|
+
if (verdict === "REVISE") {
|
|
2181
|
+
ctx.ui.notify(`Reviewer: REVISE on Step ${step.number}. Will rework in next iteration.`, "warning");
|
|
2182
|
+
// Mark step as needing rework — undo the "complete" status.
|
|
2183
|
+
// needsRework ensures isStepComplete() won't re-complete this
|
|
2184
|
+
// step based on checkbox counts alone; the worker must address
|
|
2185
|
+
// the reviewer feedback first, which will update STATUS.md and
|
|
2186
|
+
// clear the rework flag when the step is newly completed again.
|
|
2187
|
+
needsRework.add(step.number);
|
|
2188
|
+
updateStepStatus(statusPath, step.number, "in-progress");
|
|
2189
|
+
// Update baseline so the next code review for this step
|
|
2190
|
+
// diffs only the rework changes, not the original step work
|
|
2191
|
+
stepBaselineCommits.set(step.number, getHeadCommitSha());
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
// Update local cache
|
|
2199
|
+
const refreshed = parseStatusMd(readFileSync(statusPath, "utf-8"));
|
|
2200
|
+
for (const s of refreshed.steps) state.stepStatuses.set(s.number, s);
|
|
2201
|
+
updateWidgets();
|
|
2202
|
+
|
|
2203
|
+
// Check if all steps are now complete
|
|
2204
|
+
const allComplete = task.steps.every(step => {
|
|
2205
|
+
const ss = refreshed.steps.find(s => s.number === step.number);
|
|
2206
|
+
return isStepComplete(ss);
|
|
2207
|
+
});
|
|
2208
|
+
if (allComplete) break;
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
// ── Post-loop safety check: ensure all steps are actually complete ──
|
|
2212
|
+
// If the iteration cap was hit without completing all steps, fail explicitly
|
|
2213
|
+
// rather than falling through to quality gate / .DONE creation.
|
|
2214
|
+
if (state.phase === "running") {
|
|
2215
|
+
const finalStatus = parseStatusMd(readFileSync(statusPath, "utf-8"));
|
|
2216
|
+
const allStepsComplete = task.steps.every(step => {
|
|
2217
|
+
const ss = finalStatus.steps.find(s => s.number === step.number);
|
|
2218
|
+
return isStepComplete(ss);
|
|
2219
|
+
});
|
|
2220
|
+
if (!allStepsComplete) {
|
|
2221
|
+
const incomplete = task.steps
|
|
2222
|
+
.filter(step => {
|
|
2223
|
+
const ss = finalStatus.steps.find(s => s.number === step.number);
|
|
2224
|
+
return !isStepComplete(ss);
|
|
2225
|
+
})
|
|
2226
|
+
.map(s => `Step ${s.number}`)
|
|
2227
|
+
.join(", ");
|
|
2228
|
+
logExecution(statusPath, "Task incomplete", `Max iterations (${config.context.max_worker_iterations}) reached with incomplete steps: ${incomplete}`);
|
|
2229
|
+
ctx.ui.notify(`⚠️ Task incomplete — max iterations reached. Incomplete: ${incomplete}`, "error");
|
|
2230
|
+
state.phase = "error";
|
|
2231
|
+
return;
|
|
2232
|
+
}
|
|
1933
2233
|
}
|
|
1934
2234
|
|
|
1935
2235
|
// All steps done — run quality gate if enabled, then create .DONE
|
|
@@ -2079,111 +2379,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
2079
2379
|
ctx.ui.notify(`✅ Task ${task.taskId} complete!`, "success");
|
|
2080
2380
|
}
|
|
2081
2381
|
|
|
2082
|
-
async function executeStep(step: StepInfo, ctx: ExtensionContext): Promise<void> {
|
|
2083
|
-
if (!state.task || !state.config) return;
|
|
2084
|
-
|
|
2085
|
-
const task = state.task;
|
|
2086
|
-
const config = state.config;
|
|
2087
|
-
const statusPath = join(task.taskFolder, "STATUS.md");
|
|
2088
|
-
|
|
2089
|
-
// Capture git HEAD before the step starts so code reviewers can
|
|
2090
|
-
// diff the full step's changes (workers commit via checkpoints).
|
|
2091
|
-
const stepBaselineCommit = getHeadCommitSha();
|
|
2092
|
-
|
|
2093
|
-
updateStepStatus(statusPath, step.number, "in-progress");
|
|
2094
|
-
updateStatusField(statusPath, "Current Step", `Step ${step.number}: ${step.name}`);
|
|
2095
|
-
logExecution(statusPath, `Step ${step.number} started`, step.name);
|
|
2096
|
-
updateWidgets();
|
|
2097
|
-
|
|
2098
|
-
// Skip reviews for low-risk steps (Step 0 / Preflight and final step / Delivery)
|
|
2099
|
-
const _isLowRiskStep = isLowRiskStep(step.number, task.steps.length);
|
|
2100
|
-
|
|
2101
|
-
// Plan review (level ≥ 1)
|
|
2102
|
-
if (task.reviewLevel >= 1) {
|
|
2103
|
-
if (_isLowRiskStep) {
|
|
2104
|
-
const label = step.number === 0 ? "Preflight" : "final step";
|
|
2105
|
-
logExecution(statusPath, `Skip plan review`, `Step ${step.number} (${label}) — low-risk`);
|
|
2106
|
-
ctx.ui.notify(`⏭️ Skipping plan review for Step ${step.number} (${label})`, "info");
|
|
2107
|
-
} else {
|
|
2108
|
-
const verdict = await doReview("plan", step, ctx, stepBaselineCommit);
|
|
2109
|
-
if (verdict === "RETHINK") {
|
|
2110
|
-
ctx.ui.notify(`Reviewer: RETHINK on Step ${step.number} plan. Proceeding with caution.`, "warning");
|
|
2111
|
-
}
|
|
2112
|
-
}
|
|
2113
|
-
}
|
|
2114
|
-
|
|
2115
|
-
// Worker loop
|
|
2116
|
-
let noProgressCount = 0;
|
|
2117
|
-
for (let iter = 0; iter < config.context.max_worker_iterations; iter++) {
|
|
2118
|
-
if (state.phase === "paused") return;
|
|
2119
|
-
|
|
2120
|
-
// Re-read STATUS.md
|
|
2121
|
-
const currentStatus = parseStatusMd(readFileSync(statusPath, "utf-8"));
|
|
2122
|
-
const stepStatus = currentStatus.steps.find(s => s.number === step.number);
|
|
2123
|
-
if (stepStatus?.status === "complete" || (stepStatus && stepStatus.totalChecked === stepStatus.totalItems && stepStatus.totalItems > 0)) {
|
|
2124
|
-
updateStepStatus(statusPath, step.number, "complete");
|
|
2125
|
-
break;
|
|
2126
|
-
}
|
|
2127
|
-
|
|
2128
|
-
const prevChecked = stepStatus?.totalChecked || 0;
|
|
2129
|
-
state.workerIteration = iter + 1;
|
|
2130
|
-
state.totalIterations++;
|
|
2131
|
-
updateStatusField(statusPath, "Iteration", `${state.totalIterations}`);
|
|
2132
|
-
updateWidgets();
|
|
2133
|
-
|
|
2134
|
-
await runWorker(step, ctx);
|
|
2135
|
-
|
|
2136
|
-
// Check progress
|
|
2137
|
-
const afterStatus = parseStatusMd(readFileSync(statusPath, "utf-8"));
|
|
2138
|
-
const afterStep = afterStatus.steps.find(s => s.number === step.number);
|
|
2139
|
-
const afterChecked = afterStep?.totalChecked || 0;
|
|
2140
|
-
|
|
2141
|
-
if (afterChecked <= prevChecked) {
|
|
2142
|
-
noProgressCount++;
|
|
2143
|
-
if (noProgressCount >= config.context.no_progress_limit) {
|
|
2144
|
-
logExecution(statusPath, `Step ${step.number} blocked`, `No progress after ${noProgressCount} iterations`);
|
|
2145
|
-
ctx.ui.notify(`⚠️ Step ${step.number} blocked — no progress after ${noProgressCount} iterations`, "error");
|
|
2146
|
-
state.phase = "error";
|
|
2147
|
-
return;
|
|
2148
|
-
}
|
|
2149
|
-
} else {
|
|
2150
|
-
noProgressCount = 0;
|
|
2151
|
-
}
|
|
2152
|
-
|
|
2153
|
-
if (afterStep?.status === "complete" || (afterStep && afterStep.totalChecked === afterStep.totalItems && afterStep.totalItems > 0)) {
|
|
2154
|
-
updateStepStatus(statusPath, step.number, "complete");
|
|
2155
|
-
break;
|
|
2156
|
-
}
|
|
2157
|
-
}
|
|
2158
|
-
|
|
2159
|
-
// Code review (level ≥ 2)
|
|
2160
|
-
if (task.reviewLevel >= 2 && state.phase === "running") {
|
|
2161
|
-
if (_isLowRiskStep) {
|
|
2162
|
-
const label = step.number === 0 ? "Preflight" : "final step";
|
|
2163
|
-
logExecution(statusPath, `Skip code review`, `Step ${step.number} (${label}) — low-risk`);
|
|
2164
|
-
ctx.ui.notify(`⏭️ Skipping code review for Step ${step.number} (${label})`, "info");
|
|
2165
|
-
} else {
|
|
2166
|
-
const verdict = await doReview("code", step, ctx, stepBaselineCommit);
|
|
2167
|
-
if (verdict === "REVISE") {
|
|
2168
|
-
ctx.ui.notify(`Reviewer: REVISE on Step ${step.number}. Running worker to fix...`, "warning");
|
|
2169
|
-
await runWorker(step, ctx); // One more pass to address issues
|
|
2170
|
-
}
|
|
2171
|
-
}
|
|
2172
|
-
}
|
|
2173
|
-
|
|
2174
|
-
if (state.phase === "running") {
|
|
2175
|
-
updateStepStatus(statusPath, step.number, "complete");
|
|
2176
|
-
logExecution(statusPath, `Step ${step.number} complete`, step.name);
|
|
2177
|
-
// Update local cache
|
|
2178
|
-
const refreshed = parseStatusMd(readFileSync(statusPath, "utf-8"));
|
|
2179
|
-
for (const s of refreshed.steps) state.stepStatuses.set(s.number, s);
|
|
2180
|
-
updateWidgets();
|
|
2181
|
-
}
|
|
2182
|
-
}
|
|
2183
|
-
|
|
2184
2382
|
// ── Worker ───────────────────────────────────────────────────────
|
|
2185
2383
|
|
|
2186
|
-
async function runWorker(
|
|
2384
|
+
async function runWorker(remainingSteps: StepInfo[], ctx: ExtensionContext): Promise<void> {
|
|
2187
2385
|
if (!state.task || !state.config) return;
|
|
2188
2386
|
|
|
2189
2387
|
const task = state.task;
|
|
@@ -2228,8 +2426,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
2228
2426
|
"Just create the .DONE file in the task folder when complete."
|
|
2229
2427
|
: "";
|
|
2230
2428
|
|
|
2429
|
+
// Build step listing for the worker prompt — show ALL steps with status
|
|
2430
|
+
const remainingSet = new Set(remainingSteps.map(s => s.number));
|
|
2431
|
+
const stepListing = task.steps.map(s =>
|
|
2432
|
+
remainingSet.has(s.number)
|
|
2433
|
+
? ` - Step ${s.number}: ${s.name}`
|
|
2434
|
+
: ` - Step ${s.number}: ${s.name} [already complete — skip]`
|
|
2435
|
+
).join("\n");
|
|
2436
|
+
|
|
2231
2437
|
const prompt = [
|
|
2232
|
-
`Execute
|
|
2438
|
+
`Execute all remaining steps for task ${task.taskId}.`,
|
|
2233
2439
|
``,
|
|
2234
2440
|
`Task: ${task.taskId} — ${task.taskName}`,
|
|
2235
2441
|
`Task folder: ${task.taskFolder}/`,
|
|
@@ -2238,7 +2444,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
2238
2444
|
``,
|
|
2239
2445
|
`This is iteration ${state.totalIterations}.`,
|
|
2240
2446
|
`Read STATUS.md FIRST to find where you left off.`,
|
|
2241
|
-
|
|
2447
|
+
``,
|
|
2448
|
+
`Steps:`,
|
|
2449
|
+
stepListing,
|
|
2450
|
+
``,
|
|
2451
|
+
`Work through these steps in order. For each step:`,
|
|
2452
|
+
`1. Read STATUS.md to find unchecked items for that step`,
|
|
2453
|
+
`2. Complete all items for the step`,
|
|
2454
|
+
`3. Update STATUS.md step status to "complete"`,
|
|
2455
|
+
`4. Commit your changes: feat(${task.taskId}): complete Step N — description`,
|
|
2456
|
+
`5. Check for wrap-up signal files before starting the next step`,
|
|
2457
|
+
`6. Proceed to the next incomplete step`,
|
|
2242
2458
|
``,
|
|
2243
2459
|
`Wrap-up signal files: ${wrapUpFile} (primary), ${legacyWrapUpFile} (legacy)`,
|
|
2244
2460
|
`Check for either file after each checkpoint. If one exists, stop.`,
|
|
@@ -2273,15 +2489,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
2273
2489
|
// Exit summary path — set only in tmux mode (rpc-wrapper produces this file).
|
|
2274
2490
|
let exitSummaryPath: string | null = null;
|
|
2275
2491
|
|
|
2492
|
+
// Resolve context window: explicit config → model registry → 200K fallback
|
|
2493
|
+
const { contextWindow, source: contextWindowSource } = resolveContextWindow(config, ctx);
|
|
2494
|
+
const warnPct = config.context.warn_percent;
|
|
2495
|
+
const killPct = config.context.kill_percent;
|
|
2496
|
+
console.error(`[task-runner] worker context window: ${contextWindow} (${contextWindowSource})`);
|
|
2497
|
+
|
|
2276
2498
|
if (spawnMode === "tmux") {
|
|
2277
2499
|
// ── TMUX mode ────────────────────────────────────────
|
|
2278
2500
|
// Sidecar JSONL provides telemetry parity: tokens, cost, context%,
|
|
2279
2501
|
// tool calls, and retry events — same signals as subprocess mode.
|
|
2280
2502
|
// Kill via wall-clock timeout (context-% wrap-up also available via sidecar).
|
|
2281
2503
|
const sessionName = `${getTmuxPrefix()}-worker`;
|
|
2282
|
-
const contextWindow = config.context.worker_context_window;
|
|
2283
|
-
const warnPct = config.context.warn_percent;
|
|
2284
|
-
const killPct = config.context.kill_percent;
|
|
2285
2504
|
|
|
2286
2505
|
const spawned = spawnAgentTmux({
|
|
2287
2506
|
sessionName,
|
|
@@ -2373,9 +2592,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
2373
2592
|
thinking: config.worker.thinking || "off",
|
|
2374
2593
|
systemPrompt,
|
|
2375
2594
|
prompt,
|
|
2376
|
-
contextWindow
|
|
2377
|
-
warnPct
|
|
2378
|
-
killPct
|
|
2595
|
+
contextWindow,
|
|
2596
|
+
warnPct,
|
|
2597
|
+
killPct,
|
|
2379
2598
|
wrapUpFile,
|
|
2380
2599
|
onToolCall: (toolName, args) => {
|
|
2381
2600
|
state.workerToolCount++;
|
|
@@ -2405,7 +2624,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2405
2624
|
},
|
|
2406
2625
|
onContextPct: (pct) => {
|
|
2407
2626
|
state.workerContextPct = pct;
|
|
2408
|
-
if (pct >=
|
|
2627
|
+
if (pct >= warnPct) {
|
|
2409
2628
|
writeWrapUpSignal(`Wrap up (context ${Math.round(pct)}%)`);
|
|
2410
2629
|
}
|
|
2411
2630
|
updateWidgets();
|
|
@@ -760,7 +760,7 @@ export function toOrchestratorConfig(config: TaskplaneConfig): import("./types.t
|
|
|
760
760
|
tools: o.merge.tools,
|
|
761
761
|
verify: [...o.merge.verify],
|
|
762
762
|
order: o.merge.order,
|
|
763
|
-
timeout_minutes: o.merge.timeoutMinutes ??
|
|
763
|
+
timeout_minutes: o.merge.timeoutMinutes ?? 90,
|
|
764
764
|
},
|
|
765
765
|
failure: {
|
|
766
766
|
on_task_failure: o.failure.onTaskFailure,
|
|
@@ -125,7 +125,9 @@ export interface ReviewerConfig {
|
|
|
125
125
|
|
|
126
126
|
/** Context/resource limits for task execution */
|
|
127
127
|
export interface ContextConfig {
|
|
128
|
-
/** Context window size used for worker context pressure tracking
|
|
128
|
+
/** Context window size used for worker context pressure tracking.
|
|
129
|
+
* Set to 0 (default) for auto-detection from the pi model registry.
|
|
130
|
+
* When 0, the task-runner resolves at runtime: ctx.model.contextWindow → 200K fallback. */
|
|
129
131
|
workerContextWindow: number;
|
|
130
132
|
/** Warn threshold for context utilization (percent) */
|
|
131
133
|
warnPercent: number;
|
|
@@ -494,9 +496,9 @@ export const DEFAULT_TASK_RUNNER_SECTION: TaskRunnerSection = {
|
|
|
494
496
|
worker: { model: "", tools: "read,write,edit,bash,grep,find,ls", thinking: "off" },
|
|
495
497
|
reviewer: { model: "openai/gpt-5.3-codex", tools: "read,bash,grep,find,ls", thinking: "on" },
|
|
496
498
|
context: {
|
|
497
|
-
workerContextWindow:
|
|
498
|
-
warnPercent:
|
|
499
|
-
killPercent:
|
|
499
|
+
workerContextWindow: 0,
|
|
500
|
+
warnPercent: 85,
|
|
501
|
+
killPercent: 95,
|
|
500
502
|
maxWorkerIterations: 20,
|
|
501
503
|
maxReviewCycles: 2,
|
|
502
504
|
noProgressLimit: 3,
|
|
@@ -545,7 +547,7 @@ export const DEFAULT_ORCHESTRATOR_SECTION: OrchestratorSection = {
|
|
|
545
547
|
tools: "read,write,edit,bash,grep,find,ls",
|
|
546
548
|
verify: [],
|
|
547
549
|
order: "fewest-files-first",
|
|
548
|
-
timeoutMinutes:
|
|
550
|
+
timeoutMinutes: 90,
|
|
549
551
|
},
|
|
550
552
|
failure: {
|
|
551
553
|
onTaskFailure: "skip-dependents",
|
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
} from "./index.ts";
|
|
44
44
|
import { buildExecutionContext } from "./workspace.ts";
|
|
45
45
|
import { openSettingsTui } from "./settings-tui.ts";
|
|
46
|
+
import { loadProjectConfig } from "./config-loader.ts";
|
|
46
47
|
import {
|
|
47
48
|
activateSupervisor,
|
|
48
49
|
deactivateSupervisor,
|
|
@@ -767,10 +768,11 @@ export function validateModelAvailability(
|
|
|
767
768
|
runnerConfig: TaskRunnerConfig,
|
|
768
769
|
supervisorConfig: SupervisorConfig,
|
|
769
770
|
ctx: ExtensionContext,
|
|
771
|
+
agentModels?: { workerModel?: string; reviewerModel?: string },
|
|
770
772
|
): ModelCheckResult[] {
|
|
771
773
|
const entries: ModelCheckEntry[] = [
|
|
772
|
-
{ role: "Worker", modelStr: runnerConfig.worker?.model ?? "" },
|
|
773
|
-
{ role: "Reviewer", modelStr: runnerConfig.reviewer?.model ?? "" },
|
|
774
|
+
{ role: "Worker", modelStr: agentModels?.workerModel ?? (runnerConfig as any).worker?.model ?? "" },
|
|
775
|
+
{ role: "Reviewer", modelStr: agentModels?.reviewerModel ?? (runnerConfig as any).reviewer?.model ?? "" },
|
|
774
776
|
{ role: "Merger", modelStr: orchConfig.merge?.model ?? "" },
|
|
775
777
|
{ role: "Supervisor", modelStr: supervisorConfig.model ?? "" },
|
|
776
778
|
];
|
|
@@ -1401,7 +1403,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
1401
1403
|
// Validate that all configured agent models are resolvable in
|
|
1402
1404
|
// the model registry before starting. Catches misconfigured
|
|
1403
1405
|
// model names early instead of failing hours into a batch.
|
|
1404
|
-
|
|
1406
|
+
// Note: runnerConfig (TaskRunnerConfig) is a stripped type without
|
|
1407
|
+
// worker/reviewer model fields. Load the full unified config to
|
|
1408
|
+
// get the actual agent model strings (including user preferences).
|
|
1409
|
+
let agentModels: { workerModel?: string; reviewerModel?: string } | undefined;
|
|
1410
|
+
try {
|
|
1411
|
+
const fullConfig = loadProjectConfig(execCtx!.repoRoot);
|
|
1412
|
+
agentModels = {
|
|
1413
|
+
workerModel: fullConfig.taskRunner.worker.model || "",
|
|
1414
|
+
reviewerModel: fullConfig.taskRunner.reviewer.model || "",
|
|
1415
|
+
};
|
|
1416
|
+
} catch { /* fall through — validateModelAvailability handles empty strings */ }
|
|
1417
|
+
const modelResults = validateModelAvailability(orchConfig, runnerConfig, supervisorConfig, ctx, agentModels);
|
|
1405
1418
|
const modelFailures = modelResults.filter(r => r.status === "not-found");
|
|
1406
1419
|
ctx.ui.notify(formatModelValidation(modelResults), modelFailures.length > 0 ? "error" : "info");
|
|
1407
1420
|
if (modelFailures.length > 0) {
|
|
@@ -449,7 +449,7 @@ export async function spawnMergeAgent(
|
|
|
449
449
|
export function reloadMergeTimeoutMs(configRoot: string, pointerConfigRoot?: string): number {
|
|
450
450
|
try {
|
|
451
451
|
const freshConfig = loadOrchestratorConfig(configRoot, pointerConfigRoot);
|
|
452
|
-
const minutes = freshConfig.merge.timeout_minutes ??
|
|
452
|
+
const minutes = freshConfig.merge.timeout_minutes ?? 90;
|
|
453
453
|
return minutes * 60 * 1000;
|
|
454
454
|
} catch (err: unknown) {
|
|
455
455
|
// Config re-read is best-effort — fall back to default on failure
|
|
@@ -2028,7 +2028,38 @@ When the conversation reaches the config generation phase, create ALL of these
|
|
|
2028
2028
|
- \`.gitignore\` entries — add Taskplane working file patterns if not already present
|
|
2029
2029
|
|
|
2030
2030
|
Use conservative creation: check if each file exists before writing. If files
|
|
2031
|
-
already exist (partial setup), read and merge rather than overwrite
|
|
2031
|
+
already exist (partial setup), read and merge rather than overwrite.
|
|
2032
|
+
|
|
2033
|
+
### CRITICAL: Task Area Registration
|
|
2034
|
+
|
|
2035
|
+
**Every task folder MUST be registered in \`.pi/taskplane-config.json\` under
|
|
2036
|
+
\`taskRunner.taskAreas\`.** Without registration, \`/orch all\` will fail with
|
|
2037
|
+
"no task areas configured" — even if the folders and tasks physically exist.
|
|
2038
|
+
|
|
2039
|
+
When creating a task folder (e.g., \`taskplane-tasks/\`):
|
|
2040
|
+
1. Create the folder and its \`CONTEXT.md\`
|
|
2041
|
+
2. Register it in \`.pi/taskplane-config.json\`:
|
|
2042
|
+
\`\`\`json
|
|
2043
|
+
{
|
|
2044
|
+
"taskRunner": {
|
|
2045
|
+
"taskAreas": {
|
|
2046
|
+
"general": {
|
|
2047
|
+
"path": "taskplane-tasks",
|
|
2048
|
+
"prefix": "TP",
|
|
2049
|
+
"context": "taskplane-tasks/CONTEXT.md"
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
\`\`\`
|
|
2055
|
+
3. **Verify** by reading the config back to confirm the area is registered
|
|
2056
|
+
|
|
2057
|
+
When creating tasks inside an area, check that the area is registered first.
|
|
2058
|
+
If it's not (e.g., operator created the folder manually), register it before
|
|
2059
|
+
proceeding.
|
|
2060
|
+
|
|
2061
|
+
This also applies when creating tasks later in the conversation — always verify
|
|
2062
|
+
the task area is registered in the config before offering to run \`/orch all\`.`;
|
|
2032
2063
|
break;
|
|
2033
2064
|
|
|
2034
2065
|
case "pending-tasks":
|
|
@@ -2068,7 +2099,14 @@ Follow the primer's **"Script 6: Batch Planning"** section
|
|
|
2068
2099
|
5. **Offer a health check** (Script 7) if the operator prefers to assess
|
|
2069
2100
|
project state rather than create tasks
|
|
2070
2101
|
6. **Graceful fallback**: If \`gh\` CLI is unavailable, skip GitHub checks and
|
|
2071
|
-
mention it to the operator — continue with CONTEXT.md and TODO scanning
|
|
2102
|
+
mention it to the operator — continue with CONTEXT.md and TODO scanning
|
|
2103
|
+
|
|
2104
|
+
### Important: Task Area Verification
|
|
2105
|
+
|
|
2106
|
+
Before creating any tasks, verify that the target task area folder is registered
|
|
2107
|
+
in \`.pi/taskplane-config.json\` under \`taskRunner.taskAreas\`. If it's missing
|
|
2108
|
+
(e.g., the folder exists but was never registered), register it first. Without
|
|
2109
|
+
registration, \`/orch all\` will fail with "no task areas configured."`;
|
|
2072
2110
|
break;
|
|
2073
2111
|
|
|
2074
2112
|
case "completed-batch":
|
|
@@ -3045,11 +3083,16 @@ export function startHeartbeat(
|
|
|
3045
3083
|
return;
|
|
3046
3084
|
}
|
|
3047
3085
|
|
|
3048
|
-
// Update heartbeat
|
|
3086
|
+
// Update heartbeat (and refresh batchId if it was initially unknown)
|
|
3049
3087
|
try {
|
|
3050
3088
|
const lock = readLockfile(stateRoot);
|
|
3051
3089
|
if (lock && lock.sessionId === sessionId) {
|
|
3052
3090
|
lock.heartbeat = new Date().toISOString();
|
|
3091
|
+
// TP-130: batchId may have been "(initializing)" at lock creation
|
|
3092
|
+
// because the batch hadn't started yet. Refresh from live state ref.
|
|
3093
|
+
if (state.batchStateRef?.batchId && lock.batchId !== state.batchStateRef.batchId) {
|
|
3094
|
+
lock.batchId = state.batchStateRef.batchId;
|
|
3095
|
+
}
|
|
3053
3096
|
writeLockfile(stateRoot, lock);
|
|
3054
3097
|
}
|
|
3055
3098
|
} catch {
|
|
@@ -182,7 +182,7 @@ export const DEFAULT_ORCHESTRATOR_CONFIG: OrchestratorConfig = {
|
|
|
182
182
|
tools: "read,write,edit,bash,grep,find,ls",
|
|
183
183
|
verify: [],
|
|
184
184
|
order: "fewest-files-first",
|
|
185
|
-
timeout_minutes:
|
|
185
|
+
timeout_minutes: 90,
|
|
186
186
|
},
|
|
187
187
|
failure: {
|
|
188
188
|
on_task_failure: "skip-dependents",
|
|
@@ -1225,7 +1225,7 @@ export class MergeError extends Error {
|
|
|
1225
1225
|
* is generous and covers verification (go build) on large codebases.
|
|
1226
1226
|
*/
|
|
1227
1227
|
/** Default merge agent timeout. Use config.merge.timeout_minutes to override. */
|
|
1228
|
-
export const MERGE_TIMEOUT_MS =
|
|
1228
|
+
export const MERGE_TIMEOUT_MS = 90 * 60 * 1000;
|
|
1229
1229
|
|
|
1230
1230
|
/**
|
|
1231
1231
|
* Polling interval for merge result file (ms).
|
package/package.json
CHANGED
|
@@ -13,8 +13,9 @@ name: task-worker
|
|
|
13
13
|
|
|
14
14
|
The base prompt (maintained by taskplane) handles:
|
|
15
15
|
- STATUS.md-first workflow and checkpoint discipline
|
|
16
|
-
-
|
|
17
|
-
-
|
|
16
|
+
- Multi-step execution (worker handles all remaining steps per invocation)
|
|
17
|
+
- Iteration recovery (context limit → next invocation resumes from STATUS.md)
|
|
18
|
+
- Git commit conventions (per-step commits) and .DONE file creation
|
|
18
19
|
- Review response handling
|
|
19
20
|
|
|
20
21
|
Add project-specific rules below. Common examples:
|
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: task-worker
|
|
3
|
-
description: Autonomous task execution agent — works
|
|
3
|
+
description: Autonomous task execution agent — works through remaining steps with checkpoint discipline
|
|
4
4
|
tools: read,write,edit,bash,grep,find,ls
|
|
5
5
|
# model:
|
|
6
6
|
---
|
|
7
|
-
You are a task execution agent
|
|
8
|
-
|
|
9
|
-
ONLY memory.
|
|
7
|
+
You are a task execution agent. You may be invoked multiple times across
|
|
8
|
+
iterations — each invocation starts with ZERO memory of prior ones.
|
|
9
|
+
STATUS.md on disk is your ONLY memory.
|
|
10
|
+
|
|
11
|
+
Your prompt tells you which steps remain. Work through them **in order**,
|
|
12
|
+
completing each step before moving to the next.
|
|
10
13
|
|
|
11
14
|
## Resume Algorithm (MANDATORY — Do This First)
|
|
12
15
|
|
|
13
16
|
1. Read STATUS.md completely
|
|
14
|
-
2. Find the
|
|
17
|
+
2. Find the **first incomplete step** listed in your prompt
|
|
15
18
|
3. **Hydrate if needed** (see STATUS.md Hydration below)
|
|
16
19
|
4. Within that step, find the **first unchecked checkbox** (`- [ ]`)
|
|
17
20
|
5. Resume from there — do NOT redo checked items (`- [x]`)
|
|
18
|
-
6.
|
|
21
|
+
6. When a step's items are all checked, proceed to the next incomplete step
|
|
22
|
+
7. If all steps are complete, report completion
|
|
19
23
|
|
|
20
24
|
## Checkpoint Discipline (CRITICAL)
|
|
21
25
|
|
|
@@ -68,9 +72,9 @@ dozens of micro-commits that nobody reads.
|
|
|
68
72
|
|
|
69
73
|
STATUS.md is the worker's memory, not git. Checking off items in STATUS.md
|
|
70
74
|
ensures the next worker iteration knows where to resume. Git commits preserve
|
|
71
|
-
file changes at meaningful milestones. Per-checkbox
|
|
72
|
-
on git housekeeping without adding recovery value —
|
|
73
|
-
disk in the worktree.
|
|
75
|
+
file changes at meaningful milestones — one per completed step. Per-checkbox
|
|
76
|
+
commits waste tool calls on git housekeeping without adding recovery value —
|
|
77
|
+
the files are already on disk in the worktree.
|
|
74
78
|
|
|
75
79
|
## STATUS.md Hydration (MANDATORY)
|
|
76
80
|
|
|
@@ -94,7 +98,7 @@ instead of solving the problem.
|
|
|
94
98
|
|
|
95
99
|
Before implementing anything, assess whether the step needs expansion:
|
|
96
100
|
|
|
97
|
-
1. **Read the PROMPT.md step details** for
|
|
101
|
+
1. **Read the PROMPT.md step details** for the step you're entering
|
|
98
102
|
2. **Look for `⚠️ Hydrate` markers** — these signal the task creator expected
|
|
99
103
|
you to expand based on runtime discoveries
|
|
100
104
|
3. **If expansion is needed**, add checkboxes for **distinct outcomes** you've
|
|
@@ -149,9 +153,9 @@ When a reviewer returns REVISE with specific feedback items:
|
|
|
149
153
|
|
|
150
154
|
## Scope Rules
|
|
151
155
|
|
|
152
|
-
- Work
|
|
153
|
-
- Do NOT
|
|
154
|
-
- Do NOT expand task scope
|
|
156
|
+
- Work through all remaining steps listed in your prompt, **in order**
|
|
157
|
+
- Do NOT skip ahead — complete each step before starting the next
|
|
158
|
+
- Do NOT expand task scope beyond what the steps require
|
|
155
159
|
- If you discover something out of scope, note it in STATUS.md Discoveries table
|
|
156
160
|
|
|
157
161
|
## Self-Documentation
|
|
@@ -57,9 +57,9 @@ reviewer:
|
|
|
57
57
|
thinking: "off"
|
|
58
58
|
|
|
59
59
|
context:
|
|
60
|
-
worker_context_window: 200000
|
|
61
|
-
warn_percent:
|
|
62
|
-
kill_percent:
|
|
60
|
+
# worker_context_window: 200000 # 0 or omit = auto-detect from model registry; set explicitly to override
|
|
61
|
+
warn_percent: 85
|
|
62
|
+
kill_percent: 95
|
|
63
63
|
max_worker_iterations: 20
|
|
64
64
|
max_review_cycles: 2
|
|
65
65
|
no_progress_limit: 3
|