taskplane 0.25.8 → 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/rpc-wrapper.mjs +0 -1
- package/bin/taskplane.mjs +13 -6
- package/dashboard/public/app.js +44 -1
- package/extensions/task-orchestrator.ts +1 -1
- 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/config-loader.ts +53 -0
- package/extensions/taskplane/context-window.ts +66 -0
- package/extensions/taskplane/engine.ts +182 -47
- package/extensions/taskplane/execution.ts +250 -66
- package/extensions/taskplane/extension.ts +5125 -5105
- 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/path-resolver.ts +1 -1
- package/extensions/taskplane/persistence.ts +3 -0
- package/extensions/taskplane/resume.ts +86 -30
- package/extensions/taskplane/sidecar-telemetry.ts +252 -0
- package/extensions/taskplane/supervisor-primer.md +57 -0
- package/extensions/taskplane/supervisor.ts +1 -1
- package/extensions/taskplane/task-executor-core.ts +2 -5
- package/extensions/taskplane/types.ts +27 -2
- package/package.json +1 -3
- package/templates/agents/task-worker.md +429 -387
- package/templates/tasks/CONTEXT.md +3 -4
- package/extensions/task-runner.ts +0 -2784
|
@@ -13,22 +13,13 @@ 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
|
+
import { resolvePointer, loadWorkspaceConfig } from "./workspace.ts";
|
|
18
19
|
|
|
19
20
|
// ── Taskplane Package File Resolution ────────────────────────────────
|
|
20
21
|
// getNpmGlobalRoot() and resolveTaskplanePackageFile() consolidated in path-resolver.ts (TP-157)
|
|
21
22
|
|
|
22
|
-
// ── Task Runner Extension Path Resolution ────────────────────────────
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Find the task-runner extension path for lane sessions.
|
|
26
|
-
* @see resolveTaskplanePackageFile for resolution order
|
|
27
|
-
*/
|
|
28
|
-
function resolveTaskRunnerExtensionPath(repoRoot: string): string {
|
|
29
|
-
return resolveTaskplanePackageFile(repoRoot, join("extensions", "task-runner.ts"));
|
|
30
|
-
}
|
|
31
|
-
|
|
32
23
|
// ── RPC Wrapper Path Resolution ──────────────────────────────────────
|
|
33
24
|
|
|
34
25
|
/**
|
|
@@ -50,7 +41,7 @@ function resolveTaskRunnerExtensionPath(repoRoot: string): string {
|
|
|
50
41
|
/**
|
|
51
42
|
* Structured log helper for lane execution.
|
|
52
43
|
*
|
|
53
|
-
* All execution logs go to stderr
|
|
44
|
+
* All execution logs go to stderr.
|
|
54
45
|
* Format: [orch] {laneId}/{taskId}: {message}
|
|
55
46
|
* Correlation fields: batchId, laneId, taskId, sessionName.
|
|
56
47
|
* No PII — only IDs and paths.
|
|
@@ -378,9 +369,8 @@ export function resolveCanonicalTaskPaths(
|
|
|
378
369
|
|
|
379
370
|
if (isWorkspaceMode) {
|
|
380
371
|
// Workspace mode: use worktree-relative path when the task folder is
|
|
381
|
-
// inside the lane's repo
|
|
382
|
-
//
|
|
383
|
-
// must look there too.
|
|
372
|
+
// inside the lane's repo. The worker writes .DONE and STATUS.md in
|
|
373
|
+
// the worktree, so the engine must look there too.
|
|
384
374
|
if (folderNorm.startsWith(repoRootNorm + "/")) {
|
|
385
375
|
const relPath = folderNorm.slice(repoRootNorm.length + 1);
|
|
386
376
|
resolvedFolder = join(worktreePath, relPath);
|
|
@@ -1392,7 +1382,7 @@ export function computeTransitiveDependents(
|
|
|
1392
1382
|
*
|
|
1393
1383
|
* Git worktrees only contain tracked (committed) files. If a user creates
|
|
1394
1384
|
* task folders (PROMPT.md, STATUS.md) but doesn't commit them, the worktree
|
|
1395
|
-
* won't have those files and
|
|
1385
|
+
* won't have those files and the worker will fail with "file not found".
|
|
1396
1386
|
*
|
|
1397
1387
|
* This function checks each wave task's folder for untracked or modified files,
|
|
1398
1388
|
* stages them, and creates a commit on the current branch. This must run BEFORE
|
|
@@ -1446,6 +1436,124 @@ export function ensureTaskFilesCommitted(
|
|
|
1446
1436
|
|
|
1447
1437
|
if (foldersToStage.length === 0) return;
|
|
1448
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
|
+
|
|
1449
1557
|
// Stage only the task folders
|
|
1450
1558
|
for (const folder of foldersToStage) {
|
|
1451
1559
|
const addResult = runGit(["add", "--", folder], repoRoot);
|
|
@@ -1482,15 +1590,6 @@ export function ensureTaskFilesCommitted(
|
|
|
1482
1590
|
// Fast-forward (or merge) the orch branch to include the staging commit so
|
|
1483
1591
|
// that worktrees—which branch from orchBranch—see the new task files and
|
|
1484
1592
|
// workers can find their PROMPT.md / STATUS.md without an ENOENT crash.
|
|
1485
|
-
//
|
|
1486
|
-
// The orch branch was created from baseBranch before executeWave runs, so in
|
|
1487
|
-
// wave 1 it is always an ancestor of HEAD and a plain fast-forward applies.
|
|
1488
|
-
// In wave 2+ the orch branch may have advanced due to prior wave merges
|
|
1489
|
-
// (commits from worker worktrees merged back). In that case we create a merge
|
|
1490
|
-
// commit so the task files become visible without rewinding any wave history.
|
|
1491
|
-
//
|
|
1492
|
-
// Failure here is non-fatal: the commit already succeeded; if the ref update
|
|
1493
|
-
// fails the subsequent worktree allocation will produce a clear error.
|
|
1494
1593
|
if (orchBranch) {
|
|
1495
1594
|
try {
|
|
1496
1595
|
const headRes = runGit(["rev-parse", "HEAD"], repoRoot);
|
|
@@ -1500,16 +1599,12 @@ export function ensureTaskFilesCommitted(
|
|
|
1500
1599
|
const newHead = headRes.stdout.trim();
|
|
1501
1600
|
const orchTip = orchTipRes.stdout.trim();
|
|
1502
1601
|
|
|
1503
|
-
// Check whether the orch branch tip is an ancestor of the new HEAD
|
|
1504
|
-
// (i.e., a fast-forward is safe and sufficient).
|
|
1505
1602
|
const ancestorCheck = runGit(
|
|
1506
1603
|
["merge-base", "--is-ancestor", orchTip, newHead],
|
|
1507
1604
|
repoRoot,
|
|
1508
1605
|
);
|
|
1509
1606
|
|
|
1510
1607
|
if (ancestorCheck.ok) {
|
|
1511
|
-
// FF case: orch branch is behind HEAD — move it forward.
|
|
1512
|
-
// Expected-old-sha semantics guard against concurrent ref moves.
|
|
1513
1608
|
const ffResult = runGit(
|
|
1514
1609
|
["update-ref", `refs/heads/${orchBranch}`, newHead, orchTip],
|
|
1515
1610
|
repoRoot,
|
|
@@ -1527,29 +1622,16 @@ export function ensureTaskFilesCommitted(
|
|
|
1527
1622
|
});
|
|
1528
1623
|
}
|
|
1529
1624
|
} else {
|
|
1530
|
-
// Non-FF case: orch branch has advanced due to prior wave merges.
|
|
1531
|
-
// Create a merge commit so the new task files become visible in
|
|
1532
|
-
// worktrees without discarding any accumulated wave history.
|
|
1533
|
-
// Requires git ≥ 2.38 for `merge-tree --write-tree`.
|
|
1534
1625
|
const mergeTreeRes = runGit(
|
|
1535
1626
|
["merge-tree", "--write-tree", orchTip, newHead],
|
|
1536
1627
|
repoRoot,
|
|
1537
1628
|
);
|
|
1538
1629
|
if (mergeTreeRes.ok) {
|
|
1539
|
-
// First line of stdout is the merged tree SHA.
|
|
1540
|
-
// git merge-tree --write-tree exits 0 on clean merge, non-zero on conflicts.
|
|
1541
|
-
// Since it exited 0, the tree should be conflict-free, but validate
|
|
1542
|
-
// the SHA looks like a valid 40-hex OID before using it.
|
|
1543
1630
|
const mergedTree = mergeTreeRes.stdout.trim().split("\n")[0];
|
|
1544
|
-
if (
|
|
1545
|
-
|
|
1546
|
-
orchBranch,
|
|
1547
|
-
output: mergedTree.slice(0, 60),
|
|
1548
|
-
});
|
|
1549
|
-
} else {
|
|
1550
|
-
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`;
|
|
1551
1633
|
const commitTreeRes = runGit(
|
|
1552
|
-
["commit-tree", mergedTree, "-p", orchTip, "-p", newHead, "-m",
|
|
1634
|
+
["commit-tree", mergedTree, "-p", orchTip, "-p", newHead, "-m", mergeMsg],
|
|
1553
1635
|
repoRoot,
|
|
1554
1636
|
);
|
|
1555
1637
|
if (commitTreeRes.ok) {
|
|
@@ -1561,28 +1643,11 @@ export function ensureTaskFilesCommitted(
|
|
|
1561
1643
|
if (refUpdateRes.ok) {
|
|
1562
1644
|
execLog("wave", `W${waveIndex}`, `merged staging commit into orch branch (non-FF wave)`, {
|
|
1563
1645
|
orchBranch,
|
|
1564
|
-
orchTip: orchTip.slice(0, 8),
|
|
1565
|
-
newHead: newHead.slice(0, 8),
|
|
1566
1646
|
mergeCommit: mergeCommitSha.slice(0, 8),
|
|
1567
1647
|
});
|
|
1568
|
-
} else {
|
|
1569
|
-
execLog("wave", `W${waveIndex}`, `warning: failed to update orch branch ref after merge-tree (non-fatal)`, {
|
|
1570
|
-
orchBranch,
|
|
1571
|
-
error: refUpdateRes.stderr,
|
|
1572
|
-
});
|
|
1573
1648
|
}
|
|
1574
|
-
} else {
|
|
1575
|
-
execLog("wave", `W${waveIndex}`, `warning: failed to create merge commit for orch branch (non-fatal)`, {
|
|
1576
|
-
orchBranch,
|
|
1577
|
-
error: commitTreeRes.stderr,
|
|
1578
|
-
});
|
|
1579
1649
|
}
|
|
1580
|
-
}
|
|
1581
|
-
} else {
|
|
1582
|
-
execLog("wave", `W${waveIndex}`, `warning: failed to compute merge-tree for orch branch (non-fatal; requires git ≥ 2.38)`, {
|
|
1583
|
-
orchBranch,
|
|
1584
|
-
error: mergeTreeRes.stderr,
|
|
1585
|
-
});
|
|
1650
|
+
}
|
|
1586
1651
|
}
|
|
1587
1652
|
}
|
|
1588
1653
|
}
|
|
@@ -1640,7 +1705,7 @@ export function ensureTaskFilesCommitted(
|
|
|
1640
1705
|
/**
|
|
1641
1706
|
* Runtime backend selector for lane execution.
|
|
1642
1707
|
*
|
|
1643
|
-
* - `"legacy"`: Session-backed path (spawnLaneSession
|
|
1708
|
+
* - `"legacy"`: Session-backed path (spawnLaneSession, deprecated)
|
|
1644
1709
|
* - `"v2"`: Direct-child path (lane-runner → agent-host → pi --mode rpc)
|
|
1645
1710
|
*
|
|
1646
1711
|
* @since TP-105
|
|
@@ -1677,7 +1742,7 @@ export async function executeWave(
|
|
|
1677
1742
|
// ── Stage 0: Ensure task files are committed ────────────────
|
|
1678
1743
|
// Task folders may contain untracked files (PROMPT.md, STATUS.md) that
|
|
1679
1744
|
// won't appear in worktrees unless committed. Stage and commit them now,
|
|
1680
|
-
// before worktree creation, so workers can find their
|
|
1745
|
+
// before worktree creation, so workers can find their task files.
|
|
1681
1746
|
// Pass orchBranch so the staging commit is reflected in the orch branch
|
|
1682
1747
|
// before worktrees are allocated from it.
|
|
1683
1748
|
try {
|
|
@@ -2103,8 +2168,23 @@ export function buildExecutionUnit(
|
|
|
2103
2168
|
repoRoot: string,
|
|
2104
2169
|
isWorkspaceMode?: boolean,
|
|
2105
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
|
+
}
|
|
2106
2186
|
const resolved = resolveCanonicalTaskPaths(
|
|
2107
|
-
|
|
2187
|
+
taskFolder,
|
|
2108
2188
|
lane.worktreePath,
|
|
2109
2189
|
repoRoot,
|
|
2110
2190
|
isWorkspaceMode,
|
|
@@ -2257,6 +2337,110 @@ function loadLocalAgentPrompt(stateRoot: string, agentName: string): string {
|
|
|
2257
2337
|
return "";
|
|
2258
2338
|
}
|
|
2259
2339
|
|
|
2340
|
+
// ── Agent Definition Loading ─────────────────────────────────────────
|
|
2341
|
+
|
|
2342
|
+
/** Track whether an agent pointer warning has been logged this session (log once). */
|
|
2343
|
+
let _execPointerWarningLogged = false;
|
|
2344
|
+
|
|
2345
|
+
/**
|
|
2346
|
+
* Reset agent pointer warning state for testing.
|
|
2347
|
+
* @since TP-161
|
|
2348
|
+
*/
|
|
2349
|
+
export function resetPointerWarning(): void {
|
|
2350
|
+
_execPointerWarningLogged = false;
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
/**
|
|
2354
|
+
* Resolve agent files using the workspace pointer (workspace mode only).
|
|
2355
|
+
* Returns the agentRoot from the pointer, or null in repo mode / on failure.
|
|
2356
|
+
*/
|
|
2357
|
+
function resolveAgentPointerRoot(): string | null {
|
|
2358
|
+
const wsRoot = process.env.TASKPLANE_WORKSPACE_ROOT;
|
|
2359
|
+
if (!wsRoot) return null;
|
|
2360
|
+
try {
|
|
2361
|
+
const wsConfig = loadWorkspaceConfig(wsRoot);
|
|
2362
|
+
const result = resolvePointer(wsRoot, wsConfig);
|
|
2363
|
+
if (result?.warning && !_execPointerWarningLogged) {
|
|
2364
|
+
_execPointerWarningLogged = true;
|
|
2365
|
+
console.error(`[execution] pointer: ${result.warning}`);
|
|
2366
|
+
}
|
|
2367
|
+
return result?.agentRoot ?? null;
|
|
2368
|
+
} catch {
|
|
2369
|
+
return null;
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
/**
|
|
2374
|
+
* Load a complete agent definition (systemPrompt + tools + model) by name.
|
|
2375
|
+
*
|
|
2376
|
+
* Resolution order:
|
|
2377
|
+
* 1. cwd/.pi/agents/<name>.md
|
|
2378
|
+
* 2. cwd/agents/<name>.md
|
|
2379
|
+
* 3. pointer.agentRoot/<name>.md (workspace mode only)
|
|
2380
|
+
* 4. Base package templates/agents/<name>.md
|
|
2381
|
+
*
|
|
2382
|
+
* If a local file has `standalone: true` in frontmatter, it is used as-is
|
|
2383
|
+
* (no base composition). Otherwise, base + local are composed.
|
|
2384
|
+
*
|
|
2385
|
+
* @param cwd - Working directory (project root) to search for local agent files
|
|
2386
|
+
* @param name - Agent name (e.g., "task-worker", "task-reviewer")
|
|
2387
|
+
* @returns Composed agent definition, or null if no base and no local file found
|
|
2388
|
+
* @since TP-161
|
|
2389
|
+
*/
|
|
2390
|
+
export function loadAgentDef(cwd: string, name: string): { systemPrompt: string; tools: string; model: string } | null {
|
|
2391
|
+
const localPaths = [
|
|
2392
|
+
join(cwd, ".pi", "agents", `${name}.md`),
|
|
2393
|
+
join(cwd, "agents", `${name}.md`),
|
|
2394
|
+
];
|
|
2395
|
+
|
|
2396
|
+
// In workspace mode, add pointer-resolved agent root as fallback
|
|
2397
|
+
const agentRoot = resolveAgentPointerRoot();
|
|
2398
|
+
if (agentRoot) {
|
|
2399
|
+
localPaths.push(join(agentRoot, `${name}.md`));
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
// Load base from package
|
|
2403
|
+
let baseDef: { fm: Record<string, string>; body: string } | null = null;
|
|
2404
|
+
try {
|
|
2405
|
+
const basePath = resolveTaskplaneAgentTemplate(name);
|
|
2406
|
+
if (existsSync(basePath)) {
|
|
2407
|
+
baseDef = parseAgentFile(basePath);
|
|
2408
|
+
}
|
|
2409
|
+
} catch { /* fall through */ }
|
|
2410
|
+
|
|
2411
|
+
// Load local override (first found wins)
|
|
2412
|
+
let localDef: { fm: Record<string, string>; body: string } | null = null;
|
|
2413
|
+
for (const p of localPaths) {
|
|
2414
|
+
localDef = parseAgentFile(p);
|
|
2415
|
+
if (localDef) break;
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
// No base and no local → null
|
|
2419
|
+
if (!baseDef && !localDef) return null;
|
|
2420
|
+
|
|
2421
|
+
// Local with standalone: true → use local as-is, ignore base
|
|
2422
|
+
if (localDef?.fm.standalone === "true") {
|
|
2423
|
+
return {
|
|
2424
|
+
systemPrompt: localDef.body,
|
|
2425
|
+
tools: localDef.fm.tools || "read,grep,find,ls",
|
|
2426
|
+
model: localDef.fm.model || "",
|
|
2427
|
+
};
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
// Compose base + local
|
|
2431
|
+
const basePrompt = baseDef?.body || "";
|
|
2432
|
+
const localPrompt = localDef?.body || "";
|
|
2433
|
+
const composedPrompt = localPrompt
|
|
2434
|
+
? basePrompt + "\n\n---\n\n## Project-Specific Guidance\n\n" + localPrompt
|
|
2435
|
+
: basePrompt;
|
|
2436
|
+
|
|
2437
|
+
// Local frontmatter overrides base (tools, model)
|
|
2438
|
+
const tools = localDef?.fm.tools || baseDef?.fm.tools || "read,grep,find,ls";
|
|
2439
|
+
const model = localDef?.fm.model || baseDef?.fm.model || "";
|
|
2440
|
+
|
|
2441
|
+
return { systemPrompt: composedPrompt.trim(), tools, model };
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2260
2444
|
export function resolveRuntimeStateRoot(
|
|
2261
2445
|
repoRoot: string,
|
|
2262
2446
|
workspaceRoot?: string,
|