taskplane 0.25.7 → 0.26.1
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/rpc-wrapper.mjs +0 -1
- package/bin/taskplane.mjs +1 -1
- package/dashboard/public/app.js +44 -1
- package/dashboard/server.cjs +7 -6
- package/extensions/task-orchestrator.ts +1 -1
- package/extensions/taskplane/config-loader.ts +53 -0
- package/extensions/taskplane/context-window.ts +66 -0
- package/extensions/taskplane/execution.ts +111 -17
- package/extensions/taskplane/extension.ts +20 -0
- package/extensions/taskplane/merge.ts +2917 -2900
- package/extensions/taskplane/path-resolver.ts +1 -1
- package/extensions/taskplane/sidecar-telemetry.ts +252 -0
- package/extensions/taskplane/supervisor-primer.md +2 -0
- package/extensions/taskplane/supervisor.ts +1 -1
- package/extensions/taskplane/task-executor-core.ts +2 -5
- package/package.json +1 -3
- package/templates/agents/task-worker.md +387 -387
- package/templates/tasks/CONTEXT.md +3 -4
- package/extensions/task-runner.ts +0 -2784
package/bin/rpc-wrapper.mjs
CHANGED
|
@@ -294,7 +294,6 @@ function displayProgress(state) {
|
|
|
294
294
|
* Per RPC protocol spec: split on \n, strip optional trailing \r,
|
|
295
295
|
* do NOT use Node readline (splits on U+2028/U+2029).
|
|
296
296
|
*
|
|
297
|
-
* Reuses the proven pattern from task-runner.ts:910-975.
|
|
298
297
|
*/
|
|
299
298
|
function attachJsonlReader(stream, onLine) {
|
|
300
299
|
const decoder = new StringDecoder("utf8");
|
package/bin/taskplane.mjs
CHANGED
|
@@ -3184,7 +3184,7 @@ ${c.bold}Examples:${c.reset}
|
|
|
3184
3184
|
${c.bold}Getting started:${c.reset}
|
|
3185
3185
|
1. pi install npm:taskplane # Install the pi package
|
|
3186
3186
|
2. cd my-project && taskplane init # Scaffold project config
|
|
3187
|
-
3. pi # Start pi — /
|
|
3187
|
+
3. pi # Start pi — /orch is ready
|
|
3188
3188
|
`);
|
|
3189
3189
|
}
|
|
3190
3190
|
|
package/dashboard/public/app.js
CHANGED
|
@@ -1379,6 +1379,49 @@ function renderSupervisorConversation(supervisor) {
|
|
|
1379
1379
|
$supervisorConversationSection.innerHTML = html;
|
|
1380
1380
|
}
|
|
1381
1381
|
|
|
1382
|
+
/**
|
|
1383
|
+
* Human-readable labels for supervisor recovery action identifiers.
|
|
1384
|
+
* The supervisor LLM writes snake_case action names to actions.jsonl.
|
|
1385
|
+
* This map translates them to operator-friendly labels for the dashboard.
|
|
1386
|
+
*/
|
|
1387
|
+
const RECOVERY_ACTION_LABELS = {
|
|
1388
|
+
// Conflict resolution
|
|
1389
|
+
conflict_resolve_checkout_ours: "Auto-resolved merge conflict (kept task changes)",
|
|
1390
|
+
conflict_resolve_checkout_theirs: "Auto-resolved merge conflict (kept base changes)",
|
|
1391
|
+
conflict_resolve_manual: "Manual conflict resolution applied",
|
|
1392
|
+
|
|
1393
|
+
// Merge agent
|
|
1394
|
+
merge_retry: "Retried merge agent",
|
|
1395
|
+
merge_session_kill: "Terminated stalled merge agent",
|
|
1396
|
+
merge_force: "Forced merge with partial results",
|
|
1397
|
+
|
|
1398
|
+
// Worker / task
|
|
1399
|
+
worker_wrap_up: "Sent wrap-up signal to stalled worker",
|
|
1400
|
+
task_retry: "Retried failed task",
|
|
1401
|
+
task_skip: "Skipped task — unblocked dependents",
|
|
1402
|
+
wave_force_merge: "Force-merged wave with mixed results",
|
|
1403
|
+
|
|
1404
|
+
// Git / worktree
|
|
1405
|
+
lock_clear: "Cleared stale git lock file",
|
|
1406
|
+
worktree_remove: "Removed stale worktree",
|
|
1407
|
+
worktree_prune: "Pruned stale worktrees",
|
|
1408
|
+
|
|
1409
|
+
// Batch lifecycle
|
|
1410
|
+
abort_hard: "Hard-aborted batch",
|
|
1411
|
+
batch_resume: "Resumed batch after recovery",
|
|
1412
|
+
supervisor_handoff: "Supervisor session handoff",
|
|
1413
|
+
|
|
1414
|
+
// Diagnostics (usually not shown — filtered as non-recovery)
|
|
1415
|
+
initial_status_check: "Checked initial batch status",
|
|
1416
|
+
completion_status_check: "Verified batch completion status",
|
|
1417
|
+
read_state: "Read batch state",
|
|
1418
|
+
};
|
|
1419
|
+
|
|
1420
|
+
/** Format a recovery action type string into a human-readable label. */
|
|
1421
|
+
function formatRecoveryActionLabel(type) {
|
|
1422
|
+
return RECOVERY_ACTION_LABELS[type] || type.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1382
1425
|
/** Merge supervisor actions and Tier 0 recovery events into a unified timeline.
|
|
1383
1426
|
* Actions from actions.jsonl and recovery events from events.jsonl are combined
|
|
1384
1427
|
* and sorted chronologically (per R002: show both Tier 0 and supervisor actions).
|
|
@@ -1451,7 +1494,7 @@ function renderSupervisorActions(supervisor) {
|
|
|
1451
1494
|
html += ` <div class="supervisor-action-right">`;
|
|
1452
1495
|
html += ` <div class="supervisor-action-header">`;
|
|
1453
1496
|
if (tier) html += `<span class="supervisor-action-tier">${tier}</span>`;
|
|
1454
|
-
html += ` <span class="supervisor-action-type">${escapeHtml(type)}</span>`;
|
|
1497
|
+
html += ` <span class="supervisor-action-type" title="${escapeHtml(type)}">${escapeHtml(formatRecoveryActionLabel(type))}</span>`;
|
|
1455
1498
|
if (target) html += `<span class="supervisor-action-target">${escapeHtml(target)}</span>`;
|
|
1456
1499
|
if (outcome) html += `<span class="supervisor-action-outcome ${outcomeCls}">${escapeHtml(outcome)}</span>`;
|
|
1457
1500
|
html += ` </div>`;
|
package/dashboard/server.cjs
CHANGED
|
@@ -1121,18 +1121,19 @@ function buildDashboardState() {
|
|
|
1121
1121
|
|
|
1122
1122
|
// TP-164: Inject merge snapshot telemetry into the telemetry map so
|
|
1123
1123
|
// `telemetry[sessionName]` resolves for the merge pane.
|
|
1124
|
-
//
|
|
1125
|
-
//
|
|
1126
|
-
// JSONL
|
|
1124
|
+
// Snapshots are the PRIMARY source for merge telemetry — merge agents don't
|
|
1125
|
+
// write to .pi/telemetry/*.jsonl (the JSONL path is for lane workers only).
|
|
1126
|
+
// Inject snapshot data when no JSONL-backed entry exists for this session.
|
|
1127
1127
|
for (const snap of Object.values(runtimeMergeSnapshots)) {
|
|
1128
1128
|
const key = snap.sessionName;
|
|
1129
1129
|
if (!key) continue;
|
|
1130
1130
|
const agent = snap.agent;
|
|
1131
1131
|
if (!agent) continue;
|
|
1132
|
-
// Only inject if there is no existing telemetry entry
|
|
1133
|
-
//
|
|
1132
|
+
// Only inject if there is no existing JSONL-backed telemetry entry.
|
|
1133
|
+
// Merge agents don't emit to .pi/telemetry/, so existing will always be
|
|
1134
|
+
// absent unless something else wrote to it — in that case defer to it.
|
|
1134
1135
|
const existing = telemetry[key];
|
|
1135
|
-
if (!existing
|
|
1136
|
+
if (!existing) {
|
|
1136
1137
|
telemetry[key] = {
|
|
1137
1138
|
inputTokens: agent.inputTokens || 0,
|
|
1138
1139
|
outputTokens: agent.outputTokens || 0,
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* .pi/task-orchestrator.yaml — orchestrator-specific settings
|
|
19
19
|
* .pi/task-runner.yaml — task areas, worker/reviewer config (shared)
|
|
20
20
|
*
|
|
21
|
-
* Usage: pi -e extensions/task-orchestrator.ts
|
|
21
|
+
* Usage: pi -e extensions/task-orchestrator.ts
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
// Re-export all named exports for tests and other consumers
|
|
@@ -25,6 +25,8 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync } from "
|
|
|
25
25
|
import { join } from "path";
|
|
26
26
|
import { homedir } from "os";
|
|
27
27
|
import { parse as yamlParse } from "yaml";
|
|
28
|
+
import { resolvePointer, loadWorkspaceConfig } from "./workspace.ts";
|
|
29
|
+
import type { PointerResolution } from "./types.ts";
|
|
28
30
|
|
|
29
31
|
import {
|
|
30
32
|
CONFIG_VERSION,
|
|
@@ -1264,3 +1266,54 @@ export function toTaskConfig(config: TaskplaneConfig): {
|
|
|
1264
1266
|
},
|
|
1265
1267
|
};
|
|
1266
1268
|
}
|
|
1269
|
+
|
|
1270
|
+
// ── Task Runner Config Loader ───────────────────────────────────────────
|
|
1271
|
+
//
|
|
1272
|
+
// loadConfig and _resetPointerWarning for task execution consumers.
|
|
1273
|
+
|
|
1274
|
+
/** Track whether a pointer warning has been logged this session (log once). */
|
|
1275
|
+
let _pointerWarningLogged = false;
|
|
1276
|
+
|
|
1277
|
+
/**
|
|
1278
|
+
* Resolve the workspace pointer for config and agent path redirection.
|
|
1279
|
+
* Returns null in repo mode (TASKPLANE_WORKSPACE_ROOT not set).
|
|
1280
|
+
*/
|
|
1281
|
+
function resolveTaskRunnerPointer(): PointerResolution | null {
|
|
1282
|
+
const wsRoot = process.env.TASKPLANE_WORKSPACE_ROOT;
|
|
1283
|
+
if (!wsRoot) return null;
|
|
1284
|
+
try {
|
|
1285
|
+
const wsConfig = loadWorkspaceConfig(wsRoot);
|
|
1286
|
+
const result = resolvePointer(wsRoot, wsConfig);
|
|
1287
|
+
if (result?.warning && !_pointerWarningLogged) {
|
|
1288
|
+
_pointerWarningLogged = true;
|
|
1289
|
+
console.error(`[task-runner] pointer: ${result.warning}`);
|
|
1290
|
+
}
|
|
1291
|
+
return result;
|
|
1292
|
+
} catch {
|
|
1293
|
+
return null;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
/** Reset pointer warning state (for testing only). */
|
|
1298
|
+
export function _resetPointerWarning(): void {
|
|
1299
|
+
_pointerWarningLogged = false;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
/**
|
|
1303
|
+
* Load task-runner config via the unified config loader.
|
|
1304
|
+
*
|
|
1305
|
+
* Returns the legacy snake_case TaskConfig shape. Wraps loadProjectConfig
|
|
1306
|
+
* with pointer resolution and error handling.
|
|
1307
|
+
*/
|
|
1308
|
+
export function loadConfig(cwd: string): ReturnType<typeof toTaskConfig> {
|
|
1309
|
+
try {
|
|
1310
|
+
const pointer = resolveTaskRunnerPointer();
|
|
1311
|
+
const unified = loadProjectConfig(cwd, pointer?.configRoot);
|
|
1312
|
+
return toTaskConfig(unified);
|
|
1313
|
+
} catch (err: unknown) {
|
|
1314
|
+
if (err instanceof ConfigLoadError && err.code === "CONFIG_LEGACY_FIELD") {
|
|
1315
|
+
throw err;
|
|
1316
|
+
}
|
|
1317
|
+
return toTaskConfig(deepClone(DEFAULT_PROJECT_CONFIG));
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Window Resolution Utilities
|
|
3
|
+
*
|
|
4
|
+
* Canonical home for context window resolution logic used when spawning workers.
|
|
5
|
+
*
|
|
6
|
+
* The original `resolveContextWindow(config: TaskConfig, ctx: ExtensionContext)` signature
|
|
7
|
+
* has been adapted to accept only the fields it actually uses, avoiding a dependency
|
|
8
|
+
* on the task-runner-internal `TaskConfig` type. Behavior is identical.
|
|
9
|
+
*
|
|
10
|
+
* @since TP-161
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
14
|
+
|
|
15
|
+
// ── Context Window Constants ──────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
/** Default fallback context window when neither config nor model provides a value. */
|
|
18
|
+
export const FALLBACK_CONTEXT_WINDOW: number = 200_000;
|
|
19
|
+
|
|
20
|
+
// ── Context Window Resolution ─────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Resolve the effective context window size for worker spawning.
|
|
24
|
+
*
|
|
25
|
+
* Resolution order (first non-zero value wins):
|
|
26
|
+
* 1. Explicit user config (configuredWindow > 0)
|
|
27
|
+
* 2. Auto-detect from pi model registry (ctx.model.contextWindow)
|
|
28
|
+
* 3. Fallback to 200K tokens
|
|
29
|
+
*
|
|
30
|
+
* A configuredWindow of 0 (or undefined) signals "auto-detect" — the default
|
|
31
|
+
* when no explicit value is configured. This allows pi's model registry to
|
|
32
|
+
* provide the real context window for the active model.
|
|
33
|
+
*
|
|
34
|
+
* @param configuredWindow - The `worker_context_window` from config (0 = auto-detect)
|
|
35
|
+
* @param ctx - The pi ExtensionContext for model registry auto-detection, or null
|
|
36
|
+
* @returns Object with `contextWindow` (resolved size) and `source` (diagnostic label)
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* // With explicit config
|
|
40
|
+
* resolveContextWindow(500_000, ctx) // → { contextWindow: 500000, source: "explicit config" }
|
|
41
|
+
*
|
|
42
|
+
* // With auto-detect (config = 0)
|
|
43
|
+
* resolveContextWindow(0, ctx) // → { contextWindow: 200000, source: "auto-detected from anthropic/claude-opus-4-6" }
|
|
44
|
+
*
|
|
45
|
+
* // With fallback (no config, no model)
|
|
46
|
+
* resolveContextWindow(undefined, null) // → { contextWindow: 200000, source: "fallback 200000" }
|
|
47
|
+
*/
|
|
48
|
+
export function resolveContextWindow(
|
|
49
|
+
configuredWindow: number | undefined,
|
|
50
|
+
ctx: ExtensionContext | null,
|
|
51
|
+
): { contextWindow: number; source: string } {
|
|
52
|
+
// 1. Explicit user config — non-zero means the user set it deliberately
|
|
53
|
+
if (configuredWindow && configuredWindow > 0) {
|
|
54
|
+
return { contextWindow: configuredWindow, source: "explicit config" };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 2. Auto-detect from pi model registry
|
|
58
|
+
const modelWindow = ctx?.model?.contextWindow;
|
|
59
|
+
if (modelWindow && modelWindow > 0) {
|
|
60
|
+
const modelId = ctx?.model ? `${ctx.model.provider}/${ctx.model.id}` : "unknown";
|
|
61
|
+
return { contextWindow: modelWindow, source: `auto-detected from ${modelId}` };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 3. Fallback
|
|
65
|
+
return { contextWindow: FALLBACK_CONTEXT_WINDOW, source: `fallback ${FALLBACK_CONTEXT_WINDOW}` };
|
|
66
|
+
}
|
|
@@ -15,20 +15,11 @@ import { allocateLanes } from "./waves.ts";
|
|
|
15
15
|
import { resolveOperatorId } from "./naming.ts";
|
|
16
16
|
import { runGit } 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
|
|
@@ -1640,7 +1630,7 @@ export function ensureTaskFilesCommitted(
|
|
|
1640
1630
|
/**
|
|
1641
1631
|
* Runtime backend selector for lane execution.
|
|
1642
1632
|
*
|
|
1643
|
-
* - `"legacy"`: Session-backed path (spawnLaneSession
|
|
1633
|
+
* - `"legacy"`: Session-backed path (spawnLaneSession, deprecated)
|
|
1644
1634
|
* - `"v2"`: Direct-child path (lane-runner → agent-host → pi --mode rpc)
|
|
1645
1635
|
*
|
|
1646
1636
|
* @since TP-105
|
|
@@ -1677,7 +1667,7 @@ export async function executeWave(
|
|
|
1677
1667
|
// ── Stage 0: Ensure task files are committed ────────────────
|
|
1678
1668
|
// Task folders may contain untracked files (PROMPT.md, STATUS.md) that
|
|
1679
1669
|
// won't appear in worktrees unless committed. Stage and commit them now,
|
|
1680
|
-
// before worktree creation, so workers can find their
|
|
1670
|
+
// before worktree creation, so workers can find their task files.
|
|
1681
1671
|
// Pass orchBranch so the staging commit is reflected in the orch branch
|
|
1682
1672
|
// before worktrees are allocated from it.
|
|
1683
1673
|
try {
|
|
@@ -2257,6 +2247,110 @@ function loadLocalAgentPrompt(stateRoot: string, agentName: string): string {
|
|
|
2257
2247
|
return "";
|
|
2258
2248
|
}
|
|
2259
2249
|
|
|
2250
|
+
// ── Agent Definition Loading ─────────────────────────────────────────
|
|
2251
|
+
|
|
2252
|
+
/** Track whether an agent pointer warning has been logged this session (log once). */
|
|
2253
|
+
let _execPointerWarningLogged = false;
|
|
2254
|
+
|
|
2255
|
+
/**
|
|
2256
|
+
* Reset agent pointer warning state for testing.
|
|
2257
|
+
* @since TP-161
|
|
2258
|
+
*/
|
|
2259
|
+
export function resetPointerWarning(): void {
|
|
2260
|
+
_execPointerWarningLogged = false;
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
/**
|
|
2264
|
+
* Resolve agent files using the workspace pointer (workspace mode only).
|
|
2265
|
+
* Returns the agentRoot from the pointer, or null in repo mode / on failure.
|
|
2266
|
+
*/
|
|
2267
|
+
function resolveAgentPointerRoot(): string | null {
|
|
2268
|
+
const wsRoot = process.env.TASKPLANE_WORKSPACE_ROOT;
|
|
2269
|
+
if (!wsRoot) return null;
|
|
2270
|
+
try {
|
|
2271
|
+
const wsConfig = loadWorkspaceConfig(wsRoot);
|
|
2272
|
+
const result = resolvePointer(wsRoot, wsConfig);
|
|
2273
|
+
if (result?.warning && !_execPointerWarningLogged) {
|
|
2274
|
+
_execPointerWarningLogged = true;
|
|
2275
|
+
console.error(`[execution] pointer: ${result.warning}`);
|
|
2276
|
+
}
|
|
2277
|
+
return result?.agentRoot ?? null;
|
|
2278
|
+
} catch {
|
|
2279
|
+
return null;
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
/**
|
|
2284
|
+
* Load a complete agent definition (systemPrompt + tools + model) by name.
|
|
2285
|
+
*
|
|
2286
|
+
* Resolution order:
|
|
2287
|
+
* 1. cwd/.pi/agents/<name>.md
|
|
2288
|
+
* 2. cwd/agents/<name>.md
|
|
2289
|
+
* 3. pointer.agentRoot/<name>.md (workspace mode only)
|
|
2290
|
+
* 4. Base package templates/agents/<name>.md
|
|
2291
|
+
*
|
|
2292
|
+
* If a local file has `standalone: true` in frontmatter, it is used as-is
|
|
2293
|
+
* (no base composition). Otherwise, base + local are composed.
|
|
2294
|
+
*
|
|
2295
|
+
* @param cwd - Working directory (project root) to search for local agent files
|
|
2296
|
+
* @param name - Agent name (e.g., "task-worker", "task-reviewer")
|
|
2297
|
+
* @returns Composed agent definition, or null if no base and no local file found
|
|
2298
|
+
* @since TP-161
|
|
2299
|
+
*/
|
|
2300
|
+
export function loadAgentDef(cwd: string, name: string): { systemPrompt: string; tools: string; model: string } | null {
|
|
2301
|
+
const localPaths = [
|
|
2302
|
+
join(cwd, ".pi", "agents", `${name}.md`),
|
|
2303
|
+
join(cwd, "agents", `${name}.md`),
|
|
2304
|
+
];
|
|
2305
|
+
|
|
2306
|
+
// In workspace mode, add pointer-resolved agent root as fallback
|
|
2307
|
+
const agentRoot = resolveAgentPointerRoot();
|
|
2308
|
+
if (agentRoot) {
|
|
2309
|
+
localPaths.push(join(agentRoot, `${name}.md`));
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
// Load base from package
|
|
2313
|
+
let baseDef: { fm: Record<string, string>; body: string } | null = null;
|
|
2314
|
+
try {
|
|
2315
|
+
const basePath = resolveTaskplaneAgentTemplate(name);
|
|
2316
|
+
if (existsSync(basePath)) {
|
|
2317
|
+
baseDef = parseAgentFile(basePath);
|
|
2318
|
+
}
|
|
2319
|
+
} catch { /* fall through */ }
|
|
2320
|
+
|
|
2321
|
+
// Load local override (first found wins)
|
|
2322
|
+
let localDef: { fm: Record<string, string>; body: string } | null = null;
|
|
2323
|
+
for (const p of localPaths) {
|
|
2324
|
+
localDef = parseAgentFile(p);
|
|
2325
|
+
if (localDef) break;
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
// No base and no local → null
|
|
2329
|
+
if (!baseDef && !localDef) return null;
|
|
2330
|
+
|
|
2331
|
+
// Local with standalone: true → use local as-is, ignore base
|
|
2332
|
+
if (localDef?.fm.standalone === "true") {
|
|
2333
|
+
return {
|
|
2334
|
+
systemPrompt: localDef.body,
|
|
2335
|
+
tools: localDef.fm.tools || "read,grep,find,ls",
|
|
2336
|
+
model: localDef.fm.model || "",
|
|
2337
|
+
};
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
// Compose base + local
|
|
2341
|
+
const basePrompt = baseDef?.body || "";
|
|
2342
|
+
const localPrompt = localDef?.body || "";
|
|
2343
|
+
const composedPrompt = localPrompt
|
|
2344
|
+
? basePrompt + "\n\n---\n\n## Project-Specific Guidance\n\n" + localPrompt
|
|
2345
|
+
: basePrompt;
|
|
2346
|
+
|
|
2347
|
+
// Local frontmatter overrides base (tools, model)
|
|
2348
|
+
const tools = localDef?.fm.tools || baseDef?.fm.tools || "read,grep,find,ls";
|
|
2349
|
+
const model = localDef?.fm.model || baseDef?.fm.model || "";
|
|
2350
|
+
|
|
2351
|
+
return { systemPrompt: composedPrompt.trim(), tools, model };
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2260
2354
|
export function resolveRuntimeStateRoot(
|
|
2261
2355
|
repoRoot: string,
|
|
2262
2356
|
workspaceRoot?: string,
|
|
@@ -426,6 +426,26 @@ export function executeIntegration(
|
|
|
426
426
|
): IntegrationResult {
|
|
427
427
|
const { orchBranch, currentBranch, batchId } = context;
|
|
428
428
|
|
|
429
|
+
// Check if the orch branch is already an ancestor of the current branch
|
|
430
|
+
// (i.e., already merged — supervisor may have done a manual conflict resolution).
|
|
431
|
+
// If so, skip the merge entirely and go straight to cleanup.
|
|
432
|
+
const alreadyMergedCheck = deps.runGit(["merge-base", "--is-ancestor", orchBranch, "HEAD"]);
|
|
433
|
+
if (alreadyMergedCheck.ok) {
|
|
434
|
+
return performCleanup(deps, orchBranch, {
|
|
435
|
+
success: true,
|
|
436
|
+
integratedLocally: true,
|
|
437
|
+
commitCount: "0",
|
|
438
|
+
message:
|
|
439
|
+
`✅ Already integrated \u2014 all task work is present in \`${currentBranch}\`.\n` +
|
|
440
|
+
`\n` +
|
|
441
|
+
`The orch branch was merged manually (e.g., during conflict resolution) before\n` +
|
|
442
|
+
`this command ran. No additional merge was needed.\n` +
|
|
443
|
+
`\n` +
|
|
444
|
+
`🧹 Running cleanup: removing orch branch and clearing batch state.`,
|
|
445
|
+
error: "",
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
429
449
|
if (mode === "ff") {
|
|
430
450
|
// Fast-forward merge.
|
|
431
451
|
// Stash any dirty working tree files first — workspace mode leaves
|