taskplane 0.9.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/extensions/task-runner.ts +17 -60
- package/extensions/taskplane/extension.ts +90 -9
- package/extensions/taskplane/messages.ts +10 -7
- package/extensions/taskplane/persistence.ts +5 -1
- package/extensions/taskplane/supervisor.ts +7 -3
- package/extensions/taskplane/worktree.ts +124 -0
- package/package.json +1 -1
- package/templates/agents/task-worker.md +6 -5
|
@@ -174,7 +174,7 @@ const DEFAULT_CONFIG: TaskConfig = {
|
|
|
174
174
|
standards_overrides: {},
|
|
175
175
|
task_areas: {},
|
|
176
176
|
worker: { model: "", tools: "read,write,edit,bash,grep,find,ls", thinking: "off" },
|
|
177
|
-
reviewer: { model: "
|
|
177
|
+
reviewer: { model: "", tools: "read,bash,grep,find,ls", thinking: "on" },
|
|
178
178
|
context: {
|
|
179
179
|
worker_context_window: 0, warn_percent: 85, kill_percent: 95,
|
|
180
180
|
max_worker_iterations: 20, max_review_cycles: 2, no_progress_limit: 3,
|
|
@@ -633,56 +633,6 @@ function resolveRpcWrapperPath(): string {
|
|
|
633
633
|
);
|
|
634
634
|
}
|
|
635
635
|
|
|
636
|
-
/**
|
|
637
|
-
* Resolve the path to this extension file (task-runner.ts).
|
|
638
|
-
* Used to pass the extension to worker subprocesses so they have access
|
|
639
|
-
* to the review_step tool in orchestrated mode.
|
|
640
|
-
*
|
|
641
|
-
* Resolution strategy:
|
|
642
|
-
* 1. Derive from -e argument that loaded this extension
|
|
643
|
-
* 2. Package root + extensions/task-runner.ts
|
|
644
|
-
* 3. cwd/extensions/task-runner.ts (development fallback)
|
|
645
|
-
*
|
|
646
|
-
* Returns null if the extension path cannot be found (non-fatal — worker
|
|
647
|
-
* runs without review_step tool).
|
|
648
|
-
*/
|
|
649
|
-
function resolveExtensionPath(): string | null {
|
|
650
|
-
const extRelPath = join("extensions", "task-runner.ts");
|
|
651
|
-
|
|
652
|
-
// 1. Derive from the -e argument that loaded this file
|
|
653
|
-
try {
|
|
654
|
-
const args = process.argv;
|
|
655
|
-
for (let i = 0; i < args.length - 1; i++) {
|
|
656
|
-
if (args[i] === "-e" && args[i + 1]?.includes("task-runner")) {
|
|
657
|
-
const extPath = resolve(args[i + 1]);
|
|
658
|
-
if (existsSync(extPath)) return extPath;
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
} catch { /* ignore argv parsing errors */ }
|
|
662
|
-
|
|
663
|
-
// 2. Package root
|
|
664
|
-
const root = findPackageRoot();
|
|
665
|
-
if (root) {
|
|
666
|
-
const p = join(root, extRelPath);
|
|
667
|
-
if (existsSync(p)) return p;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
// 3. Development fallback
|
|
671
|
-
const devPath = join(process.cwd(), extRelPath);
|
|
672
|
-
if (existsSync(devPath)) return devPath;
|
|
673
|
-
|
|
674
|
-
return null;
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
/**
|
|
678
|
-
* Detect whether this extension instance is running inside a worker subprocess
|
|
679
|
-
* (set via TASK_RUNNER_WORKER_TOOL_MODE env var). When true, the extension only
|
|
680
|
-
* registers the review_step tool — no commands, widgets, or auto-start.
|
|
681
|
-
*/
|
|
682
|
-
function isWorkerToolMode(): boolean {
|
|
683
|
-
return process.env.TASK_RUNNER_WORKER_TOOL_MODE === "1";
|
|
684
|
-
}
|
|
685
|
-
|
|
686
636
|
/**
|
|
687
637
|
* Load an agent definition with prompt inheritance.
|
|
688
638
|
*
|
|
@@ -2080,6 +2030,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2080
2030
|
"Call review_step at step boundaries based on the task's Review Level (from STATUS.md header).",
|
|
2081
2031
|
"Review Level 0: skip all reviews. Level 1: plan review before implementing. Level 2: plan + code review. Level 3: plan + code + test review.",
|
|
2082
2032
|
"Skip reviews for Step 0 (Preflight) and the final documentation/delivery step.",
|
|
2033
|
+
"For code reviews: before starting a step, capture the current HEAD commit with `git rev-parse HEAD` and pass it as the `baseline` parameter. This lets the reviewer see only that step's changes.",
|
|
2083
2034
|
"On REVISE: read the review file in .reviews/ for detailed feedback, address the issues, commit fixes, then proceed.",
|
|
2084
2035
|
"On RETHINK: reconsider your plan approach, adjust, then implement.",
|
|
2085
2036
|
"On UNAVAILABLE: reviewer failed — proceed with caution.",
|
|
@@ -2090,9 +2041,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
2090
2041
|
[Type.Literal("plan"), Type.Literal("code")],
|
|
2091
2042
|
{ description: 'Review type: "plan" or "code"' },
|
|
2092
2043
|
),
|
|
2044
|
+
baseline: Type.Optional(Type.String({
|
|
2045
|
+
description: "Git commit SHA to use as the diff baseline for code reviews. " +
|
|
2046
|
+
"Capture HEAD before starting a step and pass it here so the reviewer " +
|
|
2047
|
+
"sees only that step's changes. If omitted, the reviewer sees the full diff against HEAD.",
|
|
2048
|
+
})),
|
|
2093
2049
|
}),
|
|
2094
2050
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
2095
|
-
const { step: stepNum, type: reviewType } = params;
|
|
2051
|
+
const { step: stepNum, type: reviewType, baseline } = params;
|
|
2096
2052
|
|
|
2097
2053
|
if (!state.task || !state.config) {
|
|
2098
2054
|
return {
|
|
@@ -2123,11 +2079,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
2123
2079
|
const requestPath = join(reviewsDir, `request-R${num}.md`);
|
|
2124
2080
|
const outputPath = join(reviewsDir, `R${num}-${reviewType}-step${stepNum}.md`);
|
|
2125
2081
|
|
|
2126
|
-
//
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2082
|
+
// Resolve step baseline commit for code reviews.
|
|
2083
|
+
// The worker should pass the pre-step HEAD SHA as `baseline` so the
|
|
2084
|
+
// reviewer sees only this step's changes (not cumulative diff).
|
|
2085
|
+
// Falls back to undefined (full diff) if baseline is not provided.
|
|
2086
|
+
const stepBaselineCommit: string | undefined =
|
|
2087
|
+
reviewType === "code" ? (baseline || undefined) : undefined;
|
|
2131
2088
|
|
|
2132
2089
|
// Find step info for the name
|
|
2133
2090
|
const stepInfo = task.steps.find(s => s.number === stepNum);
|
|
@@ -2143,7 +2100,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2143
2100
|
const reviewerDef = loadAgentDef(ctx.cwd, "task-reviewer");
|
|
2144
2101
|
const reviewerModel = config.reviewer.model
|
|
2145
2102
|
|| reviewerDef?.model
|
|
2146
|
-
|| "
|
|
2103
|
+
|| (ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "anthropic/claude-sonnet-4-20250514");
|
|
2147
2104
|
const reviewerPrompt = reviewerDef?.systemPrompt
|
|
2148
2105
|
|| "You are a code reviewer. Read the request and write your review to the specified output file.";
|
|
2149
2106
|
const systemPrompt = reviewerPrompt + "\n\n" + buildProjectContext(config, task.taskFolder);
|
|
@@ -2947,7 +2904,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2947
2904
|
writeFileSync(requestPath, request);
|
|
2948
2905
|
|
|
2949
2906
|
const reviewerDef = loadAgentDef(ctx.cwd, "task-reviewer");
|
|
2950
|
-
const reviewerModel = config.reviewer.model || reviewerDef?.model || "
|
|
2907
|
+
const reviewerModel = config.reviewer.model || reviewerDef?.model || (ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "anthropic/claude-sonnet-4-20250514");
|
|
2951
2908
|
const reviewerPrompt = reviewerDef?.systemPrompt || "You are a code reviewer. Read the request and write your review to the specified output file.";
|
|
2952
2909
|
const systemPrompt = reviewerPrompt + "\n\n" + buildProjectContext(config, task.taskFolder);
|
|
2953
2910
|
|
|
@@ -3086,7 +3043,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3086
3043
|
const reviewModel = config.quality_gate.review_model
|
|
3087
3044
|
|| config.reviewer.model
|
|
3088
3045
|
|| reviewerDef?.model
|
|
3089
|
-
|| "
|
|
3046
|
+
|| (ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "anthropic/claude-sonnet-4-20250514");
|
|
3090
3047
|
|
|
3091
3048
|
const reviewerPrompt = reviewerDef?.systemPrompt
|
|
3092
3049
|
|| "You are a quality gate reviewer. Read the review request and write your JSON verdict to the specified file.";
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
computeWaveAssignments,
|
|
16
16
|
createOrchWidget,
|
|
17
17
|
deleteBatchState,
|
|
18
|
+
deleteStaleBranches,
|
|
18
19
|
detectOrphanSessions,
|
|
19
20
|
executeLane,
|
|
20
21
|
executeOrchBatch,
|
|
@@ -407,6 +408,10 @@ export function executeIntegration(
|
|
|
407
408
|
}
|
|
408
409
|
|
|
409
410
|
if (!result.ok) {
|
|
411
|
+
// TP-052: Include branch protection hint when merge fails
|
|
412
|
+
const protectionHint = result.stderr.includes("protected") || result.stderr.includes("permission")
|
|
413
|
+
? `\n\n 💡 If the branch is protected, use --pr to create a pull request.`
|
|
414
|
+
: "";
|
|
410
415
|
return {
|
|
411
416
|
success: false,
|
|
412
417
|
integratedLocally: false,
|
|
@@ -417,7 +422,8 @@ export function executeIntegration(
|
|
|
417
422
|
`${result.stderr}\n\n` +
|
|
418
423
|
`Try:\n` +
|
|
419
424
|
` /orch-integrate --merge Create a merge commit\n` +
|
|
420
|
-
` /orch-integrate --pr Create a pull request instead
|
|
425
|
+
` /orch-integrate --pr Create a pull request instead` +
|
|
426
|
+
protectionHint,
|
|
421
427
|
};
|
|
422
428
|
}
|
|
423
429
|
// Count commits that were applied
|
|
@@ -451,6 +457,10 @@ export function executeIntegration(
|
|
|
451
457
|
}
|
|
452
458
|
|
|
453
459
|
if (!result.ok) {
|
|
460
|
+
// TP-052: Include branch protection hint when merge fails
|
|
461
|
+
const mergeProtectionHint = result.stderr.includes("protected") || result.stderr.includes("permission")
|
|
462
|
+
? `\n\n 💡 If the branch is protected, use --pr to create a pull request.`
|
|
463
|
+
: "";
|
|
454
464
|
return {
|
|
455
465
|
success: false,
|
|
456
466
|
integratedLocally: false,
|
|
@@ -460,7 +470,8 @@ export function executeIntegration(
|
|
|
460
470
|
`❌ Merge failed — there may be conflicts.\n` +
|
|
461
471
|
`${result.stderr}\n\n` +
|
|
462
472
|
`Resolve conflicts manually, or try:\n` +
|
|
463
|
-
` /orch-integrate --pr Create a pull request instead
|
|
473
|
+
` /orch-integrate --pr Create a pull request instead` +
|
|
474
|
+
mergeProtectionHint,
|
|
464
475
|
};
|
|
465
476
|
}
|
|
466
477
|
return performCleanup(deps, orchBranch, {
|
|
@@ -644,7 +655,7 @@ export function collectRepoCleanupFindings(
|
|
|
644
655
|
findings.staleWorktrees = wts.map(wt => wt.path);
|
|
645
656
|
} catch { /* best effort — git worktree list may fail in unusual states */ }
|
|
646
657
|
|
|
647
|
-
// 2. Lane branches — task/{opId}-lane-*
|
|
658
|
+
// 2. Lane branches — task/{opId}-lane-* and saved/task/{opId}-lane-*
|
|
648
659
|
try {
|
|
649
660
|
const branchResult = runGit(["branch", "--list", `task/${opId}-lane-*`], repoRoot);
|
|
650
661
|
if (branchResult.ok && branchResult.stdout.trim()) {
|
|
@@ -653,6 +664,15 @@ export function collectRepoCleanupFindings(
|
|
|
653
664
|
.map(b => b.replace(/^\*?\s+/, "").trim())
|
|
654
665
|
.filter(Boolean);
|
|
655
666
|
}
|
|
667
|
+
// Also detect saved lane branches (preserved refs from worktree removal)
|
|
668
|
+
const savedBranchResult = runGit(["branch", "--list", `saved/task/${opId}-lane-*`], repoRoot);
|
|
669
|
+
if (savedBranchResult.ok && savedBranchResult.stdout.trim()) {
|
|
670
|
+
const savedBranches = savedBranchResult.stdout
|
|
671
|
+
.split("\n")
|
|
672
|
+
.map(b => b.replace(/^\*?\s+/, "").trim())
|
|
673
|
+
.filter(Boolean);
|
|
674
|
+
findings.staleLaneBranches.push(...savedBranches);
|
|
675
|
+
}
|
|
656
676
|
} catch { /* best effort */ }
|
|
657
677
|
|
|
658
678
|
// 3. Orch branch — check if the specific orch branch still exists
|
|
@@ -899,7 +919,7 @@ export function startBatchAsync(
|
|
|
899
919
|
*
|
|
900
920
|
* @since TP-043 R002
|
|
901
921
|
*/
|
|
902
|
-
export function buildIntegrationExecutor(repoRoot: string): IntegrationExecutor {
|
|
922
|
+
export function buildIntegrationExecutor(repoRoot: string, opId?: string): IntegrationExecutor {
|
|
903
923
|
return (mode, context) => {
|
|
904
924
|
// Ensure we're on the base branch before integrating
|
|
905
925
|
const currentBranch = getCurrentBranch(repoRoot);
|
|
@@ -942,10 +962,22 @@ export function buildIntegrationExecutor(repoRoot: string): IntegrationExecutor
|
|
|
942
962
|
},
|
|
943
963
|
};
|
|
944
964
|
|
|
945
|
-
|
|
965
|
+
const result = executeIntegration(mode as IntegrateMode, {
|
|
946
966
|
...context,
|
|
947
967
|
currentBranch: context.baseBranch,
|
|
948
968
|
}, deps);
|
|
969
|
+
|
|
970
|
+
// TP-051: Clean up stale task/* and saved/* branches after successful integration.
|
|
971
|
+
// This ensures auto-mode integration (supervisor path) gets the same cleanup
|
|
972
|
+
// as the manual /orch-integrate handler.
|
|
973
|
+
if (result.success && result.integratedLocally && context.batchId && opId) {
|
|
974
|
+
try {
|
|
975
|
+
deleteStaleBranches(repoRoot, opId, context.batchId);
|
|
976
|
+
dropBatchAutostash(repoRoot, context.batchId);
|
|
977
|
+
} catch { /* best effort — don't fail integration for cleanup errors */ }
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
return result;
|
|
949
981
|
};
|
|
950
982
|
}
|
|
951
983
|
|
|
@@ -1512,7 +1544,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1512
1544
|
orchBatchState,
|
|
1513
1545
|
mode,
|
|
1514
1546
|
repoRoot,
|
|
1515
|
-
buildIntegrationExecutor(repoRoot),
|
|
1547
|
+
buildIntegrationExecutor(repoRoot, opId),
|
|
1516
1548
|
buildCiDeps(repoRoot),
|
|
1517
1549
|
sDeps,
|
|
1518
1550
|
);
|
|
@@ -1858,7 +1890,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1858
1890
|
orchBatchState,
|
|
1859
1891
|
mode,
|
|
1860
1892
|
execCtx!.repoRoot,
|
|
1861
|
-
buildIntegrationExecutor(execCtx!.repoRoot),
|
|
1893
|
+
buildIntegrationExecutor(execCtx!.repoRoot, opId),
|
|
1862
1894
|
buildCiDeps(execCtx!.repoRoot),
|
|
1863
1895
|
sDeps,
|
|
1864
1896
|
);
|
|
@@ -1893,12 +1925,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
1893
1925
|
`Batch **${orchBatchState.batchId}** completed — ` +
|
|
1894
1926
|
`${orchBatchState.succeededTasks}/${orchBatchState.totalTasks} tasks succeeded.\n\n` +
|
|
1895
1927
|
`The orch branch \`${orchBatchState.orchBranch}\` is ready to integrate.\n` +
|
|
1896
|
-
`Would you like me to integrate it, or would you prefer to review first
|
|
1928
|
+
`Would you like me to integrate it, or would you prefer to review first?\n\n` +
|
|
1929
|
+
`You can also:\n` +
|
|
1930
|
+
`• Run \`/orch-integrate\` (or \`/orch-integrate --pr\`) to integrate\n` +
|
|
1931
|
+
`• Create new tasks for the next batch\n` +
|
|
1932
|
+
`• Run a health check`,
|
|
1897
1933
|
}
|
|
1898
1934
|
: {
|
|
1899
1935
|
routingState: "no-tasks",
|
|
1900
1936
|
contextMessage:
|
|
1901
1937
|
`Batch **${orchBatchState.batchId}** ended (${orchBatchState.phase}).\n\n` +
|
|
1938
|
+
`${orchBatchState.succeededTasks} succeeded, ${orchBatchState.failedTasks} failed, ` +
|
|
1939
|
+
`${orchBatchState.skippedTasks} skipped.\n\n` +
|
|
1902
1940
|
`What would you like to do next?`,
|
|
1903
1941
|
};
|
|
1904
1942
|
transitionToRoutingMode(pi, supervisorState, postBatchContext);
|
|
@@ -2352,7 +2390,26 @@ export default function (pi: ExtensionAPI) {
|
|
|
2352
2390
|
ctx.ui.notify(notice, "info");
|
|
2353
2391
|
}
|
|
2354
2392
|
|
|
2355
|
-
// ── Step
|
|
2393
|
+
// ── Step 2a: Branch protection pre-check (TP-052) ───────
|
|
2394
|
+
// When using ff or merge mode (direct push), check if the target
|
|
2395
|
+
// branch has protection rules. If protected, warn and suggest --pr.
|
|
2396
|
+
// Graceful degradation: if gh is unavailable, skip the check.
|
|
2397
|
+
if (parsed.mode !== "pr") {
|
|
2398
|
+
const { detectBranchProtection } = await import("./supervisor.ts");
|
|
2399
|
+
const protectionStatus = detectBranchProtection(baseBranch, repoRoot);
|
|
2400
|
+
if (protectionStatus === "protected") {
|
|
2401
|
+
ctx.ui.notify(
|
|
2402
|
+
`⚠️ Branch \`${baseBranch}\` has branch protection rules enabled.\n` +
|
|
2403
|
+
`Direct merges may be blocked by your repository settings.\n\n` +
|
|
2404
|
+
`Recommended: use \`/orch-integrate --pr\` to create a pull request instead.`,
|
|
2405
|
+
"warning",
|
|
2406
|
+
);
|
|
2407
|
+
// Don't block — proceed with the attempt. The merge will fail
|
|
2408
|
+
// gracefully and show a clear error if protection blocks it.
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
// ── Step 2b: Pre-integration summary ─────────────────────
|
|
2356
2413
|
// Count commits ahead
|
|
2357
2414
|
const revListResult = runGit(
|
|
2358
2415
|
["rev-list", "--count", `${currentBranch}..${orchBranch}`],
|
|
@@ -2469,6 +2526,30 @@ export default function (pi: ExtensionAPI) {
|
|
|
2469
2526
|
dropBatchAutostash(repo.root, batchId);
|
|
2470
2527
|
}
|
|
2471
2528
|
|
|
2529
|
+
// TP-051: Delete stale task/* and saved/task/* branches from all repos.
|
|
2530
|
+
// These accumulate after each batch and clutter `git branch` output.
|
|
2531
|
+
// Deletes both current-batch branches and orphans from previous batches.
|
|
2532
|
+
const branchCleanupLines: string[] = [];
|
|
2533
|
+
for (const repo of allRepos) {
|
|
2534
|
+
const branchCleanup = deleteStaleBranches(repo.root, opId, batchId);
|
|
2535
|
+
const totalDeleted = branchCleanup.deletedTaskBranches.length + branchCleanup.deletedSavedBranches.length;
|
|
2536
|
+
if (totalDeleted > 0 || branchCleanup.failedDeletes.length > 0) {
|
|
2537
|
+
const label = repo.id === "(default)" ? "" : ` (${repo.id})`;
|
|
2538
|
+
if (branchCleanup.deletedTaskBranches.length > 0) {
|
|
2539
|
+
branchCleanupLines.push(` 🗑️ Deleted ${branchCleanup.deletedTaskBranches.length} task branch(es)${label}`);
|
|
2540
|
+
}
|
|
2541
|
+
if (branchCleanup.deletedSavedBranches.length > 0) {
|
|
2542
|
+
branchCleanupLines.push(` 🗑️ Deleted ${branchCleanup.deletedSavedBranches.length} saved branch(es)${label}`);
|
|
2543
|
+
}
|
|
2544
|
+
if (branchCleanup.failedDeletes.length > 0) {
|
|
2545
|
+
branchCleanupLines.push(` ⚠️ Failed to delete ${branchCleanup.failedDeletes.length} branch(es)${label}: ${branchCleanup.failedDeletes.join(", ")}`);
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
if (branchCleanupLines.length > 0) {
|
|
2550
|
+
ctx.ui.notify("Branch cleanup:\n" + branchCleanupLines.join("\n"), "info");
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2472
2553
|
// Run acceptance checks across all workspace repos.
|
|
2473
2554
|
// In PR mode, the orch branch is intentionally preserved for the PR,
|
|
2474
2555
|
// so we skip orch branch detection to avoid contradictory output.
|
|
@@ -51,15 +51,18 @@ export const ORCH_MESSAGES = {
|
|
|
51
51
|
}
|
|
52
52
|
if (orchBranch && succeeded > 0) {
|
|
53
53
|
lines.push("");
|
|
54
|
-
lines.push(
|
|
55
|
-
lines.push(` Your
|
|
54
|
+
lines.push(" ┌─────────────────────────────────────────────────┐");
|
|
55
|
+
lines.push(` │ Your changes are on branch: ${orchBranch}`);
|
|
56
|
+
lines.push(` │ Your ${baseBranch || "working"} branch was not modified.`);
|
|
56
57
|
if (baseBranch) {
|
|
57
|
-
lines.push(` Preview: git log ${baseBranch}..${orchBranch}`);
|
|
58
|
+
lines.push(` │ Preview: git log ${baseBranch}..${orchBranch}`);
|
|
58
59
|
}
|
|
59
|
-
lines.push("");
|
|
60
|
-
lines.push(" To
|
|
61
|
-
lines.push("
|
|
62
|
-
lines.push("
|
|
60
|
+
lines.push(" │");
|
|
61
|
+
lines.push(" │ 👉 To bring changes into your working branch:");
|
|
62
|
+
lines.push(" │");
|
|
63
|
+
lines.push(" │ /orch-integrate — merge directly (recommended)");
|
|
64
|
+
lines.push(" │ /orch-integrate --pr — create a pull request");
|
|
65
|
+
lines.push(" └─────────────────────────────────────────────────┘");
|
|
63
66
|
}
|
|
64
67
|
return lines.join("\n");
|
|
65
68
|
},
|
|
@@ -219,10 +219,14 @@ export function syncTaskOutcomesFromMonitor(
|
|
|
219
219
|
const mappedStatus = monitorToLane[snap.status];
|
|
220
220
|
const terminal = mappedStatus === "succeeded" || mappedStatus === "failed" || mappedStatus === "stalled" || mappedStatus === "skipped";
|
|
221
221
|
|
|
222
|
+
// TP-051: Use snap.observedAt (Date.now() from monitor poll) instead of
|
|
223
|
+
// snap.lastHeartbeat (STATUS.md mtime) for task start time. The mtime
|
|
224
|
+
// reflects when STATUS.md was last edited, which may be long before
|
|
225
|
+
// actual execution started (e.g., during task staging).
|
|
222
226
|
changed = upsertTaskOutcome(outcomes, {
|
|
223
227
|
taskId: lane.currentTaskId,
|
|
224
228
|
status: mappedStatus,
|
|
225
|
-
startTime: existing?.startTime ?? snap.
|
|
229
|
+
startTime: existing?.startTime ?? snap.observedAt,
|
|
226
230
|
endTime: terminal ? (existing?.endTime ?? snap.observedAt) : null,
|
|
227
231
|
exitReason: existing?.exitReason || (mappedStatus === "running" ? "Task in progress" : (snap.stallReason || "Task reached terminal state")),
|
|
228
232
|
sessionName: existing?.sessionName || lane.sessionName,
|
|
@@ -2615,15 +2615,19 @@ export async function transitionToRoutingMode(
|
|
|
2615
2615
|
// Keep batchStateRef/orchConfigRef/stateRoot — routing prompt may need them
|
|
2616
2616
|
// Keep model override — don't switch models mid-conversation
|
|
2617
2617
|
|
|
2618
|
-
//
|
|
2618
|
+
// TP-052: Send a prominent conversational message that clearly signals
|
|
2619
|
+
// the supervisor is ready for input. Uses triggerTurn to force an LLM
|
|
2620
|
+
// response, which ensures the pi TUI redraws and shows the input prompt.
|
|
2619
2621
|
pi.sendMessage(
|
|
2620
2622
|
{
|
|
2621
2623
|
customType: "supervisor-routing-transition",
|
|
2622
2624
|
content: [{
|
|
2623
2625
|
type: "text",
|
|
2624
2626
|
text:
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n` +
|
|
2628
|
+
`🔀 **Ready for your input.**\n\n` +
|
|
2629
|
+
routingContext.contextMessage +
|
|
2630
|
+
`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`,
|
|
2627
2631
|
}],
|
|
2628
2632
|
display: `Supervisor — ${routingContext.routingState}`,
|
|
2629
2633
|
},
|
|
@@ -2323,3 +2323,127 @@ export function preserveFailedLaneProgress(
|
|
|
2323
2323
|
return { results, preservedBranches, unsafeBranches };
|
|
2324
2324
|
}
|
|
2325
2325
|
|
|
2326
|
+
|
|
2327
|
+
// ── Stale Branch Cleanup (TP-051) ────────────────────────────────────
|
|
2328
|
+
|
|
2329
|
+
/**
|
|
2330
|
+
* Result of stale branch cleanup after integration.
|
|
2331
|
+
*/
|
|
2332
|
+
export interface StaleBranchCleanupResult {
|
|
2333
|
+
/** task/* branches deleted */
|
|
2334
|
+
deletedTaskBranches: string[];
|
|
2335
|
+
/** saved/task/* branches deleted */
|
|
2336
|
+
deletedSavedBranches: string[];
|
|
2337
|
+
/** Branches that failed to delete (best-effort) */
|
|
2338
|
+
failedDeletes: string[];
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
/**
|
|
2342
|
+
* Delete stale task/* and saved/* branches after integration.
|
|
2343
|
+
*
|
|
2344
|
+
* After `/orch-integrate` merges or creates a PR, the lane branches
|
|
2345
|
+
* (`task/{opId}-lane-{N}-{batchId}`) and their saved counterparts
|
|
2346
|
+
* are no longer needed. This function cleans them up.
|
|
2347
|
+
*
|
|
2348
|
+
* Cleanup scope:
|
|
2349
|
+
* 1. **Lane branches:** `task/{opId}-lane-*` (any batch from this operator)
|
|
2350
|
+
* 2. **Saved lane branches:** `saved/task/{opId}-lane-*` (preserved lane refs)
|
|
2351
|
+
* 3. **Partial-progress branches:** `saved/{opId}-*` (per-task partial progress refs)
|
|
2352
|
+
*
|
|
2353
|
+
* Targets all branches matching the operator's prefix, not just the current
|
|
2354
|
+
* batch — this also cleans up orphans from previous batches that were never
|
|
2355
|
+
* cleaned.
|
|
2356
|
+
*
|
|
2357
|
+
* All deletions are best-effort — individual failures are logged but don't
|
|
2358
|
+
* prevent other branches from being cleaned.
|
|
2359
|
+
*
|
|
2360
|
+
* @param repoRoot - Repository root directory
|
|
2361
|
+
* @param opId - Operator identifier (e.g., "henrylach")
|
|
2362
|
+
* @param batchId - Current batch ID (for logging context)
|
|
2363
|
+
* @returns Cleanup result with lists of deleted and failed branches
|
|
2364
|
+
*/
|
|
2365
|
+
export function deleteStaleBranches(
|
|
2366
|
+
repoRoot: string,
|
|
2367
|
+
opId: string,
|
|
2368
|
+
batchId: string,
|
|
2369
|
+
): StaleBranchCleanupResult {
|
|
2370
|
+
const deletedTaskBranches: string[] = [];
|
|
2371
|
+
const deletedSavedBranches: string[] = [];
|
|
2372
|
+
const failedDeletes: string[] = [];
|
|
2373
|
+
|
|
2374
|
+
// 1. Delete task/{opId}-lane-* branches
|
|
2375
|
+
const taskBranchResult = runGit(["branch", "--list", `task/${opId}-lane-*`], repoRoot);
|
|
2376
|
+
if (taskBranchResult.ok && taskBranchResult.stdout.trim()) {
|
|
2377
|
+
const branches = taskBranchResult.stdout
|
|
2378
|
+
.split("\n")
|
|
2379
|
+
.map(b => b.replace(/^\*?\s+/, "").trim())
|
|
2380
|
+
.filter(Boolean);
|
|
2381
|
+
|
|
2382
|
+
for (const branch of branches) {
|
|
2383
|
+
const deleted = deleteBranchBestEffort(branch, repoRoot);
|
|
2384
|
+
if (deleted) {
|
|
2385
|
+
deletedTaskBranches.push(branch);
|
|
2386
|
+
} else {
|
|
2387
|
+
failedDeletes.push(branch);
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
// 2. Delete saved/task/{opId}-lane-* branches (preserved lane refs)
|
|
2393
|
+
const savedTaskResult = runGit(["branch", "--list", `saved/task/${opId}-lane-*`], repoRoot);
|
|
2394
|
+
if (savedTaskResult.ok && savedTaskResult.stdout.trim()) {
|
|
2395
|
+
const branches = savedTaskResult.stdout
|
|
2396
|
+
.split("\n")
|
|
2397
|
+
.map(b => b.replace(/^\*?\s+/, "").trim())
|
|
2398
|
+
.filter(Boolean);
|
|
2399
|
+
|
|
2400
|
+
for (const branch of branches) {
|
|
2401
|
+
const deleted = deleteBranchBestEffort(branch, repoRoot);
|
|
2402
|
+
if (deleted) {
|
|
2403
|
+
deletedSavedBranches.push(branch);
|
|
2404
|
+
} else {
|
|
2405
|
+
failedDeletes.push(branch);
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
// 3. Delete saved/{opId}-*-{batchId} branches (partial-progress refs from this batch)
|
|
2411
|
+
// Pattern: saved/{opId}-{taskId}-{batchId} or saved/{opId}-{repoId}-{taskId}-{batchId}
|
|
2412
|
+
// Only deletes branches ending with the current batchId to avoid removing
|
|
2413
|
+
// partial-progress refs from other batches that the operator may still need.
|
|
2414
|
+
const savedProgressResult = runGit(["branch", "--list", `saved/${opId}-*`], repoRoot);
|
|
2415
|
+
if (savedProgressResult.ok && savedProgressResult.stdout.trim()) {
|
|
2416
|
+
const branches = savedProgressResult.stdout
|
|
2417
|
+
.split("\n")
|
|
2418
|
+
.map(b => b.replace(/^\*?\s+/, "").trim())
|
|
2419
|
+
.filter(Boolean);
|
|
2420
|
+
|
|
2421
|
+
const batchSuffix = `-${batchId}`;
|
|
2422
|
+
for (const branch of branches) {
|
|
2423
|
+
// Avoid double-deleting saved/task/* already handled above
|
|
2424
|
+
if (branch.startsWith("saved/task/")) continue;
|
|
2425
|
+
// Only delete partial-progress refs from the current batch
|
|
2426
|
+
if (!branch.endsWith(batchSuffix)) continue;
|
|
2427
|
+
const deleted = deleteBranchBestEffort(branch, repoRoot);
|
|
2428
|
+
if (deleted) {
|
|
2429
|
+
deletedSavedBranches.push(branch);
|
|
2430
|
+
} else {
|
|
2431
|
+
failedDeletes.push(branch);
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
const totalDeleted = deletedTaskBranches.length + deletedSavedBranches.length;
|
|
2437
|
+
if (totalDeleted > 0) {
|
|
2438
|
+
execLog("cleanup", "branches", `deleted ${totalDeleted} stale branch(es) for batch ${batchId}`, {
|
|
2439
|
+
taskBranches: deletedTaskBranches.length,
|
|
2440
|
+
savedBranches: deletedSavedBranches.length,
|
|
2441
|
+
failed: failedDeletes.length,
|
|
2442
|
+
});
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
return { deletedTaskBranches, deletedSavedBranches, failedDeletes };
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
|
|
2449
|
+
|
package/package.json
CHANGED
|
@@ -187,11 +187,12 @@ value.
|
|
|
187
187
|
**Example flow for a Review Level 2 task, Step 3:**
|
|
188
188
|
1. Read Step 3 requirements
|
|
189
189
|
2. Call `review_step(step=3, type="plan")` → get plan feedback
|
|
190
|
-
3.
|
|
191
|
-
4.
|
|
192
|
-
5.
|
|
193
|
-
6.
|
|
194
|
-
7.
|
|
190
|
+
3. Capture baseline: run `git rev-parse HEAD` and save the SHA
|
|
191
|
+
4. Implement Step 3
|
|
192
|
+
5. Commit changes
|
|
193
|
+
6. Call `review_step(step=3, type="code", baseline="<saved SHA>")` → get code feedback
|
|
194
|
+
7. If REVISE: fix issues, commit again
|
|
195
|
+
8. Move to Step 4
|
|
195
196
|
|
|
196
197
|
If the `review_step` tool is not available (e.g., non-orchestrated mode), skip
|
|
197
198
|
this protocol entirely — the task-runner handles reviews externally.
|