taskplane 0.4.3 → 0.5.1

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/README.md CHANGED
@@ -18,7 +18,7 @@ The taskplane dashboard runs on a local port on your system and gives you elegan
18
18
 
19
19
  ### Key Features
20
20
 
21
- - **Task Orchestrator** — Parallel multi-task execution using git worktrees for full filesystem isolation. Dependency-aware wave scheduling. Automated merges with conflict resolution.
21
+ - **Task Orchestrator** — Parallel multi-task execution using git worktrees for full filesystem isolation. Dependency-aware wave scheduling. Automated merges into a dedicated orch branch — your working branch stays stable until you choose to integrate.
22
22
  - **Task Runner** — What the Orchestrator uses for autonomous single-task execution. Worker agents run in fresh-context loops with STATUS.md as persistent memory. Every checkbox gets a git checkpoint. Cross-model reviewer agents catch what the worker agents missed.
23
23
  - **Web Dashboard** — Live browser-based monitoring via `taskplane dashboard`. SSE streaming, lane/task progress, wave visualization, batch history.
24
24
  - **Structured Tasks** — PROMPT.md defines the mission, steps, and constraints. STATUS.md tracks progress. Agents follow the plan, not vibes.
@@ -128,7 +128,7 @@ The default scaffold includes two independent example tasks, so `/orch all` give
128
128
  Important distinction:
129
129
 
130
130
  - `/task` runs in your **current branch/worktree**.
131
- - `/orch` runs tasks in **isolated worktrees** and merges back.
131
+ - `/orch` runs tasks in **isolated worktrees** on a dedicated orch branch — your working branch is never touched until you integrate.
132
132
 
133
133
  Because workers checkpoint with git commits, `/task` can capture unrelated local edits if you're changing files in parallel. For safer isolation (even with one task), prefer:
134
134
 
@@ -156,6 +156,7 @@ Orchestrator lanes execute tasks through task-runner under the hood, so `/task`
156
156
  | `/orch-abort [--hard]` | Abort batch (graceful or immediate) |
157
157
  | `/orch-deps <areas\|paths\|all>` | Show dependency graph |
158
158
  | `/orch-sessions` | List active worker sessions |
159
+ | `/orch-integrate` | Integrate completed orch batch into your working branch |
159
160
  | `/taskplane-settings` | View and edit taskplane configuration interactively |
160
161
 
161
162
  ### CLI Commands
@@ -189,14 +190,18 @@ Orchestrator lanes execute tasks through task-runner under the hood, so `/task`
189
190
 
190
191
  ┌──────▼──────┐
191
192
  │ Merge Agent │ ← Conflict resolution
192
- Integration │ & verification
193
- │ Branch │
193
+ Orch Branch │ & verification
194
+ └──────┬──────┘
195
+
196
+ ┌──────▼──────┐
197
+ │ /orch- │ ← User integrates into
198
+ │ integrate │ working branch
194
199
  └─────────────┘
195
200
  ```
196
201
 
197
202
  **Single task** (`/task`): Worker iterates in fresh-context loops. STATUS.md is persistent memory. Each checkbox → git checkpoint. Reviewer validates on completion.
198
203
 
199
- **Parallel batch** (`/orch`): Tasks are sorted into dependency waves. Each wave runs in parallel across lanes (git worktrees). Completed lanes merge into the integration branch before the next wave starts.
204
+ **Parallel batch** (`/orch`): Tasks are sorted into dependency waves. Each wave runs in parallel across lanes (git worktrees). Completed lanes merge into a dedicated orch branch. When the batch completes, use `/orch-integrate` to bring the results into your working branch (or configure auto-integration).
200
205
 
201
206
  ## Documentation
202
207
 
@@ -9,7 +9,7 @@ import { formatDiscoveryResults, runDiscovery } from "./discovery.ts";
9
9
  import { execLog, executeWave, tmuxKillSession } from "./execution.ts";
10
10
  import type { MonitorUpdateCallback } from "./execution.ts";
11
11
  import { getCurrentBranch, runGit } from "./git.ts";
12
- import { mergeWaveByRepo } from "./merge.ts";
12
+ import { attemptAutoIntegration, mergeWaveByRepo } from "./merge.ts";
13
13
  import { computeMergeFailurePolicy, formatRepoMergeSummary, ORCH_MESSAGES } from "./messages.ts";
14
14
  import { resolveOperatorId } from "./naming.ts";
15
15
  import { deleteBatchState, loadBatchHistory, persistRuntimeState, saveBatchHistory, seedPendingOutcomesForAllocatedLanes, syncTaskOutcomesFromMonitor, upsertTaskOutcome } from "./persistence.ts";
@@ -182,6 +182,26 @@ export async function executeOrchBatch(
182
182
  wavePlan = rawWaves;
183
183
  discoveryRef = discovery;
184
184
 
185
+ // ── Create orchestrator-managed branch ───────────────────────
186
+ // Created after all planning validations pass (preflight, discovery,
187
+ // graph validation, wave computation) to avoid orphan branches on
188
+ // planning-phase early exits.
189
+ // The orch branch isolates all batch work from the user's current branch.
190
+ // Worktrees branch from it; merges target it via update-ref.
191
+ const opId = resolveOperatorId(orchConfig);
192
+ const orchBranch = `orch/${opId}-${batchState.batchId}`;
193
+ const branchResult = runGit(["branch", orchBranch, batchState.baseBranch], repoRoot);
194
+ if (!branchResult.ok) {
195
+ batchState.phase = "failed";
196
+ batchState.endedAt = Date.now();
197
+ const errDetail = branchResult.stderr || branchResult.stdout || "unknown error";
198
+ batchState.errors.push(`Failed to create orch branch '${orchBranch}': ${errDetail}`);
199
+ onNotify(`❌ Failed to create orch branch '${orchBranch}': ${errDetail}`, "error");
200
+ return;
201
+ }
202
+ batchState.orchBranch = orchBranch;
203
+ execLog("batch", batchState.batchId, "created orch branch", { orchBranch, baseBranch: batchState.baseBranch });
204
+
185
205
  onNotify(
186
206
  ORCH_MESSAGES.orchStarting(batchState.batchId, rawWaves.length, batchState.totalTasks),
187
207
  "info",
@@ -253,7 +273,7 @@ export async function executeOrchBatch(
253
273
  batchState.batchId,
254
274
  batchState.pauseSignal,
255
275
  depGraph,
256
- batchState.baseBranch,
276
+ batchState.orchBranch,
257
277
  handleWaveMonitorUpdate,
258
278
  (lanes) => {
259
279
  latestAllocatedLanes = lanes;
@@ -361,7 +381,7 @@ export async function executeOrchBatch(
361
381
  orchConfig,
362
382
  repoRoot,
363
383
  batchState.batchId,
364
- batchState.baseBranch,
384
+ batchState.orchBranch,
365
385
  workspaceConfig,
366
386
  stateRoot,
367
387
  agentRoot,
@@ -481,7 +501,7 @@ export async function executeOrchBatch(
481
501
  if (waveIdx < rawWaves.length - 1 && !batchState.pauseSignal.paused) {
482
502
  const prefix = orchConfig.orchestrator.worktree_prefix;
483
503
  const resetOpId = resolveOperatorId(orchConfig);
484
- const existingWorktrees = listWorktrees(prefix, repoRoot, resetOpId);
504
+ const existingWorktrees = listWorktrees(prefix, repoRoot, resetOpId, batchState.batchId);
485
505
 
486
506
  if (existingWorktrees.length > 0) {
487
507
  onNotify(
@@ -489,7 +509,7 @@ export async function executeOrchBatch(
489
509
  "info",
490
510
  );
491
511
 
492
- const targetBranch = batchState.baseBranch;
512
+ const targetBranch = batchState.orchBranch;
493
513
  for (const wt of existingWorktrees) {
494
514
  const resetResult = safeResetWorktree(wt, targetBranch, repoRoot);
495
515
  if (!resetResult.success) {
@@ -672,11 +692,13 @@ export async function executeOrchBatch(
672
692
  }
673
693
  } catch { /* .pi dir may not exist */ }
674
694
 
675
- // Clean up worktrees — pass base branch to protect unmerged work
676
- const targetBranch = batchState.baseBranch;
695
+ // Clean up worktrees — use orchBranch to protect unmerged work.
696
+ // Lane branches were merged into orchBranch (not baseBranch), so
697
+ // unmerged-branch detection must compare against orchBranch.
698
+ const targetBranch = batchState.orchBranch;
677
699
  const cleanupOpId = resolveOperatorId(orchConfig);
678
700
  execLog("batch", batchState.batchId, "cleaning up worktrees");
679
- const removeResult = removeAllWorktrees(prefix, repoRoot, cleanupOpId, targetBranch);
701
+ const removeResult = removeAllWorktrees(prefix, repoRoot, cleanupOpId, targetBranch, batchState.batchId, orchConfig);
680
702
 
681
703
  // Log preserved branches
682
704
  for (const p of removeResult.preserved) {
@@ -750,6 +772,35 @@ export async function executeOrchBatch(
750
772
  }
751
773
  }
752
774
 
775
+ // ── Auto-Integration & Orch Branch Preservation (TP-022 Step 4) ──
776
+ // After all waves are done, optionally fast-forward baseBranch to orchBranch.
777
+ // Auto-integration never converts a successful batch into "failed" — failures
778
+ // are warnings that preserve the orch branch for manual integration.
779
+ // Gate: only run for terminal phases (completed/failed). Paused/stopped batches
780
+ // are not yet done — integration would mutate refs prematurely.
781
+ let autoIntegrated = false;
782
+ const mergedTaskCount = batchState.succeededTasks;
783
+ const isTerminalPhase = batchState.phase === "completed" || batchState.phase === "failed";
784
+ if (isTerminalPhase && !preserveWorktreesForResume && batchState.orchBranch && mergedTaskCount > 0) {
785
+ if (orchConfig.orchestrator.integration === "auto") {
786
+ autoIntegrated = attemptAutoIntegration(
787
+ batchState.orchBranch,
788
+ batchState.baseBranch,
789
+ repoRoot,
790
+ batchState.batchId,
791
+ "batch",
792
+ onNotify,
793
+ );
794
+ }
795
+ // Manual mode (default) or auto-integration skipped: show integration guidance
796
+ if (!autoIntegrated) {
797
+ onNotify(
798
+ ORCH_MESSAGES.orchIntegrationManual(batchState.orchBranch, batchState.baseBranch, mergedTaskCount),
799
+ "info",
800
+ );
801
+ }
802
+ }
803
+
753
804
  // ── TS-009: Persist terminal state ──
754
805
  persistRuntimeState("batch-terminal", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discoveryRef, stateRoot);
755
806
 
@@ -766,22 +817,35 @@ export async function executeOrchBatch(
766
817
  batchState.skippedTasks,
767
818
  batchState.blockedTasks,
768
819
  totalElapsedSec,
820
+ batchState.orchBranch,
821
+ batchState.baseBranch,
769
822
  ),
770
823
  batchState.failedTasks > 0 ? "warning" : "info",
771
824
  );
772
825
 
773
- // ── 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.
774
830
  if (batchState.phase === "completed") {
775
- try {
776
- deleteBatchState(stateRoot);
777
- execLog("state", batchState.batchId, "state file deleted on clean completion");
778
- } catch (err: unknown) {
779
- const msg = err instanceof Error ? err.message : String(err);
780
- 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
+ }
781
844
  }
782
845
  }
783
846
  }
784
847
  }
785
848
 
849
+
786
850
  // ── Dashboard Widget (Step 6) ────────────────────────────────────────
787
851
 
@@ -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
  /**
@@ -206,7 +253,7 @@ export function buildTmuxSpawnArgs(
206
253
  .map(([key, val]) => `${key}=${shellQuote(val)}`)
207
254
  .join(" ");
208
255
 
209
- const taskRunnerExtPath = join(resolve(repoRoot), "extensions", "task-runner.ts");
256
+ const taskRunnerExtPath = resolveTaskRunnerExtensionPath(repoRoot);
210
257
  const basePiCommand = `${envParts} pi --no-session -e ${shellQuote(taskRunnerExtPath)}`;
211
258
 
212
259
  // NOTE: Do not redirect lane output here. Shell redirection has proven