taskplane 0.26.1 → 0.27.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/extensions/taskplane/abort.ts +2 -1
- package/extensions/taskplane/agent-host.ts +100 -1
- package/extensions/taskplane/cleanup.ts +272 -10
- package/extensions/taskplane/engine.ts +182 -47
- package/extensions/taskplane/execution.ts +139 -49
- 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 +219 -10
- 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 +27 -2
- package/package.json +1 -1
- package/templates/agents/task-worker.md +51 -9
|
@@ -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
|
|
|
@@ -1436,6 +1436,124 @@ export function ensureTaskFilesCommitted(
|
|
|
1436
1436
|
|
|
1437
1437
|
if (foldersToStage.length === 0) return;
|
|
1438
1438
|
|
|
1439
|
+
// TP-169: When an orch branch is provided, commit task files directly on
|
|
1440
|
+
// the orch branch using a temporary git index file. This avoids polluting
|
|
1441
|
+
// the repo's current branch (e.g. main) with orchestrator-internal staging
|
|
1442
|
+
// commits, maintaining proper branch isolation in workspace mode.
|
|
1443
|
+
//
|
|
1444
|
+
// Approach:
|
|
1445
|
+
// 1. Read the orch branch's tree into a temporary index
|
|
1446
|
+
// 2. Add new/modified task files to the temporary index
|
|
1447
|
+
// 3. Write the combined tree
|
|
1448
|
+
// 4. Create a commit on the orch branch
|
|
1449
|
+
// 5. Update the orch branch ref
|
|
1450
|
+
// 6. Clean up the temporary index
|
|
1451
|
+
//
|
|
1452
|
+
// Fallback: if orch branch plumbing fails or orchBranch is not provided,
|
|
1453
|
+
// fall back to the legacy path of committing on HEAD.
|
|
1454
|
+
if (orchBranch) {
|
|
1455
|
+
const orchTipRes = runGit(["rev-parse", `refs/heads/${orchBranch}`], repoRoot);
|
|
1456
|
+
if (orchTipRes.ok) {
|
|
1457
|
+
const orchTip = orchTipRes.stdout.trim();
|
|
1458
|
+
const tmpIdx = join(repoRoot, ".git", `tmp-staging-idx-wave-${waveIndex}`);
|
|
1459
|
+
|
|
1460
|
+
try {
|
|
1461
|
+
// Read orch branch tree into temporary index
|
|
1462
|
+
const readTreeRes = runGitWithEnv(
|
|
1463
|
+
["read-tree", orchTip],
|
|
1464
|
+
repoRoot,
|
|
1465
|
+
{ GIT_INDEX_FILE: tmpIdx },
|
|
1466
|
+
);
|
|
1467
|
+
if (!readTreeRes.ok) {
|
|
1468
|
+
execLog("wave", `W${waveIndex}`, `orch branch staging: read-tree failed, falling back to HEAD commit`, {
|
|
1469
|
+
error: readTreeRes.stderr,
|
|
1470
|
+
});
|
|
1471
|
+
// Fall through to legacy path
|
|
1472
|
+
} else {
|
|
1473
|
+
// Add task files to temporary index
|
|
1474
|
+
let addFailed = false;
|
|
1475
|
+
for (const folder of foldersToStage) {
|
|
1476
|
+
const addRes = runGitWithEnv(
|
|
1477
|
+
["add", "--", folder],
|
|
1478
|
+
repoRoot,
|
|
1479
|
+
{ GIT_INDEX_FILE: tmpIdx },
|
|
1480
|
+
);
|
|
1481
|
+
if (!addRes.ok) {
|
|
1482
|
+
execLog("wave", `W${waveIndex}`, `orch branch staging: git add failed for ${folder}, falling back`, {
|
|
1483
|
+
error: addRes.stderr,
|
|
1484
|
+
});
|
|
1485
|
+
addFailed = true;
|
|
1486
|
+
break;
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
if (!addFailed) {
|
|
1491
|
+
// Write tree from temporary index
|
|
1492
|
+
const writeTreeRes = runGitWithEnv(
|
|
1493
|
+
["write-tree"],
|
|
1494
|
+
repoRoot,
|
|
1495
|
+
{ GIT_INDEX_FILE: tmpIdx },
|
|
1496
|
+
);
|
|
1497
|
+
|
|
1498
|
+
if (writeTreeRes.ok) {
|
|
1499
|
+
const tree = writeTreeRes.stdout.trim();
|
|
1500
|
+
const taskIds = foldersToStage.map(f => f.split("/").pop() || f).join(", ");
|
|
1501
|
+
const commitMsg = `chore: stage task files for orchestrator wave ${waveIndex} (${taskIds})`;
|
|
1502
|
+
|
|
1503
|
+
// Create commit directly on orch branch
|
|
1504
|
+
const commitTreeRes = runGit(
|
|
1505
|
+
["commit-tree", tree, "-p", orchTip, "-m", commitMsg],
|
|
1506
|
+
repoRoot,
|
|
1507
|
+
);
|
|
1508
|
+
|
|
1509
|
+
if (commitTreeRes.ok) {
|
|
1510
|
+
const newCommit = commitTreeRes.stdout.trim();
|
|
1511
|
+
const refUpdateRes = runGit(
|
|
1512
|
+
["update-ref", `refs/heads/${orchBranch}`, newCommit, orchTip],
|
|
1513
|
+
repoRoot,
|
|
1514
|
+
);
|
|
1515
|
+
|
|
1516
|
+
if (refUpdateRes.ok) {
|
|
1517
|
+
execLog("wave", `W${waveIndex}`, `committed ${foldersToStage.length} task folder(s) directly on orch branch`, {
|
|
1518
|
+
orchBranch,
|
|
1519
|
+
folders: foldersToStage,
|
|
1520
|
+
from: orchTip.slice(0, 8),
|
|
1521
|
+
to: newCommit.slice(0, 8),
|
|
1522
|
+
});
|
|
1523
|
+
// Clean up temp index and return — no need for legacy path
|
|
1524
|
+
try { unlinkSync(tmpIdx); } catch { /* best effort */ }
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
execLog("wave", `W${waveIndex}`, `orch branch staging: ref update failed, falling back`, {
|
|
1528
|
+
error: refUpdateRes.stderr,
|
|
1529
|
+
});
|
|
1530
|
+
} else {
|
|
1531
|
+
execLog("wave", `W${waveIndex}`, `orch branch staging: commit-tree failed, falling back`, {
|
|
1532
|
+
error: commitTreeRes.stderr,
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
} else {
|
|
1536
|
+
execLog("wave", `W${waveIndex}`, `orch branch staging: write-tree failed, falling back`, {
|
|
1537
|
+
error: writeTreeRes.stderr,
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
} catch (err: unknown) {
|
|
1543
|
+
execLog("wave", `W${waveIndex}`, `orch branch staging: unexpected error, falling back to HEAD commit`, {
|
|
1544
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1545
|
+
});
|
|
1546
|
+
} finally {
|
|
1547
|
+
// Always clean up temp index
|
|
1548
|
+
try { unlinkSync(tmpIdx); } catch { /* best effort */ }
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
// Legacy fallback: commit on HEAD and sync orch branch.
|
|
1554
|
+
// This path is used when orchBranch is not provided, or when the
|
|
1555
|
+
// plumbing-based approach above failed.
|
|
1556
|
+
|
|
1439
1557
|
// Stage only the task folders
|
|
1440
1558
|
for (const folder of foldersToStage) {
|
|
1441
1559
|
const addResult = runGit(["add", "--", folder], repoRoot);
|
|
@@ -1472,15 +1590,6 @@ export function ensureTaskFilesCommitted(
|
|
|
1472
1590
|
// Fast-forward (or merge) the orch branch to include the staging commit so
|
|
1473
1591
|
// that worktrees—which branch from orchBranch—see the new task files and
|
|
1474
1592
|
// 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
1593
|
if (orchBranch) {
|
|
1485
1594
|
try {
|
|
1486
1595
|
const headRes = runGit(["rev-parse", "HEAD"], repoRoot);
|
|
@@ -1490,16 +1599,12 @@ export function ensureTaskFilesCommitted(
|
|
|
1490
1599
|
const newHead = headRes.stdout.trim();
|
|
1491
1600
|
const orchTip = orchTipRes.stdout.trim();
|
|
1492
1601
|
|
|
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
1602
|
const ancestorCheck = runGit(
|
|
1496
1603
|
["merge-base", "--is-ancestor", orchTip, newHead],
|
|
1497
1604
|
repoRoot,
|
|
1498
1605
|
);
|
|
1499
1606
|
|
|
1500
1607
|
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
1608
|
const ffResult = runGit(
|
|
1504
1609
|
["update-ref", `refs/heads/${orchBranch}`, newHead, orchTip],
|
|
1505
1610
|
repoRoot,
|
|
@@ -1517,29 +1622,16 @@ export function ensureTaskFilesCommitted(
|
|
|
1517
1622
|
});
|
|
1518
1623
|
}
|
|
1519
1624
|
} 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
1625
|
const mergeTreeRes = runGit(
|
|
1525
1626
|
["merge-tree", "--write-tree", orchTip, newHead],
|
|
1526
1627
|
repoRoot,
|
|
1527
1628
|
);
|
|
1528
1629
|
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
1630
|
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`;
|
|
1631
|
+
if (/^[0-9a-f]{40}$/i.test(mergedTree)) {
|
|
1632
|
+
const mergeMsg = `merge: include staged task files for wave ${waveIndex} into orch branch`;
|
|
1541
1633
|
const commitTreeRes = runGit(
|
|
1542
|
-
["commit-tree", mergedTree, "-p", orchTip, "-p", newHead, "-m",
|
|
1634
|
+
["commit-tree", mergedTree, "-p", orchTip, "-p", newHead, "-m", mergeMsg],
|
|
1543
1635
|
repoRoot,
|
|
1544
1636
|
);
|
|
1545
1637
|
if (commitTreeRes.ok) {
|
|
@@ -1551,28 +1643,11 @@ export function ensureTaskFilesCommitted(
|
|
|
1551
1643
|
if (refUpdateRes.ok) {
|
|
1552
1644
|
execLog("wave", `W${waveIndex}`, `merged staging commit into orch branch (non-FF wave)`, {
|
|
1553
1645
|
orchBranch,
|
|
1554
|
-
orchTip: orchTip.slice(0, 8),
|
|
1555
|
-
newHead: newHead.slice(0, 8),
|
|
1556
1646
|
mergeCommit: mergeCommitSha.slice(0, 8),
|
|
1557
1647
|
});
|
|
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
1648
|
}
|
|
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
1649
|
}
|
|
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
|
-
});
|
|
1650
|
+
}
|
|
1576
1651
|
}
|
|
1577
1652
|
}
|
|
1578
1653
|
}
|
|
@@ -2093,8 +2168,23 @@ export function buildExecutionUnit(
|
|
|
2093
2168
|
repoRoot: string,
|
|
2094
2169
|
isWorkspaceMode?: boolean,
|
|
2095
2170
|
): ExecutionUnit {
|
|
2171
|
+
// TP-169: Guard against missing taskFolder. This can happen when
|
|
2172
|
+
// reconstructAllocatedLanes creates task stubs from persisted state
|
|
2173
|
+
// where taskFolder enrichment failed (e.g., dynamically-expanded
|
|
2174
|
+
// segments whose persisted records had empty taskFolder).
|
|
2175
|
+
const taskFolder = task.task?.taskFolder;
|
|
2176
|
+
if (!taskFolder) {
|
|
2177
|
+
throw new ExecutionError(
|
|
2178
|
+
"EXEC_MISSING_TASK_FOLDER",
|
|
2179
|
+
`Cannot build execution unit for task ${task.taskId}: taskFolder is ${taskFolder === "" ? "empty" : "undefined"}. ` +
|
|
2180
|
+
`This typically means the task's persisted record was not enriched with discovery data. ` +
|
|
2181
|
+
`Re-run discovery or check that the task exists in the task area.`,
|
|
2182
|
+
"execution",
|
|
2183
|
+
task.taskId,
|
|
2184
|
+
);
|
|
2185
|
+
}
|
|
2096
2186
|
const resolved = resolveCanonicalTaskPaths(
|
|
2097
|
-
|
|
2187
|
+
taskFolder,
|
|
2098
2188
|
lane.worktreePath,
|
|
2099
2189
|
repoRoot,
|
|
2100
2190
|
isWorkspaceMode,
|