Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 19 additions & 7 deletions extensions/taskplane/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -817,18 +817,30 @@ export async function executeOrchBatch(
batchState.skippedTasks,
batchState.blockedTasks,
totalElapsedSec,
batchState.orchBranch,
batchState.baseBranch,
),
batchState.failedTasks > 0 ? "warning" : "info",
);

// ── TS-009: Delete state file on clean completion (no failures) ──
// ── Preserve state for /orch-integrate when orch branch exists ──
// If integration is "manual" and we have an orch branch, keep the
// state file so /orch-integrate can find orchBranch and baseBranch.
// Only delete state if there's no orch branch to integrate.
if (batchState.phase === "completed") {
try {
deleteBatchState(stateRoot);
execLog("state", batchState.batchId, "state file deleted on clean completion");
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
execLog("state", batchState.batchId, `failed to delete state file: ${msg}`);
if (batchState.orchBranch) {
execLog("state", batchState.batchId, "state file preserved for /orch-integrate", {
orchBranch: batchState.orchBranch,
});
} else {
// Legacy mode (no orch branch) — clean up state
try {
deleteBatchState(stateRoot);
execLog("state", batchState.batchId, "state file deleted on clean completion");
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
execLog("state", batchState.batchId, `failed to delete state file: ${msg}`);
}
}
}
}
Expand Down
49 changes: 48 additions & 1 deletion extensions/taskplane/execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,53 @@ import type { AllocatedLane, AllocatedTask, DependencyGraph, LaneExecutionResult
import { allocateLanes } from "./waves.ts";
import { runGit } from "./git.ts";

// ── Task Runner Extension Path Resolution ────────────────────────────

/**
* Find the task-runner extension path for lane sessions.
*
* Resolution order:
* 1. Local project: {repoRoot}/extensions/task-runner.ts (for taskplane dev)
* 2. Global npm (Windows): {APPDATA}/npm/node_modules/taskplane/extensions/task-runner.ts
* 3. Global npm (Unix): /usr/local/lib/node_modules/taskplane/extensions/task-runner.ts
* 4. npm peer: resolve from pi's location
*
* @throws ExecutionError if task-runner.ts cannot be found anywhere
*/
function resolveTaskRunnerExtensionPath(repoRoot: string): string {
const extFile = join("extensions", "task-runner.ts");

// 1. Local project (taskplane development)
const localPath = join(resolve(repoRoot), extFile);
if (existsSync(localPath)) return localPath;

// 2. Global npm install paths
const home = process.env.HOME || process.env.USERPROFILE || "";
const candidates: string[] = [];
if (process.env.APPDATA) {
candidates.push(join(process.env.APPDATA, "npm", "node_modules", "taskplane", extFile));
}
if (home) {
candidates.push(join(home, "AppData", "Roaming", "npm", "node_modules", "taskplane", extFile));
candidates.push(join(home, ".npm-global", "lib", "node_modules", "taskplane", extFile));
}
candidates.push(join("/usr", "local", "lib", "node_modules", "taskplane", extFile));

// 3. Peer of pi's package
try {
const piPath = process.argv[1] || "";
const piPkgDir = resolve(piPath, "..", "..");
candidates.push(join(piPkgDir, "..", "taskplane", extFile));
} catch { /* ignore */ }

for (const candidate of candidates) {
if (existsSync(candidate)) return candidate;
}

// Fallback: return the local path (will fail at spawn time with a clear error)
return localPath;
}

// ── Execution Helpers ────────────────────────────────────────────────

/**
Expand Down Expand Up @@ -206,7 +253,7 @@ export function buildTmuxSpawnArgs(
.map(([key, val]) => `${key}=${shellQuote(val)}`)
.join(" ");

const taskRunnerExtPath = join(resolve(repoRoot), "extensions", "task-runner.ts");
const taskRunnerExtPath = resolveTaskRunnerExtensionPath(repoRoot);
const basePiCommand = `${envParts} pi --no-session -e ${shellQuote(taskRunnerExtPath)}`;

// NOTE: Do not redirect lane output here. Shell redirection has proven
Expand Down
13 changes: 12 additions & 1 deletion extensions/taskplane/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const ORCH_MESSAGES = {
`🔀 [Wave ${waveNum}] Merge: placeholder — Step 3 (TS-008) will replace with mergeWave()`,
orchWorktreeReset: (waveNum: number, lanes: number) =>
`🔄 Resetting ${lanes} worktree(s) to target branch HEAD after wave ${waveNum}`,
orchBatchComplete: (batchId: string, succeeded: number, failed: number, skipped: number, blocked: number, elapsedSec: number) => {
orchBatchComplete: (batchId: string, succeeded: number, failed: number, skipped: number, blocked: number, elapsedSec: number, orchBranch?: string, baseBranch?: string) => {
const lines = [`\n🏁 Batch ${batchId} complete: ${succeeded} succeeded, ${failed} failed, ${skipped} skipped, ${blocked} blocked (${elapsedSec}s)`];
if (failed > 0 || blocked > 0) {
lines.push("");
Expand All @@ -48,6 +48,17 @@ export const ORCH_MESSAGES = {
lines.push(" • /orch-resume — retry from the failed wave");
lines.push(" • /orch-abort — clean up and start fresh");
}
if (orchBranch && succeeded > 0) {
lines.push("");
lines.push(` ℹ Orch branch: ${orchBranch}`);
if (baseBranch) {
lines.push(` Review changes: git log ${baseBranch}..${orchBranch}`);
}
lines.push(" Next steps:");
lines.push(" • /orch-integrate — fast-forward into your branch");
lines.push(" • /orch-integrate --merge — merge (if branches diverged)");
lines.push(" • /orch-integrate --pr — push and open a PR");
}
return lines.join("\n");
},
orchBatchFailed: (batchId: string, reason: string) =>
Expand Down