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.
@@ -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 — /task and /orch are ready
3187
+ 3. pi # Start pi — /orch is ready
3188
3188
  `);
3189
3189
  }
3190
3190
 
@@ -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>`;
@@ -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
- // Only inject when a JSONL-backed entry is absent or stale the snapshot
1125
- // provides real-time tool-call / cost / context data even before the
1126
- // JSONL accumulator has accumulated enough events.
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, or if the snapshot
1133
- // is more recent than the latest accumulator update.
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 || (snap.updatedAt && snap.updatedAt > (existing._updatedAt || 0))) {
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 -e extensions/task-runner.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 (same pattern as task-runner.ts).
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 (same logic as TASK_AUTOSTART resolution).
382
- // The worker writes .DONE and STATUS.md in the worktree, so the engine
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 TASK_AUTOSTART will fail with "file not found".
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 → task-runner TASK_AUTOSTART)
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 TASK_AUTOSTART paths.
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