taskplane 0.26.1 → 0.28.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/README.md +4 -1
- package/bin/taskplane.mjs +12 -5
- package/dashboard/public/app.js +162 -13
- package/extensions/taskplane/abort.ts +2 -1
- package/extensions/taskplane/agent-host.ts +100 -1
- package/extensions/taskplane/cleanup.ts +272 -10
- package/extensions/taskplane/discovery.ts +1818 -1508
- package/extensions/taskplane/engine.ts +182 -47
- package/extensions/taskplane/execution.ts +172 -51
- package/extensions/taskplane/extension.ts +5125 -5125
- package/extensions/taskplane/formatting.ts +70 -11
- package/extensions/taskplane/git.ts +34 -0
- package/extensions/taskplane/lane-runner.ts +586 -46
- package/extensions/taskplane/merge.ts +3128 -2917
- package/extensions/taskplane/persistence.ts +3 -0
- package/extensions/taskplane/resume.ts +86 -30
- package/extensions/taskplane/supervisor-primer.md +55 -0
- package/extensions/taskplane/types.ts +52 -3
- package/package.json +1 -1
- package/skills/create-taskplane-task/SKILL.md +58 -0
- package/skills/create-taskplane-task/references/prompt-template.md +39 -0
- package/templates/agents/task-worker-segment.md +44 -0
- package/templates/agents/task-worker.md +429 -387
|
@@ -13,7 +13,7 @@ import { resolvePacketPaths, buildRuntimeAgentId } from "./types.ts";
|
|
|
13
13
|
import { readRegistrySnapshot, readLaneSnapshot, isTerminalStatus, isProcessAlive, detectOrphans, markOrphansCrashed, buildRegistrySnapshot, writeRegistrySnapshot } from "./process-registry.ts";
|
|
14
14
|
import { allocateLanes } from "./waves.ts";
|
|
15
15
|
import { resolveOperatorId } from "./naming.ts";
|
|
16
|
-
import { runGit } from "./git.ts";
|
|
16
|
+
import { runGit, runGitWithEnv } from "./git.ts";
|
|
17
17
|
import { resolveTaskplanePackageFile, resolveTaskplaneAgentTemplate } from "./path-resolver.ts";
|
|
18
18
|
import { resolvePointer, loadWorkspaceConfig } from "./workspace.ts";
|
|
19
19
|
|
|
@@ -662,6 +662,27 @@ export function parseWorktreeStatusMd(
|
|
|
662
662
|
*
|
|
663
663
|
* @since TP-070
|
|
664
664
|
*/
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Parse STATUS.md directly from a known absolute path.
|
|
668
|
+
* Unlike parseWorktreeStatusMdAsync, this does NOT re-resolve the path —
|
|
669
|
+
* it reads exactly the file you point it to. Use this when the caller
|
|
670
|
+
* already has the authoritative statusPath (e.g., from buildExecutionUnit).
|
|
671
|
+
*
|
|
672
|
+
* @since TP-501
|
|
673
|
+
*/
|
|
674
|
+
export async function parseStatusMdAtPath(
|
|
675
|
+
statusPath: string,
|
|
676
|
+
): Promise<{ parsed: ParsedWorktreeStatus | null; error: string | null }> {
|
|
677
|
+
return parseStatusMdContent(statusPath);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Parse STATUS.md by resolving the path from taskFolder + worktree context.
|
|
682
|
+
* Use parseStatusMdAtPath instead when the caller already has the authoritative path.
|
|
683
|
+
*
|
|
684
|
+
* @since TP-070
|
|
685
|
+
*/
|
|
665
686
|
export async function parseWorktreeStatusMdAsync(
|
|
666
687
|
taskFolder: string,
|
|
667
688
|
worktreePath: string,
|
|
@@ -669,8 +690,13 @@ export async function parseWorktreeStatusMdAsync(
|
|
|
669
690
|
isWorkspaceMode?: boolean,
|
|
670
691
|
): Promise<{ parsed: ParsedWorktreeStatus | null; error: string | null }> {
|
|
671
692
|
const resolved = resolveCanonicalTaskPaths(taskFolder, worktreePath, repoRoot, isWorkspaceMode);
|
|
672
|
-
|
|
693
|
+
return parseStatusMdContent(resolved.statusPath);
|
|
694
|
+
}
|
|
673
695
|
|
|
696
|
+
/** Shared STATUS.md content parser — reads and parses from a known path. Handles file-not-found. */
|
|
697
|
+
async function parseStatusMdContent(
|
|
698
|
+
statusPath: string,
|
|
699
|
+
): Promise<{ parsed: ParsedWorktreeStatus | null; error: string | null }> {
|
|
674
700
|
if (!(await fileExistsAsync(statusPath))) {
|
|
675
701
|
return { parsed: null, error: `STATUS.md not found at ${statusPath}` };
|
|
676
702
|
}
|
|
@@ -1188,7 +1214,7 @@ export async function monitorLanes(
|
|
|
1188
1214
|
const unit = buildExecutionUnit(lane, task, repoRoot, isWorkspaceMode);
|
|
1189
1215
|
const donePath = unit.packet.donePath;
|
|
1190
1216
|
const statusPath = unit.packet.statusPath;
|
|
1191
|
-
const statusResult = await
|
|
1217
|
+
const statusResult = await parseStatusMdAtPath(statusPath);
|
|
1192
1218
|
|
|
1193
1219
|
const snapshot = await resolveTaskMonitorState(
|
|
1194
1220
|
task.taskId,
|
|
@@ -1436,6 +1462,124 @@ export function ensureTaskFilesCommitted(
|
|
|
1436
1462
|
|
|
1437
1463
|
if (foldersToStage.length === 0) return;
|
|
1438
1464
|
|
|
1465
|
+
// TP-169: When an orch branch is provided, commit task files directly on
|
|
1466
|
+
// the orch branch using a temporary git index file. This avoids polluting
|
|
1467
|
+
// the repo's current branch (e.g. main) with orchestrator-internal staging
|
|
1468
|
+
// commits, maintaining proper branch isolation in workspace mode.
|
|
1469
|
+
//
|
|
1470
|
+
// Approach:
|
|
1471
|
+
// 1. Read the orch branch's tree into a temporary index
|
|
1472
|
+
// 2. Add new/modified task files to the temporary index
|
|
1473
|
+
// 3. Write the combined tree
|
|
1474
|
+
// 4. Create a commit on the orch branch
|
|
1475
|
+
// 5. Update the orch branch ref
|
|
1476
|
+
// 6. Clean up the temporary index
|
|
1477
|
+
//
|
|
1478
|
+
// Fallback: if orch branch plumbing fails or orchBranch is not provided,
|
|
1479
|
+
// fall back to the legacy path of committing on HEAD.
|
|
1480
|
+
if (orchBranch) {
|
|
1481
|
+
const orchTipRes = runGit(["rev-parse", `refs/heads/${orchBranch}`], repoRoot);
|
|
1482
|
+
if (orchTipRes.ok) {
|
|
1483
|
+
const orchTip = orchTipRes.stdout.trim();
|
|
1484
|
+
const tmpIdx = join(repoRoot, ".git", `tmp-staging-idx-wave-${waveIndex}`);
|
|
1485
|
+
|
|
1486
|
+
try {
|
|
1487
|
+
// Read orch branch tree into temporary index
|
|
1488
|
+
const readTreeRes = runGitWithEnv(
|
|
1489
|
+
["read-tree", orchTip],
|
|
1490
|
+
repoRoot,
|
|
1491
|
+
{ GIT_INDEX_FILE: tmpIdx },
|
|
1492
|
+
);
|
|
1493
|
+
if (!readTreeRes.ok) {
|
|
1494
|
+
execLog("wave", `W${waveIndex}`, `orch branch staging: read-tree failed, falling back to HEAD commit`, {
|
|
1495
|
+
error: readTreeRes.stderr,
|
|
1496
|
+
});
|
|
1497
|
+
// Fall through to legacy path
|
|
1498
|
+
} else {
|
|
1499
|
+
// Add task files to temporary index
|
|
1500
|
+
let addFailed = false;
|
|
1501
|
+
for (const folder of foldersToStage) {
|
|
1502
|
+
const addRes = runGitWithEnv(
|
|
1503
|
+
["add", "--", folder],
|
|
1504
|
+
repoRoot,
|
|
1505
|
+
{ GIT_INDEX_FILE: tmpIdx },
|
|
1506
|
+
);
|
|
1507
|
+
if (!addRes.ok) {
|
|
1508
|
+
execLog("wave", `W${waveIndex}`, `orch branch staging: git add failed for ${folder}, falling back`, {
|
|
1509
|
+
error: addRes.stderr,
|
|
1510
|
+
});
|
|
1511
|
+
addFailed = true;
|
|
1512
|
+
break;
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
if (!addFailed) {
|
|
1517
|
+
// Write tree from temporary index
|
|
1518
|
+
const writeTreeRes = runGitWithEnv(
|
|
1519
|
+
["write-tree"],
|
|
1520
|
+
repoRoot,
|
|
1521
|
+
{ GIT_INDEX_FILE: tmpIdx },
|
|
1522
|
+
);
|
|
1523
|
+
|
|
1524
|
+
if (writeTreeRes.ok) {
|
|
1525
|
+
const tree = writeTreeRes.stdout.trim();
|
|
1526
|
+
const taskIds = foldersToStage.map(f => f.split("/").pop() || f).join(", ");
|
|
1527
|
+
const commitMsg = `chore: stage task files for orchestrator wave ${waveIndex} (${taskIds})`;
|
|
1528
|
+
|
|
1529
|
+
// Create commit directly on orch branch
|
|
1530
|
+
const commitTreeRes = runGit(
|
|
1531
|
+
["commit-tree", tree, "-p", orchTip, "-m", commitMsg],
|
|
1532
|
+
repoRoot,
|
|
1533
|
+
);
|
|
1534
|
+
|
|
1535
|
+
if (commitTreeRes.ok) {
|
|
1536
|
+
const newCommit = commitTreeRes.stdout.trim();
|
|
1537
|
+
const refUpdateRes = runGit(
|
|
1538
|
+
["update-ref", `refs/heads/${orchBranch}`, newCommit, orchTip],
|
|
1539
|
+
repoRoot,
|
|
1540
|
+
);
|
|
1541
|
+
|
|
1542
|
+
if (refUpdateRes.ok) {
|
|
1543
|
+
execLog("wave", `W${waveIndex}`, `committed ${foldersToStage.length} task folder(s) directly on orch branch`, {
|
|
1544
|
+
orchBranch,
|
|
1545
|
+
folders: foldersToStage,
|
|
1546
|
+
from: orchTip.slice(0, 8),
|
|
1547
|
+
to: newCommit.slice(0, 8),
|
|
1548
|
+
});
|
|
1549
|
+
// Clean up temp index and return — no need for legacy path
|
|
1550
|
+
try { unlinkSync(tmpIdx); } catch { /* best effort */ }
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
execLog("wave", `W${waveIndex}`, `orch branch staging: ref update failed, falling back`, {
|
|
1554
|
+
error: refUpdateRes.stderr,
|
|
1555
|
+
});
|
|
1556
|
+
} else {
|
|
1557
|
+
execLog("wave", `W${waveIndex}`, `orch branch staging: commit-tree failed, falling back`, {
|
|
1558
|
+
error: commitTreeRes.stderr,
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
} else {
|
|
1562
|
+
execLog("wave", `W${waveIndex}`, `orch branch staging: write-tree failed, falling back`, {
|
|
1563
|
+
error: writeTreeRes.stderr,
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
} catch (err: unknown) {
|
|
1569
|
+
execLog("wave", `W${waveIndex}`, `orch branch staging: unexpected error, falling back to HEAD commit`, {
|
|
1570
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1571
|
+
});
|
|
1572
|
+
} finally {
|
|
1573
|
+
// Always clean up temp index
|
|
1574
|
+
try { unlinkSync(tmpIdx); } catch { /* best effort */ }
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// Legacy fallback: commit on HEAD and sync orch branch.
|
|
1580
|
+
// This path is used when orchBranch is not provided, or when the
|
|
1581
|
+
// plumbing-based approach above failed.
|
|
1582
|
+
|
|
1439
1583
|
// Stage only the task folders
|
|
1440
1584
|
for (const folder of foldersToStage) {
|
|
1441
1585
|
const addResult = runGit(["add", "--", folder], repoRoot);
|
|
@@ -1472,15 +1616,6 @@ export function ensureTaskFilesCommitted(
|
|
|
1472
1616
|
// Fast-forward (or merge) the orch branch to include the staging commit so
|
|
1473
1617
|
// that worktrees—which branch from orchBranch—see the new task files and
|
|
1474
1618
|
// workers can find their PROMPT.md / STATUS.md without an ENOENT crash.
|
|
1475
|
-
//
|
|
1476
|
-
// The orch branch was created from baseBranch before executeWave runs, so in
|
|
1477
|
-
// wave 1 it is always an ancestor of HEAD and a plain fast-forward applies.
|
|
1478
|
-
// In wave 2+ the orch branch may have advanced due to prior wave merges
|
|
1479
|
-
// (commits from worker worktrees merged back). In that case we create a merge
|
|
1480
|
-
// commit so the task files become visible without rewinding any wave history.
|
|
1481
|
-
//
|
|
1482
|
-
// Failure here is non-fatal: the commit already succeeded; if the ref update
|
|
1483
|
-
// fails the subsequent worktree allocation will produce a clear error.
|
|
1484
1619
|
if (orchBranch) {
|
|
1485
1620
|
try {
|
|
1486
1621
|
const headRes = runGit(["rev-parse", "HEAD"], repoRoot);
|
|
@@ -1490,16 +1625,12 @@ export function ensureTaskFilesCommitted(
|
|
|
1490
1625
|
const newHead = headRes.stdout.trim();
|
|
1491
1626
|
const orchTip = orchTipRes.stdout.trim();
|
|
1492
1627
|
|
|
1493
|
-
// Check whether the orch branch tip is an ancestor of the new HEAD
|
|
1494
|
-
// (i.e., a fast-forward is safe and sufficient).
|
|
1495
1628
|
const ancestorCheck = runGit(
|
|
1496
1629
|
["merge-base", "--is-ancestor", orchTip, newHead],
|
|
1497
1630
|
repoRoot,
|
|
1498
1631
|
);
|
|
1499
1632
|
|
|
1500
1633
|
if (ancestorCheck.ok) {
|
|
1501
|
-
// FF case: orch branch is behind HEAD — move it forward.
|
|
1502
|
-
// Expected-old-sha semantics guard against concurrent ref moves.
|
|
1503
1634
|
const ffResult = runGit(
|
|
1504
1635
|
["update-ref", `refs/heads/${orchBranch}`, newHead, orchTip],
|
|
1505
1636
|
repoRoot,
|
|
@@ -1517,29 +1648,16 @@ export function ensureTaskFilesCommitted(
|
|
|
1517
1648
|
});
|
|
1518
1649
|
}
|
|
1519
1650
|
} else {
|
|
1520
|
-
// Non-FF case: orch branch has advanced due to prior wave merges.
|
|
1521
|
-
// Create a merge commit so the new task files become visible in
|
|
1522
|
-
// worktrees without discarding any accumulated wave history.
|
|
1523
|
-
// Requires git ≥ 2.38 for `merge-tree --write-tree`.
|
|
1524
1651
|
const mergeTreeRes = runGit(
|
|
1525
1652
|
["merge-tree", "--write-tree", orchTip, newHead],
|
|
1526
1653
|
repoRoot,
|
|
1527
1654
|
);
|
|
1528
1655
|
if (mergeTreeRes.ok) {
|
|
1529
|
-
// First line of stdout is the merged tree SHA.
|
|
1530
|
-
// git merge-tree --write-tree exits 0 on clean merge, non-zero on conflicts.
|
|
1531
|
-
// Since it exited 0, the tree should be conflict-free, but validate
|
|
1532
|
-
// the SHA looks like a valid 40-hex OID before using it.
|
|
1533
1656
|
const mergedTree = mergeTreeRes.stdout.trim().split("\n")[0];
|
|
1534
|
-
if (
|
|
1535
|
-
|
|
1536
|
-
orchBranch,
|
|
1537
|
-
output: mergedTree.slice(0, 60),
|
|
1538
|
-
});
|
|
1539
|
-
} else {
|
|
1540
|
-
const mergeCommitMsg = `merge: include staged task files for wave ${waveIndex} into orch branch`;
|
|
1657
|
+
if (/^[0-9a-f]{40}$/i.test(mergedTree)) {
|
|
1658
|
+
const mergeMsg = `merge: include staged task files for wave ${waveIndex} into orch branch`;
|
|
1541
1659
|
const commitTreeRes = runGit(
|
|
1542
|
-
["commit-tree", mergedTree, "-p", orchTip, "-p", newHead, "-m",
|
|
1660
|
+
["commit-tree", mergedTree, "-p", orchTip, "-p", newHead, "-m", mergeMsg],
|
|
1543
1661
|
repoRoot,
|
|
1544
1662
|
);
|
|
1545
1663
|
if (commitTreeRes.ok) {
|
|
@@ -1551,28 +1669,11 @@ export function ensureTaskFilesCommitted(
|
|
|
1551
1669
|
if (refUpdateRes.ok) {
|
|
1552
1670
|
execLog("wave", `W${waveIndex}`, `merged staging commit into orch branch (non-FF wave)`, {
|
|
1553
1671
|
orchBranch,
|
|
1554
|
-
orchTip: orchTip.slice(0, 8),
|
|
1555
|
-
newHead: newHead.slice(0, 8),
|
|
1556
1672
|
mergeCommit: mergeCommitSha.slice(0, 8),
|
|
1557
1673
|
});
|
|
1558
|
-
} else {
|
|
1559
|
-
execLog("wave", `W${waveIndex}`, `warning: failed to update orch branch ref after merge-tree (non-fatal)`, {
|
|
1560
|
-
orchBranch,
|
|
1561
|
-
error: refUpdateRes.stderr,
|
|
1562
|
-
});
|
|
1563
1674
|
}
|
|
1564
|
-
} else {
|
|
1565
|
-
execLog("wave", `W${waveIndex}`, `warning: failed to create merge commit for orch branch (non-fatal)`, {
|
|
1566
|
-
orchBranch,
|
|
1567
|
-
error: commitTreeRes.stderr,
|
|
1568
|
-
});
|
|
1569
1675
|
}
|
|
1570
|
-
}
|
|
1571
|
-
} else {
|
|
1572
|
-
execLog("wave", `W${waveIndex}`, `warning: failed to compute merge-tree for orch branch (non-fatal; requires git ≥ 2.38)`, {
|
|
1573
|
-
orchBranch,
|
|
1574
|
-
error: mergeTreeRes.stderr,
|
|
1575
|
-
});
|
|
1676
|
+
}
|
|
1576
1677
|
}
|
|
1577
1678
|
}
|
|
1578
1679
|
}
|
|
@@ -2093,8 +2194,23 @@ export function buildExecutionUnit(
|
|
|
2093
2194
|
repoRoot: string,
|
|
2094
2195
|
isWorkspaceMode?: boolean,
|
|
2095
2196
|
): ExecutionUnit {
|
|
2197
|
+
// TP-169: Guard against missing taskFolder. This can happen when
|
|
2198
|
+
// reconstructAllocatedLanes creates task stubs from persisted state
|
|
2199
|
+
// where taskFolder enrichment failed (e.g., dynamically-expanded
|
|
2200
|
+
// segments whose persisted records had empty taskFolder).
|
|
2201
|
+
const taskFolder = task.task?.taskFolder;
|
|
2202
|
+
if (!taskFolder) {
|
|
2203
|
+
throw new ExecutionError(
|
|
2204
|
+
"EXEC_MISSING_TASK_FOLDER",
|
|
2205
|
+
`Cannot build execution unit for task ${task.taskId}: taskFolder is ${taskFolder === "" ? "empty" : "undefined"}. ` +
|
|
2206
|
+
`This typically means the task's persisted record was not enriched with discovery data. ` +
|
|
2207
|
+
`Re-run discovery or check that the task exists in the task area.`,
|
|
2208
|
+
"execution",
|
|
2209
|
+
task.taskId,
|
|
2210
|
+
);
|
|
2211
|
+
}
|
|
2096
2212
|
const resolved = resolveCanonicalTaskPaths(
|
|
2097
|
-
|
|
2213
|
+
taskFolder,
|
|
2098
2214
|
lane.worktreePath,
|
|
2099
2215
|
repoRoot,
|
|
2100
2216
|
isWorkspaceMode,
|
|
@@ -2426,6 +2542,7 @@ export async function executeLaneV2(
|
|
|
2426
2542
|
// rules: checkpoint discipline, STATUS.md resume algorithm, review_step instructions.
|
|
2427
2543
|
// The local file (.pi/agents/task-worker.md) adds project-specific guidance.
|
|
2428
2544
|
let workerSystemPrompt = "You are a task execution agent. Read STATUS.md first, find unchecked items, work on them, checkpoint after each.";
|
|
2545
|
+
let workerSegmentPrompt = "";
|
|
2429
2546
|
try {
|
|
2430
2547
|
const basePrompt = loadBaseAgentPrompt("task-worker");
|
|
2431
2548
|
const localPrompt = loadLocalAgentPrompt(stateRoot, "task-worker");
|
|
@@ -2436,6 +2553,9 @@ export async function executeLaneV2(
|
|
|
2436
2553
|
} else if (localPrompt) {
|
|
2437
2554
|
workerSystemPrompt = localPrompt;
|
|
2438
2555
|
}
|
|
2556
|
+
// Load segment-scoped prompt overlay (appended when isSegmentScoped)
|
|
2557
|
+
const segPrompt = loadBaseAgentPrompt("task-worker-segment");
|
|
2558
|
+
if (segPrompt) workerSegmentPrompt = segPrompt;
|
|
2439
2559
|
} catch { /* use default */ }
|
|
2440
2560
|
|
|
2441
2561
|
execLog(laneId, "LANE", `starting Runtime V2 execution of ${lane.tasks.length} task(s)`, {
|
|
@@ -2482,6 +2602,7 @@ export async function executeLaneV2(
|
|
|
2482
2602
|
workerTools: "read,write,edit,bash,grep,find,ls",
|
|
2483
2603
|
workerThinking: "",
|
|
2484
2604
|
workerSystemPrompt,
|
|
2605
|
+
workerSegmentPrompt,
|
|
2485
2606
|
reviewerModel: extraEnvVars?.TASKPLANE_REVIEWER_MODEL || "",
|
|
2486
2607
|
reviewerThinking: extraEnvVars?.TASKPLANE_REVIEWER_THINKING || "",
|
|
2487
2608
|
reviewerTools: extraEnvVars?.TASKPLANE_REVIEWER_TOOLS || "",
|