taskplane 0.5.2 → 0.5.4

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.
@@ -155,20 +155,30 @@ export function buildLaneEnvVars(
155
155
  repoRoot: string,
156
156
  workspaceRoot?: string,
157
157
  ): Record<string, string> {
158
- // TASK_AUTOSTART needs a path relative to the worktree root.
159
- // The promptPath is absolute (from the main repo). We need the
160
- // relative portion from the repo root, which will be the same
161
- // relative path in the worktree since worktrees mirror the repo structure.
158
+ // TASK_AUTOSTART: resolve the prompt path for the lane session.
159
+ //
160
+ // In workspace mode, tasks may live in a different repo than the lane's
161
+ // worktree (e.g., task PROMPT.md in shared-libs, worker runs in api-service).
162
+ // Always use the absolute path — task-runner's resolve(cwd, autoPath) handles
163
+ // absolute paths correctly, and this avoids broken relative paths when the
164
+ // task folder is outside the lane's repo.
165
+ //
166
+ // In repo mode (no workspace), we still use relative paths from repoRoot
167
+ // because the worktree mirrors the repo structure and the task folder is
168
+ // inside the repo.
162
169
  const repoRootNorm = resolve(repoRoot).replace(/\\/g, "/");
163
170
  const promptNorm = resolve(promptPath).replace(/\\/g, "/");
164
171
 
165
172
  let relativePath: string;
166
- if (promptNorm.startsWith(repoRootNorm + "/")) {
173
+ if (workspaceRoot) {
174
+ // Workspace mode: always use absolute path for cross-repo safety
175
+ relativePath = resolve(promptPath);
176
+ } else if (promptNorm.startsWith(repoRootNorm + "/")) {
177
+ // Repo mode: relative path from repo root (mirrors into worktree)
167
178
  relativePath = promptNorm.slice(repoRootNorm.length + 1);
168
179
  } else {
169
- // External task folder (workspace mode): prompt path is outside repo root.
170
- // Use the absolute path as-is — task-runner accepts absolute TASK_AUTOSTART paths.
171
- relativePath = promptPath;
180
+ // Fallback: absolute path
181
+ relativePath = resolve(promptPath);
172
182
  }
173
183
 
174
184
  const nodePathEntries: string[] = [join(repoRoot, "node_modules")];
@@ -403,21 +413,26 @@ export function resolveCanonicalTaskPaths(
403
413
  taskFolder: string,
404
414
  worktreePath: string,
405
415
  repoRoot: string,
416
+ isWorkspaceMode?: boolean,
406
417
  ): ResolvedTaskPaths {
407
418
  const repoRootNorm = resolve(repoRoot).replace(/\\/g, "/");
408
419
  const folderNorm = resolve(taskFolder).replace(/\\/g, "/");
409
420
 
410
421
  let resolvedFolder: string;
411
422
 
412
- if (folderNorm.startsWith(repoRootNorm + "/")) {
413
- // Case 1: Task folder is inside the repo root.
423
+ if (isWorkspaceMode) {
424
+ // Workspace mode: task folder may live in a different repo than
425
+ // the lane's worktree. Always use the absolute canonical path —
426
+ // .DONE and STATUS.md are written by workers to the original
427
+ // task folder (via absolute TASK_AUTOSTART path), not to the worktree.
428
+ resolvedFolder = resolve(taskFolder);
429
+ } else if (folderNorm.startsWith(repoRootNorm + "/")) {
430
+ // Repo mode: task folder is inside the repo root.
414
431
  // Translate to equivalent path in the worktree.
415
432
  const relativePath = folderNorm.slice(repoRootNorm.length + 1);
416
433
  resolvedFolder = join(worktreePath, relativePath);
417
434
  } else {
418
- // Case 2: Task folder is outside the repo root (workspace mode).
419
- // Use the absolute path directly — task state lives in the
420
- // canonical task folder, not mirrored in any worktree.
435
+ // Fallback: use absolute path directly.
421
436
  resolvedFolder = resolve(taskFolder);
422
437
  }
423
438
 
@@ -474,8 +489,9 @@ export function resolveTaskDonePath(
474
489
  taskFolder: string,
475
490
  worktreePath: string,
476
491
  repoRoot: string,
492
+ isWorkspaceMode?: boolean,
477
493
  ): string {
478
- return resolveCanonicalTaskPaths(taskFolder, worktreePath, repoRoot).donePath;
494
+ return resolveCanonicalTaskPaths(taskFolder, worktreePath, repoRoot, isWorkspaceMode).donePath;
479
495
  }
480
496
 
481
497
  /**
@@ -603,10 +619,11 @@ export async function pollUntilTaskComplete(
603
619
  config: OrchestratorConfig,
604
620
  repoRoot: string,
605
621
  pauseSignal: { paused: boolean },
622
+ isWorkspaceMode?: boolean,
606
623
  ): Promise<{ status: LaneTaskStatus; exitReason: string; doneFileFound: boolean }> {
607
624
  const sessionName = lane.tmuxSessionName;
608
625
  const laneId = lane.laneId;
609
- const resolved = resolveCanonicalTaskPaths(task.task.taskFolder, lane.worktreePath, repoRoot);
626
+ const resolved = resolveCanonicalTaskPaths(task.task.taskFolder, lane.worktreePath, repoRoot, isWorkspaceMode);
610
627
  const donePath = resolved.donePath;
611
628
  const statusPath = resolved.statusPath;
612
629
  const laneLogPath = resolveLaneLogPath(lane, task);
@@ -822,6 +839,7 @@ export async function executeLane(
822
839
  repoRoot: string,
823
840
  pauseSignal: { paused: boolean },
824
841
  workspaceRoot?: string,
842
+ isWorkspaceMode?: boolean,
825
843
  ): Promise<LaneExecutionResult> {
826
844
  const laneId = lane.laneId;
827
845
  const laneStartTime = Date.now();
@@ -867,6 +885,7 @@ export async function executeLane(
867
885
  config,
868
886
  repoRoot,
869
887
  pauseSignal,
888
+ isWorkspaceMode,
870
889
  );
871
890
 
872
891
  taskOutcome = {
@@ -993,9 +1012,10 @@ export function parseWorktreeStatusMd(
993
1012
  taskFolder: string,
994
1013
  worktreePath: string,
995
1014
  repoRoot: string,
1015
+ isWorkspaceMode?: boolean,
996
1016
  ): { parsed: ParsedWorktreeStatus | null; error: string | null } {
997
1017
  // Use canonical resolver for consistent path translation
998
- const resolved = resolveCanonicalTaskPaths(taskFolder, worktreePath, repoRoot);
1018
+ const resolved = resolveCanonicalTaskPaths(taskFolder, worktreePath, repoRoot, isWorkspaceMode);
999
1019
  const statusPath = resolved.statusPath;
1000
1020
 
1001
1021
  if (!existsSync(statusPath)) {
@@ -1313,6 +1333,7 @@ export async function monitorLanes(
1313
1333
  pauseSignal: { paused: boolean },
1314
1334
  waveNumber: number = 1,
1315
1335
  onUpdate?: MonitorUpdateCallback,
1336
+ isWorkspaceMode?: boolean,
1316
1337
  ): Promise<MonitorState> {
1317
1338
  const pollIntervalMs = (config.monitoring.poll_interval || 5) * 1000;
1318
1339
  const stallTimeoutMs = (config.failure.stall_timeout || 30) * 60_000;
@@ -1402,8 +1423,8 @@ export async function monitorLanes(
1402
1423
  currentTaskId = task.taskId;
1403
1424
 
1404
1425
  const tracker = getOrCreateTracker(task.taskId, now);
1405
- const donePath = resolveTaskDonePath(task.task.taskFolder, lane.worktreePath, repoRoot);
1406
- const statusResult = parseWorktreeStatusMd(task.task.taskFolder, lane.worktreePath, repoRoot);
1426
+ const donePath = resolveTaskDonePath(task.task.taskFolder, lane.worktreePath, repoRoot, isWorkspaceMode);
1427
+ const statusResult = parseWorktreeStatusMd(task.task.taskFolder, lane.worktreePath, repoRoot, isWorkspaceMode);
1407
1428
 
1408
1429
  const snapshot = resolveTaskMonitorState(
1409
1430
  task.taskId,
@@ -1809,8 +1830,9 @@ export async function executeWave(
1809
1830
  // In workspace mode, pass the workspace root so lane sessions can find .pi/ config.
1810
1831
  // configPath is .pi/taskplane-workspace.yaml → parent of parent is workspace root.
1811
1832
  const wsRoot = workspaceConfig ? dirname(dirname(workspaceConfig.configPath)) : undefined;
1833
+ const isWsMode = !!workspaceConfig;
1812
1834
  const lanePromises = lanes.map(lane =>
1813
- executeLane(lane, config, repoRoot, wavePauseSignal, wsRoot),
1835
+ executeLane(lane, config, repoRoot, wavePauseSignal, wsRoot, isWsMode),
1814
1836
  );
1815
1837
 
1816
1838
  // Start monitoring as a sibling async loop
@@ -1822,6 +1844,7 @@ export async function executeWave(
1822
1844
  wavePauseSignal,
1823
1845
  waveIndex,
1824
1846
  onMonitorUpdate,
1847
+ isWsMode,
1825
1848
  );
1826
1849
 
1827
1850
  // ── Stage 4: Wait for all lanes + apply policy ───────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskplane",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "AI agent orchestration for pi — parallel task execution with checkpoint discipline",
5
5
  "keywords": [
6
6
  "pi-package",