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 +10 -5
- package/extensions/taskplane/engine.ts +79 -15
- package/extensions/taskplane/execution.ts +48 -1
- package/extensions/taskplane/extension.ts +562 -2
- package/extensions/taskplane/merge.ts +182 -27
- package/extensions/taskplane/messages.ts +30 -1
- package/extensions/taskplane/resume.ts +128 -11
- package/extensions/taskplane/waves.ts +16 -2
- package/extensions/taskplane/worktree.ts +3 -3
- package/package.json +1 -1
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
|
|
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**
|
|
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
|
-
│
|
|
193
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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 —
|
|
676
|
-
|
|
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
|
-
// ──
|
|
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
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
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 =
|
|
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
|