taskplane 0.3.1 → 0.4.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.
@@ -261,6 +261,8 @@ export function buildMergeRequest(
261
261
  * @param repoRoot - Main repository root (merge happens here)
262
262
  * @param mergeRequestPath - Path to the merge request temp file
263
263
  * @param config - Orchestrator config (for model, tools)
264
+ * @param stateRoot - Root for state files (batch state, merge results). Stays at workspace root.
265
+ * @param agentRoot - Root for agent prompts. When pointer is resolved, this is the config repo's agent dir. Falls back to `<stateRoot>/.pi/agents/` or `<repoRoot>/.pi/agents/`.
264
266
  * @throws MergeError if spawn fails after retries
265
267
  */
266
268
  export function spawnMergeAgent(
@@ -270,6 +272,7 @@ export function spawnMergeAgent(
270
272
  mergeRequestPath: string,
271
273
  config: OrchestratorConfig,
272
274
  stateRoot?: string,
275
+ agentRoot?: string,
273
276
  ): void {
274
277
  execLog("merge", sessionName, "preparing to spawn merge agent", {
275
278
  mergeWorkDir,
@@ -304,7 +307,7 @@ export function spawnMergeAgent(
304
307
  "pi --no-session",
305
308
  modelArgs,
306
309
  toolsArgs,
307
- `--append-system-prompt ${shellQuote(join(stateRoot ?? repoRoot, ".pi", "agents", "task-merger.md"))}`,
310
+ `--append-system-prompt ${shellQuote(agentRoot ? join(agentRoot, "task-merger.md") : join(stateRoot ?? repoRoot, ".pi", "agents", "task-merger.md"))}`,
308
311
  `@${shellQuote(mergeRequestPath)}`,
309
312
  ].filter(Boolean).join(" ");
310
313
 
@@ -509,6 +512,7 @@ export function mergeWave(
509
512
  batchId: string,
510
513
  baseBranch: string,
511
514
  stateRoot?: string,
515
+ agentRoot?: string,
512
516
  ): MergeWaveResult {
513
517
  const startTime = Date.now();
514
518
  const tmuxPrefix = config.orchestrator.tmux_prefix;
@@ -648,10 +652,11 @@ export function mergeWave(
648
652
  writeFileSync(requestFilePath, mergeRequestContent, "utf-8");
649
653
 
650
654
  // Spawn merge agent in the isolated merge worktree
651
- spawnMergeAgent(sessionName, repoRoot, mergeWorkDir, requestFilePath, config, stateRoot);
655
+ spawnMergeAgent(sessionName, repoRoot, mergeWorkDir, requestFilePath, config, stateRoot, agentRoot);
652
656
 
653
- // Wait for result
654
- const mergeResult = waitForMergeResult(resultFilePath, sessionName);
657
+ // Wait for result — use configured timeout (default 10 min, was 5 min)
658
+ const timeoutMs = (config.merge.timeout_minutes ?? 10) * 60 * 1000;
659
+ const mergeResult = waitForMergeResult(resultFilePath, sessionName, timeoutMs);
655
660
 
656
661
  // Clean up request file (leave result file for debugging)
657
662
  try {
@@ -894,6 +899,7 @@ export function mergeWaveByRepo(
894
899
  baseBranch: string,
895
900
  workspaceConfig?: WorkspaceConfig | null,
896
901
  stateRoot?: string,
902
+ agentRoot?: string,
897
903
  ): MergeWaveResult {
898
904
  const startTime = Date.now();
899
905
 
@@ -947,6 +953,7 @@ export function mergeWaveByRepo(
947
953
  batchId,
948
954
  baseBranch,
949
955
  stateRoot,
956
+ agentRoot,
950
957
  );
951
958
  // Attach empty repoResults for consistent shape
952
959
  return { ...result, repoResults: [] };
@@ -991,6 +998,7 @@ export function mergeWaveByRepo(
991
998
  batchId,
992
999
  groupBaseBranch,
993
1000
  stateRoot,
1001
+ agentRoot,
994
1002
  );
995
1003
 
996
1004
  // Accumulate lane results
@@ -499,14 +499,19 @@ export async function resumeOrchBatch(
499
499
  onNotify: (message: string, level: "info" | "warning" | "error") => void,
500
500
  onMonitorUpdate?: MonitorUpdateCallback,
501
501
  workspaceConfig?: WorkspaceConfig | null,
502
+ workspaceRoot?: string,
503
+ agentRoot?: string,
502
504
  ): Promise<void> {
503
505
  const repoRoot = cwd;
506
+ // State files (.pi/batch-state.json, lane-state, etc.) belong in the workspace root,
507
+ // which is where .pi/ config lives. In repo mode, stateRoot === repoRoot.
508
+ const stateRoot = workspaceRoot ?? cwd;
504
509
  const prefix = orchConfig.orchestrator.tmux_prefix;
505
510
 
506
511
  // ── 1. Load persisted state ──────────────────────────────────
507
512
  let persistedState: PersistedBatchState | null;
508
513
  try {
509
- persistedState = loadBatchState(repoRoot);
514
+ persistedState = loadBatchState(stateRoot);
510
515
  } catch (err: unknown) {
511
516
  if (err instanceof StateFileError) {
512
517
  onNotify(
@@ -898,6 +903,8 @@ export async function resumeOrchBatch(
898
903
  batchState.batchId,
899
904
  batchState.baseBranch,
900
905
  workspaceConfig,
906
+ stateRoot,
907
+ agentRoot,
901
908
  );
902
909
 
903
910
  if (reExecMergeResult.status === "succeeded") {
@@ -989,7 +996,7 @@ export async function resumeOrchBatch(
989
996
  }
990
997
  }
991
998
 
992
- persistRuntimeState("resume-reconciliation", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery ?? null, repoRoot);
999
+ persistRuntimeState("resume-reconciliation", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery ?? null, stateRoot);
993
1000
 
994
1001
  // ── 10. Continue wave execution ──────────────────────────────
995
1002
  // We need to execute remaining waves starting from resumeWaveIndex.
@@ -1001,13 +1008,13 @@ export async function resumeOrchBatch(
1001
1008
  // Check pause signal
1002
1009
  if (batchState.pauseSignal.paused) {
1003
1010
  batchState.phase = "paused";
1004
- persistRuntimeState("pause-before-wave", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, repoRoot);
1011
+ persistRuntimeState("pause-before-wave", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, stateRoot);
1005
1012
  onNotify(`⏸️ Batch paused before wave ${waveIdx + 1}.`, "warning");
1006
1013
  break;
1007
1014
  }
1008
1015
 
1009
1016
  batchState.currentWaveIndex = waveIdx;
1010
- persistRuntimeState("wave-index-change", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, repoRoot);
1017
+ persistRuntimeState("wave-index-change", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, stateRoot);
1011
1018
 
1012
1019
  // Get wave tasks, filtering out completed/failed/blocked ones.
1013
1020
  let waveTasks = persistedState.wavePlan[waveIdx].filter(
@@ -1043,7 +1050,7 @@ export async function resumeOrchBatch(
1043
1050
  const handleResumeMonitorUpdate: MonitorUpdateCallback = (monitorState) => {
1044
1051
  const changed = syncTaskOutcomesFromMonitor(monitorState, allTaskOutcomes);
1045
1052
  if (changed) {
1046
- persistRuntimeState("task-transition", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, repoRoot);
1053
+ persistRuntimeState("task-transition", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, stateRoot);
1047
1054
  }
1048
1055
  onMonitorUpdate?.(monitorState);
1049
1056
  };
@@ -1068,7 +1075,7 @@ export async function resumeOrchBatch(
1068
1075
  encounteredRepoRoots.add(resolveRepoRoot(lane.repoId, repoRoot, workspaceConfig));
1069
1076
  }
1070
1077
  if (seedPendingOutcomesForAllocatedLanes(lanes, allTaskOutcomes)) {
1071
- persistRuntimeState("wave-lanes-allocated", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, repoRoot);
1078
+ persistRuntimeState("wave-lanes-allocated", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, stateRoot);
1072
1079
  }
1073
1080
  },
1074
1081
  workspaceConfig,
@@ -1105,7 +1112,7 @@ export async function resumeOrchBatch(
1105
1112
  batchState.blockedTaskIds.add(blocked);
1106
1113
  }
1107
1114
 
1108
- persistRuntimeState("wave-execution-complete", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, repoRoot);
1115
+ persistRuntimeState("wave-execution-complete", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, stateRoot);
1109
1116
 
1110
1117
  const elapsedSec = Math.round((waveResult.endedAt - waveResult.startedAt) / 1000);
1111
1118
  onNotify(
@@ -1123,13 +1130,13 @@ export async function resumeOrchBatch(
1123
1130
  if (waveResult.stoppedEarly) {
1124
1131
  if (waveResult.policyApplied === "stop-all") {
1125
1132
  batchState.phase = "stopped";
1126
- persistRuntimeState("stop-all", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, repoRoot);
1133
+ persistRuntimeState("stop-all", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, stateRoot);
1127
1134
  onNotify(ORCH_MESSAGES.orchBatchStopped(batchState.batchId, "stop-all"), "error");
1128
1135
  break;
1129
1136
  }
1130
1137
  if (waveResult.policyApplied === "stop-wave") {
1131
1138
  batchState.phase = "stopped";
1132
- persistRuntimeState("stop-wave", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, repoRoot);
1139
+ persistRuntimeState("stop-wave", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, stateRoot);
1133
1140
  onNotify(ORCH_MESSAGES.orchBatchStopped(batchState.batchId, "stop-wave"), "error");
1134
1141
  break;
1135
1142
  }
@@ -1163,7 +1170,7 @@ export async function resumeOrchBatch(
1163
1170
 
1164
1171
  if (mergeableLaneCount > 0) {
1165
1172
  batchState.phase = "merging";
1166
- persistRuntimeState("merge-start", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, repoRoot);
1173
+ persistRuntimeState("merge-start", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, stateRoot);
1167
1174
  onNotify(ORCH_MESSAGES.orchMergeStart(waveIdx + 1, mergeableLaneCount), "info");
1168
1175
 
1169
1176
  mergeResult = mergeWaveByRepo(
@@ -1175,6 +1182,8 @@ export async function resumeOrchBatch(
1175
1182
  batchState.batchId,
1176
1183
  batchState.baseBranch,
1177
1184
  workspaceConfig,
1185
+ stateRoot,
1186
+ agentRoot,
1178
1187
  );
1179
1188
  batchState.mergeResults.push(mergeResult);
1180
1189
 
@@ -1223,7 +1232,7 @@ export async function resumeOrchBatch(
1223
1232
  }
1224
1233
 
1225
1234
  batchState.phase = "executing";
1226
- persistRuntimeState("merge-complete", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, repoRoot);
1235
+ persistRuntimeState("merge-complete", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, stateRoot);
1227
1236
  } else if (mixedOutcomeLanes.length > 0) {
1228
1237
  const mixedIds = mixedOutcomeLanes.map(l => `lane-${l.laneNumber}`).join(", ");
1229
1238
  mergeResult = {
@@ -1255,7 +1264,7 @@ export async function resumeOrchBatch(
1255
1264
 
1256
1265
  batchState.phase = policyResult.targetPhase;
1257
1266
  batchState.errors.push(policyResult.errorMessage);
1258
- persistRuntimeState(policyResult.persistTrigger, batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, repoRoot);
1267
+ persistRuntimeState(policyResult.persistTrigger, batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, stateRoot);
1259
1268
  onNotify(policyResult.notifyMessage, policyResult.notifyLevel);
1260
1269
  preserveWorktreesForResume = true;
1261
1270
  break;
@@ -1325,7 +1334,7 @@ export async function resumeOrchBatch(
1325
1334
  }
1326
1335
  }
1327
1336
 
1328
- persistRuntimeState("batch-terminal", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, repoRoot);
1337
+ persistRuntimeState("batch-terminal", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, stateRoot);
1329
1338
 
1330
1339
  if (batchState.phase === "paused" || batchState.phase === "stopped") {
1331
1340
  execLog("resume", batchState.batchId, "resumed batch ended in non-terminal state", { phase: batchState.phase });
@@ -1344,7 +1353,7 @@ export async function resumeOrchBatch(
1344
1353
 
1345
1354
  if (batchState.phase === "completed") {
1346
1355
  try {
1347
- deleteBatchState(repoRoot);
1356
+ deleteBatchState(stateRoot);
1348
1357
  execLog("state", batchState.batchId, "state file deleted on clean resume completion");
1349
1358
  } catch {
1350
1359
  // Best-effort