taskplane 0.29.2 → 0.30.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.
Files changed (41) hide show
  1. package/bin/gitignore-patterns.mjs +11 -8
  2. package/bin/rpc-wrapper.mjs +410 -357
  3. package/bin/taskplane.mjs +533 -250
  4. package/extensions/reviewer-extension.ts +17 -11
  5. package/extensions/taskplane/abort.ts +50 -18
  6. package/extensions/taskplane/agent-bridge-extension.ts +232 -105
  7. package/extensions/taskplane/agent-host.ts +224 -97
  8. package/extensions/taskplane/cleanup.ts +71 -42
  9. package/extensions/taskplane/config-loader.ts +142 -58
  10. package/extensions/taskplane/config-schema.ts +6 -13
  11. package/extensions/taskplane/config.ts +10 -2
  12. package/extensions/taskplane/diagnostic-reports.ts +59 -47
  13. package/extensions/taskplane/diagnostics.ts +13 -13
  14. package/extensions/taskplane/discovery.ts +35 -61
  15. package/extensions/taskplane/engine-worker.ts +53 -46
  16. package/extensions/taskplane/engine.ts +1760 -602
  17. package/extensions/taskplane/execution.ts +426 -206
  18. package/extensions/taskplane/extension.ts +1073 -598
  19. package/extensions/taskplane/formatting.ts +136 -124
  20. package/extensions/taskplane/git.ts +0 -2
  21. package/extensions/taskplane/lane-runner.ts +542 -311
  22. package/extensions/taskplane/mailbox.ts +57 -49
  23. package/extensions/taskplane/merge.ts +662 -383
  24. package/extensions/taskplane/messages.ts +109 -51
  25. package/extensions/taskplane/migrations.ts +1 -1
  26. package/extensions/taskplane/path-resolver.ts +8 -9
  27. package/extensions/taskplane/persistence.ts +425 -262
  28. package/extensions/taskplane/process-registry.ts +36 -7
  29. package/extensions/taskplane/quality-gate.ts +107 -55
  30. package/extensions/taskplane/resume.ts +774 -267
  31. package/extensions/taskplane/sessions.ts +1 -1
  32. package/extensions/taskplane/settings-tui.ts +505 -164
  33. package/extensions/taskplane/sidecar-telemetry.ts +25 -10
  34. package/extensions/taskplane/supervisor.ts +477 -270
  35. package/extensions/taskplane/task-executor-core.ts +178 -53
  36. package/extensions/taskplane/types.ts +186 -108
  37. package/extensions/taskplane/verification.ts +27 -22
  38. package/extensions/taskplane/waves.ts +59 -43
  39. package/extensions/taskplane/workspace.ts +14 -12
  40. package/extensions/taskplane/worktree.ts +218 -196
  41. package/package.json +14 -2
@@ -2,16 +2,61 @@
2
2
  * Lane execution, monitoring, wave execution loop
3
3
  * @module orch/execution
4
4
  */
5
- import { readFileSync, existsSync, statSync, unlinkSync, mkdirSync, writeFileSync, copyFileSync } from "fs";
5
+ import {
6
+ readFileSync,
7
+ existsSync,
8
+ statSync,
9
+ unlinkSync,
10
+ mkdirSync,
11
+ writeFileSync,
12
+ copyFileSync,
13
+ } from "fs";
6
14
  import { access as fsAccess, readFile as fsReadFile, stat as fsStat } from "fs/promises";
7
15
  import { join, dirname, basename, resolve, relative, delimiter as pathDelimiter } from "path";
8
16
  import { userInfo } from "os";
9
17
 
10
- import { DONE_GRACE_MS, EXECUTION_POLL_INTERVAL_MS, ExecutionError, SESSION_SPAWN_RETRY_MAX } from "./types.ts";
11
- import type { AllocatedLane, AllocatedTask, DependencyGraph, LaneExecutionResult, LaneMonitorSnapshot, LaneTaskOutcome, LaneTaskStatus, MonitorState, MtimeTracker, OrchestratorConfig, ParsedTask, TaskMonitorSnapshot, WaveExecutionResult, WorkspaceConfig, ExecutionUnit, PacketPaths, RuntimeAgentId, RuntimeAgentRole, RuntimeLaneSnapshot, SupervisorAlertCallback } from "./types.ts";
18
+ import {
19
+ DONE_GRACE_MS,
20
+ EXECUTION_POLL_INTERVAL_MS,
21
+ ExecutionError,
22
+ SESSION_SPAWN_RETRY_MAX,
23
+ } from "./types.ts";
24
+ import type {
25
+ AllocatedLane,
26
+ AllocatedTask,
27
+ DependencyGraph,
28
+ LaneExecutionResult,
29
+ LaneMonitorSnapshot,
30
+ LaneTaskOutcome,
31
+ LaneTaskStatus,
32
+ MonitorState,
33
+ MtimeTracker,
34
+ OrchestratorConfig,
35
+ ParsedTask,
36
+ TaskMonitorSnapshot,
37
+ WaveExecutionResult,
38
+ WorkspaceConfig,
39
+ ExecutionUnit,
40
+ PacketPaths,
41
+ RuntimeAgentId,
42
+ RuntimeAgentRole,
43
+ RuntimeLaneSnapshot,
44
+ RuntimeRegistry,
45
+ SupervisorAlertCallback,
46
+ } from "./types.ts";
12
47
  import { resolvePacketPaths, buildRuntimeAgentId } from "./types.ts";
13
48
  import type { TaskExitDiagnostic } from "./diagnostics.ts";
14
- import { readRegistrySnapshot, readLaneSnapshot, isTerminalStatus, isProcessAlive, detectOrphans, markOrphansCrashed, buildRegistrySnapshot, writeRegistrySnapshot, writeLaneSnapshot } from "./process-registry.ts";
49
+ import {
50
+ readRegistrySnapshot,
51
+ readLaneSnapshot,
52
+ isTerminalStatus,
53
+ isProcessAlive,
54
+ detectOrphans,
55
+ markOrphansCrashed,
56
+ buildRegistrySnapshot,
57
+ writeRegistrySnapshot,
58
+ writeLaneSnapshot,
59
+ } from "./process-registry.ts";
15
60
  import { allocateLanes } from "./waves.ts";
16
61
  import { resolveOperatorId } from "./naming.ts";
17
62
  import { runGit, runGitWithEnv } from "./git.ts";
@@ -51,7 +96,15 @@ export function execLog(
51
96
  laneId: string,
52
97
  taskId: string,
53
98
  message: string,
54
- extra?: Record<string, string | number | boolean>,
99
+ // TP-195: widened from `Record<string, string|number|boolean>` to
100
+ // `Record<string, unknown>` so callers can pass structured values
101
+ // (string[] arrays, repo objects, etc.) without TS errors. Runtime
102
+ // stringification via `${v}` is unchanged: primitives render as today,
103
+ // arrays render with comma separators (existing behavior), objects
104
+ // render as `[object Object]` (already today's behavior — see
105
+ // historic execLog calls in engine.ts/resume.ts that have always been
106
+ // passing structured payloads). No runtime change.
107
+ extra?: Record<string, unknown>,
55
108
  ): void {
56
109
  const prefix = `[orch] ${laneId}/${taskId}`;
57
110
  if (extra) {
@@ -74,7 +127,11 @@ export function execLog(
74
127
  * @returns true if agent is alive
75
128
  * @since TP-112
76
129
  */
77
- export function isV2AgentAlive(agentIdOrSessionName: string, _runtimeBackend?: RuntimeBackend, laneNumber?: number): boolean {
130
+ export function isV2AgentAlive(
131
+ agentIdOrSessionName: string,
132
+ _runtimeBackend?: RuntimeBackend,
133
+ laneNumber?: number,
134
+ ): boolean {
78
135
  // Read the registry from the global state root.
79
136
  // Since this is a pure liveness check, we scan for matching agentId
80
137
  // patterns: direct match, or lane-session + "-worker" suffix.
@@ -85,15 +142,24 @@ export function isV2AgentAlive(agentIdOrSessionName: string, _runtimeBackend?: R
85
142
  if (manifest && !isTerminalStatus(manifest.status) && isProcessAlive(manifest.pid)) return true;
86
143
  // Try worker suffix (monitor uses lane session name, registry uses agentId)
87
144
  const workerManifest = agents[`${agentIdOrSessionName}-worker`];
88
- if (workerManifest && !isTerminalStatus(workerManifest.status) && isProcessAlive(workerManifest.pid)) return true;
145
+ if (
146
+ workerManifest &&
147
+ !isTerminalStatus(workerManifest.status) &&
148
+ isProcessAlive(workerManifest.pid)
149
+ )
150
+ return true;
89
151
  // TP-148: In workspace mode, laneSessionId includes repoId and uses a local
90
152
  // lane number (e.g., "orch-henry-api-lane-1") while the V2 registry uses
91
153
  // global lane numbers without repoId (e.g., "orch-henry-lane-3-worker").
92
154
  // Fall back to scanning the registry by global lane number when provided.
93
155
  if (laneNumber != null) {
94
156
  for (const agent of Object.values(agents)) {
95
- if (agent.laneNumber === laneNumber && agent.role === "worker" &&
96
- !isTerminalStatus(agent.status) && isProcessAlive(agent.pid)) {
157
+ if (
158
+ agent.laneNumber === laneNumber &&
159
+ agent.role === "worker" &&
160
+ !isTerminalStatus(agent.status) &&
161
+ isProcessAlive(agent.pid)
162
+ ) {
97
163
  return true;
98
164
  }
99
165
  }
@@ -102,14 +168,14 @@ export function isV2AgentAlive(agentIdOrSessionName: string, _runtimeBackend?: R
102
168
  }
103
169
 
104
170
  /** Cached registry for V2 liveness checks within a monitor cycle. @since TP-112 */
105
- let _v2LivenessRegistryCache: import("./process-registry.ts").RuntimeRegistry | null = null;
171
+ let _v2LivenessRegistryCache: RuntimeRegistry | null = null;
106
172
 
107
173
  /**
108
174
  * Set the V2 liveness registry cache for the current monitor cycle.
109
175
  * Called at the start of each monitor poll to avoid re-reading the file per-task.
110
176
  * @since TP-112
111
177
  */
112
- export function setV2LivenessRegistryCache(registry: import("./process-registry.ts").RuntimeRegistry | null): void {
178
+ export function setV2LivenessRegistryCache(registry: RuntimeRegistry | null): void {
113
179
  _v2LivenessRegistryCache = registry;
114
180
  }
115
181
 
@@ -125,11 +191,11 @@ export function killV2LaneAgents(
125
191
  sessionName: string,
126
192
  options?: { stateRoot?: string; batchId?: string; logContext?: string; laneNumber?: number },
127
193
  ): void {
128
- const registry = _v2LivenessRegistryCache ?? (
129
- options?.stateRoot && options?.batchId
194
+ const registry =
195
+ _v2LivenessRegistryCache ??
196
+ (options?.stateRoot && options?.batchId
130
197
  ? readRegistrySnapshot(options.stateRoot, options.batchId)
131
- : null
132
- );
198
+ : null);
133
199
  if (!registry) return;
134
200
 
135
201
  const agents = registry.agents;
@@ -138,25 +204,38 @@ export function killV2LaneAgents(
138
204
  for (const suffix of ["-worker", "-reviewer", ""]) {
139
205
  const key = `${sessionName}${suffix}`;
140
206
  const manifest = agents[key];
141
- if (manifest && !isTerminalStatus(manifest.status) && isProcessAlive(manifest.pid) && !killedPids.has(manifest.pid)) {
207
+ if (
208
+ manifest &&
209
+ !isTerminalStatus(manifest.status) &&
210
+ isProcessAlive(manifest.pid) &&
211
+ !killedPids.has(manifest.pid)
212
+ ) {
142
213
  try {
143
214
  process.kill(manifest.pid, "SIGTERM");
144
215
  killedPids.add(manifest.pid);
145
216
  execLog(logContext, key, `killed V2 agent (PID ${manifest.pid})`);
146
- } catch { /* already dead */ }
217
+ } catch {
218
+ /* already dead */
219
+ }
147
220
  }
148
221
  }
149
222
  // TP-148: Workspace-mode fallback — match by global lane number when
150
223
  // session name lookup misses (repoId/local-vs-global lane mismatch).
151
224
  if (options?.laneNumber != null) {
152
225
  for (const agent of Object.values(agents)) {
153
- if (agent.laneNumber === options.laneNumber &&
154
- !isTerminalStatus(agent.status) && isProcessAlive(agent.pid) && !killedPids.has(agent.pid)) {
226
+ if (
227
+ agent.laneNumber === options.laneNumber &&
228
+ !isTerminalStatus(agent.status) &&
229
+ isProcessAlive(agent.pid) &&
230
+ !killedPids.has(agent.pid)
231
+ ) {
155
232
  try {
156
233
  process.kill(agent.pid, "SIGTERM");
157
234
  killedPids.add(agent.pid);
158
235
  execLog(logContext, agent.agentId, `killed V2 agent by lane number (PID ${agent.pid})`);
159
- } catch { /* already dead */ }
236
+ } catch {
237
+ /* already dead */
238
+ }
160
239
  }
161
240
  }
162
241
  }
@@ -164,7 +243,6 @@ export function killV2LaneAgents(
164
243
 
165
244
  // ── Async File/Status Helpers (TP-070) ───────────────────────────────
166
245
 
167
-
168
246
  /**
169
247
  * Async version of readTaskStatusTail — reads STATUS.md tail without
170
248
  * blocking the event loop.
@@ -215,10 +293,7 @@ function laneSessionIdOf(lane: Pick<AllocatedLane, "laneSessionId">): string {
215
293
  * Logs are written under the lane worktree to keep per-lane execution
216
294
  * artifacts colocated with task state and available after failures.
217
295
  */
218
- export function resolveLaneLogPath(
219
- lane: AllocatedLane,
220
- task: AllocatedTask,
221
- ): string {
296
+ export function resolveLaneLogPath(lane: AllocatedLane, task: AllocatedTask): string {
222
297
  return join(lane.worktreePath, ".pi", "orch-logs", `${laneSessionIdOf(lane)}-${task.taskId}.log`);
223
298
  }
224
299
 
@@ -227,10 +302,7 @@ export function resolveLaneLogPath(
227
302
  *
228
303
  * Relative paths avoid Windows drive-letter parsing issues in shell redirection.
229
304
  */
230
- export function resolveLaneLogRelativePath(
231
- lane: AllocatedLane,
232
- task: AllocatedTask,
233
- ): string {
305
+ export function resolveLaneLogRelativePath(lane: AllocatedLane, task: AllocatedTask): string {
234
306
  return join(".pi", "orch-logs", `${laneSessionIdOf(lane)}-${task.taskId}.log`).replace(/\\/g, "/");
235
307
  }
236
308
 
@@ -449,7 +521,6 @@ export function resolveTaskDonePath(
449
521
  return resolveCanonicalTaskPaths(taskFolder, worktreePath, repoRoot, isWorkspaceMode).donePath;
450
522
  }
451
523
 
452
-
453
524
  /*
454
525
  * Removed in TP-120 while decommissioning the legacy session backend.
455
526
  *
@@ -466,7 +537,11 @@ export async function pollUntilTaskComplete(
466
537
  _pauseSignal: { paused: boolean },
467
538
  _isWorkspaceMode?: boolean,
468
539
  ): Promise<{ status: LaneTaskStatus; exitReason: string; doneFileFound: boolean }> {
469
- return { status: "failed", exitReason: "Legacy pollUntilTaskComplete removed — use V2 lane-runner", doneFileFound: false };
540
+ return {
541
+ status: "failed",
542
+ exitReason: "Legacy pollUntilTaskComplete removed — use V2 lane-runner",
543
+ doneFileFound: false,
544
+ };
470
545
  }
471
546
 
472
547
  // ── Post-Task Commit ─────────────────────────────────────────────────
@@ -485,11 +560,7 @@ export async function pollUntilTaskComplete(
485
560
  * @param task - The task that just completed
486
561
  * @param laneId - Lane identifier for logging
487
562
  */
488
- function commitTaskArtifacts(
489
- lane: AllocatedLane,
490
- task: AllocatedTask,
491
- laneId: string,
492
- ): void {
563
+ function commitTaskArtifacts(lane: AllocatedLane, task: AllocatedTask, laneId: string): void {
493
564
  const worktreePath = lane.worktreePath;
494
565
 
495
566
  // Check if there are any uncommitted changes in the worktree
@@ -502,7 +573,11 @@ function commitTaskArtifacts(
502
573
  // Stage all changes in the worktree
503
574
  const addResult = runGit(["add", "-A"], worktreePath);
504
575
  if (!addResult.ok) {
505
- execLog(laneId, task.taskId, `post-task stage failed (non-fatal): ${addResult.stderr.slice(0, 200)}`);
576
+ execLog(
577
+ laneId,
578
+ task.taskId,
579
+ `post-task stage failed (non-fatal): ${addResult.stderr.slice(0, 200)}`,
580
+ );
506
581
  return;
507
582
  }
508
583
 
@@ -514,7 +589,11 @@ function commitTaskArtifacts(
514
589
  if (!commitResult.ok) {
515
590
  // "nothing to commit" is not an error — worker may have already committed
516
591
  if (!commitResult.stderr.includes("nothing to commit")) {
517
- execLog(laneId, task.taskId, `post-task commit failed (non-fatal): ${commitResult.stderr.slice(0, 200)}`);
592
+ execLog(
593
+ laneId,
594
+ task.taskId,
595
+ `post-task commit failed (non-fatal): ${commitResult.stderr.slice(0, 200)}`,
596
+ );
518
597
  }
519
598
  return;
520
599
  }
@@ -524,9 +603,6 @@ function commitTaskArtifacts(
524
603
  });
525
604
  }
526
605
 
527
-
528
-
529
-
530
606
  // ── STATUS.md Parsing for Worktree ───────────────────────────────────
531
607
 
532
608
  /**
@@ -583,7 +659,10 @@ export function parseWorktreeStatusMd(
583
659
  content = readFileSync(statusPath, "utf-8");
584
660
  mtime = statSync(statusPath).mtimeMs;
585
661
  } catch (err: unknown) {
586
- return { parsed: null, error: `Cannot read STATUS.md: ${err instanceof Error ? err.message : String(err)}` };
662
+ return {
663
+ parsed: null,
664
+ error: `Cannot read STATUS.md: ${err instanceof Error ? err.message : String(err)}`,
665
+ };
587
666
  }
588
667
 
589
668
  // Parse using same regex patterns as task-runner's parseStatusMd
@@ -607,7 +686,7 @@ export function parseWorktreeStatusMd(
607
686
  const stepMatch = line.match(/^###\s+Step\s+(\d+):\s*(.+)/);
608
687
  if (stepMatch) {
609
688
  if (currentStep) {
610
- const totalChecked = currentStep.checkboxes.filter(c => c).length;
689
+ const totalChecked = currentStep.checkboxes.filter((c) => c).length;
611
690
  steps.push({
612
691
  number: currentStep.number,
613
692
  name: currentStep.name,
@@ -641,7 +720,7 @@ export function parseWorktreeStatusMd(
641
720
  }
642
721
  }
643
722
  if (currentStep) {
644
- const totalChecked = currentStep.checkboxes.filter(c => c).length;
723
+ const totalChecked = currentStep.checkboxes.filter((c) => c).length;
645
724
  steps.push({
646
725
  number: currentStep.number,
647
726
  name: currentStep.name,
@@ -708,7 +787,10 @@ async function parseStatusMdContent(
708
787
  content = await fsReadFile(statusPath, "utf-8");
709
788
  mtime = (await fsStat(statusPath)).mtimeMs;
710
789
  } catch (err: unknown) {
711
- return { parsed: null, error: `Cannot read STATUS.md: ${err instanceof Error ? err.message : String(err)}` };
790
+ return {
791
+ parsed: null,
792
+ error: `Cannot read STATUS.md: ${err instanceof Error ? err.message : String(err)}`,
793
+ };
712
794
  }
713
795
 
714
796
  // Parse logic is identical to the sync version
@@ -732,7 +814,7 @@ async function parseStatusMdContent(
732
814
  const stepMatch = line.match(/^###\s+Step\s+(\d+):\s*(.+)/);
733
815
  if (stepMatch) {
734
816
  if (currentStep) {
735
- const totalChecked = currentStep.checkboxes.filter(c => c).length;
817
+ const totalChecked = currentStep.checkboxes.filter((c) => c).length;
736
818
  steps.push({
737
819
  number: currentStep.number,
738
820
  name: currentStep.name,
@@ -766,7 +848,7 @@ async function parseStatusMdContent(
766
848
  }
767
849
  }
768
850
  if (currentStep) {
769
- const totalChecked = currentStep.checkboxes.filter(c => c).length;
851
+ const totalChecked = currentStep.checkboxes.filter((c) => c).length;
770
852
  steps.push({
771
853
  number: currentStep.number,
772
854
  name: currentStep.name,
@@ -782,7 +864,6 @@ async function parseStatusMdContent(
782
864
  };
783
865
  }
784
866
 
785
-
786
867
  // ── State Resolution ─────────────────────────────────────────────────
787
868
 
788
869
  /**
@@ -829,7 +910,7 @@ export async function resolveTaskMonitorState(
829
910
  // Snapshot not written yet OR snapshot still points to a prior task.
830
911
  // Assume alive initially, but if stale for >30s consult the registry
831
912
  // to avoid indefinite false "running" if the lane-runner died.
832
- const staleMs = snap?.updatedAt ? (now - snap.updatedAt) : 0;
913
+ const staleMs = snap?.updatedAt ? now - snap.updatedAt : 0;
833
914
  const trackerAgeMs = now - tracker.firstObservedAt;
834
915
  if (staleMs > 30_000) {
835
916
  // Snapshot hasn't been updated for 30s+ — check registry as fallback.
@@ -875,7 +956,7 @@ export async function resolveTaskMonitorState(
875
956
  const trackerAgeMs = now - tracker.firstObservedAt;
876
957
  if (
877
958
  snap.updatedAt &&
878
- (now - snap.updatedAt) > stallTimeoutMs / 2 &&
959
+ now - snap.updatedAt > stallTimeoutMs / 2 &&
879
960
  trackerAgeMs >= 60_000 &&
880
961
  !isV2AgentAlive(sessionName, runtimeBackend, v2Context?.laneNumber)
881
962
  ) {
@@ -918,13 +999,13 @@ export async function resolveTaskMonitorState(
918
999
  }
919
1000
 
920
1001
  // Find the current step (first in-progress, or first not-started after last complete)
921
- const inProgress = steps.find(s => s.status === "in-progress");
1002
+ const inProgress = steps.find((s) => s.status === "in-progress");
922
1003
  if (inProgress) {
923
1004
  currentStepName = inProgress.name;
924
1005
  currentStepNumber = inProgress.number;
925
1006
  } else {
926
1007
  // Find first not-started step
927
- const notStarted = steps.find(s => s.status === "not-started");
1008
+ const notStarted = steps.find((s) => s.status === "not-started");
928
1009
  if (notStarted) {
929
1010
  currentStepName = notStarted.name;
930
1011
  currentStepNumber = notStarted.number;
@@ -979,7 +1060,7 @@ export async function resolveTaskMonitorState(
979
1060
  sessionAlive &&
980
1061
  tracker.statusFileSeenOnce &&
981
1062
  tracker.stallTimerStart !== null &&
982
- (now - tracker.stallTimerStart) >= stallTimeoutMs
1063
+ now - tracker.stallTimerStart >= stallTimeoutMs
983
1064
  ) {
984
1065
  const stallMinutes = Math.round((now - tracker.stallTimerStart) / 60_000);
985
1066
  const stallReason = `STATUS.md unchanged for ${stallMinutes} minutes (threshold: ${Math.round(stallTimeoutMs / 60_000)} min)`;
@@ -1052,7 +1133,6 @@ export async function resolveTaskMonitorState(
1052
1133
  };
1053
1134
  }
1054
1135
 
1055
-
1056
1136
  // ── Core Monitor Loop ────────────────────────────────────────────────
1057
1137
 
1058
1138
  /**
@@ -1136,10 +1216,15 @@ export async function monitorLanes(
1136
1216
  // Build the total task count
1137
1217
  const tasksTotal = lanes.reduce((sum, lane) => sum + lane.tasks.length, 0);
1138
1218
 
1139
- execLog("monitor", "ALL", `starting monitoring for ${lanes.length} lane(s), ${tasksTotal} task(s)`, {
1140
- pollIntervalMs,
1141
- stallTimeoutMin: Math.round(stallTimeoutMs / 60_000),
1142
- });
1219
+ execLog(
1220
+ "monitor",
1221
+ "ALL",
1222
+ `starting monitoring for ${lanes.length} lane(s), ${tasksTotal} task(s)`,
1223
+ {
1224
+ pollIntervalMs,
1225
+ stallTimeoutMin: Math.round(stallTimeoutMs / 60_000),
1226
+ },
1227
+ );
1143
1228
 
1144
1229
  while (true) {
1145
1230
  const now = Date.now();
@@ -1239,17 +1324,23 @@ export async function monitorLanes(
1239
1324
  stallTimeoutMs,
1240
1325
  now,
1241
1326
  runtimeBackend,
1242
- (runtimeBackend === "v2" && batchId) ? {
1243
- stateRoot: stateRootForRegistry ?? repoRoot,
1244
- batchId,
1245
- laneNumber: lane.laneNumber,
1246
- } : undefined,
1327
+ runtimeBackend === "v2" && batchId
1328
+ ? {
1329
+ stateRoot: stateRootForRegistry ?? repoRoot,
1330
+ batchId,
1331
+ laneNumber: lane.laneNumber,
1332
+ }
1333
+ : undefined,
1247
1334
  );
1248
1335
 
1249
1336
  currentTaskSnapshot = snapshot;
1250
1337
 
1251
1338
  // Check if this task just became terminal
1252
- if (snapshot.status === "succeeded" || snapshot.status === "failed" || snapshot.status === "stalled") {
1339
+ if (
1340
+ snapshot.status === "succeeded" ||
1341
+ snapshot.status === "failed" ||
1342
+ snapshot.status === "stalled"
1343
+ ) {
1253
1344
  terminalTasks.set(task.taskId, snapshot);
1254
1345
  if (snapshot.status === "succeeded") {
1255
1346
  completedTasks.push(task.taskId);
@@ -1322,8 +1413,12 @@ export async function monitorLanes(
1322
1413
  // Log summary only on state changes (lane completes or fails) — not every poll
1323
1414
  const currentStateKey = `${totalDone}/${totalFailed}`;
1324
1415
  if (currentStateKey !== lastMonitorStateKey) {
1325
- const activeLanes = laneSnapshots.filter(l => l.currentTaskId !== null);
1326
- execLog("monitor", "ALL", `poll #${pollCount}: ${totalDone}/${tasksTotal} done, ${totalFailed} failed, ${activeLanes.length} active lane(s)`);
1416
+ const activeLanes = laneSnapshots.filter((l) => l.currentTaskId !== null);
1417
+ execLog(
1418
+ "monitor",
1419
+ "ALL",
1420
+ `poll #${pollCount}: ${totalDone}/${tasksTotal} done, ${totalFailed} failed, ${activeLanes.length} active lane(s)`,
1421
+ );
1327
1422
  lastMonitorStateKey = currentStateKey;
1328
1423
  }
1329
1424
 
@@ -1340,12 +1435,12 @@ export async function monitorLanes(
1340
1435
  }
1341
1436
 
1342
1437
  // Wait for next poll cycle
1343
- await new Promise(r => setTimeout(r, pollIntervalMs));
1438
+ await new Promise((r) => setTimeout(r, pollIntervalMs));
1344
1439
  }
1345
1440
 
1346
1441
  // Reached here due to pause signal — return current state
1347
1442
  const now = Date.now();
1348
- const laneSnapshots: LaneMonitorSnapshot[] = lanes.map(lane => ({
1443
+ const laneSnapshots: LaneMonitorSnapshot[] = lanes.map((lane) => ({
1349
1444
  laneId: lane.laneId,
1350
1445
  laneNumber: lane.laneNumber,
1351
1446
  sessionName: laneSessionIdOf(lane),
@@ -1354,7 +1449,7 @@ export async function monitorLanes(
1354
1449
  currentTaskSnapshot: null,
1355
1450
  completedTasks: [],
1356
1451
  failedTasks: [],
1357
- remainingTasks: lane.tasks.map(t => t.taskId),
1452
+ remainingTasks: lane.tasks.map((t) => t.taskId),
1358
1453
  }));
1359
1454
 
1360
1455
  setV2LivenessRegistryCache(null);
@@ -1370,7 +1465,6 @@ export async function monitorLanes(
1370
1465
  };
1371
1466
  }
1372
1467
 
1373
-
1374
1468
  // ── Transitive Dependent Computation ─────────────────────────────────
1375
1469
 
1376
1470
  /**
@@ -1414,7 +1508,6 @@ export function computeTransitiveDependents(
1414
1508
  return blocked;
1415
1509
  }
1416
1510
 
1417
-
1418
1511
  // ── Pre-flight: Commit Untracked Task Files ─────────────────────────
1419
1512
 
1420
1513
  /**
@@ -1499,29 +1592,31 @@ export function ensureTaskFilesCommitted(
1499
1592
 
1500
1593
  try {
1501
1594
  // Read orch branch tree into temporary index
1502
- const readTreeRes = runGitWithEnv(
1503
- ["read-tree", orchTip],
1504
- repoRoot,
1505
- { GIT_INDEX_FILE: tmpIdx },
1506
- );
1595
+ const readTreeRes = runGitWithEnv(["read-tree", orchTip], repoRoot, { GIT_INDEX_FILE: tmpIdx });
1507
1596
  if (!readTreeRes.ok) {
1508
- execLog("wave", `W${waveIndex}`, `orch branch staging: read-tree failed, falling back to HEAD commit`, {
1509
- error: readTreeRes.stderr,
1510
- });
1597
+ execLog(
1598
+ "wave",
1599
+ `W${waveIndex}`,
1600
+ `orch branch staging: read-tree failed, falling back to HEAD commit`,
1601
+ {
1602
+ error: readTreeRes.stderr,
1603
+ },
1604
+ );
1511
1605
  // Fall through to legacy path
1512
1606
  } else {
1513
1607
  // Add task files to temporary index
1514
1608
  let addFailed = false;
1515
1609
  for (const folder of foldersToStage) {
1516
- const addRes = runGitWithEnv(
1517
- ["add", "--", folder],
1518
- repoRoot,
1519
- { GIT_INDEX_FILE: tmpIdx },
1520
- );
1610
+ const addRes = runGitWithEnv(["add", "--", folder], repoRoot, { GIT_INDEX_FILE: tmpIdx });
1521
1611
  if (!addRes.ok) {
1522
- execLog("wave", `W${waveIndex}`, `orch branch staging: git add failed for ${folder}, falling back`, {
1523
- error: addRes.stderr,
1524
- });
1612
+ execLog(
1613
+ "wave",
1614
+ `W${waveIndex}`,
1615
+ `orch branch staging: git add failed for ${folder}, falling back`,
1616
+ {
1617
+ error: addRes.stderr,
1618
+ },
1619
+ );
1525
1620
  addFailed = true;
1526
1621
  break;
1527
1622
  }
@@ -1529,15 +1624,11 @@ export function ensureTaskFilesCommitted(
1529
1624
 
1530
1625
  if (!addFailed) {
1531
1626
  // Write tree from temporary index
1532
- const writeTreeRes = runGitWithEnv(
1533
- ["write-tree"],
1534
- repoRoot,
1535
- { GIT_INDEX_FILE: tmpIdx },
1536
- );
1627
+ const writeTreeRes = runGitWithEnv(["write-tree"], repoRoot, { GIT_INDEX_FILE: tmpIdx });
1537
1628
 
1538
1629
  if (writeTreeRes.ok) {
1539
1630
  const tree = writeTreeRes.stdout.trim();
1540
- const taskIds = foldersToStage.map(f => f.split("/").pop() || f).join(", ");
1631
+ const taskIds = foldersToStage.map((f) => f.split("/").pop() || f).join(", ");
1541
1632
  const commitMsg = `chore: stage task files for orchestrator wave ${waveIndex} (${taskIds})`;
1542
1633
 
1543
1634
  // Create commit directly on orch branch
@@ -1554,14 +1645,23 @@ export function ensureTaskFilesCommitted(
1554
1645
  );
1555
1646
 
1556
1647
  if (refUpdateRes.ok) {
1557
- execLog("wave", `W${waveIndex}`, `committed ${foldersToStage.length} task folder(s) directly on orch branch`, {
1558
- orchBranch,
1559
- folders: foldersToStage,
1560
- from: orchTip.slice(0, 8),
1561
- to: newCommit.slice(0, 8),
1562
- });
1648
+ execLog(
1649
+ "wave",
1650
+ `W${waveIndex}`,
1651
+ `committed ${foldersToStage.length} task folder(s) directly on orch branch`,
1652
+ {
1653
+ orchBranch,
1654
+ folders: foldersToStage,
1655
+ from: orchTip.slice(0, 8),
1656
+ to: newCommit.slice(0, 8),
1657
+ },
1658
+ );
1563
1659
  // Clean up temp index and return — no need for legacy path
1564
- try { unlinkSync(tmpIdx); } catch { /* best effort */ }
1660
+ try {
1661
+ unlinkSync(tmpIdx);
1662
+ } catch {
1663
+ /* best effort */
1664
+ }
1565
1665
  return;
1566
1666
  }
1567
1667
  execLog("wave", `W${waveIndex}`, `orch branch staging: ref update failed, falling back`, {
@@ -1580,12 +1680,21 @@ export function ensureTaskFilesCommitted(
1580
1680
  }
1581
1681
  }
1582
1682
  } catch (err: unknown) {
1583
- execLog("wave", `W${waveIndex}`, `orch branch staging: unexpected error, falling back to HEAD commit`, {
1584
- error: err instanceof Error ? err.message : String(err),
1585
- });
1683
+ execLog(
1684
+ "wave",
1685
+ `W${waveIndex}`,
1686
+ `orch branch staging: unexpected error, falling back to HEAD commit`,
1687
+ {
1688
+ error: err instanceof Error ? err.message : String(err),
1689
+ },
1690
+ );
1586
1691
  } finally {
1587
1692
  // Always clean up temp index
1588
- try { unlinkSync(tmpIdx); } catch { /* best effort */ }
1693
+ try {
1694
+ unlinkSync(tmpIdx);
1695
+ } catch {
1696
+ /* best effort */
1697
+ }
1589
1698
  }
1590
1699
  }
1591
1700
  }
@@ -1609,7 +1718,7 @@ export function ensureTaskFilesCommitted(
1609
1718
  }
1610
1719
 
1611
1720
  // Commit
1612
- const taskIds = foldersToStage.map(f => f.split("/").pop() || f).join(", ");
1721
+ const taskIds = foldersToStage.map((f) => f.split("/").pop() || f).join(", ");
1613
1722
  const commitMsg = `chore: stage task files for orchestrator wave ${waveIndex} (${taskIds})`;
1614
1723
  const commitResult = runGit(["commit", "-m", commitMsg], repoRoot);
1615
1724
  if (!commitResult.ok) {
@@ -1622,10 +1731,15 @@ export function ensureTaskFilesCommitted(
1622
1731
  );
1623
1732
  }
1624
1733
 
1625
- execLog("wave", `W${waveIndex}`, `committed ${foldersToStage.length} task folder(s) to ensure worktree visibility`, {
1626
- folders: foldersToStage,
1627
- commit: commitResult.stdout.trim().split("\n")[0],
1628
- });
1734
+ execLog(
1735
+ "wave",
1736
+ `W${waveIndex}`,
1737
+ `committed ${foldersToStage.length} task folder(s) to ensure worktree visibility`,
1738
+ {
1739
+ folders: foldersToStage,
1740
+ commit: commitResult.stdout.trim().split("\n")[0],
1741
+ },
1742
+ );
1629
1743
 
1630
1744
  // Fast-forward (or merge) the orch branch to include the staging commit so
1631
1745
  // that worktrees—which branch from orchBranch—see the new task files and
@@ -1639,10 +1753,7 @@ export function ensureTaskFilesCommitted(
1639
1753
  const newHead = headRes.stdout.trim();
1640
1754
  const orchTip = orchTipRes.stdout.trim();
1641
1755
 
1642
- const ancestorCheck = runGit(
1643
- ["merge-base", "--is-ancestor", orchTip, newHead],
1644
- repoRoot,
1645
- );
1756
+ const ancestorCheck = runGit(["merge-base", "--is-ancestor", orchTip, newHead], repoRoot);
1646
1757
 
1647
1758
  if (ancestorCheck.ok) {
1648
1759
  const ffResult = runGit(
@@ -1662,10 +1773,7 @@ export function ensureTaskFilesCommitted(
1662
1773
  });
1663
1774
  }
1664
1775
  } else {
1665
- const mergeTreeRes = runGit(
1666
- ["merge-tree", "--write-tree", orchTip, newHead],
1667
- repoRoot,
1668
- );
1776
+ const mergeTreeRes = runGit(["merge-tree", "--write-tree", orchTip, newHead], repoRoot);
1669
1777
  if (mergeTreeRes.ok) {
1670
1778
  const mergedTree = mergeTreeRes.stdout.trim().split("\n")[0];
1671
1779
  if (/^[0-9a-f]{40}$/i.test(mergedTree)) {
@@ -1692,10 +1800,15 @@ export function ensureTaskFilesCommitted(
1692
1800
  }
1693
1801
  }
1694
1802
  } catch (refErr: unknown) {
1695
- execLog("wave", `W${waveIndex}`, `warning: orch branch ref update threw unexpectedly (non-fatal)`, {
1696
- orchBranch,
1697
- error: refErr instanceof Error ? refErr.message : String(refErr),
1698
- });
1803
+ execLog(
1804
+ "wave",
1805
+ `W${waveIndex}`,
1806
+ `warning: orch branch ref update threw unexpectedly (non-fatal)`,
1807
+ {
1808
+ orchBranch,
1809
+ error: refErr instanceof Error ? refErr.message : String(refErr),
1810
+ },
1811
+ );
1699
1812
  }
1700
1813
  }
1701
1814
  }
@@ -1768,8 +1881,18 @@ export async function executeWave(
1768
1881
  runtimeBackend?: RuntimeBackend,
1769
1882
  onSupervisorAlert?: SupervisorAlertCallback,
1770
1883
  supervisorAutonomy: "interactive" | "supervised" | "autonomous" = "autonomous",
1771
- reviewerConfig?: { model?: string; thinking?: string; tools?: string; excludeExtensions?: string[] },
1772
- workerConfig?: { model?: string; thinking?: string; tools?: string; excludeExtensions?: string[] } | null,
1884
+ reviewerConfig?: {
1885
+ model?: string;
1886
+ thinking?: string;
1887
+ tools?: string;
1888
+ excludeExtensions?: string[];
1889
+ },
1890
+ workerConfig?: {
1891
+ model?: string;
1892
+ thinking?: string;
1893
+ tools?: string;
1894
+ excludeExtensions?: string[];
1895
+ } | null,
1773
1896
  workerExcludeExtensions?: string[],
1774
1897
  onLaneTerminated?: import("./types.ts").LaneTerminatedCallback,
1775
1898
  onLaneRespawned?: (laneNumber: number, agentId: string, batchId: string) => void,
@@ -1814,7 +1937,15 @@ export async function executeWave(
1814
1937
  }
1815
1938
 
1816
1939
  // ── Stage 1: Allocate lanes ──────────────────────────────────
1817
- const allocResult = allocateLanes(waveTasks, pending, config, repoRoot, batchId, orchBranch, workspaceConfig);
1940
+ const allocResult = allocateLanes(
1941
+ waveTasks,
1942
+ pending,
1943
+ config,
1944
+ repoRoot,
1945
+ batchId,
1946
+ orchBranch,
1947
+ workspaceConfig,
1948
+ );
1818
1949
 
1819
1950
  if (!allocResult.success) {
1820
1951
  const errMsg = allocResult.error?.message || "Unknown allocation failure";
@@ -1859,7 +1990,11 @@ export async function executeWave(
1859
1990
  const isWsMode = !!workspaceConfig;
1860
1991
  const backend: RuntimeBackend = "v2";
1861
1992
  if (runtimeBackend && runtimeBackend !== "v2") {
1862
- execLog("wave", `W${waveIndex}`, `legacy runtime backend '${runtimeBackend}' requested but ignored; using Runtime V2`);
1993
+ execLog(
1994
+ "wave",
1995
+ `W${waveIndex}`,
1996
+ `legacy runtime backend '${runtimeBackend}' requested but ignored; using Runtime V2`,
1997
+ );
1863
1998
  }
1864
1999
  execLog("wave", `W${waveIndex}`, "using Runtime V2 backend (executeLaneV2)");
1865
2000
 
@@ -1870,19 +2005,39 @@ export async function executeWave(
1870
2005
  const snapshotStateRoot = resolveRuntimeStateRoot(repoRoot, wsRoot);
1871
2006
  for (const lane of lanes) {
1872
2007
  try {
1873
- const snapPath = join(snapshotStateRoot, ".pi", "runtime", batchId, "lanes", `lane-${lane.laneNumber}.json`);
2008
+ const snapPath = join(
2009
+ snapshotStateRoot,
2010
+ ".pi",
2011
+ "runtime",
2012
+ batchId,
2013
+ "lanes",
2014
+ `lane-${lane.laneNumber}.json`,
2015
+ );
1874
2016
  if (existsSync(snapPath)) unlinkSync(snapPath);
1875
- } catch { /* best effort */ }
2017
+ } catch {
2018
+ /* best effort */
2019
+ }
1876
2020
  }
1877
2021
 
1878
- const lanePromises = lanes.map(lane =>
1879
- executeLaneV2(lane, config, repoRoot, wavePauseSignal, wsRoot, isWsMode, {
1880
- ORCH_BATCH_ID: batchId,
1881
- TASKPLANE_SUPERVISOR_AUTONOMY: supervisorAutonomy,
1882
- ...buildWorkerEnv(workerConfig),
1883
- ...buildReviewerEnv(reviewerConfig),
1884
- ...buildWorkerExcludeEnv(workerExcludeExtensions),
1885
- }, onSupervisorAlert, onLaneTerminated, onLaneRespawned),
2022
+ const lanePromises = lanes.map((lane) =>
2023
+ executeLaneV2(
2024
+ lane,
2025
+ config,
2026
+ repoRoot,
2027
+ wavePauseSignal,
2028
+ wsRoot,
2029
+ isWsMode,
2030
+ {
2031
+ ORCH_BATCH_ID: batchId,
2032
+ TASKPLANE_SUPERVISOR_AUTONOMY: supervisorAutonomy,
2033
+ ...buildWorkerEnv(workerConfig),
2034
+ ...buildReviewerEnv(reviewerConfig),
2035
+ ...buildWorkerExcludeEnv(workerExcludeExtensions),
2036
+ },
2037
+ onSupervisorAlert,
2038
+ onLaneTerminated,
2039
+ onLaneRespawned,
2040
+ ),
1886
2041
  );
1887
2042
 
1888
2043
  // Start monitoring as a sibling async loop
@@ -1929,7 +2084,7 @@ export async function executeWave(
1929
2084
  return {
1930
2085
  laneNumber: lanes[idx].laneNumber,
1931
2086
  laneId: lanes[idx].laneId,
1932
- tasks: lanes[idx].tasks.map(t => ({
2087
+ tasks: lanes[idx].tasks.map((t) => ({
1933
2088
  taskId: t.taskId,
1934
2089
  status: "failed" as LaneTaskStatus,
1935
2090
  startTime: null,
@@ -1947,8 +2102,8 @@ export async function executeWave(
1947
2102
 
1948
2103
  // For stop-wave: if any task failed, set pause to prevent next wave
1949
2104
  if (policy === "stop-wave") {
1950
- const hasFailure = laneResults.some(lr =>
1951
- lr.tasks.some(t => t.status === "failed" || t.status === "stalled"),
2105
+ const hasFailure = laneResults.some((lr) =>
2106
+ lr.tasks.some((t) => t.status === "failed" || t.status === "stalled"),
1952
2107
  );
1953
2108
  if (hasFailure) {
1954
2109
  wavePauseSignal.paused = true;
@@ -1991,21 +2146,24 @@ export async function executeWave(
1991
2146
  // Compute blocked tasks for future waves (skip-dependents policy)
1992
2147
  let blockedTaskIds: string[] = [];
1993
2148
  if (policy === "skip-dependents" && failedTaskIds.length > 0) {
1994
- const blocked = computeTransitiveDependents(
1995
- new Set(failedTaskIds),
1996
- dependencyGraph,
1997
- );
2149
+ const blocked = computeTransitiveDependents(new Set(failedTaskIds), dependencyGraph);
1998
2150
  blockedTaskIds = [...blocked].sort();
1999
2151
  if (blockedTaskIds.length > 0) {
2000
- execLog("wave", `W${waveIndex}`, `skip-dependents: ${blockedTaskIds.length} task(s) blocked for future waves`, {
2001
- blocked: blockedTaskIds.join(","),
2002
- });
2152
+ execLog(
2153
+ "wave",
2154
+ `W${waveIndex}`,
2155
+ `skip-dependents: ${blockedTaskIds.length} task(s) blocked for future waves`,
2156
+ {
2157
+ blocked: blockedTaskIds.join(","),
2158
+ },
2159
+ );
2003
2160
  }
2004
2161
  }
2005
2162
 
2006
2163
  // Determine overall wave status
2007
- const stoppedEarly = policy === "stop-all" && failedTaskIds.length > 0
2008
- || policy === "stop-wave" && failedTaskIds.length > 0;
2164
+ const stoppedEarly =
2165
+ (policy === "stop-all" && failedTaskIds.length > 0) ||
2166
+ (policy === "stop-wave" && failedTaskIds.length > 0);
2009
2167
 
2010
2168
  let overallStatus: WaveExecutionResult["overallStatus"];
2011
2169
  if (policy === "stop-all" && failedTaskIds.length > 0) {
@@ -2085,9 +2243,7 @@ export async function executeWithStopAll(
2085
2243
 
2086
2244
  // Check if any task failed
2087
2245
  if (!abortTriggered) {
2088
- const hasFailure = result.tasks.some(
2089
- t => t.status === "failed" || t.status === "stalled",
2090
- );
2246
+ const hasFailure = result.tasks.some((t) => t.status === "failed" || t.status === "stalled");
2091
2247
  if (hasFailure) {
2092
2248
  // First failure detected — trigger stop-all
2093
2249
  abortTriggered = true;
@@ -2095,7 +2251,7 @@ export async function executeWithStopAll(
2095
2251
 
2096
2252
  // Determine which task failed first for logging
2097
2253
  const firstFailed = result.tasks
2098
- .filter(t => t.status === "failed" || t.status === "stalled")
2254
+ .filter((t) => t.status === "failed" || t.status === "stalled")
2099
2255
  .sort((a, b) => {
2100
2256
  // Sort by startTime, then by taskId for deterministic tie-break
2101
2257
  const timeA = a.startTime || 0;
@@ -2104,9 +2260,14 @@ export async function executeWithStopAll(
2104
2260
  return a.taskId.localeCompare(b.taskId);
2105
2261
  })[0];
2106
2262
 
2107
- execLog("wave", `W${waveIndex}`, `stop-all triggered by ${firstFailed?.taskId || "unknown"} in ${lanes[idx].laneId}`, {
2108
- session: laneSessionIdOf(lanes[idx]),
2109
- });
2263
+ execLog(
2264
+ "wave",
2265
+ `W${waveIndex}`,
2266
+ `stop-all triggered by ${firstFailed?.taskId || "unknown"} in ${lanes[idx].laneId}`,
2267
+ {
2268
+ session: laneSessionIdOf(lanes[idx]),
2269
+ },
2270
+ );
2110
2271
 
2111
2272
  // Kill ALL lane sessions immediately
2112
2273
  for (const lane of lanes) {
@@ -2122,7 +2283,11 @@ export async function executeWithStopAll(
2122
2283
  if (!abortTriggered) {
2123
2284
  abortTriggered = true;
2124
2285
  pauseSignal.paused = true;
2125
- execLog("wave", `W${waveIndex}`, `stop-all triggered by lane error in ${lanes[idx].laneId}: ${errMsg}`);
2286
+ execLog(
2287
+ "wave",
2288
+ `W${waveIndex}`,
2289
+ `stop-all triggered by lane error in ${lanes[idx].laneId}: ${errMsg}`,
2290
+ );
2126
2291
  for (const lane of lanes) {
2127
2292
  killV2LaneAgents(laneSessionIdOf(lane), { laneNumber: lane.laneNumber });
2128
2293
  }
@@ -2132,7 +2297,7 @@ export async function executeWithStopAll(
2132
2297
  const failedResult: LaneExecutionResult = {
2133
2298
  laneNumber: lanes[idx].laneNumber,
2134
2299
  laneId: lanes[idx].laneId,
2135
- tasks: lanes[idx].tasks.map(t => ({
2300
+ tasks: lanes[idx].tasks.map((t) => ({
2136
2301
  taskId: t.taskId,
2137
2302
  status: "failed" as LaneTaskStatus,
2138
2303
  startTime: null,
@@ -2155,14 +2320,17 @@ export async function executeWithStopAll(
2155
2320
  await Promise.allSettled(wrappedPromises);
2156
2321
 
2157
2322
  // Fill in any null results (shouldn't happen, but defensive)
2158
- return results.map((r, idx) => r || {
2159
- laneNumber: lanes[idx].laneNumber,
2160
- laneId: lanes[idx].laneId,
2161
- tasks: [],
2162
- overallStatus: "failed" as const,
2163
- startTime: Date.now(),
2164
- endTime: Date.now(),
2165
- });
2323
+ return results.map(
2324
+ (r, idx) =>
2325
+ r || {
2326
+ laneNumber: lanes[idx].laneNumber,
2327
+ laneId: lanes[idx].laneId,
2328
+ tasks: [],
2329
+ overallStatus: "failed" as const,
2330
+ startTime: Date.now(),
2331
+ endTime: Date.now(),
2332
+ },
2333
+ );
2166
2334
  }
2167
2335
 
2168
2336
  // ── Runtime V2 Bridge Helpers (TP-102) ─────────────────────────────────────
@@ -2223,8 +2391,8 @@ export function buildExecutionUnit(
2223
2391
  throw new ExecutionError(
2224
2392
  "EXEC_MISSING_TASK_FOLDER",
2225
2393
  `Cannot build execution unit for task ${task.taskId}: taskFolder is ${taskFolder === "" ? "empty" : "undefined"}. ` +
2226
- `This typically means the task's persisted record was not enriched with discovery data. ` +
2227
- `Re-run discovery or check that the task exists in the task area.`,
2394
+ `This typically means the task's persisted record was not enriched with discovery data. ` +
2395
+ `Re-run discovery or check that the task exists in the task area.`,
2228
2396
  "execution",
2229
2397
  task.taskId,
2230
2398
  );
@@ -2248,18 +2416,17 @@ export function buildExecutionUnit(
2248
2416
  // the execution repo (cross-repo segment). When they're the same repo,
2249
2417
  // resolve packet paths inside the worktree so .DONE, STATUS.md etc. are
2250
2418
  // written to the worktree (not the original repo outside the worktree).
2251
- const useAbsolutePacketPath = task.task.packetTaskPath
2252
- && packetHomeRepoId !== executionRepoId;
2419
+ const useAbsolutePacketPath = task.task.packetTaskPath && packetHomeRepoId !== executionRepoId;
2253
2420
 
2254
2421
  const packet = useAbsolutePacketPath
2255
2422
  ? resolvePacketPaths(task.task.packetTaskPath!)
2256
2423
  : {
2257
- promptPath: resolved.taskFolderResolved + "/PROMPT.md",
2258
- statusPath: resolved.statusPath,
2259
- donePath: resolved.donePath,
2260
- reviewsDir: resolved.taskFolderResolved + "/.reviews",
2261
- taskFolder: resolved.taskFolderResolved,
2262
- };
2424
+ promptPath: resolved.taskFolderResolved + "/PROMPT.md",
2425
+ statusPath: resolved.statusPath,
2426
+ donePath: resolved.donePath,
2427
+ reviewsDir: resolved.taskFolderResolved + "/.reviews",
2428
+ taskFolder: resolved.taskFolderResolved,
2429
+ };
2263
2430
 
2264
2431
  return {
2265
2432
  id,
@@ -2339,7 +2506,9 @@ function parseAgentFile(filePath: string): { fm: Record<string, string>; body: s
2339
2506
  if (m) fm[m[1]] = m[2].trim();
2340
2507
  }
2341
2508
  return { fm, body: raw.slice(fmEnd + 3).trim() };
2342
- } catch { return null; }
2509
+ } catch {
2510
+ return null;
2511
+ }
2343
2512
  }
2344
2513
 
2345
2514
  /**
@@ -2357,7 +2526,9 @@ function loadBaseAgentPrompt(agentName: string): string {
2357
2526
  const def = parseAgentFile(resolved);
2358
2527
  if (def?.body) return def.body;
2359
2528
  }
2360
- } catch { /* fall through */ }
2529
+ } catch {
2530
+ /* fall through */
2531
+ }
2361
2532
  return "";
2362
2533
  }
2363
2534
 
@@ -2433,11 +2604,11 @@ function resolveAgentPointerRoot(): string | null {
2433
2604
  * @returns Composed agent definition, or null if no base and no local file found
2434
2605
  * @since TP-161
2435
2606
  */
2436
- export function loadAgentDef(cwd: string, name: string): { systemPrompt: string; tools: string; model: string } | null {
2437
- const localPaths = [
2438
- join(cwd, ".pi", "agents", `${name}.md`),
2439
- join(cwd, "agents", `${name}.md`),
2440
- ];
2607
+ export function loadAgentDef(
2608
+ cwd: string,
2609
+ name: string,
2610
+ ): { systemPrompt: string; tools: string; model: string } | null {
2611
+ const localPaths = [join(cwd, ".pi", "agents", `${name}.md`), join(cwd, "agents", `${name}.md`)];
2441
2612
 
2442
2613
  // In workspace mode, add pointer-resolved agent root as fallback
2443
2614
  const agentRoot = resolveAgentPointerRoot();
@@ -2452,7 +2623,9 @@ export function loadAgentDef(cwd: string, name: string): { systemPrompt: string;
2452
2623
  if (existsSync(basePath)) {
2453
2624
  baseDef = parseAgentFile(basePath);
2454
2625
  }
2455
- } catch { /* fall through */ }
2626
+ } catch {
2627
+ /* fall through */
2628
+ }
2456
2629
 
2457
2630
  // Load local override (first found wins)
2458
2631
  let localDef: { fm: Record<string, string>; body: string } | null = null;
@@ -2487,10 +2660,7 @@ export function loadAgentDef(cwd: string, name: string): { systemPrompt: string;
2487
2660
  return { systemPrompt: composedPrompt.trim(), tools, model };
2488
2661
  }
2489
2662
 
2490
- export function resolveRuntimeStateRoot(
2491
- repoRoot: string,
2492
- workspaceRoot?: string,
2493
- ): string {
2663
+ export function resolveRuntimeStateRoot(repoRoot: string, workspaceRoot?: string): string {
2494
2664
  return workspaceRoot ?? repoRoot;
2495
2665
  }
2496
2666
 
@@ -2532,13 +2702,21 @@ function parseJsonArrayEnv(value?: string): string[] {
2532
2702
  if (!value) return [];
2533
2703
  try {
2534
2704
  const parsed = JSON.parse(value);
2535
- if (Array.isArray(parsed)) return parsed.filter((v: unknown): v is string => typeof v === "string");
2536
- } catch { /* ignore malformed */ }
2705
+ if (Array.isArray(parsed))
2706
+ return parsed.filter((v: unknown): v is string => typeof v === "string");
2707
+ } catch {
2708
+ /* ignore malformed */
2709
+ }
2537
2710
  return [];
2538
2711
  }
2539
2712
 
2540
2713
  export function buildReviewerEnv(
2541
- reviewerConfig?: { model?: string; thinking?: string; tools?: string; excludeExtensions?: string[] } | null,
2714
+ reviewerConfig?: {
2715
+ model?: string;
2716
+ thinking?: string;
2717
+ tools?: string;
2718
+ excludeExtensions?: string[];
2719
+ } | null,
2542
2720
  ): Record<string, string> {
2543
2721
  const env: Record<string, string> = {};
2544
2722
  if (reviewerConfig?.model) env.TASKPLANE_REVIEWER_MODEL = reviewerConfig.model;
@@ -2560,7 +2738,12 @@ export function buildReviewerEnv(
2560
2738
  * @since TP-181
2561
2739
  */
2562
2740
  export function buildWorkerEnv(
2563
- workerConfig?: { model?: string; thinking?: string; tools?: string; excludeExtensions?: string[] } | null,
2741
+ workerConfig?: {
2742
+ model?: string;
2743
+ thinking?: string;
2744
+ tools?: string;
2745
+ excludeExtensions?: string[];
2746
+ } | null,
2564
2747
  ): Record<string, string> {
2565
2748
  const env: Record<string, string> = {};
2566
2749
  if (workerConfig?.model) env.TASKPLANE_WORKER_MODEL = workerConfig.model;
@@ -2620,7 +2803,8 @@ export async function executeLaneV2(
2620
2803
  // The base template (templates/agents/task-worker.md) contains critical behavioral
2621
2804
  // rules: checkpoint discipline, STATUS.md resume algorithm, review_step instructions.
2622
2805
  // The local file (.pi/agents/task-worker.md) adds project-specific guidance.
2623
- let workerSystemPrompt = "You are a task execution agent. Read STATUS.md first, find unchecked items, work on them, checkpoint after each.";
2806
+ let workerSystemPrompt =
2807
+ "You are a task execution agent. Read STATUS.md first, find unchecked items, work on them, checkpoint after each.";
2624
2808
  let workerSegmentPrompt = "";
2625
2809
  try {
2626
2810
  const basePrompt = loadBaseAgentPrompt("task-worker");
@@ -2635,7 +2819,9 @@ export async function executeLaneV2(
2635
2819
  // Load segment-scoped prompt overlay (appended when isSegmentScoped)
2636
2820
  const segPrompt = loadBaseAgentPrompt("task-worker-segment");
2637
2821
  if (segPrompt) workerSegmentPrompt = segPrompt;
2638
- } catch { /* use default */ }
2822
+ } catch {
2823
+ /* use default */
2824
+ }
2639
2825
 
2640
2826
  execLog(laneId, "LANE", `starting Runtime V2 execution of ${lane.tasks.length} task(s)`, {
2641
2827
  worktree: lane.worktreePath,
@@ -2647,16 +2833,26 @@ export async function executeLaneV2(
2647
2833
  // this lane number is lifted before new alerts begin to flow.
2648
2834
  if (onLaneRespawned) {
2649
2835
  try {
2650
- onLaneRespawned(lane.laneNumber, buildRuntimeAgentId(agentIdPrefix, lane.laneNumber, "worker"), batchId);
2836
+ onLaneRespawned(
2837
+ lane.laneNumber,
2838
+ buildRuntimeAgentId(agentIdPrefix, lane.laneNumber, "worker"),
2839
+ batchId,
2840
+ );
2651
2841
  } catch (err) {
2652
- execLog(laneId, "LANE", `lane-respawned callback failed: ${err instanceof Error ? err.message : String(err)}`);
2842
+ execLog(
2843
+ laneId,
2844
+ "LANE",
2845
+ `lane-respawned callback failed: ${err instanceof Error ? err.message : String(err)}`,
2846
+ );
2653
2847
  }
2654
2848
  }
2655
2849
 
2656
2850
  for (const task of lane.tasks) {
2657
2851
  const taskSegmentId = task.task.activeSegmentId ?? null;
2658
2852
  if (shouldSkipRemaining || pauseSignal.paused) {
2659
- const reason = pauseSignal.paused ? "Skipped due to pause signal" : "Skipped due to prior task failure in lane";
2853
+ const reason = pauseSignal.paused
2854
+ ? "Skipped due to pause signal"
2855
+ : "Skipped due to prior task failure in lane";
2660
2856
  outcomes.push({
2661
2857
  taskId: task.taskId,
2662
2858
  status: "skipped",
@@ -2674,10 +2870,12 @@ export async function executeLaneV2(
2674
2870
  // Build execution unit
2675
2871
  const unit = buildExecutionUnit(lane, task, repoRoot, isWorkspaceMode);
2676
2872
 
2677
- const rawAutonomy = String(extraEnvVars?.TASKPLANE_SUPERVISOR_AUTONOMY ?? "autonomous").toLowerCase();
2873
+ const rawAutonomy = String(
2874
+ extraEnvVars?.TASKPLANE_SUPERVISOR_AUTONOMY ?? "autonomous",
2875
+ ).toLowerCase();
2678
2876
  const supervisorAutonomy: LaneRunnerConfig["supervisorAutonomy"] =
2679
- (rawAutonomy === "interactive" || rawAutonomy === "supervised" || rawAutonomy === "autonomous")
2680
- ? rawAutonomy as LaneRunnerConfig["supervisorAutonomy"]
2877
+ rawAutonomy === "interactive" || rawAutonomy === "supervised" || rawAutonomy === "autonomous"
2878
+ ? (rawAutonomy as LaneRunnerConfig["supervisorAutonomy"])
2681
2879
  : "autonomous";
2682
2880
 
2683
2881
  const laneRunnerConfig: LaneRunnerConfig = {
@@ -2701,12 +2899,26 @@ export async function executeLaneV2(
2701
2899
  reviewerTools: extraEnvVars?.TASKPLANE_REVIEWER_TOOLS || "",
2702
2900
  // TP-180: Extension exclusion lists from config
2703
2901
  workerExcludeExtensions: parseJsonArrayEnv(extraEnvVars?.TASKPLANE_WORKER_EXCLUDE_EXTENSIONS),
2704
- reviewerExcludeExtensions: parseJsonArrayEnv(extraEnvVars?.TASKPLANE_REVIEWER_EXCLUDE_EXTENSIONS),
2902
+ reviewerExcludeExtensions: parseJsonArrayEnv(
2903
+ extraEnvVars?.TASKPLANE_REVIEWER_EXCLUDE_EXTENSIONS,
2904
+ ),
2705
2905
  supervisorAutonomy,
2706
- projectName: config.project?.name || "project",
2906
+ // TP-195: replaced `config.project?.name` (no `project` field on
2907
+ // `OrchestratorConfig`; always undefined) with the env-var read
2908
+ // already used elsewhere in the codebase (lane-runner.ts:668 sets
2909
+ // `TASKPLANE_PROJECT_NAME` from the same source). When the env
2910
+ // var is unset, falls through to the same `"project"` literal as
2911
+ // before — behavior-neutral.
2912
+ projectName: extraEnvVars?.TASKPLANE_PROJECT_NAME || "project",
2707
2913
  maxIterations: 20,
2708
2914
  noProgressLimit: 3,
2709
- maxWorkerMinutes: config.failure?.maxWorkerMinutes || 120,
2915
+ // TP-195: read the canonical `max_worker_minutes` field (snake_case
2916
+ // per `OrchestratorConfig.failure` in types.ts). The previous code
2917
+ // read a non-existent `maxWorkerMinutes` camelCase alias — always
2918
+ // undefined — silently ignoring any operator-set value. Honoring
2919
+ // the config is the intended behavior; default of 120 preserved
2920
+ // when the field is unset.
2921
+ maxWorkerMinutes: config.failure?.max_worker_minutes || 120,
2710
2922
  warnPercent: 85,
2711
2923
  killPercent: 95,
2712
2924
  onSupervisorAlert,
@@ -2811,13 +3023,22 @@ export async function executeLaneV2(
2811
3023
  progress: null,
2812
3024
  updatedAt: Date.now(),
2813
3025
  };
2814
- writeLaneSnapshot(stateRoot, batchId, lane.laneNumber, spawnFailureSnapshot as unknown as Record<string, unknown>);
3026
+ writeLaneSnapshot(
3027
+ stateRoot,
3028
+ batchId,
3029
+ lane.laneNumber,
3030
+ spawnFailureSnapshot as unknown as Record<string, unknown>,
3031
+ );
2815
3032
  } catch (snapErr) {
2816
3033
  // Best effort — if the snapshot write fails, the monitor's
2817
3034
  // 30s-staleness fallback (snap with old updatedAt) eventually
2818
3035
  // kicks in via the registry liveness check. Log so this is
2819
3036
  // visible in operator diagnostics, but do NOT throw.
2820
- execLog(laneId, task.taskId, `spawn-failure snapshot write failed (non-fatal): ${snapErr instanceof Error ? snapErr.message : String(snapErr)}`);
3037
+ execLog(
3038
+ laneId,
3039
+ task.taskId,
3040
+ `spawn-failure snapshot write failed (non-fatal): ${snapErr instanceof Error ? snapErr.message : String(snapErr)}`,
3041
+ );
2821
3042
  }
2822
3043
 
2823
3044
  shouldSkipRemaining = true;
@@ -2825,8 +3046,8 @@ export async function executeLaneV2(
2825
3046
  }
2826
3047
 
2827
3048
  const endTime = Date.now();
2828
- const succeeded = outcomes.every(o => o.status === "succeeded");
2829
- const failed = outcomes.some(o => o.status === "failed" || o.status === "stalled");
3049
+ const succeeded = outcomes.every((o) => o.status === "succeeded");
3050
+ const failed = outcomes.some((o) => o.status === "failed" || o.status === "stalled");
2830
3051
 
2831
3052
  return {
2832
3053
  laneNumber: lane.laneNumber,
@@ -2839,4 +3060,3 @@ export async function executeLaneV2(
2839
3060
  }
2840
3061
 
2841
3062
  // ── /orch Command — Full Execution (Step 5) ─────────────────────────
2842
-