taskplane 0.22.18 → 0.23.0
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/dashboard/public/app.js +365 -7
- package/dashboard/public/index.html +16 -0
- package/dashboard/public/style.css +105 -0
- package/dashboard/server.cjs +199 -0
- package/extensions/task-runner.ts +40 -286
- package/extensions/taskplane/abort.ts +11 -1
- package/extensions/taskplane/agent-bridge-extension.ts +159 -0
- package/extensions/taskplane/agent-host.ts +686 -0
- package/extensions/taskplane/engine.ts +75 -3
- package/extensions/taskplane/execution.ts +403 -9
- package/extensions/taskplane/extension.ts +322 -28
- package/extensions/taskplane/lane-runner.ts +567 -0
- package/extensions/taskplane/mailbox.ts +349 -1
- package/extensions/taskplane/merge.ts +208 -51
- package/extensions/taskplane/process-registry.ts +345 -0
- package/extensions/taskplane/resume.ts +185 -47
- package/extensions/taskplane/supervisor.ts +16 -12
- package/extensions/taskplane/task-executor-core.ts +553 -0
- package/extensions/taskplane/types.ts +517 -1
- package/package.json +1 -1
- package/skills/create-taskplane-task/SKILL.md +41 -33
- package/skills/create-taskplane-task/references/prompt-template.md +3 -3
|
@@ -6,7 +6,8 @@ import { existsSync, readdirSync, readFileSync, unlinkSync } from "fs";
|
|
|
6
6
|
import { join, resolve } from "path";
|
|
7
7
|
|
|
8
8
|
import { formatDiscoveryResults, runDiscovery } from "./discovery.ts";
|
|
9
|
-
import { computeTransitiveDependents, execLog, executeLane, executeWave, tmuxKillSession } from "./execution.ts";
|
|
9
|
+
import { computeTransitiveDependents, execLog, executeLane, executeLaneV2, executeWave, tmuxKillSession } from "./execution.ts";
|
|
10
|
+
import type { RuntimeBackend } from "./execution.ts";
|
|
10
11
|
import type { MonitorUpdateCallback } from "./execution.ts";
|
|
11
12
|
// classifyExit no longer called directly — Tier 0 uses exitDiagnostic.classification
|
|
12
13
|
// from the diagnostic-reports pipeline (populated by assembleDiagnosticInput).
|
|
@@ -87,6 +88,7 @@ async function attemptWorkerCrashRetry(
|
|
|
87
88
|
onNotify: (message: string, level: "info" | "warning" | "error") => void,
|
|
88
89
|
stateRoot: string,
|
|
89
90
|
runnerConfig?: TaskRunnerConfig,
|
|
91
|
+
runtimeBackend?: RuntimeBackend,
|
|
90
92
|
): Promise<{ retriedCount: number; succeededRetries: string[]; failedRetries: string[] }> {
|
|
91
93
|
if (!batchState.resilience) {
|
|
92
94
|
batchState.resilience = defaultResilienceState();
|
|
@@ -221,7 +223,8 @@ async function attemptWorkerCrashRetry(
|
|
|
221
223
|
// may be paused due to stop-wave policy, but Tier 0 retry should
|
|
222
224
|
// attempt recovery before the stop decision takes effect (R002-4).
|
|
223
225
|
const retryPauseSignal = { paused: false };
|
|
224
|
-
const
|
|
226
|
+
const retryExecutor = (runtimeBackend === "v2") ? executeLaneV2 : executeLane;
|
|
227
|
+
const retryResult = await retryExecutor(
|
|
225
228
|
retryLane,
|
|
226
229
|
orchConfig,
|
|
227
230
|
repoRoot,
|
|
@@ -374,6 +377,7 @@ async function attemptModelFallbackRetry(
|
|
|
374
377
|
onNotify: (message: string, level: "info" | "warning" | "error") => void,
|
|
375
378
|
stateRoot: string,
|
|
376
379
|
runnerConfig?: TaskRunnerConfig,
|
|
380
|
+
runtimeBackend?: RuntimeBackend,
|
|
377
381
|
): Promise<{ retriedCount: number; succeededRetries: string[]; failedRetries: string[] }> {
|
|
378
382
|
// Short-circuit: if model fallback is disabled, skip entirely
|
|
379
383
|
const modelFallbackMode = runnerConfig?.model_fallback ?? "inherit";
|
|
@@ -487,7 +491,8 @@ async function attemptModelFallbackRetry(
|
|
|
487
491
|
// the task-runner to use the session model instead of configured model.
|
|
488
492
|
// TP-089: Also include ORCH_BATCH_ID so mailbox steering works for retries.
|
|
489
493
|
const modelFallbackEnv = { TASKPLANE_MODEL_FALLBACK: "1", ORCH_BATCH_ID: batchState.batchId };
|
|
490
|
-
const
|
|
494
|
+
const retryExecutor = (runtimeBackend === "v2") ? executeLaneV2 : executeLane;
|
|
495
|
+
const retryResult = await retryExecutor(
|
|
491
496
|
retryLane,
|
|
492
497
|
orchConfig,
|
|
493
498
|
repoRoot,
|
|
@@ -623,6 +628,8 @@ async function attemptStaleWorktreeRecovery(
|
|
|
623
628
|
onMonitorUpdate: MonitorUpdateCallback | undefined,
|
|
624
629
|
onLanesAllocated: (lanes: AllocatedLane[]) => void,
|
|
625
630
|
stateRoot: string,
|
|
631
|
+
runtimeBackend?: RuntimeBackend,
|
|
632
|
+
onSupervisorAlert?: SupervisorAlertCallback,
|
|
626
633
|
): Promise<WaveExecutionResult | null> {
|
|
627
634
|
// Only attempt recovery for ALLOC_WORKTREE_FAILED
|
|
628
635
|
if (!waveResult.allocationError || waveResult.allocationError.code !== "ALLOC_WORKTREE_FAILED") {
|
|
@@ -722,12 +729,51 @@ async function attemptStaleWorktreeRecovery(
|
|
|
722
729
|
onMonitorUpdate,
|
|
723
730
|
onLanesAllocated,
|
|
724
731
|
workspaceConfig,
|
|
732
|
+
runtimeBackend,
|
|
733
|
+
onSupervisorAlert,
|
|
725
734
|
);
|
|
726
735
|
|
|
727
736
|
return retryResult;
|
|
728
737
|
}
|
|
729
738
|
|
|
730
739
|
|
|
740
|
+
export interface RuntimeBackendSelection {
|
|
741
|
+
backend: RuntimeBackend;
|
|
742
|
+
isSingleTask: boolean;
|
|
743
|
+
isRepoMode: boolean;
|
|
744
|
+
isDirectPromptTarget: boolean;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Select execution backend for a batch under the TP-105 scope guard.
|
|
749
|
+
*
|
|
750
|
+
* Runtime V2 is enabled only for a single-task batch in repo mode when
|
|
751
|
+
* the original target is exactly one direct PROMPT.md path.
|
|
752
|
+
*/
|
|
753
|
+
export function selectRuntimeBackend(
|
|
754
|
+
args: string,
|
|
755
|
+
rawWaves: string[][],
|
|
756
|
+
workspaceConfig?: WorkspaceConfig | null,
|
|
757
|
+
): RuntimeBackendSelection {
|
|
758
|
+
const isSingleTask = rawWaves.length === 1 && rawWaves[0]?.length === 1;
|
|
759
|
+
const isRepoMode = !workspaceConfig;
|
|
760
|
+
const argTokens = args.trim().split(/\s+/).filter(Boolean);
|
|
761
|
+
const isDirectPromptTarget =
|
|
762
|
+
argTokens.length === 1 && /PROMPT\.md$/i.test(argTokens[0]);
|
|
763
|
+
|
|
764
|
+
// TP-108: Runtime V2 for all repo-mode batches.
|
|
765
|
+
// TP-109: Workspace mode also uses V2 now that packet-home paths are
|
|
766
|
+
// threaded through execution and resume (worktree-relative .DONE check).
|
|
767
|
+
const backend: RuntimeBackend = "v2";
|
|
768
|
+
|
|
769
|
+
return {
|
|
770
|
+
backend,
|
|
771
|
+
isSingleTask,
|
|
772
|
+
isRepoMode,
|
|
773
|
+
isDirectPromptTarget,
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
|
|
731
777
|
// ── /orch Execution Engine ───────────────────────────────────────────
|
|
732
778
|
|
|
733
779
|
/**
|
|
@@ -1049,6 +1095,20 @@ export async function executeOrchBatch(
|
|
|
1049
1095
|
// ── TS-009: Persist state on batch start (after wave computation) ──
|
|
1050
1096
|
persistRuntimeState("batch-start", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discoveryRef, stateRoot);
|
|
1051
1097
|
|
|
1098
|
+
// ── TP-105: Runtime V2 backend selection ────────────────────
|
|
1099
|
+
// Use Runtime V2 (no-TMUX lane-runner) when ALL conditions are met:
|
|
1100
|
+
// 1. Exactly one task in the batch
|
|
1101
|
+
// 2. Repo mode (not workspace mode — workspace deferred to TP-109)
|
|
1102
|
+
// 3. The user target is a single direct PROMPT.md path
|
|
1103
|
+
// Otherwise, fall back to the legacy TMUX-backed path.
|
|
1104
|
+
const backendSelection = selectRuntimeBackend(args, rawWaves, workspaceConfig);
|
|
1105
|
+
const selectedBackend = backendSelection.backend;
|
|
1106
|
+
|
|
1107
|
+
if (selectedBackend === "v2") {
|
|
1108
|
+
execLog("batch", batchState.batchId, "Runtime V2 backend selected");
|
|
1109
|
+
onNotify("🚀 Using Runtime V2 backend (no-TMUX direct execution)", "info");
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1052
1112
|
for (let waveIdx = 0; waveIdx < rawWaves.length; waveIdx++) {
|
|
1053
1113
|
// Check pause signal before starting each wave
|
|
1054
1114
|
if (batchState.pauseSignal.paused) {
|
|
@@ -1134,6 +1194,8 @@ export async function executeOrchBatch(
|
|
|
1134
1194
|
handleWaveMonitorUpdate,
|
|
1135
1195
|
onLanesAllocatedCb,
|
|
1136
1196
|
workspaceConfig,
|
|
1197
|
+
selectedBackend,
|
|
1198
|
+
emitAlert,
|
|
1137
1199
|
);
|
|
1138
1200
|
|
|
1139
1201
|
// ── TP-039: Tier 0 — Stale worktree recovery ────────────
|
|
@@ -1153,6 +1215,8 @@ export async function executeOrchBatch(
|
|
|
1153
1215
|
handleWaveMonitorUpdate,
|
|
1154
1216
|
onLanesAllocatedCb,
|
|
1155
1217
|
stateRoot,
|
|
1218
|
+
selectedBackend,
|
|
1219
|
+
emitAlert,
|
|
1156
1220
|
);
|
|
1157
1221
|
if (retryResult) {
|
|
1158
1222
|
const staleRecovered = !retryResult.allocationError;
|
|
@@ -1219,6 +1283,7 @@ export async function executeOrchBatch(
|
|
|
1219
1283
|
onNotify,
|
|
1220
1284
|
stateRoot,
|
|
1221
1285
|
runnerConfig,
|
|
1286
|
+
selectedBackend,
|
|
1222
1287
|
);
|
|
1223
1288
|
if (modelFallbackOutcome.succeededRetries.length > 0) {
|
|
1224
1289
|
// Recompute blocked tasks after model fallback successes
|
|
@@ -1252,6 +1317,8 @@ export async function executeOrchBatch(
|
|
|
1252
1317
|
allTaskOutcomes,
|
|
1253
1318
|
onNotify,
|
|
1254
1319
|
stateRoot,
|
|
1320
|
+
undefined,
|
|
1321
|
+
selectedBackend,
|
|
1255
1322
|
);
|
|
1256
1323
|
if (retryOutcome.succeededRetries.length > 0) {
|
|
1257
1324
|
// Recompute blockedTaskIds from remaining failures (R002-2).
|
|
@@ -1468,6 +1535,8 @@ export async function executeOrchBatch(
|
|
|
1468
1535
|
agentRoot,
|
|
1469
1536
|
runnerConfig.testing_commands,
|
|
1470
1537
|
mergeHealthMonitor,
|
|
1538
|
+
undefined, // forceMixedOutcome
|
|
1539
|
+
selectedBackend,
|
|
1471
1540
|
);
|
|
1472
1541
|
} finally {
|
|
1473
1542
|
// TP-056: Always stop the health monitor when merge phase ends
|
|
@@ -1686,6 +1755,9 @@ export async function executeOrchBatch(
|
|
|
1686
1755
|
stateRoot,
|
|
1687
1756
|
agentRoot,
|
|
1688
1757
|
runnerConfig.testing_commands,
|
|
1758
|
+
undefined, // healthMonitor
|
|
1759
|
+
undefined, // forceMixedOutcome
|
|
1760
|
+
selectedBackend,
|
|
1689
1761
|
);
|
|
1690
1762
|
},
|
|
1691
1763
|
persist: (trigger) => persistRuntimeState(trigger, batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discoveryRef, stateRoot),
|
|
@@ -9,7 +9,9 @@ import { join, dirname, basename, resolve, relative, delimiter as pathDelimiter
|
|
|
9
9
|
import { userInfo } from "os";
|
|
10
10
|
|
|
11
11
|
import { DONE_GRACE_MS, EXECUTION_POLL_INTERVAL_MS, ExecutionError, SESSION_SPAWN_RETRY_MAX } from "./types.ts";
|
|
12
|
-
import type { AllocatedLane, AllocatedTask, DependencyGraph, LaneExecutionResult, LaneMonitorSnapshot, LaneTaskOutcome, LaneTaskStatus, MonitorState, MtimeTracker, OrchestratorConfig, ParsedTask, TaskMonitorSnapshot, WaveExecutionResult, WorkspaceConfig } from "./types.ts";
|
|
12
|
+
import type { AllocatedLane, AllocatedTask, DependencyGraph, LaneExecutionResult, LaneMonitorSnapshot, LaneTaskOutcome, LaneTaskStatus, MonitorState, MtimeTracker, OrchestratorConfig, ParsedTask, TaskMonitorSnapshot, WaveExecutionResult, WorkspaceConfig, ExecutionUnit, PacketPaths, RuntimeAgentId, RuntimeAgentRole, SupervisorAlertCallback } from "./types.ts";
|
|
13
|
+
import { resolvePacketPaths, buildRuntimeAgentId } from "./types.ts";
|
|
14
|
+
import { readRegistrySnapshot, isTerminalStatus, isProcessAlive } from "./process-registry.ts";
|
|
13
15
|
import { allocateLanes } from "./waves.ts";
|
|
14
16
|
import { runGit } from "./git.ts";
|
|
15
17
|
|
|
@@ -266,6 +268,63 @@ export function killLaneAndChildren(sessionName: string): void {
|
|
|
266
268
|
tmuxKillSession(sessionName);
|
|
267
269
|
}
|
|
268
270
|
|
|
271
|
+
/**
|
|
272
|
+
* TP-112: Check if a V2 agent is alive via process registry.
|
|
273
|
+
* Returns true if the agent's PID is running and status is non-terminal.
|
|
274
|
+
* Returns false if no registry, no entry, terminal status, or dead PID.
|
|
275
|
+
*
|
|
276
|
+
* @param agentIdOrSessionName - Agent ID or session name to look up
|
|
277
|
+
* @param runtimeBackend - Must be "v2" (caller should guard)
|
|
278
|
+
* @returns true if agent is alive
|
|
279
|
+
* @since TP-112
|
|
280
|
+
*/
|
|
281
|
+
export function isV2AgentAlive(agentIdOrSessionName: string, _runtimeBackend?: RuntimeBackend): boolean {
|
|
282
|
+
// Read the registry from the global state root.
|
|
283
|
+
// Since this is a pure liveness check, we scan for matching agentId
|
|
284
|
+
// patterns: direct match, or lane-session + "-worker" suffix.
|
|
285
|
+
if (!_v2LivenessRegistryCache) return false;
|
|
286
|
+
const agents = _v2LivenessRegistryCache.agents;
|
|
287
|
+
// Direct match
|
|
288
|
+
const manifest = agents[agentIdOrSessionName];
|
|
289
|
+
if (manifest && !isTerminalStatus(manifest.status) && isProcessAlive(manifest.pid)) return true;
|
|
290
|
+
// Try worker suffix (monitor uses lane session name, registry uses agentId)
|
|
291
|
+
const workerManifest = agents[`${agentIdOrSessionName}-worker`];
|
|
292
|
+
if (workerManifest && !isTerminalStatus(workerManifest.status) && isProcessAlive(workerManifest.pid)) return true;
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Cached registry for V2 liveness checks within a monitor cycle. @since TP-112 */
|
|
297
|
+
let _v2LivenessRegistryCache: import("./process-registry.ts").RuntimeRegistry | null = null;
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Set the V2 liveness registry cache for the current monitor cycle.
|
|
301
|
+
* Called at the start of each monitor poll to avoid re-reading the file per-task.
|
|
302
|
+
* @since TP-112
|
|
303
|
+
*/
|
|
304
|
+
export function setV2LivenessRegistryCache(registry: import("./process-registry.ts").RuntimeRegistry | null): void {
|
|
305
|
+
_v2LivenessRegistryCache = registry;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* TP-112: Kill V2 lane agents (worker + reviewer) by PID from the registry.
|
|
310
|
+
* Used for stall termination on the V2 path.
|
|
311
|
+
* @since TP-112
|
|
312
|
+
*/
|
|
313
|
+
export function killV2LaneAgents(sessionName: string): void {
|
|
314
|
+
if (!_v2LivenessRegistryCache) return;
|
|
315
|
+
const agents = _v2LivenessRegistryCache.agents;
|
|
316
|
+
for (const suffix of ["-worker", "-reviewer", ""]) {
|
|
317
|
+
const key = `${sessionName}${suffix}`;
|
|
318
|
+
const manifest = agents[key];
|
|
319
|
+
if (manifest && !isTerminalStatus(manifest.status) && isProcessAlive(manifest.pid)) {
|
|
320
|
+
try {
|
|
321
|
+
process.kill(manifest.pid, "SIGTERM");
|
|
322
|
+
execLog("monitor", key, `killed V2 agent (PID ${manifest.pid}) on stall`);
|
|
323
|
+
} catch { /* already dead */ }
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
269
328
|
// ── Async TMUX Helpers (TP-070) ──────────────────────────────────────
|
|
270
329
|
|
|
271
330
|
/**
|
|
@@ -1692,8 +1751,16 @@ export async function resolveTaskMonitorState(
|
|
|
1692
1751
|
tracker: MtimeTracker,
|
|
1693
1752
|
stallTimeoutMs: number,
|
|
1694
1753
|
now: number,
|
|
1754
|
+
runtimeBackend?: RuntimeBackend,
|
|
1695
1755
|
): Promise<TaskMonitorSnapshot> {
|
|
1696
|
-
|
|
1756
|
+
// TP-112: Backend-aware liveness check.
|
|
1757
|
+
// V2: check process registry (PID liveness). Legacy: check TMUX session.
|
|
1758
|
+
let sessionAlive: boolean;
|
|
1759
|
+
if (runtimeBackend === "v2") {
|
|
1760
|
+
sessionAlive = isV2AgentAlive(sessionName, runtimeBackend);
|
|
1761
|
+
} else {
|
|
1762
|
+
sessionAlive = await tmuxHasSessionAsync(sessionName);
|
|
1763
|
+
}
|
|
1697
1764
|
const doneFileFound = await fileExistsAsync(donePath);
|
|
1698
1765
|
|
|
1699
1766
|
// Build base snapshot from parsed status
|
|
@@ -1784,12 +1851,17 @@ export async function resolveTaskMonitorState(
|
|
|
1784
1851
|
const stallMinutes = Math.round((now - tracker.stallTimerStart) / 60_000);
|
|
1785
1852
|
const stallReason = `STATUS.md unchanged for ${stallMinutes} minutes (threshold: ${Math.round(stallTimeoutMs / 60_000)} min)`;
|
|
1786
1853
|
|
|
1787
|
-
// Kill the
|
|
1788
|
-
execLog("monitor", taskId, `stall detected — killing
|
|
1854
|
+
// Kill the agent (backend-aware)
|
|
1855
|
+
execLog("monitor", taskId, `stall detected — killing agent`, {
|
|
1789
1856
|
session: sessionName,
|
|
1790
1857
|
stallMinutes,
|
|
1858
|
+
backend: runtimeBackend ?? "legacy",
|
|
1791
1859
|
});
|
|
1792
|
-
|
|
1860
|
+
if (runtimeBackend === "v2") {
|
|
1861
|
+
killV2LaneAgents(sessionName);
|
|
1862
|
+
} else {
|
|
1863
|
+
killLaneAndChildren(sessionName);
|
|
1864
|
+
}
|
|
1793
1865
|
|
|
1794
1866
|
return {
|
|
1795
1867
|
taskId,
|
|
@@ -1893,6 +1965,9 @@ export async function monitorLanes(
|
|
|
1893
1965
|
waveNumber: number = 1,
|
|
1894
1966
|
onUpdate?: MonitorUpdateCallback,
|
|
1895
1967
|
isWorkspaceMode?: boolean,
|
|
1968
|
+
runtimeBackend?: RuntimeBackend,
|
|
1969
|
+
batchId?: string,
|
|
1970
|
+
stateRootForRegistry?: string,
|
|
1896
1971
|
): Promise<MonitorState> {
|
|
1897
1972
|
const pollIntervalMs = (config.monitoring.poll_interval || 5) * 1000;
|
|
1898
1973
|
const stallTimeoutMs = (config.failure.stall_timeout || 30) * 60_000;
|
|
@@ -1941,6 +2016,17 @@ export async function monitorLanes(
|
|
|
1941
2016
|
const now = Date.now();
|
|
1942
2017
|
pollCount++;
|
|
1943
2018
|
|
|
2019
|
+
// TP-112: Refresh V2 liveness registry cache once per poll cycle
|
|
2020
|
+
if (runtimeBackend === "v2" && batchId) {
|
|
2021
|
+
try {
|
|
2022
|
+
setV2LivenessRegistryCache(readRegistrySnapshot(stateRootForRegistry ?? repoRoot, batchId));
|
|
2023
|
+
} catch {
|
|
2024
|
+
setV2LivenessRegistryCache(null);
|
|
2025
|
+
}
|
|
2026
|
+
} else {
|
|
2027
|
+
setV2LivenessRegistryCache(null);
|
|
2028
|
+
}
|
|
2029
|
+
|
|
1944
2030
|
// Check pause signal
|
|
1945
2031
|
if (pauseSignal.paused) {
|
|
1946
2032
|
execLog("monitor", "ALL", "pause signal detected — stopping monitoring");
|
|
@@ -1993,6 +2079,7 @@ export async function monitorLanes(
|
|
|
1993
2079
|
tracker,
|
|
1994
2080
|
stallTimeoutMs,
|
|
1995
2081
|
now,
|
|
2082
|
+
runtimeBackend,
|
|
1996
2083
|
);
|
|
1997
2084
|
|
|
1998
2085
|
currentTaskSnapshot = snapshot;
|
|
@@ -2031,7 +2118,10 @@ export async function monitorLanes(
|
|
|
2031
2118
|
allTerminal = false;
|
|
2032
2119
|
}
|
|
2033
2120
|
|
|
2034
|
-
|
|
2121
|
+
// TP-112: Backend-aware lane liveness for snapshot
|
|
2122
|
+
const sessionAlive = runtimeBackend === "v2"
|
|
2123
|
+
? isV2AgentAlive(lane.tmuxSessionName, runtimeBackend)
|
|
2124
|
+
: await tmuxHasSessionAsync(lane.tmuxSessionName);
|
|
2035
2125
|
|
|
2036
2126
|
laneSnapshots.push({
|
|
2037
2127
|
laneId: lane.laneId,
|
|
@@ -2082,6 +2172,7 @@ export async function monitorLanes(
|
|
|
2082
2172
|
total: tasksTotal,
|
|
2083
2173
|
polls: pollCount,
|
|
2084
2174
|
});
|
|
2175
|
+
setV2LivenessRegistryCache(null);
|
|
2085
2176
|
return monitorState;
|
|
2086
2177
|
}
|
|
2087
2178
|
|
|
@@ -2103,6 +2194,7 @@ export async function monitorLanes(
|
|
|
2103
2194
|
remainingTasks: lane.tasks.map(t => t.taskId),
|
|
2104
2195
|
}));
|
|
2105
2196
|
|
|
2197
|
+
setV2LivenessRegistryCache(null);
|
|
2106
2198
|
return {
|
|
2107
2199
|
lanes: laneSnapshots,
|
|
2108
2200
|
tasksDone: 0,
|
|
@@ -2296,6 +2388,16 @@ export function ensureTaskFilesCommitted(
|
|
|
2296
2388
|
* @param workspaceConfig - Workspace configuration for repo routing (null/undefined = repo mode)
|
|
2297
2389
|
* @returns WaveExecutionResult with outcomes and blocked task IDs
|
|
2298
2390
|
*/
|
|
2391
|
+
/**
|
|
2392
|
+
* Runtime backend selector for lane execution.
|
|
2393
|
+
*
|
|
2394
|
+
* - `"legacy"`: TMUX-backed path (spawnLaneSession → task-runner TASK_AUTOSTART)
|
|
2395
|
+
* - `"v2"`: Direct-child path (lane-runner → agent-host → pi --mode rpc)
|
|
2396
|
+
*
|
|
2397
|
+
* @since TP-105
|
|
2398
|
+
*/
|
|
2399
|
+
export type RuntimeBackend = "legacy" | "v2";
|
|
2400
|
+
|
|
2299
2401
|
export async function executeWave(
|
|
2300
2402
|
waveTasks: string[],
|
|
2301
2403
|
waveIndex: number,
|
|
@@ -2309,6 +2411,8 @@ export async function executeWave(
|
|
|
2309
2411
|
onMonitorUpdate?: MonitorUpdateCallback,
|
|
2310
2412
|
onLanesAllocated?: (lanes: AllocatedLane[]) => void,
|
|
2311
2413
|
workspaceConfig?: WorkspaceConfig | null,
|
|
2414
|
+
runtimeBackend?: RuntimeBackend,
|
|
2415
|
+
onSupervisorAlert?: SupervisorAlertCallback,
|
|
2312
2416
|
): Promise<WaveExecutionResult> {
|
|
2313
2417
|
const startedAt = Date.now();
|
|
2314
2418
|
const policy = config.failure.on_task_failure;
|
|
@@ -2391,14 +2495,20 @@ export async function executeWave(
|
|
|
2391
2495
|
// configPath is .pi/taskplane-workspace.yaml → parent of parent is workspace root.
|
|
2392
2496
|
const wsRoot = workspaceConfig ? dirname(dirname(workspaceConfig.configPath)) : undefined;
|
|
2393
2497
|
const isWsMode = !!workspaceConfig;
|
|
2498
|
+
const backend = runtimeBackend ?? "legacy";
|
|
2499
|
+
if (backend === "v2") {
|
|
2500
|
+
execLog("wave", `W${waveIndex}`, "using Runtime V2 backend (executeLaneV2)");
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2394
2503
|
const lanePromises = lanes.map(lane =>
|
|
2395
|
-
|
|
2396
|
-
ORCH_BATCH_ID: batchId,
|
|
2397
|
-
|
|
2504
|
+
backend === "v2"
|
|
2505
|
+
? executeLaneV2(lane, config, repoRoot, wavePauseSignal, wsRoot, isWsMode, { ORCH_BATCH_ID: batchId }, onSupervisorAlert)
|
|
2506
|
+
: executeLane(lane, config, repoRoot, wavePauseSignal, wsRoot, isWsMode, { ORCH_BATCH_ID: batchId }),
|
|
2398
2507
|
);
|
|
2399
2508
|
|
|
2400
2509
|
// Start monitoring as a sibling async loop
|
|
2401
2510
|
// Monitor runs concurrently and stops when all lanes are terminal or paused
|
|
2511
|
+
const monitorStateRoot = resolveRuntimeStateRoot(repoRoot, wsRoot);
|
|
2402
2512
|
const monitorPromise = monitorLanes(
|
|
2403
2513
|
lanes,
|
|
2404
2514
|
config,
|
|
@@ -2407,6 +2517,9 @@ export async function executeWave(
|
|
|
2407
2517
|
waveIndex,
|
|
2408
2518
|
onMonitorUpdate,
|
|
2409
2519
|
isWsMode,
|
|
2520
|
+
backend,
|
|
2521
|
+
batchId,
|
|
2522
|
+
monitorStateRoot,
|
|
2410
2523
|
);
|
|
2411
2524
|
|
|
2412
2525
|
// ── Stage 4: Wait for all lanes + apply policy ───────────────
|
|
@@ -2671,5 +2784,286 @@ export async function executeWithStopAll(
|
|
|
2671
2784
|
});
|
|
2672
2785
|
}
|
|
2673
2786
|
|
|
2787
|
+
// ── Runtime V2 Bridge Helpers (TP-102) ─────────────────────────────────────
|
|
2788
|
+
//
|
|
2789
|
+
// These helpers bridge between existing legacy data structures
|
|
2790
|
+
// (AllocatedLane, AllocatedTask, resolveCanonicalTaskPaths) and
|
|
2791
|
+
// Runtime V2 contracts (ExecutionUnit, PacketPaths, RuntimeAgentId).
|
|
2792
|
+
//
|
|
2793
|
+
// They are additive — existing code paths continue to work.
|
|
2794
|
+
// Runtime V2 consumers can start using these to avoid coupling to
|
|
2795
|
+
// TMUX naming, cwd-derived paths, or extension lifecycle assumptions.
|
|
2796
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
2797
|
+
|
|
2798
|
+
/**
|
|
2799
|
+
* Build a Runtime V2 ExecutionUnit from existing legacy structures.
|
|
2800
|
+
*
|
|
2801
|
+
* Translates the current AllocatedLane + AllocatedTask into the new
|
|
2802
|
+
* ExecutionUnit contract with explicit packet-path authority.
|
|
2803
|
+
*
|
|
2804
|
+
* Uses `resolveCanonicalTaskPaths` to derive packet paths through
|
|
2805
|
+
* the existing resolution logic (worktree-relative, cross-repo copy,
|
|
2806
|
+
* archive fallback). This preserves current behavior while surfacing
|
|
2807
|
+
* it through the Runtime V2 contract.
|
|
2808
|
+
*
|
|
2809
|
+
* **Cross-repo packet authority (TP-109):** In workspace mode, when the
|
|
2810
|
+
* task packet home repo differs from the execution repo, the legacy path
|
|
2811
|
+
* copies packet files into the worktree under `.taskplane-tasks/`. The
|
|
2812
|
+
* resolved `packet` paths here point to that execution-local copy.
|
|
2813
|
+
* This is by design: the worker reads/writes STATUS.md and creates .DONE
|
|
2814
|
+
* in the worktree, and resume checks both the worktree-relative path and
|
|
2815
|
+
* the original discovery path for .DONE detection.
|
|
2816
|
+
*
|
|
2817
|
+
* `packetHomeRepoId` identifies the source repo that *owns* the task
|
|
2818
|
+
* (for discovery and routing), while `packet.taskFolder` is the
|
|
2819
|
+
* authoritative *working* location where artifacts are read/written
|
|
2820
|
+
* during execution. Resume reconciliation (TP-109) resolves both paths.
|
|
2821
|
+
*
|
|
2822
|
+
* @param lane - Allocated lane containing worktree and identity info
|
|
2823
|
+
* @param task - Allocated task to build an execution unit for
|
|
2824
|
+
* @param repoRoot - Main repository root
|
|
2825
|
+
* @param isWorkspaceMode - Whether workspace mode is active
|
|
2826
|
+
* @returns A fully-resolved ExecutionUnit
|
|
2827
|
+
*
|
|
2828
|
+
* @since TP-102
|
|
2829
|
+
*/
|
|
2830
|
+
export function buildExecutionUnit(
|
|
2831
|
+
lane: AllocatedLane,
|
|
2832
|
+
task: AllocatedTask,
|
|
2833
|
+
repoRoot: string,
|
|
2834
|
+
isWorkspaceMode?: boolean,
|
|
2835
|
+
): ExecutionUnit {
|
|
2836
|
+
const resolved = resolveCanonicalTaskPaths(
|
|
2837
|
+
task.task.taskFolder,
|
|
2838
|
+
lane.worktreePath,
|
|
2839
|
+
repoRoot,
|
|
2840
|
+
isWorkspaceMode,
|
|
2841
|
+
);
|
|
2842
|
+
|
|
2843
|
+
const executionRepoId = lane.repoId ?? "default";
|
|
2844
|
+
const packetHomeRepoId = task.task.packetRepoId ?? executionRepoId;
|
|
2845
|
+
|
|
2846
|
+
// Build a segment-style ID if this is a segment execution,
|
|
2847
|
+
// otherwise use the plain task ID.
|
|
2848
|
+
const segmentId = task.task.activeSegmentId ?? null;
|
|
2849
|
+
const id = segmentId ?? task.taskId;
|
|
2850
|
+
|
|
2851
|
+
return {
|
|
2852
|
+
id,
|
|
2853
|
+
taskId: task.taskId,
|
|
2854
|
+
segmentId,
|
|
2855
|
+
executionRepoId,
|
|
2856
|
+
packetHomeRepoId,
|
|
2857
|
+
worktreePath: lane.worktreePath,
|
|
2858
|
+
packet: {
|
|
2859
|
+
promptPath: resolved.taskFolderResolved + "/PROMPT.md",
|
|
2860
|
+
statusPath: resolved.statusPath,
|
|
2861
|
+
donePath: resolved.donePath,
|
|
2862
|
+
reviewsDir: resolved.taskFolderResolved + "/.reviews",
|
|
2863
|
+
taskFolder: resolved.taskFolderResolved,
|
|
2864
|
+
},
|
|
2865
|
+
task: task.task,
|
|
2866
|
+
};
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2869
|
+
/**
|
|
2870
|
+
* Build a RuntimeAgentId for a lane's agent from existing naming.
|
|
2871
|
+
*
|
|
2872
|
+
* Bridges the current TMUX session naming convention into a
|
|
2873
|
+
* Runtime V2 stable agent ID. The output is compatible with
|
|
2874
|
+
* existing supervisor tools and mailbox addressing.
|
|
2875
|
+
*
|
|
2876
|
+
* @param lane - Allocated lane with TMUX session name
|
|
2877
|
+
* @param role - Agent role
|
|
2878
|
+
* @param mergeIndex - Merge wave index (only for merge agents)
|
|
2879
|
+
* @returns Canonical agent ID
|
|
2880
|
+
*
|
|
2881
|
+
* @since TP-102
|
|
2882
|
+
*/
|
|
2883
|
+
export function buildAgentIdFromLane(
|
|
2884
|
+
lane: AllocatedLane,
|
|
2885
|
+
role: RuntimeAgentRole,
|
|
2886
|
+
mergeIndex?: number,
|
|
2887
|
+
): RuntimeAgentId {
|
|
2888
|
+
// The current tmuxSessionName is already in the right format
|
|
2889
|
+
// (e.g., "orch-henrylach-lane-1"). We derive agent IDs from it
|
|
2890
|
+
// by appending the role suffix, matching the existing convention.
|
|
2891
|
+
if (role === "merger" && mergeIndex != null) {
|
|
2892
|
+
// Merge agents use a different naming pattern
|
|
2893
|
+
const prefix = lane.tmuxSessionName.replace(/-lane-\d+$/, "");
|
|
2894
|
+
return `${prefix}-merge-${mergeIndex}`;
|
|
2895
|
+
}
|
|
2896
|
+
if (role === "lane-runner") {
|
|
2897
|
+
return lane.tmuxSessionName;
|
|
2898
|
+
}
|
|
2899
|
+
return `${lane.tmuxSessionName}-${role}`;
|
|
2900
|
+
}
|
|
2901
|
+
|
|
2902
|
+
/**
|
|
2903
|
+
* Resolve the Runtime V2 state root from available context.
|
|
2904
|
+
*
|
|
2905
|
+
* The state root is where `.pi/runtime/` artifacts live. In workspace
|
|
2906
|
+
* mode this is the workspace root; in repo mode it's the repo root.
|
|
2907
|
+
*
|
|
2908
|
+
* This centralizes the resolution so Runtime V2 code doesn't need
|
|
2909
|
+
* to repeat the workspace-vs-repo logic.
|
|
2910
|
+
*
|
|
2911
|
+
* @param repoRoot - Main repository root
|
|
2912
|
+
* @param workspaceRoot - Workspace root (undefined in repo mode)
|
|
2913
|
+
* @returns Absolute path to use as the state root for .pi/ artifacts
|
|
2914
|
+
*
|
|
2915
|
+
* @since TP-102
|
|
2916
|
+
*/
|
|
2917
|
+
export function resolveRuntimeStateRoot(
|
|
2918
|
+
repoRoot: string,
|
|
2919
|
+
workspaceRoot?: string,
|
|
2920
|
+
): string {
|
|
2921
|
+
return workspaceRoot ?? repoRoot;
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
// ── Runtime V2 Lane Execution (TP-105) ────────────────────────────
|
|
2925
|
+
|
|
2926
|
+
import { executeTaskV2, type LaneRunnerConfig, type LaneRunnerTaskResult } from "./lane-runner.ts";
|
|
2927
|
+
|
|
2928
|
+
/**
|
|
2929
|
+
* Execute a lane using the Runtime V2 headless backend.
|
|
2930
|
+
*
|
|
2931
|
+
* This replaces the legacy TMUX-backed `executeLane()` for lanes that
|
|
2932
|
+
* should run on the new direct-child architecture. It uses the
|
|
2933
|
+
* lane-runner module which spawns workers via agent-host.ts instead
|
|
2934
|
+
* of TMUX sessions.
|
|
2935
|
+
*
|
|
2936
|
+
* The function signature is deliberately close to the legacy
|
|
2937
|
+
* `executeLane()` to minimize integration churn in the engine.
|
|
2938
|
+
* The key difference: no TMUX sessions are created.
|
|
2939
|
+
*
|
|
2940
|
+
* @since TP-105
|
|
2941
|
+
*/
|
|
2942
|
+
export async function executeLaneV2(
|
|
2943
|
+
lane: AllocatedLane,
|
|
2944
|
+
config: OrchestratorConfig,
|
|
2945
|
+
repoRoot: string,
|
|
2946
|
+
pauseSignal: { paused: boolean },
|
|
2947
|
+
workspaceRoot?: string,
|
|
2948
|
+
isWorkspaceMode?: boolean,
|
|
2949
|
+
extraEnvVars?: Record<string, string>,
|
|
2950
|
+
onSupervisorAlert?: SupervisorAlertCallback,
|
|
2951
|
+
): Promise<LaneExecutionResult> {
|
|
2952
|
+
const laneId = lane.laneId;
|
|
2953
|
+
const laneStartTime = Date.now();
|
|
2954
|
+
const outcomes: LaneTaskOutcome[] = [];
|
|
2955
|
+
let shouldSkipRemaining = false;
|
|
2956
|
+
|
|
2957
|
+
const stateRoot = resolveRuntimeStateRoot(repoRoot, workspaceRoot);
|
|
2958
|
+
const batchId = config.orchestrator?.batchId || extraEnvVars?.ORCH_BATCH_ID || String(Date.now());
|
|
2959
|
+
|
|
2960
|
+
// Build agent ID prefix from orchestrator config
|
|
2961
|
+
const tmuxPrefix = config.orchestrator?.tmux_prefix ?? "orch";
|
|
2962
|
+
const opId = config.orchestrator?.operatorId || "op";
|
|
2963
|
+
const agentIdPrefix = `${tmuxPrefix}-${opId}`;
|
|
2964
|
+
|
|
2965
|
+
// Load worker agent definition for system prompt
|
|
2966
|
+
let workerSystemPrompt = "You are a task execution agent. Read STATUS.md first, find unchecked items, work on them, checkpoint after each.";
|
|
2967
|
+
try {
|
|
2968
|
+
const agentPath = join(stateRoot, ".pi", "agents", "task-worker.md");
|
|
2969
|
+
if (existsSync(agentPath)) {
|
|
2970
|
+
const raw = readFileSync(agentPath, "utf-8");
|
|
2971
|
+
const fmEnd = raw.indexOf("---", 4);
|
|
2972
|
+
if (fmEnd > 0) {
|
|
2973
|
+
workerSystemPrompt = raw.slice(fmEnd + 3).trim();
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
2976
|
+
} catch { /* use default */ }
|
|
2977
|
+
|
|
2978
|
+
execLog(laneId, "LANE", `starting Runtime V2 execution of ${lane.tasks.length} task(s)`, {
|
|
2979
|
+
worktree: lane.worktreePath,
|
|
2980
|
+
agentPrefix: agentIdPrefix,
|
|
2981
|
+
});
|
|
2982
|
+
|
|
2983
|
+
for (const task of lane.tasks) {
|
|
2984
|
+
if (shouldSkipRemaining || pauseSignal.paused) {
|
|
2985
|
+
const reason = pauseSignal.paused ? "Skipped due to pause signal" : "Skipped due to prior task failure in lane";
|
|
2986
|
+
outcomes.push({
|
|
2987
|
+
taskId: task.taskId,
|
|
2988
|
+
status: "skipped",
|
|
2989
|
+
startTime: null,
|
|
2990
|
+
endTime: null,
|
|
2991
|
+
exitReason: reason,
|
|
2992
|
+
sessionName: buildRuntimeAgentId(agentIdPrefix, lane.laneNumber, "worker"),
|
|
2993
|
+
doneFileFound: false,
|
|
2994
|
+
});
|
|
2995
|
+
continue;
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2998
|
+
// Build execution unit
|
|
2999
|
+
const unit = buildExecutionUnit(lane, task, repoRoot, isWorkspaceMode);
|
|
3000
|
+
|
|
3001
|
+
const laneRunnerConfig: LaneRunnerConfig = {
|
|
3002
|
+
batchId,
|
|
3003
|
+
agentIdPrefix,
|
|
3004
|
+
laneNumber: lane.laneNumber,
|
|
3005
|
+
worktreePath: lane.worktreePath,
|
|
3006
|
+
branch: lane.branch,
|
|
3007
|
+
repoId: lane.repoId ?? "default",
|
|
3008
|
+
stateRoot,
|
|
3009
|
+
workerModel: "",
|
|
3010
|
+
workerTools: "read,write,edit,bash,grep,find,ls",
|
|
3011
|
+
workerThinking: "",
|
|
3012
|
+
workerSystemPrompt,
|
|
3013
|
+
maxIterations: 20,
|
|
3014
|
+
noProgressLimit: 3,
|
|
3015
|
+
maxWorkerMinutes: config.failure?.maxWorkerMinutes || 30,
|
|
3016
|
+
warnPercent: 85,
|
|
3017
|
+
killPercent: 95,
|
|
3018
|
+
onSupervisorAlert,
|
|
3019
|
+
};
|
|
3020
|
+
|
|
3021
|
+
try {
|
|
3022
|
+
const result = await executeTaskV2(unit, laneRunnerConfig, pauseSignal);
|
|
3023
|
+
outcomes.push(result.outcome);
|
|
3024
|
+
|
|
3025
|
+
// Commit artifacts after success (same as legacy path)
|
|
3026
|
+
if (result.outcome.status === "succeeded") {
|
|
3027
|
+
commitTaskArtifacts(lane, task, laneId);
|
|
3028
|
+
// Reset worktree for next task
|
|
3029
|
+
if (lane.tasks.indexOf(task) < lane.tasks.length - 1) {
|
|
3030
|
+
runGit(["checkout", "--", "."], lane.worktreePath);
|
|
3031
|
+
runGit(["clean", "-fd"], lane.worktreePath);
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
|
|
3035
|
+
if (result.outcome.status === "failed" || result.outcome.status === "stalled") {
|
|
3036
|
+
shouldSkipRemaining = true;
|
|
3037
|
+
}
|
|
3038
|
+
} catch (err: unknown) {
|
|
3039
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
3040
|
+
execLog(laneId, task.taskId, `Runtime V2 execution error: ${errMsg}`);
|
|
3041
|
+
outcomes.push({
|
|
3042
|
+
taskId: task.taskId,
|
|
3043
|
+
status: "failed",
|
|
3044
|
+
startTime: Date.now(),
|
|
3045
|
+
endTime: Date.now(),
|
|
3046
|
+
exitReason: `Runtime V2 execution error: ${errMsg}`,
|
|
3047
|
+
sessionName: buildRuntimeAgentId(agentIdPrefix, lane.laneNumber, "worker"),
|
|
3048
|
+
doneFileFound: false,
|
|
3049
|
+
});
|
|
3050
|
+
shouldSkipRemaining = true;
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
const endTime = Date.now();
|
|
3055
|
+
const succeeded = outcomes.every(o => o.status === "succeeded");
|
|
3056
|
+
const failed = outcomes.some(o => o.status === "failed" || o.status === "stalled");
|
|
3057
|
+
|
|
3058
|
+
return {
|
|
3059
|
+
laneNumber: lane.laneNumber,
|
|
3060
|
+
laneId,
|
|
3061
|
+
tasks: outcomes,
|
|
3062
|
+
overallStatus: succeeded ? "succeeded" : failed ? "failed" : "partial",
|
|
3063
|
+
startTime: laneStartTime,
|
|
3064
|
+
endTime,
|
|
3065
|
+
};
|
|
3066
|
+
}
|
|
3067
|
+
|
|
2674
3068
|
// ── /orch Command — Full Execution (Step 5) ─────────────────────────
|
|
2675
3069
|
|