taskplane 0.5.0 → 0.5.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.
@@ -817,18 +817,30 @@ export async function executeOrchBatch(
817
817
  batchState.skippedTasks,
818
818
  batchState.blockedTasks,
819
819
  totalElapsedSec,
820
+ batchState.orchBranch,
821
+ batchState.baseBranch,
820
822
  ),
821
823
  batchState.failedTasks > 0 ? "warning" : "info",
822
824
  );
823
825
 
824
- // ── TS-009: Delete state file on clean completion (no failures) ──
826
+ // ── Preserve state for /orch-integrate when orch branch exists ──
827
+ // If integration is "manual" and we have an orch branch, keep the
828
+ // state file so /orch-integrate can find orchBranch and baseBranch.
829
+ // Only delete state if there's no orch branch to integrate.
825
830
  if (batchState.phase === "completed") {
826
- try {
827
- deleteBatchState(stateRoot);
828
- execLog("state", batchState.batchId, "state file deleted on clean completion");
829
- } catch (err: unknown) {
830
- const msg = err instanceof Error ? err.message : String(err);
831
- execLog("state", batchState.batchId, `failed to delete state file: ${msg}`);
831
+ if (batchState.orchBranch) {
832
+ execLog("state", batchState.batchId, "state file preserved for /orch-integrate", {
833
+ orchBranch: batchState.orchBranch,
834
+ });
835
+ } else {
836
+ // Legacy mode (no orch branch) clean up state
837
+ try {
838
+ deleteBatchState(stateRoot);
839
+ execLog("state", batchState.batchId, "state file deleted on clean completion");
840
+ } catch (err: unknown) {
841
+ const msg = err instanceof Error ? err.message : String(err);
842
+ execLog("state", batchState.batchId, `failed to delete state file: ${msg}`);
843
+ }
832
844
  }
833
845
  }
834
846
  }
@@ -11,6 +11,53 @@ import type { AllocatedLane, AllocatedTask, DependencyGraph, LaneExecutionResult
11
11
  import { allocateLanes } from "./waves.ts";
12
12
  import { runGit } from "./git.ts";
13
13
 
14
+ // ── Task Runner Extension Path Resolution ────────────────────────────
15
+
16
+ /**
17
+ * Find the task-runner extension path for lane sessions.
18
+ *
19
+ * Resolution order:
20
+ * 1. Local project: {repoRoot}/extensions/task-runner.ts (for taskplane dev)
21
+ * 2. Global npm (Windows): {APPDATA}/npm/node_modules/taskplane/extensions/task-runner.ts
22
+ * 3. Global npm (Unix): /usr/local/lib/node_modules/taskplane/extensions/task-runner.ts
23
+ * 4. npm peer: resolve from pi's location
24
+ *
25
+ * @throws ExecutionError if task-runner.ts cannot be found anywhere
26
+ */
27
+ function resolveTaskRunnerExtensionPath(repoRoot: string): string {
28
+ const extFile = join("extensions", "task-runner.ts");
29
+
30
+ // 1. Local project (taskplane development)
31
+ const localPath = join(resolve(repoRoot), extFile);
32
+ if (existsSync(localPath)) return localPath;
33
+
34
+ // 2. Global npm install paths
35
+ const home = process.env.HOME || process.env.USERPROFILE || "";
36
+ const candidates: string[] = [];
37
+ if (process.env.APPDATA) {
38
+ candidates.push(join(process.env.APPDATA, "npm", "node_modules", "taskplane", extFile));
39
+ }
40
+ if (home) {
41
+ candidates.push(join(home, "AppData", "Roaming", "npm", "node_modules", "taskplane", extFile));
42
+ candidates.push(join(home, ".npm-global", "lib", "node_modules", "taskplane", extFile));
43
+ }
44
+ candidates.push(join("/usr", "local", "lib", "node_modules", "taskplane", extFile));
45
+
46
+ // 3. Peer of pi's package
47
+ try {
48
+ const piPath = process.argv[1] || "";
49
+ const piPkgDir = resolve(piPath, "..", "..");
50
+ candidates.push(join(piPkgDir, "..", "taskplane", extFile));
51
+ } catch { /* ignore */ }
52
+
53
+ for (const candidate of candidates) {
54
+ if (existsSync(candidate)) return candidate;
55
+ }
56
+
57
+ // Fallback: return the local path (will fail at spawn time with a clear error)
58
+ return localPath;
59
+ }
60
+
14
61
  // ── Execution Helpers ────────────────────────────────────────────────
15
62
 
16
63
  /**
@@ -142,9 +189,11 @@ export function buildLaneEnvVars(
142
189
  };
143
190
 
144
191
  // In workspace mode, the worktree cwd is inside a repo — not the workspace root.
145
- // The task-runner needs TASKPLANE_WORKSPACE_ROOT to find .pi/task-runner.yaml
192
+ // The task-runner needs TASKPLANE_WORKSPACE_ROOT to find .pi/ config
146
193
  // and resolve task area paths from the correct base directory.
147
- if (workspaceRoot && workspaceRoot !== repoRoot) {
194
+ // Always set when workspaceRoot is provided (workspace mode), regardless of
195
+ // whether it equals repoRoot (it often does — cwd is the workspace root).
196
+ if (workspaceRoot) {
148
197
  vars.TASKPLANE_WORKSPACE_ROOT = workspaceRoot;
149
198
  }
150
199
 
@@ -206,7 +255,7 @@ export function buildTmuxSpawnArgs(
206
255
  .map(([key, val]) => `${key}=${shellQuote(val)}`)
207
256
  .join(" ");
208
257
 
209
- const taskRunnerExtPath = join(resolve(repoRoot), "extensions", "task-runner.ts");
258
+ const taskRunnerExtPath = resolveTaskRunnerExtensionPath(repoRoot);
210
259
  const basePiCommand = `${envParts} pi --no-session -e ${shellQuote(taskRunnerExtPath)}`;
211
260
 
212
261
  // NOTE: Do not redirect lane output here. Shell redirection has proven
@@ -36,7 +36,7 @@ export const ORCH_MESSAGES = {
36
36
  `🔀 [Wave ${waveNum}] Merge: placeholder — Step 3 (TS-008) will replace with mergeWave()`,
37
37
  orchWorktreeReset: (waveNum: number, lanes: number) =>
38
38
  `🔄 Resetting ${lanes} worktree(s) to target branch HEAD after wave ${waveNum}`,
39
- orchBatchComplete: (batchId: string, succeeded: number, failed: number, skipped: number, blocked: number, elapsedSec: number) => {
39
+ orchBatchComplete: (batchId: string, succeeded: number, failed: number, skipped: number, blocked: number, elapsedSec: number, orchBranch?: string, baseBranch?: string) => {
40
40
  const lines = [`\n🏁 Batch ${batchId} complete: ${succeeded} succeeded, ${failed} failed, ${skipped} skipped, ${blocked} blocked (${elapsedSec}s)`];
41
41
  if (failed > 0 || blocked > 0) {
42
42
  lines.push("");
@@ -48,6 +48,17 @@ export const ORCH_MESSAGES = {
48
48
  lines.push(" • /orch-resume — retry from the failed wave");
49
49
  lines.push(" • /orch-abort — clean up and start fresh");
50
50
  }
51
+ if (orchBranch && succeeded > 0) {
52
+ lines.push("");
53
+ lines.push(` ℹ Orch branch: ${orchBranch}`);
54
+ if (baseBranch) {
55
+ lines.push(` Review changes: git log ${baseBranch}..${orchBranch}`);
56
+ }
57
+ lines.push(" Next steps:");
58
+ lines.push(" • /orch-integrate — fast-forward into your branch");
59
+ lines.push(" • /orch-integrate --merge — merge (if branches diverged)");
60
+ lines.push(" • /orch-integrate --pr — push and open a PR");
61
+ }
51
62
  return lines.join("\n");
52
63
  },
53
64
  orchBatchFailed: (batchId: string, reason: string) =>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskplane",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "AI agent orchestration for pi — parallel task execution with checkpoint discipline",
5
5
  "keywords": [
6
6
  "pi-package",