taskplane 0.14.0 → 0.15.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.
@@ -627,8 +627,23 @@ function renderMergeAgents(batch, tmuxSessions) {
627
627
  const showRepos = knownRepos.length >= 2;
628
628
  const telemetry = currentData?.telemetry || {};
629
629
 
630
- // Check for active merge sessions (convention: orch-merge-*)
631
- const mergeSessions = (tmuxSessions || []).filter(s => s.startsWith("orch-merge"));
630
+ // Check for active merge sessions (convention: {prefix}-{opId}-merge-{N})
631
+ const mergeSessions = (tmuxSessions || []).filter(s => s.includes("-merge-"));
632
+
633
+ // Derive merge session name from lane session naming pattern.
634
+ // Lane sessions: "{prefix}-{opId}-lane-{N}", merge sessions: "{prefix}-{opId}-merge-{N}".
635
+ // Extract the prefix-opId part from the first lane and use it to construct merge names.
636
+ const lanes = batch?.lanes || [];
637
+ let mergePrefix = "orch-merge"; // fallback for legacy/unknown patterns
638
+ if (lanes.length > 0 && lanes[0].tmuxSessionName) {
639
+ const laneName = lanes[0].tmuxSessionName;
640
+ const laneMatch = laneName.match(/^(.+)-lane-\d+$/);
641
+ if (laneMatch) {
642
+ mergePrefix = laneMatch[1] + "-merge";
643
+ }
644
+ }
645
+ // Helper: get merge session name for a lane number
646
+ const getMergeSessionName = (laneNum) => `${mergePrefix}-${laneNum}`;
632
647
 
633
648
  if (mergeResults.length === 0 && mergeSessions.length === 0) {
634
649
  $mergeBody.innerHTML = '<div class="empty-state">No merge agents active</div>';
@@ -639,6 +654,9 @@ function renderMergeAgents(batch, tmuxSessions) {
639
654
  html += '<th>Wave</th><th>Status</th><th>Session</th><th>Telemetry</th><th>Attach</th><th>Details</th>';
640
655
  html += '</tr></thead><tbody>';
641
656
 
657
+ // Track sessions shown in wave result rows so we don't duplicate them below
658
+ const shownSessions = new Set();
659
+
642
660
  // Show merge results
643
661
  for (const mr of mergeResults) {
644
662
  // Repo filtering: if a repo is selected and this merge has repoResults,
@@ -653,12 +671,19 @@ function renderMergeAgents(batch, tmuxSessions) {
653
671
  : mr.status === "partial" ? "status-stalled"
654
672
  : "status-failed";
655
673
 
656
- // Look for matching tmux session
657
- const sessionName = `orch-merge-w${mr.waveIndex + 1}`;
658
- const alive = tmuxSet.has(sessionName);
659
-
660
- // Look for merge telemetry data
661
- const mergeTel = telemetry[sessionName] || telemetry[`orch-merge-${mr.waveIndex + 1}`] || null;
674
+ // Look for matching tmux merge sessions for this wave result.
675
+ // Merge sessions follow the naming pattern: {prefix}-{opId}-merge-{laneNumber}
676
+ // (e.g., "orch-henrylach-merge-1"). Find any alive merge sessions.
677
+ const waveMergeSessions = mergeSessions.filter(s => tmuxSet.has(s));
678
+ const sessionName = waveMergeSessions.length > 0 ? waveMergeSessions[0] : null;
679
+ const alive = sessionName !== null;
680
+ if (alive) shownSessions.add(sessionName);
681
+
682
+ // Look for merge telemetry data — check all merge sessions
683
+ let mergeTel = null;
684
+ for (const ms of mergeSessions) {
685
+ if (telemetry[ms]) { mergeTel = telemetry[ms]; break; }
686
+ }
662
687
 
663
688
  html += `<tr>`;
664
689
  html += `<td style="font-family:var(--font-mono);">Wave ${mr.waveIndex + 1}</td>`;
@@ -718,8 +743,7 @@ function renderMergeAgents(batch, tmuxSessions) {
718
743
 
719
744
  // Show active merge sessions not yet in results
720
745
  for (const sess of mergeSessions) {
721
- const alreadyShown = mergeResults.some((mr) => `orch-merge-w${mr.waveIndex + 1}` === sess);
722
- if (alreadyShown) continue;
746
+ if (shownSessions.has(sess)) continue;
723
747
 
724
748
  const sessTel = telemetry[sess] || null;
725
749
  const cmd = `tmux attach -t ${sess}`;
@@ -2603,16 +2603,25 @@ export default function (pi: ExtensionAPI) {
2603
2603
  // If context limit is hit mid-task, the next iteration picks up from
2604
2604
  // the first incomplete step via STATUS.md — same recovery mechanism.
2605
2605
 
2606
- // Mark all incomplete steps as in-progress
2606
+ // Mark only the first incomplete step as in-progress
2607
2607
  {
2608
2608
  const currentStatus = parseStatusMd(readFileSync(statusPath, "utf-8"));
2609
+ let foundFirstIncomplete = false;
2609
2610
  for (const step of task.steps) {
2610
2611
  const ss = currentStatus.steps.find(s => s.number === step.number);
2611
2612
  if (ss?.status === "complete") continue;
2612
2613
 
2613
- // Mark step as in-progress and log its start
2614
- updateStepStatus(statusPath, step.number, "in-progress");
2615
- logExecution(statusPath, `Step ${step.number} started`, step.name);
2614
+ if (!foundFirstIncomplete) {
2615
+ // Mark the first incomplete step as in-progress
2616
+ updateStepStatus(statusPath, step.number, "in-progress");
2617
+ logExecution(statusPath, `Step ${step.number} started`, step.name);
2618
+ foundFirstIncomplete = true;
2619
+ } else {
2620
+ // Ensure future steps show as not-started, not in-progress
2621
+ if (ss?.status === "in-progress") {
2622
+ updateStepStatus(statusPath, step.number, "not-started");
2623
+ }
2624
+ }
2616
2625
  }
2617
2626
  }
2618
2627
 
@@ -1344,282 +1344,11 @@ export default function (pi: ExtensionAPI) {
1344
1344
 
1345
1345
  if (!requireExecCtx(ctx)) return;
1346
1346
 
1347
- // ── TP-128: Transition from routing-mode supervisor to batch execution ──
1348
- // If the supervisor is active in routing mode (conversational, no batch),
1349
- // deactivate it so the batch can start fresh with monitoring-mode supervisor.
1350
- // This enables the workflow: /orch → conversation → "run the tasks" → /orch all
1351
- // without the operator needing to know about internal mode distinctions.
1352
- if (supervisorState.active && supervisorState.routingContext) {
1353
- await deactivateSupervisor(pi, supervisorState);
1347
+ // ── TP-061: Delegate to shared helper ────────────────────
1348
+ const result = await doOrchStart(args, ctx);
1349
+ if (result.error) {
1350
+ ctx.ui.notify(result.message, "warning");
1354
1351
  }
1355
-
1356
- // Prevent concurrent batch execution (merging is an active state)
1357
- if (orchBatchState.phase !== "idle" && orchBatchState.phase !== "completed" && orchBatchState.phase !== "failed" && orchBatchState.phase !== "stopped") {
1358
- ctx.ui.notify(
1359
- `⚠️ A batch is already ${orchBatchState.phase} (${orchBatchState.batchId}). ` +
1360
- `Use /orch-pause to pause or wait for completion.`,
1361
- "warning",
1362
- );
1363
- return;
1364
- }
1365
-
1366
- // Root references from execution context.
1367
- // Currently all .pi state, orphan detection, batch state, abort signal,
1368
- // and discovery operations use repoRoot for consistency with engine.ts,
1369
- // resume.ts, and execution.ts which all alias cwd → repoRoot.
1370
- // In repo mode workspaceRoot === repoRoot, so this is safe.
1371
- // TODO(workspace-mode): when workspace mode is fully threaded through
1372
- // engine/resume/execution, split state root from git root.
1373
- const { repoRoot } = execCtx!;
1374
-
1375
- // ── Orphan detection (TS-009 Step 3) ─────────────────────
1376
- const orphanResult = detectOrphanSessions(
1377
- orchConfig.orchestrator.tmux_prefix,
1378
- repoRoot,
1379
- );
1380
-
1381
- switch (orphanResult.recommendedAction) {
1382
- case "resume": {
1383
- // Safety net: if the persisted phase is not actually resumable (e.g. "failed",
1384
- // "stopped") — which can happen when the batch crashed after writing a terminal
1385
- // phase but before /orch-abort cleaned up — auto-delete the state file and
1386
- // fall through to start fresh rather than blocking the user with a catch-22.
1387
- const resumablePhases = ["paused", "executing", "merging"];
1388
- const phase = orphanResult.loadedState?.phase ?? "";
1389
- const hasOrphans = orphanResult.orphanSessions.length > 0;
1390
- if (!hasOrphans && !resumablePhases.includes(phase)) {
1391
- try { deleteBatchState(repoRoot); } catch { /* best effort */ }
1392
- ctx.ui.notify(
1393
- `🧹 Cleared non-resumable stale batch (${orphanResult.loadedState?.batchId}, phase=${phase}). Starting fresh.`,
1394
- "info",
1395
- );
1396
- break; // fall through to start a new batch
1397
- }
1398
- // Genuinely resumable or has live orphan sessions — prompt user
1399
- ctx.ui.notify(orphanResult.userMessage, "warning");
1400
- return;
1401
- }
1402
-
1403
- case "abort-orphans":
1404
- // Orphan sessions without usable state
1405
- ctx.ui.notify(orphanResult.userMessage, "warning");
1406
- return;
1407
-
1408
- case "cleanup-stale":
1409
- // No orphans + stale/completed state file — auto-delete and continue
1410
- try {
1411
- deleteBatchState(repoRoot);
1412
- } catch {
1413
- // Best-effort cleanup — proceed even if delete fails
1414
- }
1415
- if (orphanResult.userMessage) {
1416
- ctx.ui.notify(orphanResult.userMessage, "info");
1417
- }
1418
- break;
1419
-
1420
- case "paused-corrupt":
1421
- // Corrupt/unreadable state file — do NOT auto-delete.
1422
- // Enter paused phase so operator-visible state reflects the issue,
1423
- // notify user, refresh widget, then stop.
1424
- orchBatchState.phase = "paused";
1425
- orchBatchState.errors.push(orphanResult.userMessage);
1426
- updateOrchWidget();
1427
- ctx.ui.notify(orphanResult.userMessage, "warning");
1428
- return;
1429
-
1430
- case "start-fresh":
1431
- // No orphans, no state file — proceed normally
1432
- break;
1433
- }
1434
-
1435
- // ── Model availability pre-flight ────────────────────────
1436
- // Validate that all configured agent models are resolvable in
1437
- // the model registry before starting. Catches misconfigured
1438
- // model names early instead of failing hours into a batch.
1439
- // Note: runnerConfig (TaskRunnerConfig) is a stripped type without
1440
- // worker/reviewer model fields. Load the full unified config to
1441
- // get the actual agent model strings (including user preferences).
1442
- let agentModels: { workerModel?: string; reviewerModel?: string } | undefined;
1443
- try {
1444
- const fullConfig = loadProjectConfig(execCtx!.repoRoot);
1445
- agentModels = {
1446
- workerModel: fullConfig.taskRunner.worker.model || "",
1447
- reviewerModel: fullConfig.taskRunner.reviewer.model || "",
1448
- };
1449
- } catch { /* fall through — validateModelAvailability handles empty strings */ }
1450
- const modelResults = validateModelAvailability(orchConfig, runnerConfig, supervisorConfig, ctx, agentModels);
1451
- const modelFailures = modelResults.filter(r => r.status === "not-found");
1452
- ctx.ui.notify(formatModelValidation(modelResults), modelFailures.length > 0 ? "error" : "info");
1453
- if (modelFailures.length > 0) {
1454
- ctx.ui.notify(
1455
- `❌ Cannot start batch — ${modelFailures.length} model(s) not found: ` +
1456
- modelFailures.map(f => `${f.role} (${f.modelStr})`).join(", ") +
1457
- `.\n\nFix the model configuration and try again.`,
1458
- "error",
1459
- );
1460
- return;
1461
- }
1462
-
1463
- // Reset batch state for new execution
1464
- orchBatchState = freshOrchBatchState();
1465
- latestMonitorState = null;
1466
-
1467
- // ── TP-040: Set launching phase synchronously ────────────
1468
- // Mark as "launching" before the setTimeout detach so that
1469
- // /orch-status, /orch-pause, /orch-abort issued immediately
1470
- // after /orch returns can see that a batch is being started.
1471
- // The engine will transition from "launching" → "planning"
1472
- // on the next tick when it actually begins work.
1473
- orchBatchState.phase = "launching";
1474
- orchBatchState.startedAt = Date.now();
1475
- updateOrchWidget();
1476
-
1477
- // ── TP-040: Non-blocking engine launch ───────────────────
1478
- // Start the engine without awaiting — the command handler returns
1479
- // immediately so the pi session remains interactive (enables
1480
- // supervisor agent and operator conversation during batch).
1481
- // The .catch() error boundary ensures unhandled rejections from
1482
- // the engine are surfaced to the operator and reflected in state.
1483
- startBatchAsync(
1484
- () => executeOrchBatch(
1485
- args,
1486
- orchConfig,
1487
- runnerConfig,
1488
- repoRoot,
1489
- orchBatchState,
1490
- (message, level) => {
1491
- ctx.ui.notify(message, level);
1492
- updateOrchWidget(); // Refresh widget on every phase message
1493
- },
1494
- (monState: MonitorState) => {
1495
- const changed = !latestMonitorState ||
1496
- latestMonitorState.totalDone !== monState.totalDone ||
1497
- latestMonitorState.totalFailed !== monState.totalFailed ||
1498
- latestMonitorState.lanes.some((l, i) =>
1499
- l.currentTaskId !== monState.lanes[i]?.currentTaskId ||
1500
- l.currentStep !== monState.lanes[i]?.currentStep ||
1501
- l.completedChecks !== monState.lanes[i]?.completedChecks,
1502
- );
1503
- latestMonitorState = monState;
1504
- if (changed) updateOrchWidget(); // Only refresh on actual state change
1505
- },
1506
- execCtx!.workspaceConfig,
1507
- execCtx!.workspaceRoot,
1508
- execCtx!.pointer?.agentRoot,
1509
- ),
1510
- orchBatchState,
1511
- ctx,
1512
- updateOrchWidget,
1513
- // TP-043: Deferred supervisor deactivation (R002-1).
1514
- // Integration is ONLY triggered when batch completes successfully
1515
- // (phase === "completed"). For paused/stopped/crash states, the
1516
- // supervisor is deactivated immediately — no integration on partial
1517
- // batches.
1518
- // TP-043 Step 2: Batch summary is generated on all terminal paths
1519
- // before supervisor deactivation.
1520
- () => {
1521
- const mode = orchConfig.orchestrator.integration;
1522
- // TP-043: Build summary deps for all terminal paths
1523
- const opId = resolveOperatorId(orchConfig);
1524
- const sDeps: SummaryDeps = {
1525
- opId,
1526
- diagnostics: orchBatchState.diagnostics ?? null,
1527
- mergeResults: (orchBatchState.mergeResults || []).map(mr => ({
1528
- waveIndex: mr.waveIndex,
1529
- status: mr.status,
1530
- failedLane: mr.failedLane,
1531
- failureReason: mr.failureReason,
1532
- })),
1533
- };
1534
- if (
1535
- orchBatchState.phase === "completed" &&
1536
- (mode === "supervised" || mode === "auto")
1537
- ) {
1538
- // Supervisor stays alive — trigger programmatic integration
1539
- // flow. Supervisor deactivates itself after integration
1540
- // completes (or fails) via the callback in
1541
- // triggerSupervisorIntegration. Summary generated there.
1542
- triggerSupervisorIntegration(
1543
- pi,
1544
- supervisorState,
1545
- orchBatchState,
1546
- mode,
1547
- repoRoot,
1548
- buildIntegrationExecutor(repoRoot, opId),
1549
- buildCiDeps(repoRoot),
1550
- sDeps,
1551
- );
1552
- return;
1553
- }
1554
- // Non-completed phase or manual mode — deactivate immediately.
1555
- // Inform operator if integration was expected but skipped.
1556
- if (
1557
- (mode === "supervised" || mode === "auto") &&
1558
- orchBatchState.phase !== "completed"
1559
- ) {
1560
- pi.sendMessage(
1561
- {
1562
- customType: "supervisor-integration-skipped",
1563
- content: [{
1564
- type: "text",
1565
- text:
1566
- `📋 **Batch ended** (phase: ${orchBatchState.phase}). ` +
1567
- `Integration skipped — only completed batches are eligible.\n` +
1568
- `Use \`/orch-resume\` to continue or \`/orch-integrate\` manually after resolving issues.`,
1569
- }],
1570
- display: `Integration skipped — batch ${orchBatchState.phase}`,
1571
- },
1572
- { triggerTurn: false },
1573
- );
1574
- }
1575
- // TP-043: Generate summary before transition
1576
- presentBatchSummary(pi, orchBatchState, execCtx!.workspaceRoot, opId, orchBatchState.diagnostics, sDeps.mergeResults);
1577
- // TP-128: Transition to routing mode instead of deactivating.
1578
- // The operator can continue the conversation (integrate, plan
1579
- // next batch, create tasks) without re-invoking /orch.
1580
- const postBatchContext: SupervisorRoutingContext = orchBatchState.phase === "completed"
1581
- ? {
1582
- routingState: "completed-batch",
1583
- contextMessage:
1584
- `Batch **${orchBatchState.batchId}** completed — ` +
1585
- `${orchBatchState.succeededTasks}/${orchBatchState.totalTasks} tasks succeeded.\n\n` +
1586
- `The orch branch \`${orchBatchState.orchBranch}\` is ready to integrate.\n` +
1587
- `Would you like me to integrate it, or would you prefer to review first?\n\n` +
1588
- `You can also:\n` +
1589
- `• Run \`/orch-integrate\` (or \`/orch-integrate --pr\`) to integrate\n` +
1590
- `• Create new tasks for the next batch\n` +
1591
- `• Run a health check`,
1592
- }
1593
- : {
1594
- routingState: "no-tasks",
1595
- contextMessage:
1596
- `Batch **${orchBatchState.batchId}** ended (${orchBatchState.phase}).\n\n` +
1597
- `${orchBatchState.succeededTasks} succeeded, ${orchBatchState.failedTasks} failed, ` +
1598
- `${orchBatchState.skippedTasks} skipped.\n\n` +
1599
- `What would you like to do next?`,
1600
- };
1601
- transitionToRoutingMode(pi, supervisorState, postBatchContext);
1602
- },
1603
- );
1604
-
1605
- // ── TP-041: Activate supervisor agent ────────────────────
1606
- // After the engine is launched (non-blocking), activate the
1607
- // supervisor in this pi session. The system prompt is rebuilt
1608
- // dynamically on each LLM turn from the live batchState ref,
1609
- // ensuring batch metadata (batchId, wave/task counts) is always
1610
- // current even though the engine populates it asynchronously.
1611
- // Model override is resolved inside activateSupervisor via ctx.
1612
- // Uses workspaceRoot (not repoRoot) so lockfile/events/batch-state
1613
- // all resolve to the same .pi tree the engine writes to (R006-1).
1614
- activateSupervisor(
1615
- pi,
1616
- supervisorState,
1617
- orchBatchState,
1618
- orchConfig,
1619
- supervisorConfig,
1620
- execCtx!.workspaceRoot,
1621
- ctx,
1622
- );
1623
1352
  },
1624
1353
  });
1625
1354
 
@@ -1734,6 +1463,261 @@ export default function (pi: ExtensionAPI) {
1734
1463
  // Each helper extracts the core logic from its command handler so both
1735
1464
  // the slash command and the registered tool can call the same function.
1736
1465
 
1466
+ /**
1467
+ * Core logic for starting a batch. Used by both `/orch <target>` command
1468
+ * and the `orch_start` tool.
1469
+ *
1470
+ * Performs all pre-start guards (execution context, concurrent batch,
1471
+ * routing-mode transition, orphan detection, model validation), then
1472
+ * launches the engine asynchronously and activates the supervisor.
1473
+ *
1474
+ * Returns an immediate ACK with batch ID, task count, and wave info,
1475
+ * or an error message if the batch cannot be started.
1476
+ *
1477
+ * @since TP-061
1478
+ */
1479
+ async function doOrchStart(target: string, ctx: ExtensionContext): Promise<{ message: string; error?: boolean }> {
1480
+ // Target validation
1481
+ const trimmedTarget = target?.trim();
1482
+ if (!trimmedTarget) {
1483
+ return {
1484
+ message: "❌ Target is required. Use \"all\" to run all pending tasks, or specify a task area name or path.",
1485
+ error: true,
1486
+ };
1487
+ }
1488
+
1489
+ if (!execCtx) {
1490
+ return {
1491
+ message: "❌ Orchestrator not initialized. Workspace configuration failed at startup.\nFix the workspace config or remove it to use repo mode, then restart.",
1492
+ error: true,
1493
+ };
1494
+ }
1495
+
1496
+ // TP-128: Transition from routing-mode supervisor to batch execution
1497
+ if (supervisorState.active && supervisorState.routingContext) {
1498
+ await deactivateSupervisor(pi, supervisorState);
1499
+ }
1500
+
1501
+ // Prevent concurrent batch execution
1502
+ if (orchBatchState.phase !== "idle" && orchBatchState.phase !== "completed" && orchBatchState.phase !== "failed" && orchBatchState.phase !== "stopped") {
1503
+ return {
1504
+ message: `⚠️ A batch is already ${orchBatchState.phase} (${orchBatchState.batchId}). Use /orch-pause to pause or wait for completion.`,
1505
+ error: true,
1506
+ };
1507
+ }
1508
+
1509
+ const { repoRoot } = execCtx;
1510
+
1511
+ // Orphan detection
1512
+ const orphanResult = detectOrphanSessions(
1513
+ orchConfig.orchestrator.tmux_prefix,
1514
+ repoRoot,
1515
+ );
1516
+
1517
+ switch (orphanResult.recommendedAction) {
1518
+ case "resume": {
1519
+ const resumablePhases = ["paused", "executing", "merging"];
1520
+ const phase = orphanResult.loadedState?.phase ?? "";
1521
+ const hasOrphans = orphanResult.orphanSessions.length > 0;
1522
+ if (!hasOrphans && !resumablePhases.includes(phase)) {
1523
+ try { deleteBatchState(repoRoot); } catch { /* best effort */ }
1524
+ ctx.ui.notify(
1525
+ `🧹 Cleared non-resumable stale batch (${orphanResult.loadedState?.batchId}, phase=${phase}). Starting fresh.`,
1526
+ "info",
1527
+ );
1528
+ break;
1529
+ }
1530
+ return { message: orphanResult.userMessage, error: true };
1531
+ }
1532
+ case "abort-orphans":
1533
+ return { message: orphanResult.userMessage, error: true };
1534
+ case "cleanup-stale":
1535
+ try { deleteBatchState(repoRoot); } catch { /* best effort */ }
1536
+ if (orphanResult.userMessage) {
1537
+ ctx.ui.notify(orphanResult.userMessage, "info");
1538
+ }
1539
+ break;
1540
+ case "paused-corrupt":
1541
+ orchBatchState.phase = "paused";
1542
+ orchBatchState.errors.push(orphanResult.userMessage);
1543
+ updateOrchWidget();
1544
+ return { message: orphanResult.userMessage, error: true };
1545
+ case "start-fresh":
1546
+ break;
1547
+ }
1548
+
1549
+ // Model availability pre-flight
1550
+ let agentModels: { workerModel?: string; reviewerModel?: string } | undefined;
1551
+ try {
1552
+ const fullConfig = loadProjectConfig(execCtx.repoRoot);
1553
+ agentModels = {
1554
+ workerModel: fullConfig.taskRunner.worker.model || "",
1555
+ reviewerModel: fullConfig.taskRunner.reviewer.model || "",
1556
+ };
1557
+ } catch { /* fall through */ }
1558
+ const modelResults = validateModelAvailability(orchConfig, runnerConfig, supervisorConfig, ctx, agentModels);
1559
+ const modelFailures = modelResults.filter(r => r.status === "not-found");
1560
+ ctx.ui.notify(formatModelValidation(modelResults), modelFailures.length > 0 ? "error" : "info");
1561
+ if (modelFailures.length > 0) {
1562
+ return {
1563
+ message: `❌ Cannot start batch — ${modelFailures.length} model(s) not found: ` +
1564
+ modelFailures.map(f => `${f.role} (${f.modelStr})`).join(", ") +
1565
+ `.\n\nFix the model configuration and try again.`,
1566
+ error: true,
1567
+ };
1568
+ }
1569
+
1570
+ // Pre-discovery: count pending tasks for the ACK response.
1571
+ // This is a lightweight synchronous check before launching the async engine.
1572
+ let pendingTaskCount = 0;
1573
+ try {
1574
+ const preDiscovery = runDiscovery(trimmedTarget, runnerConfig.task_areas, execCtx.workspaceRoot, {
1575
+ dependencySource: orchConfig.dependencies.source,
1576
+ useDependencyCache: orchConfig.dependencies.cache,
1577
+ workspaceConfig: execCtx.workspaceConfig,
1578
+ });
1579
+ pendingTaskCount = preDiscovery.pending.size;
1580
+ if (pendingTaskCount === 0) {
1581
+ return {
1582
+ message: `No pending tasks found for target "${trimmedTarget}". Nothing to execute.`,
1583
+ error: true,
1584
+ };
1585
+ }
1586
+ } catch {
1587
+ // Non-fatal — engine will re-run discovery and handle errors
1588
+ }
1589
+
1590
+ // Reset batch state for new execution
1591
+ orchBatchState = freshOrchBatchState();
1592
+ latestMonitorState = null;
1593
+
1594
+ orchBatchState.phase = "launching";
1595
+ orchBatchState.startedAt = Date.now();
1596
+ updateOrchWidget();
1597
+
1598
+ // Non-blocking engine launch
1599
+ startBatchAsync(
1600
+ () => executeOrchBatch(
1601
+ trimmedTarget,
1602
+ orchConfig,
1603
+ runnerConfig,
1604
+ repoRoot,
1605
+ orchBatchState,
1606
+ (message, level) => {
1607
+ ctx.ui.notify(message, level);
1608
+ updateOrchWidget();
1609
+ },
1610
+ (monState: MonitorState) => {
1611
+ const changed = !latestMonitorState ||
1612
+ latestMonitorState.totalDone !== monState.totalDone ||
1613
+ latestMonitorState.totalFailed !== monState.totalFailed ||
1614
+ latestMonitorState.lanes.some((l, i) =>
1615
+ l.currentTaskId !== monState.lanes[i]?.currentTaskId ||
1616
+ l.currentStep !== monState.lanes[i]?.currentStep ||
1617
+ l.completedChecks !== monState.lanes[i]?.completedChecks,
1618
+ );
1619
+ latestMonitorState = monState;
1620
+ if (changed) updateOrchWidget();
1621
+ },
1622
+ execCtx!.workspaceConfig,
1623
+ execCtx!.workspaceRoot,
1624
+ execCtx!.pointer?.agentRoot,
1625
+ ),
1626
+ orchBatchState,
1627
+ ctx,
1628
+ updateOrchWidget,
1629
+ () => {
1630
+ const mode = orchConfig.orchestrator.integration;
1631
+ const opId = resolveOperatorId(orchConfig);
1632
+ const sDeps: SummaryDeps = {
1633
+ opId,
1634
+ diagnostics: orchBatchState.diagnostics ?? null,
1635
+ mergeResults: (orchBatchState.mergeResults || []).map(mr => ({
1636
+ waveIndex: mr.waveIndex,
1637
+ status: mr.status,
1638
+ failedLane: mr.failedLane,
1639
+ failureReason: mr.failureReason,
1640
+ })),
1641
+ };
1642
+ if (
1643
+ orchBatchState.phase === "completed" &&
1644
+ (mode === "supervised" || mode === "auto")
1645
+ ) {
1646
+ triggerSupervisorIntegration(
1647
+ pi,
1648
+ supervisorState,
1649
+ orchBatchState,
1650
+ mode,
1651
+ repoRoot,
1652
+ buildIntegrationExecutor(repoRoot, opId),
1653
+ buildCiDeps(repoRoot),
1654
+ sDeps,
1655
+ );
1656
+ return;
1657
+ }
1658
+ if (
1659
+ (mode === "supervised" || mode === "auto") &&
1660
+ orchBatchState.phase !== "completed"
1661
+ ) {
1662
+ pi.sendMessage(
1663
+ {
1664
+ customType: "supervisor-integration-skipped",
1665
+ content: [{
1666
+ type: "text",
1667
+ text:
1668
+ `📋 **Batch ended** (phase: ${orchBatchState.phase}). ` +
1669
+ `Integration skipped — only completed batches are eligible.\n` +
1670
+ `Use \`/orch-resume\` to continue or \`/orch-integrate\` manually after resolving issues.`,
1671
+ }],
1672
+ display: `Integration skipped — batch ${orchBatchState.phase}`,
1673
+ },
1674
+ { triggerTurn: false },
1675
+ );
1676
+ }
1677
+ presentBatchSummary(pi, orchBatchState, execCtx!.workspaceRoot, opId, orchBatchState.diagnostics, sDeps.mergeResults);
1678
+ const postBatchContext: SupervisorRoutingContext = orchBatchState.phase === "completed"
1679
+ ? {
1680
+ routingState: "completed-batch",
1681
+ contextMessage:
1682
+ `Batch **${orchBatchState.batchId}** completed — ` +
1683
+ `${orchBatchState.succeededTasks}/${orchBatchState.totalTasks} tasks succeeded.\n\n` +
1684
+ `The orch branch \`${orchBatchState.orchBranch}\` is ready to integrate.\n` +
1685
+ `Would you like me to integrate it, or would you prefer to review first?\n\n` +
1686
+ `You can also:\n` +
1687
+ `• Run \`/orch-integrate\` (or \`/orch-integrate --pr\`) to integrate\n` +
1688
+ `• Create new tasks for the next batch\n` +
1689
+ `• Run a health check`,
1690
+ }
1691
+ : {
1692
+ routingState: "no-tasks",
1693
+ contextMessage:
1694
+ `Batch **${orchBatchState.batchId}** ended (${orchBatchState.phase}).\n\n` +
1695
+ `${orchBatchState.succeededTasks} succeeded, ${orchBatchState.failedTasks} failed, ` +
1696
+ `${orchBatchState.skippedTasks} skipped.\n\n` +
1697
+ `What would you like to do next?`,
1698
+ };
1699
+ transitionToRoutingMode(pi, supervisorState, postBatchContext);
1700
+ },
1701
+ );
1702
+
1703
+ // Activate supervisor agent
1704
+ activateSupervisor(
1705
+ pi,
1706
+ supervisorState,
1707
+ orchBatchState,
1708
+ orchConfig,
1709
+ supervisorConfig,
1710
+ execCtx!.workspaceRoot,
1711
+ ctx,
1712
+ );
1713
+
1714
+ return {
1715
+ message: `🚀 Batch launching (target: "${trimmedTarget}", ${pendingTaskCount} pending task${pendingTaskCount === 1 ? "" : "s"}). ` +
1716
+ `Batch ID will be assigned during planning. ` +
1717
+ `The engine is running asynchronously — use orch_status() to check progress.`,
1718
+ };
1719
+ }
1720
+
1737
1721
  /**
1738
1722
  * Core logic for orch-status. Returns a formatted status string.
1739
1723
  * Reads in-memory state first, falls back to disk if idle.
@@ -2772,6 +2756,39 @@ export default function (pi: ExtensionAPI) {
2772
2756
  },
2773
2757
  });
2774
2758
 
2759
+ pi.registerTool({
2760
+ name: "orch_start",
2761
+ label: "Start Batch",
2762
+ description:
2763
+ "Start a new orchestration batch. Target is \"all\" to run all pending tasks, " +
2764
+ "or a specific task area name or path. The batch runs asynchronously — " +
2765
+ "use orch_status() to monitor progress.",
2766
+ promptSnippet: "orch_start(target) — start a new batch",
2767
+ promptGuidelines: [
2768
+ "Call orch_start to begin executing pending tasks as a batch.",
2769
+ 'Use target="all" to run all pending tasks, or specify a task area name or path.',
2770
+ "Cannot start if a batch is already running — check orch_status() first.",
2771
+ "The batch runs asynchronously. The tool returns immediately with an ACK.",
2772
+ "After starting, use orch_status() to track progress.",
2773
+ ],
2774
+ parameters: Type.Object({
2775
+ target: Type.String({
2776
+ description: 'Target to run: "all" for all pending tasks, or a task area name/path',
2777
+ }),
2778
+ }),
2779
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
2780
+ try {
2781
+ const result = await doOrchStart(params.target, ctx);
2782
+ return { content: [{ type: "text" as const, text: result.message }], details: undefined };
2783
+ } catch (err) {
2784
+ return {
2785
+ content: [{ type: "text" as const, text: `Error starting batch: ${err instanceof Error ? err.message : String(err)}` }],
2786
+ details: undefined,
2787
+ };
2788
+ }
2789
+ },
2790
+ });
2791
+
2775
2792
  // ── Settings TUI ─────────────────────────────────────────────────
2776
2793
 
2777
2794
  pi.registerCommand("taskplane-settings", {
@@ -436,6 +436,7 @@ export function buildDashboardViewModel(
436
436
  return {
437
437
  phase: batchState.phase,
438
438
  batchId: batchState.batchId,
439
+ orchBranch: batchState.orchBranch || batchState.baseBranch || "",
439
440
  waveProgress,
440
441
  elapsed,
441
442
  summary,
@@ -684,7 +685,7 @@ export function createOrchWidget(
684
685
  } else if (vm.phase === "merging") {
685
686
  lines.push("");
686
687
  lines.push(truncateToWidth(
687
- theme.fg("accent", " 🔀 Merging lane branches into develop..."),
688
+ theme.fg("accent", ` 🔀 Merging lane branches into ${vm.orchBranch || "orch branch"}...`),
688
689
  width,
689
690
  ));
690
691
  } else if (vm.phase === "paused") {
@@ -2120,6 +2120,7 @@ ${guardrailsSection}
2120
2120
 
2121
2121
  You can invoke these tools directly — no need to ask the operator or use slash commands:
2122
2122
 
2123
+ - **orch_start(target)** — Start a new batch. Target is \`"all"\` for all pending tasks, or a task area name/path.
2123
2124
  - **orch_status()** — Check current batch status (phase, wave progress, task counts, elapsed time)
2124
2125
  - **orch_pause()** — Pause the running batch (current tasks finish, no new tasks start)
2125
2126
  - **orch_resume(force?)** — Resume a paused or interrupted batch. Use \`force=true\` for stuck batches.
@@ -2130,6 +2131,7 @@ You can invoke these tools directly — no need to ask the operator or use slash
2130
2131
  ### When to Use These Tools
2131
2132
 
2132
2133
  Use tools **proactively** when the situation calls for it:
2134
+ - Operator asks to run tasks or start a batch → call \`orch_start(target="all")\` (or a specific area)
2133
2135
  - Operator asks "how's it going?" → call \`orch_status()\` first, then summarize
2134
2136
  - Batch paused due to a failure you diagnosed and fixed → call \`orch_resume()\`
2135
2137
  - Batch completed successfully → offer to call \`orch_integrate(mode="pr")\` or the operator's preferred mode
@@ -2398,13 +2400,14 @@ Use these to:
2398
2400
  ### Orchestrator Tools
2399
2401
 
2400
2402
  You also have orchestrator tools available for batch management:
2403
+ - **orch_start(target)** — Start a new batch (target: "all" or a task area name/path)
2401
2404
  - **orch_status()** — Check batch status
2402
2405
  - **orch_resume(force?)** — Resume a paused batch
2403
2406
  - **orch_integrate(mode?, force?, branch?)** — Integrate completed batch (modes: "fast-forward", "merge", "pr")
2404
2407
  - **orch_pause()** — Pause running batch
2405
2408
  - **orch_abort(hard?)** — Abort running batch
2406
2409
 
2407
- Use these when the conversation leads to batch operations (e.g., integrating a completed batch).
2410
+ Use these when the conversation leads to batch operations (e.g., starting a batch, integrating a completed batch).
2408
2411
 
2409
2412
  ## Operational Knowledge
2410
2413
 
@@ -1957,6 +1957,7 @@ export interface OrchLaneCardData {
1957
1957
  export interface OrchDashboardViewModel {
1958
1958
  phase: OrchBatchPhase;
1959
1959
  batchId: string;
1960
+ orchBranch: string; // e.g., "orch/henry-20260318T140000" — merge target branch
1960
1961
  waveProgress: string; // e.g., "2/3"
1961
1962
  elapsed: string; // e.g., "2m 14s"
1962
1963
  summary: OrchSummaryCounts;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskplane",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "AI agent orchestration for pi — parallel task execution with checkpoint discipline",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -311,7 +311,7 @@ Verify every task against this before reporting the launch command:
311
311
  - [ ] `## Context to Read First` lists only needed Tier 3 docs
312
312
  - [ ] `## File Scope` lists files/dirs the task will touch
313
313
  - [ ] Each step has checkboxes with verifiable outcomes
314
- - [ ] Explicit testing step with commands
314
+ - [ ] Explicit testing step with full-suite command (per-step targeted tests are in the worker prompt)
315
315
  - [ ] `## Do NOT` guardrails
316
316
  - [ ] "Must Update" and "Check If Affected" doc lists
317
317
  - [ ] `## Git Commit Convention` section (from template)
@@ -360,6 +360,9 @@ files and make sure their file scopes reflect that.
360
360
  lists, docs drift from reality and future tasks work from stale context.
361
361
  - **Testing step required.** Workers can't distinguish pre-existing failures from
362
362
  regressions they caused — every task needs a clean test pass to stay unblocked.
363
+ The Testing & Verification step runs the **full** test suite as a quality gate.
364
+ Implementation steps should use **targeted tests** (e.g., `--changed` or
365
+ specific test files) for fast feedback — the worker prompt handles this.
363
366
  - **Self-contained PROMPT.md.** The worker starts with a fresh context and no
364
367
  memory of the conversation that created the task. Everything it needs to begin
365
368
  must be in PROMPT.md and the referenced docs.
@@ -80,15 +80,17 @@ Copy this template when creating a new task. Replace all `[bracketed]` fields.
80
80
  - [ ] [Specific, verifiable task]
81
81
  - [ ] [Specific, verifiable task]
82
82
  - [ ] [Specific, verifiable task]
83
+ - [ ] Run targeted tests: `[test command] --changed` or specific test files
83
84
 
84
85
  **Artifacts:**
85
86
  - `path/to/file` (new | modified)
86
87
 
87
88
  ### Step [N-1]: Testing & Verification
88
89
 
89
- > ZERO test failures allowed.
90
+ > ZERO test failures allowed. This step runs the FULL test suite as a quality gate.
91
+ > (Earlier steps should use targeted tests for fast feedback — see worker prompt.)
90
92
 
91
- - [ ] Run unit tests: `[test command from task-runner.yaml]`
93
+ - [ ] Run FULL test suite: `[test command from task-runner.yaml]`
92
94
  - [ ] Run integration tests (if applicable)
93
95
  - [ ] Fix all failures
94
96
  - [ ] Build passes: `[build command]`
@@ -193,7 +195,7 @@ this from PROMPT.md.
193
195
  ### Step [N-1]: Testing & Verification
194
196
  **Status:** ⬜ Not Started
195
197
 
196
- - [ ] Unit tests passing
198
+ - [ ] FULL test suite passing
197
199
  - [ ] Integration tests (if applicable)
198
200
  - [ ] All failures fixed
199
201
  - [ ] Build passes
@@ -18,6 +18,7 @@ name: task-worker
18
18
  - Git commit conventions (per-step commits) and .DONE file creation
19
19
  - Review protocol (inline reviews via review_step tool when available)
20
20
  - Review response handling
21
+ - Test execution strategy (targeted tests during steps, full suite at gate)
21
22
 
22
23
  Add project-specific rules below. Common examples:
23
24
  - Preferred package manager (pnpm, yarn, bun)
@@ -38,6 +38,7 @@ Use these to:
38
38
  ### Orchestrator Tools
39
39
 
40
40
  You also have orchestrator tools available for batch management:
41
+ - **orch_start(target)** — Start a new batch (target: "all" or a task area name/path)
41
42
  - **orch_status()** — Check batch status
42
43
  - **orch_resume(force?)** — Resume a paused batch
43
44
  - **orch_integrate(mode?, force?, branch?)** — Integrate completed batch (modes: "fast-forward", "merge", "pr")
@@ -138,6 +138,7 @@ Read it now before doing anything else. It is your primary reference.
138
138
 
139
139
  You can invoke these tools directly — no need to ask the operator or use slash commands:
140
140
 
141
+ - **orch_start(target)** — Start a new batch. Target is `"all"` for all pending tasks, or a task area name/path.
141
142
  - **orch_status()** — Check current batch status (phase, wave progress, task counts, elapsed time)
142
143
  - **orch_pause()** — Pause the running batch (current tasks finish, no new tasks start)
143
144
  - **orch_resume(force?)** — Resume a paused or interrupted batch. Use `force=true` for stuck batches.
@@ -148,6 +149,7 @@ You can invoke these tools directly — no need to ask the operator or use slash
148
149
  ### When to Use These Tools
149
150
 
150
151
  Use tools **proactively** when the situation calls for it:
152
+ - Operator asks to run tasks or start a batch → call `orch_start(target="all")` (or a specific area)
151
153
  - Operator asks "how's it going?" → call `orch_status()` first, then summarize
152
154
  - Batch paused due to a failure you diagnosed and fixed → call `orch_resume()`
153
155
  - Batch completed successfully → offer to call `orch_integrate(mode="pr")` or the operator's preferred mode
@@ -219,3 +219,50 @@ Do NOT:
219
219
  Blockers section and move to the next checkbox
220
220
  - If a test fails, fix it. If the fix is out of scope, document and continue.
221
221
  - If a dependency is missing, document in STATUS.md and stop.
222
+
223
+ ## Test Execution Strategy
224
+
225
+ Run tests at two different scopes depending on where you are in the task:
226
+
227
+ ### During implementation steps (targeted tests)
228
+
229
+ After implementing each step, run **targeted tests** for fast feedback:
230
+
231
+ ```bash
232
+ cd extensions && npx vitest run --changed
233
+ ```
234
+
235
+ - Vitest's `--changed` flag uses git to find modified files since the last commit
236
+ and runs only tests related to those files.
237
+ - Workers commit at step boundaries, so between commits the changed set is
238
+ exactly "what this step modified" — this naturally targets the right tests.
239
+ - Alternatively, run specific test files that cover the code you modified:
240
+ `npx vitest run tests/some-specific.test.ts`
241
+ - **If `--changed` returns no tests:** That's fine — it means your changes don't
242
+ have directly related test files. The full suite in the Testing step will catch
243
+ any indirect regressions.
244
+ - **If targeted tests fail:** Fix the failure before proceeding. Don't accumulate
245
+ failures across steps.
246
+
247
+ ### During the Testing & Verification step (full suite)
248
+
249
+ Run the **full test suite** as a quality gate:
250
+
251
+ ```bash
252
+ cd extensions && npx vitest run
253
+ ```
254
+
255
+ - ALL tests must pass — zero failures allowed.
256
+ - This is the definitive check before marking the task complete.
257
+ - The merge agent and CI run the full suite again after this — you have safety nets,
258
+ but catch issues here first.
259
+
260
+ ### Key principle
261
+
262
+ Fast feedback during implementation, full verification at the gate. Three full-suite
263
+ checkpoints protect against regressions even when intermediate steps use targeted tests:
264
+ 1. The Testing & Verification step (before `.DONE`)
265
+ 2. The merge agent (before merging to the orchestrator branch)
266
+ 3. CI (before merging to main)
267
+
268
+