taskplane 0.1.14 → 0.1.16
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/bin/taskplane.mjs +317 -9
- package/extensions/taskplane/abort.ts +461 -466
- package/extensions/taskplane/config.ts +17 -12
- package/extensions/taskplane/discovery.ts +168 -32
- package/extensions/taskplane/engine.ts +781 -758
- package/extensions/taskplane/execution.ts +112 -48
- package/extensions/taskplane/extension.ts +780 -693
- package/extensions/taskplane/git.ts +25 -7
- package/extensions/taskplane/index.ts +23 -22
- package/extensions/taskplane/merge.ts +18 -16
- package/extensions/taskplane/messages.ts +146 -134
- package/extensions/taskplane/persistence.ts +1136 -1121
- package/extensions/taskplane/resume.ts +1102 -1092
- package/extensions/taskplane/types.ts +243 -3
- package/extensions/taskplane/waves.ts +894 -900
- package/extensions/taskplane/workspace.ts +382 -0
- package/extensions/taskplane/worktree.ts +113 -11
- package/package.json +1 -1
- package/templates/config/task-orchestrator.yaml +86 -89
- package/templates/config/task-runner.yaml +95 -99
|
@@ -118,7 +118,8 @@ export function buildLaneEnvVars(
|
|
|
118
118
|
if (promptNorm.startsWith(repoRootNorm + "/")) {
|
|
119
119
|
relativePath = promptNorm.slice(repoRootNorm.length + 1);
|
|
120
120
|
} else {
|
|
121
|
-
//
|
|
121
|
+
// External task folder (workspace mode): prompt path is outside repo root.
|
|
122
|
+
// Use the absolute path as-is — task-runner accepts absolute TASK_AUTOSTART paths.
|
|
122
123
|
relativePath = promptPath;
|
|
123
124
|
}
|
|
124
125
|
|
|
@@ -300,44 +301,122 @@ export function readTaskStatusTail(
|
|
|
300
301
|
}
|
|
301
302
|
|
|
302
303
|
/**
|
|
303
|
-
*
|
|
304
|
+
* Result of canonical task-folder path resolution.
|
|
304
305
|
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
306
|
+
* Encapsulates the resolved task folder, .DONE path, and STATUS.md path
|
|
307
|
+
* so callers don't need to re-derive them with inconsistent logic.
|
|
308
|
+
*/
|
|
309
|
+
export interface ResolvedTaskPaths {
|
|
310
|
+
/** Absolute path to the resolved task folder (may be in worktree or external) */
|
|
311
|
+
taskFolderResolved: string;
|
|
312
|
+
/** Absolute path to the .DONE file */
|
|
313
|
+
donePath: string;
|
|
314
|
+
/** Absolute path to the STATUS.md file */
|
|
315
|
+
statusPath: string;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Canonical task-folder path resolver.
|
|
307
320
|
*
|
|
308
|
-
*
|
|
321
|
+
* Single source of truth for translating a task folder path (as stored in
|
|
322
|
+
* ParsedTask) into the correct filesystem paths for .DONE and STATUS.md
|
|
323
|
+
* probing. Handles two cases:
|
|
324
|
+
*
|
|
325
|
+
* 1. **Task folder inside repoRoot** (monorepo / repo mode):
|
|
326
|
+
* Strip the repoRoot prefix to get a relative path, then join with
|
|
327
|
+
* worktreePath. This is the existing behavior — worktrees mirror the
|
|
328
|
+
* repo structure so the relative path is the same.
|
|
329
|
+
*
|
|
330
|
+
* 2. **Task folder outside repoRoot** (workspace mode with external tasks root):
|
|
331
|
+
* The task folder is not inside the execution repo. Use the absolute
|
|
332
|
+
* task folder path directly — the .DONE and STATUS.md files live in
|
|
333
|
+
* the canonical task folder, not in any worktree.
|
|
334
|
+
*
|
|
335
|
+
* Both branches include archive fallback: if the primary location doesn't
|
|
336
|
+
* exist, check `<parent>/archive/<taskDirName>/` for relocated task folders.
|
|
337
|
+
*
|
|
338
|
+
* @param taskFolder - Absolute task folder path (from ParsedTask.taskFolder)
|
|
309
339
|
* @param worktreePath - Absolute path to the lane worktree
|
|
310
340
|
* @param repoRoot - Absolute path to the main repository root
|
|
311
|
-
* @returns
|
|
341
|
+
* @returns Resolved paths for task folder, .DONE, and STATUS.md
|
|
312
342
|
*/
|
|
313
|
-
export function
|
|
343
|
+
export function resolveCanonicalTaskPaths(
|
|
314
344
|
taskFolder: string,
|
|
315
345
|
worktreePath: string,
|
|
316
346
|
repoRoot: string,
|
|
317
|
-
):
|
|
347
|
+
): ResolvedTaskPaths {
|
|
318
348
|
const repoRootNorm = resolve(repoRoot).replace(/\\/g, "/");
|
|
319
349
|
const folderNorm = resolve(taskFolder).replace(/\\/g, "/");
|
|
320
350
|
|
|
321
|
-
let
|
|
351
|
+
let resolvedFolder: string;
|
|
352
|
+
|
|
322
353
|
if (folderNorm.startsWith(repoRootNorm + "/")) {
|
|
323
|
-
|
|
354
|
+
// Case 1: Task folder is inside the repo root.
|
|
355
|
+
// Translate to equivalent path in the worktree.
|
|
356
|
+
const relativePath = folderNorm.slice(repoRootNorm.length + 1);
|
|
357
|
+
resolvedFolder = join(worktreePath, relativePath);
|
|
324
358
|
} else {
|
|
325
|
-
|
|
359
|
+
// Case 2: Task folder is outside the repo root (workspace mode).
|
|
360
|
+
// Use the absolute path directly — task state lives in the
|
|
361
|
+
// canonical task folder, not mirrored in any worktree.
|
|
362
|
+
resolvedFolder = resolve(taskFolder);
|
|
326
363
|
}
|
|
327
364
|
|
|
328
|
-
|
|
329
|
-
|
|
365
|
+
// Check primary location
|
|
366
|
+
const primaryDone = join(resolvedFolder, ".DONE");
|
|
367
|
+
const primaryStatus = join(resolvedFolder, "STATUS.md");
|
|
368
|
+
if (existsSync(primaryDone) || existsSync(primaryStatus)) {
|
|
369
|
+
return {
|
|
370
|
+
taskFolderResolved: resolvedFolder,
|
|
371
|
+
donePath: primaryDone,
|
|
372
|
+
statusPath: primaryStatus,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
330
375
|
|
|
331
|
-
//
|
|
376
|
+
// Archive fallback: worker may have archived the task folder during the
|
|
332
377
|
// "Documentation & Delivery" step, moving it under `.../archive/TASK-ID/`.
|
|
333
|
-
|
|
334
|
-
const parts =
|
|
335
|
-
const taskDirName = parts[parts.length - 1];
|
|
336
|
-
const
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
378
|
+
const resolvedNorm = resolve(resolvedFolder).replace(/\\/g, "/");
|
|
379
|
+
const parts = resolvedNorm.split("/");
|
|
380
|
+
const taskDirName = parts[parts.length - 1];
|
|
381
|
+
const parentDir = parts.slice(0, -1).join("/");
|
|
382
|
+
const archiveFolder = join(parentDir, "archive", taskDirName);
|
|
383
|
+
const archiveDone = join(archiveFolder, ".DONE");
|
|
384
|
+
const archiveStatus = join(archiveFolder, "STATUS.md");
|
|
385
|
+
|
|
386
|
+
if (existsSync(archiveDone) || existsSync(archiveStatus)) {
|
|
387
|
+
return {
|
|
388
|
+
taskFolderResolved: archiveFolder,
|
|
389
|
+
donePath: archiveDone,
|
|
390
|
+
statusPath: archiveStatus,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Return primary paths even if nothing exists yet (caller probes existsSync)
|
|
395
|
+
return {
|
|
396
|
+
taskFolderResolved: resolvedFolder,
|
|
397
|
+
donePath: primaryDone,
|
|
398
|
+
statusPath: primaryStatus,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Resolve the path to a task's .DONE file inside a worktree.
|
|
404
|
+
*
|
|
405
|
+
* Delegates to `resolveCanonicalTaskPaths` for consistent path resolution
|
|
406
|
+
* across repo mode (task folder inside repo) and workspace mode (external
|
|
407
|
+
* task folder).
|
|
408
|
+
*
|
|
409
|
+
* @param taskFolder - Absolute task folder path (from main repo)
|
|
410
|
+
* @param worktreePath - Absolute path to the lane worktree
|
|
411
|
+
* @param repoRoot - Absolute path to the main repository root
|
|
412
|
+
* @returns Absolute path to the .DONE file in the worktree
|
|
413
|
+
*/
|
|
414
|
+
export function resolveTaskDonePath(
|
|
415
|
+
taskFolder: string,
|
|
416
|
+
worktreePath: string,
|
|
417
|
+
repoRoot: string,
|
|
418
|
+
): string {
|
|
419
|
+
return resolveCanonicalTaskPaths(taskFolder, worktreePath, repoRoot).donePath;
|
|
341
420
|
}
|
|
342
421
|
|
|
343
422
|
/**
|
|
@@ -467,8 +546,9 @@ export async function pollUntilTaskComplete(
|
|
|
467
546
|
): Promise<{ status: LaneTaskStatus; exitReason: string; doneFileFound: boolean }> {
|
|
468
547
|
const sessionName = lane.tmuxSessionName;
|
|
469
548
|
const laneId = lane.laneId;
|
|
470
|
-
const
|
|
471
|
-
const
|
|
549
|
+
const resolved = resolveCanonicalTaskPaths(task.task.taskFolder, lane.worktreePath, repoRoot);
|
|
550
|
+
const donePath = resolved.donePath;
|
|
551
|
+
const statusPath = resolved.statusPath;
|
|
472
552
|
const laneLogPath = resolveLaneLogPath(lane, task);
|
|
473
553
|
|
|
474
554
|
execLog(laneId, task.taskId, "polling for completion", {
|
|
@@ -788,30 +868,12 @@ export function parseWorktreeStatusMd(
|
|
|
788
868
|
worktreePath: string,
|
|
789
869
|
repoRoot: string,
|
|
790
870
|
): { parsed: ParsedWorktreeStatus | null; error: string | null } {
|
|
791
|
-
//
|
|
792
|
-
const
|
|
793
|
-
const
|
|
794
|
-
|
|
795
|
-
let relativePath: string;
|
|
796
|
-
if (folderNorm.startsWith(repoRootNorm + "/")) {
|
|
797
|
-
relativePath = folderNorm.slice(repoRootNorm.length + 1);
|
|
798
|
-
} else {
|
|
799
|
-
relativePath = taskFolder;
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
let statusPath = join(worktreePath, relativePath, "STATUS.md");
|
|
871
|
+
// Use canonical resolver for consistent path translation
|
|
872
|
+
const resolved = resolveCanonicalTaskPaths(taskFolder, worktreePath, repoRoot);
|
|
873
|
+
const statusPath = resolved.statusPath;
|
|
803
874
|
|
|
804
875
|
if (!existsSync(statusPath)) {
|
|
805
|
-
|
|
806
|
-
const parts = relativePath.replace(/\\/g, "/").split("/");
|
|
807
|
-
const taskDirName = parts[parts.length - 1];
|
|
808
|
-
const parentParts = parts.slice(0, -1);
|
|
809
|
-
const archiveStatusPath = join(worktreePath, ...parentParts, "archive", taskDirName, "STATUS.md");
|
|
810
|
-
if (existsSync(archiveStatusPath)) {
|
|
811
|
-
statusPath = archiveStatusPath;
|
|
812
|
-
} else {
|
|
813
|
-
return { parsed: null, error: `STATUS.md not found at ${statusPath}` };
|
|
814
|
-
}
|
|
876
|
+
return { parsed: null, error: `STATUS.md not found at ${statusPath}` };
|
|
815
877
|
}
|
|
816
878
|
|
|
817
879
|
let content: string;
|
|
@@ -1403,7 +1465,7 @@ export function computeTransitiveDependents(
|
|
|
1403
1465
|
*
|
|
1404
1466
|
* This function checks each wave task's folder for untracked or modified files,
|
|
1405
1467
|
* stages them, and creates a commit on the current branch. This must run BEFORE
|
|
1406
|
-
* allocateLanes() so that worktrees (which are based on the
|
|
1468
|
+
* allocateLanes() so that worktrees (which are based on the batch's base branch)
|
|
1407
1469
|
* include the task files.
|
|
1408
1470
|
*
|
|
1409
1471
|
* Only task-specific folders are staged — no other working tree changes are touched.
|
|
@@ -1522,6 +1584,7 @@ export function ensureTaskFilesCommitted(
|
|
|
1522
1584
|
* @param batchId - Batch ID for naming
|
|
1523
1585
|
* @param pauseSignal - Shared pause signal (mutated by stop-wave policy)
|
|
1524
1586
|
* @param dependencyGraph - Dependency graph for computing transitive dependents
|
|
1587
|
+
* @param baseBranch - Branch to base worktrees on (captured at batch start)
|
|
1525
1588
|
* @param onMonitorUpdate - Optional callback for dashboard updates during monitoring
|
|
1526
1589
|
* @param onLanesAllocated - Optional callback fired after lane allocation succeeds
|
|
1527
1590
|
* @returns WaveExecutionResult with outcomes and blocked task IDs
|
|
@@ -1535,6 +1598,7 @@ export async function executeWave(
|
|
|
1535
1598
|
batchId: string,
|
|
1536
1599
|
pauseSignal: { paused: boolean },
|
|
1537
1600
|
dependencyGraph: DependencyGraph,
|
|
1601
|
+
baseBranch: string,
|
|
1538
1602
|
onMonitorUpdate?: MonitorUpdateCallback,
|
|
1539
1603
|
onLanesAllocated?: (lanes: AllocatedLane[]) => void,
|
|
1540
1604
|
): Promise<WaveExecutionResult> {
|
|
@@ -1576,7 +1640,7 @@ export async function executeWave(
|
|
|
1576
1640
|
}
|
|
1577
1641
|
|
|
1578
1642
|
// ── Stage 1: Allocate lanes ──────────────────────────────────
|
|
1579
|
-
const allocResult = allocateLanes(waveTasks, pending, config, repoRoot, batchId);
|
|
1643
|
+
const allocResult = allocateLanes(waveTasks, pending, config, repoRoot, batchId, baseBranch);
|
|
1580
1644
|
|
|
1581
1645
|
if (!allocResult.success) {
|
|
1582
1646
|
const errMsg = allocResult.error?.message || "Unknown allocation failure";
|