taskplane 0.7.0 → 0.7.2

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.
@@ -308,9 +308,17 @@ function renderSummary(batch) {
308
308
  let batchChecked = 0, batchTotal = 0;
309
309
  const waveStats = wavePlan.map((taskIds, waveIdx) => {
310
310
  let wChecked = 0, wTotal = 0;
311
+ let allSucceeded = taskIds.length > 0;
311
312
  for (const tid of taskIds) {
312
313
  const t = taskMap.get(tid);
313
- if (t && t.statusData) {
314
+ if (!t || t.status !== "succeeded") allSucceeded = false;
315
+ if (t && t.status === "succeeded" && t.statusData) {
316
+ // Succeeded task with statusData: count as fully done even if
317
+ // STATUS.md checkboxes weren't all ticked before .DONE was created
318
+ const total = t.statusData.total || 1;
319
+ wChecked += total;
320
+ wTotal += total;
321
+ } else if (t && t.statusData) {
314
322
  wChecked += t.statusData.checked || 0;
315
323
  wTotal += t.statusData.total || 0;
316
324
  } else if (t && t.status === "succeeded") {
@@ -322,7 +330,7 @@ function renderSummary(batch) {
322
330
  }
323
331
  batchChecked += wChecked;
324
332
  batchTotal += wTotal;
325
- return { waveIdx, taskIds, checked: wChecked, total: wTotal };
333
+ return { waveIdx, taskIds, checked: wChecked, total: wTotal, allSucceeded };
326
334
  });
327
335
 
328
336
  const overallPct = batchTotal > 0 ? Math.round((batchChecked / batchTotal) * 100) : 0;
@@ -333,15 +341,19 @@ function renderSummary(batch) {
333
341
  for (const ws of waveStats) {
334
342
  const segWidthPct = batchTotal > 0 ? (ws.total / batchTotal) * 100 : (100 / waveStats.length);
335
343
  const fillPct = ws.total > 0 ? (ws.checked / ws.total) * 100 : 0;
336
- const isDone = ws.checked === ws.total && ws.total > 0;
337
- const isCurrent = ws.waveIdx === currentWaveIdx && batch.phase === "executing";
344
+ const checkboxDone = ws.checked === ws.total && ws.total > 0;
345
+ const pastWave = ws.waveIdx < currentWaveIdx;
346
+ const batchDone = batch.phase === "completed" || batch.phase === "merging";
347
+ const isDone = checkboxDone || pastWave || batchDone || ws.allSucceeded;
348
+ const isCurrent = ws.waveIdx === currentWaveIdx && (batch.phase === "executing" || batch.phase === "merging");
338
349
  const isFuture = ws.waveIdx > currentWaveIdx && batch.phase === "executing";
339
350
 
340
351
  const fillClass = isDone ? "pct-hi" : fillPct > 50 ? "pct-mid" : fillPct > 0 ? "pct-low" : "pct-0";
352
+ const fillWidth = isDone ? 100 : fillPct;
341
353
  const segClass = isCurrent ? "wave-seg-current" : isFuture ? "wave-seg-future" : "";
342
354
 
343
355
  barHtml += `<div class="wave-seg ${segClass}" style="width:${segWidthPct.toFixed(1)}%" title="W${ws.waveIdx + 1}: ${ws.checked}/${ws.total} checkboxes (${ws.taskIds.join(', ')})">`;
344
- barHtml += ` <div class="wave-seg-fill ${fillClass}" style="width:${fillPct.toFixed(1)}%"></div>`;
356
+ barHtml += ` <div class="wave-seg-fill ${fillClass}" style="width:${fillWidth.toFixed(1)}%"></div>`;
345
357
  barHtml += ` <span class="wave-seg-label">W${ws.waveIdx + 1}</span>`;
346
358
  barHtml += `</div>`;
347
359
  }
@@ -12,7 +12,7 @@
12
12
  */
13
13
 
14
14
  import { loadProjectConfig, toOrchestratorConfig, toTaskRunnerConfig, hasConfigFiles } from "./config-loader.ts";
15
- export { hasConfigFiles } from "./config-loader.ts";
15
+ export { hasConfigFiles, resolveConfigRoot } from "./config-loader.ts";
16
16
  import type { OrchestratorConfig, TaskRunnerConfig } from "./types.ts";
17
17
  import type { SupervisorConfig } from "./supervisor.ts";
18
18
  import { DEFAULT_SUPERVISOR_CONFIG } from "./supervisor.ts";
@@ -1058,7 +1058,7 @@ export async function executeOrchBatch(
1058
1058
  laneCount: mergeableLaneCount,
1059
1059
  }, onEngineEvent);
1060
1060
 
1061
- mergeResult = mergeWaveByRepo(
1061
+ mergeResult = await mergeWaveByRepo(
1062
1062
  waveResult.allocatedLanes,
1063
1063
  waveResult,
1064
1064
  waveIdx + 1,
@@ -1237,14 +1237,14 @@ export async function executeOrchBatch(
1237
1237
  const mergeRepoId = extractFailedRepoId(mergeResult) ?? null;
1238
1238
  const mergeFailedLane = mergeResult.failedLane ?? undefined;
1239
1239
 
1240
- const retryOutcome = applyMergeRetryLoop(
1240
+ const retryOutcome = await applyMergeRetryLoop(
1241
1241
  mergeResult,
1242
1242
  waveIdx,
1243
1243
  batchState.resilience.retryCountByScope,
1244
1244
  {
1245
- performMerge: () => {
1245
+ performMerge: async () => {
1246
1246
  batchState.phase = "merging";
1247
- return mergeWaveByRepo(
1247
+ return await mergeWaveByRepo(
1248
1248
  waveResult.allocatedLanes,
1249
1249
  waveResult,
1250
1250
  waveIdx + 1,
@@ -1986,8 +1986,8 @@ export async function executeOrchBatch(
1986
1986
  const totalElapsedSec = Math.round((batchState.endedAt - batchState.startedAt) / 1000);
1987
1987
 
1988
1988
  // Determine final batch state. Cast to OrchBatchPhase to bypass control-flow
1989
- // narrowing — mergeWave() is synchronous but could leave phase as "merging"
1990
- // if an unexpected throw occurs between setting "merging" and restoring "executing".
1989
+ // narrowing — mergeWave() could leave phase as "merging" if an unexpected
1990
+ // throw occurs between setting "merging" and restoring "executing".
1991
1991
  if ((batchState.phase as OrchBatchPhase) === "executing" || (batchState.phase as OrchBatchPhase) === "merging") {
1992
1992
  // Normal completion (not stopped, paused, or aborted)
1993
1993
  if (batchState.failedTasks > 0) {
@@ -46,6 +46,7 @@ import { openSettingsTui } from "./settings-tui.ts";
46
46
  import {
47
47
  activateSupervisor,
48
48
  deactivateSupervisor,
49
+ transitionToRoutingMode,
49
50
  freshSupervisorState,
50
51
  registerSupervisorPromptHook,
51
52
  checkSupervisorLockOnStartup,
@@ -55,8 +56,9 @@ import {
55
56
  DEFAULT_SUPERVISOR_CONFIG,
56
57
  triggerSupervisorIntegration,
57
58
  presentBatchSummary,
59
+ resolveModelFromString,
58
60
  } from "./supervisor.ts";
59
- import type { SupervisorConfig, IntegrationExecutor, CiDeps, SummaryDeps } from "./supervisor.ts";
61
+ import type { SupervisorConfig, SupervisorRoutingContext, IntegrationExecutor, CiDeps, SummaryDeps } from "./supervisor.ts";
60
62
  import type {
61
63
  AbortMode,
62
64
  ExecutionContext,
@@ -719,6 +721,127 @@ export function collectRepoCleanupFindings(
719
721
  * This prevents unhandled promise rejections from crashing the session
720
722
  * or leaving batch state inconsistent.
721
723
  */
724
+
725
+ // ── Model Availability Pre-Flight ───────────────────────────────────
726
+
727
+ /**
728
+ * A single model configuration to validate.
729
+ */
730
+ interface ModelCheckEntry {
731
+ /** Role label for display (e.g., "Worker", "Reviewer") */
732
+ role: string;
733
+ /** Model string from config (empty = inherit session model) */
734
+ modelStr: string;
735
+ }
736
+
737
+ /**
738
+ * Result of a single model availability check.
739
+ */
740
+ export interface ModelCheckResult {
741
+ role: string;
742
+ modelStr: string;
743
+ status: "inherit" | "found" | "not-found";
744
+ resolvedName?: string;
745
+ }
746
+
747
+ /**
748
+ * Validate that all configured agent models are available in the model registry.
749
+ *
750
+ * Checks worker, reviewer, merger, and supervisor model settings. Models set to
751
+ * empty string ("") or not configured inherit the session model and are always valid.
752
+ *
753
+ * Does NOT validate API keys (that would require side-effectful setModel calls).
754
+ * This catches the most common misconfiguration: specifying a model that isn't
755
+ * registered in pi (wrong name, missing provider, etc.).
756
+ *
757
+ * @param orchConfig - Orchestrator configuration
758
+ * @param runnerConfig - Task runner configuration
759
+ * @param supervisorConfig - Supervisor configuration
760
+ * @param ctx - Extension context with model registry
761
+ * @returns Array of check results (one per role)
762
+ *
763
+ * @since v0.7.2
764
+ */
765
+ export function validateModelAvailability(
766
+ orchConfig: OrchestratorConfig,
767
+ runnerConfig: TaskRunnerConfig,
768
+ supervisorConfig: SupervisorConfig,
769
+ ctx: ExtensionContext,
770
+ ): ModelCheckResult[] {
771
+ const entries: ModelCheckEntry[] = [
772
+ { role: "Worker", modelStr: runnerConfig.worker?.model ?? "" },
773
+ { role: "Reviewer", modelStr: runnerConfig.reviewer?.model ?? "" },
774
+ { role: "Merger", modelStr: orchConfig.merge?.model ?? "" },
775
+ { role: "Supervisor", modelStr: supervisorConfig.model ?? "" },
776
+ ];
777
+
778
+ const sessionModel = ctx.model;
779
+ const results: ModelCheckResult[] = [];
780
+
781
+ for (const entry of entries) {
782
+ if (!entry.modelStr) {
783
+ // Empty = inherit session model
784
+ results.push({
785
+ role: entry.role,
786
+ modelStr: "(inherit)",
787
+ status: "inherit",
788
+ resolvedName: sessionModel
789
+ ? `${(sessionModel as any).provider ?? ""}/${sessionModel.id}`.replace(/^\//, "")
790
+ : "session default",
791
+ });
792
+ continue;
793
+ }
794
+
795
+ const resolved = resolveModelFromString(entry.modelStr, ctx);
796
+ if (resolved) {
797
+ results.push({
798
+ role: entry.role,
799
+ modelStr: entry.modelStr,
800
+ status: "found",
801
+ resolvedName: `${(resolved as any).provider ?? ""}/${resolved.id}`.replace(/^\//, ""),
802
+ });
803
+ } else {
804
+ results.push({
805
+ role: entry.role,
806
+ modelStr: entry.modelStr,
807
+ status: "not-found",
808
+ });
809
+ }
810
+ }
811
+
812
+ return results;
813
+ }
814
+
815
+ /**
816
+ * Format model validation results for display.
817
+ *
818
+ * @param results - Model check results from validateModelAvailability
819
+ * @returns Formatted string for ctx.ui.notify
820
+ */
821
+ export function formatModelValidation(results: ModelCheckResult[]): string {
822
+ const lines: string[] = ["Model Configuration:"];
823
+ let hasFailure = false;
824
+
825
+ for (const r of results) {
826
+ if (r.status === "inherit") {
827
+ lines.push(` ✅ ${r.role.padEnd(12)} inherit → ${r.resolvedName}`);
828
+ } else if (r.status === "found") {
829
+ lines.push(` ✅ ${r.role.padEnd(12)} ${r.modelStr} → ${r.resolvedName}`);
830
+ } else {
831
+ lines.push(` ❌ ${r.role.padEnd(12)} ${r.modelStr} — NOT FOUND in model registry`);
832
+ hasFailure = true;
833
+ }
834
+ }
835
+
836
+ if (hasFailure) {
837
+ lines.push("");
838
+ lines.push(" Fix: update the model in .pi/taskplane-config.json or /taskplane-settings,");
839
+ lines.push(" or remove the override to inherit the session model.");
840
+ }
841
+
842
+ return lines.join("\n");
843
+ }
844
+
722
845
  export function startBatchAsync(
723
846
  engineFn: () => Promise<void>,
724
847
  batchState: import("./types.ts").OrchBatchRuntimeState,
@@ -1186,6 +1309,15 @@ export default function (pi: ExtensionAPI) {
1186
1309
 
1187
1310
  if (!requireExecCtx(ctx)) return;
1188
1311
 
1312
+ // ── TP-128: Transition from routing-mode supervisor to batch execution ──
1313
+ // If the supervisor is active in routing mode (conversational, no batch),
1314
+ // deactivate it so the batch can start fresh with monitoring-mode supervisor.
1315
+ // This enables the workflow: /orch → conversation → "run the tasks" → /orch all
1316
+ // without the operator needing to know about internal mode distinctions.
1317
+ if (supervisorState.active && supervisorState.routingContext) {
1318
+ await deactivateSupervisor(pi, supervisorState);
1319
+ }
1320
+
1189
1321
  // Prevent concurrent batch execution (merging is an active state)
1190
1322
  if (orchBatchState.phase !== "idle" && orchBatchState.phase !== "completed" && orchBatchState.phase !== "failed" && orchBatchState.phase !== "stopped") {
1191
1323
  ctx.ui.notify(
@@ -1265,6 +1397,23 @@ export default function (pi: ExtensionAPI) {
1265
1397
  break;
1266
1398
  }
1267
1399
 
1400
+ // ── Model availability pre-flight ────────────────────────
1401
+ // Validate that all configured agent models are resolvable in
1402
+ // the model registry before starting. Catches misconfigured
1403
+ // model names early instead of failing hours into a batch.
1404
+ const modelResults = validateModelAvailability(orchConfig, runnerConfig, supervisorConfig, ctx);
1405
+ const modelFailures = modelResults.filter(r => r.status === "not-found");
1406
+ ctx.ui.notify(formatModelValidation(modelResults), modelFailures.length > 0 ? "error" : "info");
1407
+ if (modelFailures.length > 0) {
1408
+ ctx.ui.notify(
1409
+ `❌ Cannot start batch — ${modelFailures.length} model(s) not found: ` +
1410
+ modelFailures.map(f => `${f.role} (${f.modelStr})`).join(", ") +
1411
+ `.\n\nFix the model configuration and try again.`,
1412
+ "error",
1413
+ );
1414
+ return;
1415
+ }
1416
+
1268
1417
  // Reset batch state for new execution
1269
1418
  orchBatchState = freshOrchBatchState();
1270
1419
  latestMonitorState = null;
@@ -1377,9 +1526,33 @@ export default function (pi: ExtensionAPI) {
1377
1526
  { triggerTurn: false },
1378
1527
  );
1379
1528
  }
1380
- // TP-043: Generate summary before deactivation (manual mode or non-completed)
1529
+ // TP-043: Generate summary before transition
1381
1530
  presentBatchSummary(pi, orchBatchState, execCtx!.workspaceRoot, opId, orchBatchState.diagnostics, sDeps.mergeResults);
1382
- deactivateSupervisor(pi, supervisorState);
1531
+ // TP-128: Transition to routing mode instead of deactivating.
1532
+ // The operator can continue the conversation (integrate, plan
1533
+ // next batch, create tasks) without re-invoking /orch.
1534
+ const postBatchContext: SupervisorRoutingContext = orchBatchState.phase === "completed"
1535
+ ? {
1536
+ routingState: "completed-batch",
1537
+ contextMessage:
1538
+ `Batch **${orchBatchState.batchId}** completed — ` +
1539
+ `${orchBatchState.succeededTasks}/${orchBatchState.totalTasks} tasks succeeded.\n\n` +
1540
+ `The orch branch \`${orchBatchState.orchBranch}\` is ready to integrate.\n` +
1541
+ `Would you like me to integrate it, or would you prefer to review first?\n\n` +
1542
+ `You can also:\n` +
1543
+ `• Run \`/orch-integrate\` (or \`/orch-integrate --pr\`) to integrate\n` +
1544
+ `• Create new tasks for the next batch\n` +
1545
+ `• Run a health check`,
1546
+ }
1547
+ : {
1548
+ routingState: "no-tasks",
1549
+ contextMessage:
1550
+ `Batch **${orchBatchState.batchId}** ended (${orchBatchState.phase}).\n\n` +
1551
+ `${orchBatchState.succeededTasks} succeeded, ${orchBatchState.failedTasks} failed, ` +
1552
+ `${orchBatchState.skippedTasks} skipped.\n\n` +
1553
+ `What would you like to do next?`,
1554
+ };
1555
+ transitionToRoutingMode(pi, supervisorState, postBatchContext);
1383
1556
  },
1384
1557
  );
1385
1558
 
@@ -1697,9 +1870,25 @@ export default function (pi: ExtensionAPI) {
1697
1870
  { triggerTurn: false },
1698
1871
  );
1699
1872
  }
1700
- // TP-043: Generate summary before deactivation
1873
+ // TP-043: Generate summary before transition
1701
1874
  presentBatchSummary(pi, orchBatchState, execCtx!.workspaceRoot, opId, orchBatchState.diagnostics, sDeps.mergeResults);
1702
- deactivateSupervisor(pi, supervisorState);
1875
+ // TP-128: Transition to routing mode (same as /orch onTerminal)
1876
+ const postBatchContext: SupervisorRoutingContext = orchBatchState.phase === "completed"
1877
+ ? {
1878
+ routingState: "completed-batch",
1879
+ contextMessage:
1880
+ `Batch **${orchBatchState.batchId}** completed — ` +
1881
+ `${orchBatchState.succeededTasks}/${orchBatchState.totalTasks} tasks succeeded.\n\n` +
1882
+ `The orch branch \`${orchBatchState.orchBranch}\` is ready to integrate.\n` +
1883
+ `Would you like me to integrate it, or would you prefer to review first?`,
1884
+ }
1885
+ : {
1886
+ routingState: "no-tasks",
1887
+ contextMessage:
1888
+ `Batch **${orchBatchState.batchId}** ended (${orchBatchState.phase}).\n\n` +
1889
+ `What would you like to do next?`,
1890
+ };
1891
+ transitionToRoutingMode(pi, supervisorState, postBatchContext);
1703
1892
  },
1704
1893
  );
1705
1894
 
@@ -11,7 +11,7 @@ import { resolveOperatorId } from "./naming.ts";
11
11
  import { MERGE_POLL_INTERVAL_MS, MERGE_RESULT_GRACE_MS, MERGE_RESULT_READ_RETRIES, MERGE_RESULT_READ_RETRY_DELAY_MS, MERGE_SPAWN_RETRY_MAX, MERGE_TIMEOUT_MAX_RETRIES, MERGE_TIMEOUT_MS, MergeError, VALID_MERGE_STATUSES } from "./types.ts";
12
12
  import type { AllocatedLane, LaneExecutionResult, MergeLaneResult, MergeResult, MergeResultStatus, MergeWaveResult, OrchestratorConfig, RepoMergeOutcome, TaskRunnerConfig, TransactionRecord, TransactionStatus, VerificationBaselineResult, WaveExecutionResult, WorkspaceConfig } from "./types.ts";
13
13
  import { resolveBaseBranch, resolveRepoRoot } from "./waves.ts";
14
- import { generateMergeWorktreePath, sleepSync } from "./worktree.ts";
14
+ import { generateMergeWorktreePath, sleepAsync, sleepSync } from "./worktree.ts";
15
15
  import { getCurrentBranch, runGit } from "./git.ts";
16
16
  import { ORCH_MESSAGES } from "./messages.ts";
17
17
  import { loadOrchestratorConfig } from "./config.ts";
@@ -353,7 +353,7 @@ export function buildMergeRequest(
353
353
  * @param agentRoot - Root for agent prompts. When pointer is resolved, this is the config repo's agent dir. Falls back to `<stateRoot>/.pi/agents/` or `<repoRoot>/.pi/agents/`.
354
354
  * @throws MergeError if spawn fails after retries
355
355
  */
356
- export function spawnMergeAgent(
356
+ export async function spawnMergeAgent(
357
357
  sessionName: string,
358
358
  repoRoot: string,
359
359
  mergeWorkDir: string,
@@ -361,7 +361,7 @@ export function spawnMergeAgent(
361
361
  config: OrchestratorConfig,
362
362
  stateRoot?: string,
363
363
  agentRoot?: string,
364
- ): void {
364
+ ): Promise<void> {
365
365
  execLog("merge", sessionName, "preparing to spawn merge agent", {
366
366
  mergeWorkDir,
367
367
  mergeRequestPath,
@@ -371,7 +371,7 @@ export function spawnMergeAgent(
371
371
  if (tmuxHasSession(sessionName)) {
372
372
  execLog("merge", sessionName, "killing stale merge session");
373
373
  tmuxKillSession(sessionName);
374
- sleepSync(500);
374
+ await sleepAsync(500);
375
375
  }
376
376
 
377
377
  // Build the pi command for the merge agent.
@@ -424,7 +424,7 @@ export function spawnMergeAgent(
424
424
  execLog("merge", sessionName, `merge spawn attempt ${attempt} failed: ${lastError}`);
425
425
 
426
426
  if (attempt <= MERGE_SPAWN_RETRY_MAX) {
427
- sleepSync(attempt * 1000);
427
+ await sleepAsync(attempt * 1000);
428
428
  }
429
429
  }
430
430
 
@@ -480,11 +480,11 @@ const SUCCESSFUL_MERGE_STATUSES = new Set<string>(["SUCCESS", "CONFLICT_RESOLVED
480
480
  * @returns Validated MergeResult
481
481
  * @throws MergeError on timeout, session death, or invalid result
482
482
  */
483
- export function waitForMergeResult(
483
+ export async function waitForMergeResult(
484
484
  resultPath: string,
485
485
  sessionName: string,
486
486
  timeoutMs: number = MERGE_TIMEOUT_MS,
487
- ): MergeResult {
487
+ ): Promise<MergeResult> {
488
488
  const startTime = Date.now();
489
489
  let sessionDiedAt: number | null = null;
490
490
 
@@ -559,7 +559,7 @@ export function waitForMergeResult(
559
559
  // parseMergeResult already retries, so if it throws, it's final.
560
560
  if (err instanceof MergeError && err.code === "MERGE_RESULT_INVALID") {
561
561
  // Wait a bit and try once more (file might still be in flight)
562
- sleepSync(MERGE_RESULT_READ_RETRY_DELAY_MS);
562
+ await sleepAsync(MERGE_RESULT_READ_RETRY_DELAY_MS);
563
563
  if (existsSync(resultPath)) {
564
564
  try {
565
565
  return parseMergeResult(resultPath);
@@ -605,7 +605,7 @@ export function waitForMergeResult(
605
605
  }
606
606
 
607
607
  // Poll interval
608
- sleepSync(MERGE_POLL_INTERVAL_MS);
608
+ await sleepAsync(MERGE_POLL_INTERVAL_MS);
609
609
  }
610
610
  }
611
611
 
@@ -931,7 +931,7 @@ function runPostMergeVerification(
931
931
  * @param baseBranch - Branch to merge into (captured at batch start)
932
932
  * @returns MergeWaveResult with per-lane outcomes
933
933
  */
934
- export function mergeWave(
934
+ export async function mergeWave(
935
935
  completedLanes: AllocatedLane[],
936
936
  waveResult: WaveExecutionResult,
937
937
  waveIndex: number,
@@ -943,7 +943,7 @@ export function mergeWave(
943
943
  agentRoot?: string,
944
944
  testingCommands?: Record<string, string>,
945
945
  repoId?: string,
946
- ): MergeWaveResult {
946
+ ): Promise<MergeWaveResult> {
947
947
  const startTime = Date.now();
948
948
  const tmuxPrefix = config.orchestrator.tmux_prefix;
949
949
  const opId = resolveOperatorId(config);
@@ -1008,7 +1008,7 @@ export function mergeWave(
1008
1008
  forceRemoveMergeWorktree(mergeWorkDir, repoRoot, `W${waveIndex}`);
1009
1009
  if (existsSync(mergeWorkDir)) {
1010
1010
  // Force cleanup didn't fully remove — wait and retry once
1011
- sleepSync(500);
1011
+ await sleepAsync(500);
1012
1012
  forceRemoveMergeWorktree(mergeWorkDir, repoRoot, `W${waveIndex}`);
1013
1013
  }
1014
1014
  try {
@@ -1242,14 +1242,14 @@ export function mergeWave(
1242
1242
  }
1243
1243
 
1244
1244
  // Re-spawn merge agent for the retry
1245
- spawnMergeAgent(sessionName, repoRoot, mergeWorkDir, requestFilePath, config, stateRoot, agentRoot);
1245
+ await spawnMergeAgent(sessionName, repoRoot, mergeWorkDir, requestFilePath, config, stateRoot, agentRoot);
1246
1246
  } else {
1247
1247
  // First attempt: spawn merge agent
1248
- spawnMergeAgent(sessionName, repoRoot, mergeWorkDir, requestFilePath, config, stateRoot, agentRoot);
1248
+ await spawnMergeAgent(sessionName, repoRoot, mergeWorkDir, requestFilePath, config, stateRoot, agentRoot);
1249
1249
  }
1250
1250
 
1251
1251
  try {
1252
- mergeResult = waitForMergeResult(resultFilePath, sessionName, currentTimeoutMs);
1252
+ mergeResult = await waitForMergeResult(resultFilePath, sessionName, currentTimeoutMs);
1253
1253
  lastTimeoutError = null;
1254
1254
  break; // Success — exit retry loop
1255
1255
  } catch (waitErr: unknown) {
@@ -1732,7 +1732,7 @@ export function mergeWave(
1732
1732
  forceRemoveMergeWorktree(mergeWorkDir, repoRoot, `W${waveIndex}`);
1733
1733
  try {
1734
1734
  // Small delay to ensure worktree lock is released
1735
- sleepSync(500);
1735
+ await sleepAsync(500);
1736
1736
  spawnSync("git", ["branch", "-D", tempBranch], { cwd: repoRoot });
1737
1737
  } catch { /* best effort */ }
1738
1738
  }
@@ -1845,7 +1845,7 @@ export function groupLanesByRepo(
1845
1845
  * @param workspaceConfig - Workspace configuration (null in repo mode)
1846
1846
  * @returns MergeWaveResult with per-lane and per-repo outcomes
1847
1847
  */
1848
- export function mergeWaveByRepo(
1848
+ export async function mergeWaveByRepo(
1849
1849
  completedLanes: AllocatedLane[],
1850
1850
  waveResult: WaveExecutionResult,
1851
1851
  waveIndex: number,
@@ -1857,7 +1857,7 @@ export function mergeWaveByRepo(
1857
1857
  stateRoot?: string,
1858
1858
  agentRoot?: string,
1859
1859
  testingCommands?: Record<string, string>,
1860
- ): MergeWaveResult {
1860
+ ): Promise<MergeWaveResult> {
1861
1861
  const startTime = Date.now();
1862
1862
 
1863
1863
  // Build lane outcome lookup for merge eligibility (same logic as mergeWave).
@@ -1901,7 +1901,7 @@ export function mergeWaveByRepo(
1901
1901
  // In repo mode (single group with repoId=undefined), delegate directly
1902
1902
  // to mergeWave() for zero-overhead backward compatibility.
1903
1903
  if (repoGroups.length === 1 && repoGroups[0].repoId === undefined) {
1904
- const result = mergeWave(
1904
+ const result = await mergeWave(
1905
1905
  completedLanes,
1906
1906
  waveResult,
1907
1907
  waveIndex,
@@ -1956,7 +1956,7 @@ export function mergeWaveByRepo(
1956
1956
  allocatedLanes: waveResult.allocatedLanes.filter(l => groupLaneNumbers.has(l.laneNumber)),
1957
1957
  };
1958
1958
 
1959
- const groupResult = mergeWave(
1959
+ const groupResult = await mergeWave(
1960
1960
  group.lanes,
1961
1961
  filteredWaveResult,
1962
1962
  waveIndex,
@@ -698,12 +698,12 @@ export function extractFailedRepoId(mergeResult: MergeWaveResult): string | unde
698
698
  * @returns Outcome describing what happened during the retry cycle
699
699
  * @since TP-033 R006
700
700
  */
701
- export function applyMergeRetryLoop(
701
+ export async function applyMergeRetryLoop(
702
702
  mergeResult: MergeWaveResult,
703
703
  waveIdx: number,
704
704
  retryCountByScope: Record<string, number>,
705
705
  callbacks: MergeRetryCallbacks,
706
- ): MergeRetryLoopOutcome {
706
+ ): Promise<MergeRetryLoopOutcome> {
707
707
  let currentResult = mergeResult;
708
708
 
709
709
  // Classify the initial failure
@@ -766,12 +766,12 @@ export function applyMergeRetryLoop(
766
766
  );
767
767
 
768
768
  if (lastDecision.cooldownMs > 0) {
769
- callbacks.sleep(lastDecision.cooldownMs);
769
+ await callbacks.sleep(lastDecision.cooldownMs);
770
770
  }
771
771
 
772
772
  // Re-invoke merge
773
773
  callbacks.persist("merge-retry-start");
774
- currentResult = callbacks.performMerge();
774
+ currentResult = await callbacks.performMerge();
775
775
  callbacks.updateMergeResult(currentResult);
776
776
  callbacks.persist("merge-retry-complete");
777
777
 
@@ -1246,7 +1246,7 @@ export async function resumeOrchBatch(
1246
1246
  allocatedLanes: reExecAllocatedLanes,
1247
1247
  };
1248
1248
 
1249
- const reExecMergeResult = mergeWaveByRepo(
1249
+ const reExecMergeResult = await mergeWaveByRepo(
1250
1250
  reExecAllocatedLanes,
1251
1251
  syntheticWaveResult,
1252
1252
  RE_EXEC_WAVE_INDEX,
@@ -1451,7 +1451,7 @@ export async function resumeOrchBatch(
1451
1451
  batchState.phase = "merging";
1452
1452
  persistRuntimeState("merge-retry-start", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, stateRoot);
1453
1453
 
1454
- const mergeRetryResult = mergeWaveByRepo(
1454
+ const mergeRetryResult = await mergeWaveByRepo(
1455
1455
  mergeRetryLanes,
1456
1456
  syntheticWaveResult,
1457
1457
  waveIdx + 1,
@@ -1630,7 +1630,7 @@ export async function resumeOrchBatch(
1630
1630
  persistRuntimeState("merge-start", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, stateRoot);
1631
1631
  onNotify(ORCH_MESSAGES.orchMergeStart(waveIdx + 1, mergeableLaneCount), "info");
1632
1632
 
1633
- mergeResult = mergeWaveByRepo(
1633
+ mergeResult = await mergeWaveByRepo(
1634
1634
  waveResult.allocatedLanes,
1635
1635
  waveResult,
1636
1636
  waveIdx + 1,
@@ -1762,14 +1762,14 @@ export async function resumeOrchBatch(
1762
1762
  batchState.resilience = defaultResilienceState();
1763
1763
  }
1764
1764
 
1765
- const retryOutcome = applyMergeRetryLoop(
1765
+ const retryOutcome = await applyMergeRetryLoop(
1766
1766
  mergeResult,
1767
1767
  waveIdx,
1768
1768
  batchState.resilience.retryCountByScope,
1769
1769
  {
1770
- performMerge: () => {
1770
+ performMerge: async () => {
1771
1771
  batchState.phase = "merging";
1772
- return mergeWaveByRepo(
1772
+ return await mergeWaveByRepo(
1773
1773
  waveResult.allocatedLanes,
1774
1774
  waveResult,
1775
1775
  waveIdx + 1,
@@ -2149,13 +2149,30 @@ Read the relevant script section now before starting the conversation.
2149
2149
  - Summarize what you'll create before writing files — let the operator confirm
2150
2150
  - If the operator says "just give me defaults", do it and move on
2151
2151
 
2152
+ ## Starting a Batch
2153
+
2154
+ When the operator wants to run pending tasks, use the \`/orch all\` command.
2155
+ You can invoke it directly — it will seamlessly transition you from conversational
2156
+ mode to batch monitoring mode. Examples of operator intent:
2157
+
2158
+ - "run the open tasks" → respond with a brief confirmation, then invoke \`/orch all\`
2159
+ - "start the batch" → invoke \`/orch all\`
2160
+ - "run just the platform tasks" → invoke \`/orch platform\` (with the area name)
2161
+
2162
+ Before starting, you may optionally:
2163
+ - Show a quick summary of pending tasks and wave plan (\`/orch-plan all\`)
2164
+ - Ask for confirmation if the operator's intent was ambiguous
2165
+
2166
+ After \`/orch all\` starts, your system prompt will automatically switch to
2167
+ batch monitoring mode. You'll have full visibility into wave progress, task
2168
+ outcomes, and can handle failures.
2169
+
2152
2170
  ## What You Must NEVER Do
2153
2171
 
2154
- 1. Never start a batch execution (that's \`/orch all\` or \`/orch <areas>\`)
2155
- 2. Never modify existing code files (only create config/scaffolding)
2156
- 3. Never \`git push\` to any remote
2157
- 4. Never overwrite existing config files without asking
2158
- 5. Never make assumptions about project conventions — detect them
2172
+ 1. Never modify existing code files (only create config/scaffolding)
2173
+ 2. Never \`git push\` to any remote
2174
+ 3. Never overwrite existing config files without asking
2175
+ 4. Never make assumptions about project conventions — detect them
2159
2176
  `;
2160
2177
 
2161
2178
  return prompt;
@@ -2252,7 +2269,7 @@ export function freshSupervisorState(): SupervisorState {
2252
2269
  * @returns The resolved Model, or undefined if not found
2253
2270
  * @since TP-041
2254
2271
  */
2255
- function resolveModelFromString(
2272
+ export function resolveModelFromString(
2256
2273
  modelStr: string,
2257
2274
  ctx: ExtensionContext,
2258
2275
  ): Model<Api> | undefined {
@@ -2504,6 +2521,78 @@ export async function deactivateSupervisor(
2504
2521
  state.pendingSummaryDeps = null;
2505
2522
  }
2506
2523
 
2524
+ /**
2525
+ * Transition the supervisor from batch-monitoring mode back to routing mode.
2526
+ *
2527
+ * Called after a batch completes (or fails/pauses) instead of fully deactivating.
2528
+ * Tears down batch-monitoring infrastructure (lockfile, heartbeat, event tailer)
2529
+ * but keeps the supervisor active with a routing context — so the operator can
2530
+ * continue the conversation (plan next batch, create tasks, integrate, etc.)
2531
+ * without needing to re-invoke `/orch`.
2532
+ *
2533
+ * This enables the continuous workflow:
2534
+ * /orch → conversation → "run the tasks" → batch runs → batch completes →
2535
+ * conversation continues → "create more tasks" → "run them" → repeat
2536
+ *
2537
+ * @param pi - The ExtensionAPI instance
2538
+ * @param state - Supervisor state to transition
2539
+ * @param routingContext - The routing context for the new conversational mode
2540
+ *
2541
+ * @since TP-128
2542
+ */
2543
+ export async function transitionToRoutingMode(
2544
+ pi: ExtensionAPI,
2545
+ state: SupervisorState,
2546
+ routingContext: SupervisorRoutingContext,
2547
+ ): Promise<void> {
2548
+ if (!state.active) return;
2549
+
2550
+ // Tear down batch-monitoring infrastructure
2551
+ stopEventTailer(state.eventTailer);
2552
+
2553
+ if (state.heartbeatTimer) {
2554
+ clearInterval(state.heartbeatTimer);
2555
+ state.heartbeatTimer = null;
2556
+ }
2557
+
2558
+ // Remove lockfile (no active batch to protect)
2559
+ if (state.stateRoot && state.lockSessionId) {
2560
+ const currentLock = readLockfile(state.stateRoot);
2561
+ if (!currentLock || currentLock.sessionId === state.lockSessionId) {
2562
+ removeLockfile(state.stateRoot);
2563
+ }
2564
+ }
2565
+ state.lockSessionId = "";
2566
+
2567
+ // Present deferred batch summary if any
2568
+ if (state.pendingSummaryDeps && state.batchStateRef && state.stateRoot) {
2569
+ const deps = state.pendingSummaryDeps;
2570
+ presentBatchSummary(pi, state.batchStateRef, state.stateRoot, deps.opId, deps.diagnostics, deps.mergeResults);
2571
+ state.pendingSummaryDeps = null;
2572
+ }
2573
+
2574
+ // Switch to routing mode — keep supervisor active with new context
2575
+ state.routingContext = routingContext;
2576
+ state.batchId = "";
2577
+ // Keep batchStateRef/orchConfigRef/stateRoot — routing prompt may need them
2578
+ // Keep model override — don't switch models mid-conversation
2579
+
2580
+ // Notify the operator that conversational mode is back
2581
+ pi.sendMessage(
2582
+ {
2583
+ customType: "supervisor-routing-transition",
2584
+ content: [{
2585
+ type: "text",
2586
+ text:
2587
+ `🔀 **Supervisor returning to conversational mode.**\n\n` +
2588
+ routingContext.contextMessage,
2589
+ }],
2590
+ display: `Supervisor — ${routingContext.routingState}`,
2591
+ },
2592
+ { triggerTurn: true, deliverAs: "nextTurn" },
2593
+ );
2594
+ }
2595
+
2507
2596
  /**
2508
2597
  * Register the before_agent_start hook for persistent system prompt injection.
2509
2598
  *
@@ -1736,7 +1736,7 @@ export type MergeRetryLoopOutcome =
1736
1736
  */
1737
1737
  export interface MergeRetryCallbacks {
1738
1738
  /** Re-invoke mergeWaveByRepo and return the new result */
1739
- performMerge: () => MergeWaveResult;
1739
+ performMerge: () => MergeWaveResult | Promise<MergeWaveResult>;
1740
1740
  /** Persist batch state with a trigger label */
1741
1741
  persist: (trigger: string) => void;
1742
1742
  /** Log a message */
@@ -1746,7 +1746,7 @@ export interface MergeRetryCallbacks {
1746
1746
  /** Update the merge result in tracking arrays */
1747
1747
  updateMergeResult: (result: MergeWaveResult) => void;
1748
1748
  /** Sleep for cooldown (allows test injection) */
1749
- sleep: (ms: number) => void;
1749
+ sleep: (ms: number) => void | Promise<void>;
1750
1750
  /**
1751
1751
  * Optional callback fired when a retry attempt is about to be executed.
1752
1752
  * Provides the retry decision with classification, attempt count, and cooldown
@@ -598,6 +598,19 @@ export function sleepSync(ms: number): void {
598
598
  }
599
599
  }
600
600
 
601
+ /**
602
+ * Async sleep for a given number of milliseconds.
603
+ *
604
+ * Unlike `sleepSync`, this yields the event loop so that other async work
605
+ * (supervisor heartbeats, user input, dashboard updates) can proceed while
606
+ * waiting. Use this in async code paths such as merge polling.
607
+ *
608
+ * @param ms - Milliseconds to sleep
609
+ */
610
+ export function sleepAsync(ms: number): Promise<void> {
611
+ return new Promise((resolve) => setTimeout(resolve, ms));
612
+ }
613
+
601
614
  /**
602
615
  * Determine if a git worktree remove error is retriable.
603
616
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskplane",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "AI agent orchestration for pi — parallel task execution with checkpoint discipline",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: task-reviewer
3
3
  # tools: read,write,bash,grep,find,ls
4
- # model: openai/gpt-5.3-codex
4
+ # model:
5
5
  # standalone: true
6
6
  ---
7
7
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: task-worker
3
3
  # tools: read,write,edit,bash,grep,find,ls
4
- # model: anthropic/claude-sonnet-4-20250514
4
+ # model:
5
5
  # standalone: true
6
6
  ---
7
7
 
@@ -1,215 +1,215 @@
1
- ---
2
- name: task-merger
3
- description: Merges lane branches into the integration branch with conflict resolution and post-merge verification
4
- tools: read,write,edit,bash,grep,find,ls
5
- model: ""
6
- ---
7
-
8
- You are a merge agent. You merge a task lane branch into the integration branch.
9
-
10
- ## Your Environment
11
-
12
- You are running in an **isolated merge worktree** — a separate copy of the
13
- repository created specifically for this merge. The correct target branch is
14
- already checked out. The user's main working directory is untouched.
15
-
16
- **Do NOT** checkout any other branch. Simply merge the source branch into
17
- the current HEAD.
18
-
19
- ## Your Job
20
-
21
- 1. Read the merge request provided in your prompt
22
- 2. Execute the merge
23
- 3. Handle any conflicts
24
- 4. Verify the result
25
- 5. Write your outcome to the specified result file
26
-
27
- ## Merge Procedure
28
-
29
- ### Step 1: Verify Current State
30
-
31
- ```bash
32
- git branch --show-current
33
- git log --oneline -1
34
- ```
35
-
36
- Confirm you are on the expected branch. **Do NOT switch branches.**
37
- The worktree is clean by construction — skip dirty-worktree checks.
38
-
39
- ### Step 2: Attempt Merge
40
-
41
- ```bash
42
- git merge {source_branch} --no-ff -m "{merge_message}"
43
- ```
44
-
45
- Use the source branch and merge message from the merge request.
46
-
47
- ### Step 3: Handle Result
48
-
49
- **If merge succeeds (no conflicts):**
50
- - Proceed to Verification (Step 4)
51
-
52
- **If merge has conflicts:**
53
- 1. List conflicted files:
54
- ```bash
55
- git diff --name-only --diff-filter=U
56
- ```
57
- 2. Classify each conflict using the Conflict Classification table below
58
- 3. For auto-resolvable conflicts: resolve them, then `git add` the resolved files
59
- 4. If ALL conflicts are resolved:
60
- ```bash
61
- git add .
62
- git commit -m "merge: resolved conflicts in {source_branch} → {target_branch}"
63
- ```
64
- Proceed to Verification (Step 4) — status will be `CONFLICT_RESOLVED`
65
- 5. If ANY conflict is **not** auto-resolvable:
66
- ```bash
67
- git merge --abort
68
- ```
69
- Write a `CONFLICT_UNRESOLVED` result and stop.
70
-
71
- ### Step 4: Verification
72
-
73
- Run each verification command from the merge request. Typical commands:
74
-
75
- ```bash
76
- npm test # Unit/integration checks
77
- npm run build # Build/compile checks
78
- ```
79
-
80
- **If verification passes:** Write result with `status: "SUCCESS"` (or
81
- `"CONFLICT_RESOLVED"` if conflicts were auto-resolved).
82
-
83
- **If verification fails:**
84
- ```bash
85
- git revert HEAD --no-edit # Undo the merge commit
86
- ```
87
- Write a `BUILD_FAILURE` result with the error output from the failed command.
88
-
89
- ---
90
-
91
- ## Conflict Classification
92
-
93
- | Type | Auto-Resolvable | Resolution Strategy |
94
- |------|-----------------|---------------------|
95
- | Different files modified | N/A (git handles automatically) | No action needed |
96
- | Same file, different sections | Yes — accept both changes | Edit file to include both changes, remove conflict markers |
97
- | Same file, same lines | **No** — needs human review | Abort merge immediately |
98
- | Generated files (`package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`) | Yes — regenerate | Run package manager install command to regenerate |
99
- | `STATUS.md` / `.DONE` files | Yes — keep both | Accept incoming STATUS.md; keep `.DONE` markers |
100
- | `CONTEXT.md` (append-only sections) | Yes — keep both additions | Merge both additions into relevant sections |
101
-
102
- ### Auto-Resolution Rules
103
-
104
- 1. **Same file, different sections:** Open the file, identify conflict markers
105
- (`<<<<<<<`, `=======`, `>>>>>>>`). If the conflicting hunks are in clearly
106
- different sections, keep both changes and remove markers.
107
-
108
- 2. **Generated files:** Do NOT manually edit. Regenerate lockfiles using your
109
- project's package manager command (for example `npm install`,
110
- `pnpm install`, or `yarn install`), then `git add` the regenerated file.
111
-
112
- 3. **STATUS.md:** These are per-task tracking files. Accept theirs:
113
- ```bash
114
- git checkout --theirs STATUS.md && git add STATUS.md
115
- ```
116
-
117
- 4. **`.DONE` marker files:** Keep marker files if either side created one.
118
-
119
- 5. **Same lines / ambiguous conflicts:** Do NOT attempt to resolve. Run
120
- `git merge --abort` and report `CONFLICT_UNRESOLVED`.
121
-
122
- ---
123
-
124
- ## Result File Format
125
-
126
- Write your result as JSON to the path specified in the merge request
127
- (`result_file` field). The file must be valid JSON with this structure:
128
-
129
- ```json
130
- {
131
- "status": "SUCCESS",
132
- "source_branch": "task/lane-1-abc123",
133
- "target_branch": "main",
134
- "merge_commit": "abc1234def5678",
135
- "conflicts": [],
136
- "verification": {
137
- "ran": true,
138
- "passed": true,
139
- "output": ""
140
- }
141
- }
142
- ```
143
-
144
- ### Field Reference
145
-
146
- | Field | Type | Description |
147
- |-------|------|-------------|
148
- | `status` | string | One of: `SUCCESS`, `CONFLICT_RESOLVED`, `CONFLICT_UNRESOLVED`, `BUILD_FAILURE` |
149
- | `source_branch` | string | The lane branch that was merged (from merge request) |
150
- | `target_branch` | string | Target branch from merge request (typically integration branch, e.g. `main`) |
151
- | `merge_commit` | string | Merge commit SHA (present only if merge succeeded) |
152
- | `conflicts` | array | List of conflict entries (empty if no conflicts) |
153
- | `conflicts[].file` | string | Path to conflicted file |
154
- | `conflicts[].type` | string | Classification (`different-sections`, `same-lines`, `generated`, `status-file`) |
155
- | `conflicts[].resolved` | boolean | Whether conflict was auto-resolved |
156
- | `conflicts[].resolution` | string | Resolution summary |
157
- | `verification.ran` | boolean | Whether verification commands were executed |
158
- | `verification.passed` | boolean | Whether verification commands passed |
159
- | `verification.output` | string | Verification output (useful on failures) |
160
-
161
- ### Status Definitions
162
-
163
- | Status | Meaning | Orchestrator Action |
164
- |--------|---------|---------------------|
165
- | `SUCCESS` | Merge completed, verification passed | Continue to next lane |
166
- | `CONFLICT_RESOLVED` | Conflicts auto-resolved, verification passed | Log details, continue |
167
- | `CONFLICT_UNRESOLVED` | Conflict requires human intervention | Pause batch, notify user |
168
- | `BUILD_FAILURE` | Merge succeeded but verification failed (merge reverted) | Pause batch, notify user |
169
-
170
- ### Example: Conflict Resolved
171
-
172
- ```json
173
- {
174
- "status": "CONFLICT_RESOLVED",
175
- "source_branch": "task/lane-2-abc123",
176
- "target_branch": "main",
177
- "merge_commit": "def4567abc8901",
178
- "conflicts": [
179
- {
180
- "file": "package-lock.json",
181
- "type": "generated",
182
- "resolved": true,
183
- "resolution": "regenerated via npm install"
184
- },
185
- {
186
- "file": "src/routes/api.ts",
187
- "type": "different-sections",
188
- "resolved": true,
189
- "resolution": "kept both route additions"
190
- }
191
- ],
192
- "verification": {
193
- "ran": true,
194
- "passed": true,
195
- "output": ""
196
- }
197
- }
198
- ```
199
-
200
- ### Example: Build Failure
201
-
202
- ```json
203
- {
204
- "status": "BUILD_FAILURE",
205
- "source_branch": "task/lane-1-abc123",
206
- "target_branch": "main",
207
- "merge_commit": "",
208
- "conflicts": [],
209
- "verification": {
210
- "ran": true,
211
- "passed": false,
212
- "output": "src/server.ts:42:17 - error TS2304: Cannot find name 'createApiRouter'"
213
- }
214
- }
215
- ```
1
+ ---
2
+ name: task-merger
3
+ description: Merges lane branches into the integration branch with conflict resolution and post-merge verification
4
+ tools: read,write,edit,bash,grep,find,ls
5
+ # model:
6
+ ---
7
+
8
+ You are a merge agent. You merge a task lane branch into the integration branch.
9
+
10
+ ## Your Environment
11
+
12
+ You are running in an **isolated merge worktree** — a separate copy of the
13
+ repository created specifically for this merge. The correct target branch is
14
+ already checked out. The user's main working directory is untouched.
15
+
16
+ **Do NOT** checkout any other branch. Simply merge the source branch into
17
+ the current HEAD.
18
+
19
+ ## Your Job
20
+
21
+ 1. Read the merge request provided in your prompt
22
+ 2. Execute the merge
23
+ 3. Handle any conflicts
24
+ 4. Verify the result
25
+ 5. Write your outcome to the specified result file
26
+
27
+ ## Merge Procedure
28
+
29
+ ### Step 1: Verify Current State
30
+
31
+ ```bash
32
+ git branch --show-current
33
+ git log --oneline -1
34
+ ```
35
+
36
+ Confirm you are on the expected branch. **Do NOT switch branches.**
37
+ The worktree is clean by construction — skip dirty-worktree checks.
38
+
39
+ ### Step 2: Attempt Merge
40
+
41
+ ```bash
42
+ git merge {source_branch} --no-ff -m "{merge_message}"
43
+ ```
44
+
45
+ Use the source branch and merge message from the merge request.
46
+
47
+ ### Step 3: Handle Result
48
+
49
+ **If merge succeeds (no conflicts):**
50
+ - Proceed to Verification (Step 4)
51
+
52
+ **If merge has conflicts:**
53
+ 1. List conflicted files:
54
+ ```bash
55
+ git diff --name-only --diff-filter=U
56
+ ```
57
+ 2. Classify each conflict using the Conflict Classification table below
58
+ 3. For auto-resolvable conflicts: resolve them, then `git add` the resolved files
59
+ 4. If ALL conflicts are resolved:
60
+ ```bash
61
+ git add .
62
+ git commit -m "merge: resolved conflicts in {source_branch} → {target_branch}"
63
+ ```
64
+ Proceed to Verification (Step 4) — status will be `CONFLICT_RESOLVED`
65
+ 5. If ANY conflict is **not** auto-resolvable:
66
+ ```bash
67
+ git merge --abort
68
+ ```
69
+ Write a `CONFLICT_UNRESOLVED` result and stop.
70
+
71
+ ### Step 4: Verification
72
+
73
+ Run each verification command from the merge request. Typical commands:
74
+
75
+ ```bash
76
+ npm test # Unit/integration checks
77
+ npm run build # Build/compile checks
78
+ ```
79
+
80
+ **If verification passes:** Write result with `status: "SUCCESS"` (or
81
+ `"CONFLICT_RESOLVED"` if conflicts were auto-resolved).
82
+
83
+ **If verification fails:**
84
+ ```bash
85
+ git revert HEAD --no-edit # Undo the merge commit
86
+ ```
87
+ Write a `BUILD_FAILURE` result with the error output from the failed command.
88
+
89
+ ---
90
+
91
+ ## Conflict Classification
92
+
93
+ | Type | Auto-Resolvable | Resolution Strategy |
94
+ |------|-----------------|---------------------|
95
+ | Different files modified | N/A (git handles automatically) | No action needed |
96
+ | Same file, different sections | Yes — accept both changes | Edit file to include both changes, remove conflict markers |
97
+ | Same file, same lines | **No** — needs human review | Abort merge immediately |
98
+ | Generated files (`package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`) | Yes — regenerate | Run package manager install command to regenerate |
99
+ | `STATUS.md` / `.DONE` files | Yes — keep both | Accept incoming STATUS.md; keep `.DONE` markers |
100
+ | `CONTEXT.md` (append-only sections) | Yes — keep both additions | Merge both additions into relevant sections |
101
+
102
+ ### Auto-Resolution Rules
103
+
104
+ 1. **Same file, different sections:** Open the file, identify conflict markers
105
+ (`<<<<<<<`, `=======`, `>>>>>>>`). If the conflicting hunks are in clearly
106
+ different sections, keep both changes and remove markers.
107
+
108
+ 2. **Generated files:** Do NOT manually edit. Regenerate lockfiles using your
109
+ project's package manager command (for example `npm install`,
110
+ `pnpm install`, or `yarn install`), then `git add` the regenerated file.
111
+
112
+ 3. **STATUS.md:** These are per-task tracking files. Accept theirs:
113
+ ```bash
114
+ git checkout --theirs STATUS.md && git add STATUS.md
115
+ ```
116
+
117
+ 4. **`.DONE` marker files:** Keep marker files if either side created one.
118
+
119
+ 5. **Same lines / ambiguous conflicts:** Do NOT attempt to resolve. Run
120
+ `git merge --abort` and report `CONFLICT_UNRESOLVED`.
121
+
122
+ ---
123
+
124
+ ## Result File Format
125
+
126
+ Write your result as JSON to the path specified in the merge request
127
+ (`result_file` field). The file must be valid JSON with this structure:
128
+
129
+ ```json
130
+ {
131
+ "status": "SUCCESS",
132
+ "source_branch": "task/lane-1-abc123",
133
+ "target_branch": "main",
134
+ "merge_commit": "abc1234def5678",
135
+ "conflicts": [],
136
+ "verification": {
137
+ "ran": true,
138
+ "passed": true,
139
+ "output": ""
140
+ }
141
+ }
142
+ ```
143
+
144
+ ### Field Reference
145
+
146
+ | Field | Type | Description |
147
+ |-------|------|-------------|
148
+ | `status` | string | One of: `SUCCESS`, `CONFLICT_RESOLVED`, `CONFLICT_UNRESOLVED`, `BUILD_FAILURE` |
149
+ | `source_branch` | string | The lane branch that was merged (from merge request) |
150
+ | `target_branch` | string | Target branch from merge request (typically integration branch, e.g. `main`) |
151
+ | `merge_commit` | string | Merge commit SHA (present only if merge succeeded) |
152
+ | `conflicts` | array | List of conflict entries (empty if no conflicts) |
153
+ | `conflicts[].file` | string | Path to conflicted file |
154
+ | `conflicts[].type` | string | Classification (`different-sections`, `same-lines`, `generated`, `status-file`) |
155
+ | `conflicts[].resolved` | boolean | Whether conflict was auto-resolved |
156
+ | `conflicts[].resolution` | string | Resolution summary |
157
+ | `verification.ran` | boolean | Whether verification commands were executed |
158
+ | `verification.passed` | boolean | Whether verification commands passed |
159
+ | `verification.output` | string | Verification output (useful on failures) |
160
+
161
+ ### Status Definitions
162
+
163
+ | Status | Meaning | Orchestrator Action |
164
+ |--------|---------|---------------------|
165
+ | `SUCCESS` | Merge completed, verification passed | Continue to next lane |
166
+ | `CONFLICT_RESOLVED` | Conflicts auto-resolved, verification passed | Log details, continue |
167
+ | `CONFLICT_UNRESOLVED` | Conflict requires human intervention | Pause batch, notify user |
168
+ | `BUILD_FAILURE` | Merge succeeded but verification failed (merge reverted) | Pause batch, notify user |
169
+
170
+ ### Example: Conflict Resolved
171
+
172
+ ```json
173
+ {
174
+ "status": "CONFLICT_RESOLVED",
175
+ "source_branch": "task/lane-2-abc123",
176
+ "target_branch": "main",
177
+ "merge_commit": "def4567abc8901",
178
+ "conflicts": [
179
+ {
180
+ "file": "package-lock.json",
181
+ "type": "generated",
182
+ "resolved": true,
183
+ "resolution": "regenerated via npm install"
184
+ },
185
+ {
186
+ "file": "src/routes/api.ts",
187
+ "type": "different-sections",
188
+ "resolved": true,
189
+ "resolution": "kept both route additions"
190
+ }
191
+ ],
192
+ "verification": {
193
+ "ran": true,
194
+ "passed": true,
195
+ "output": ""
196
+ }
197
+ }
198
+ ```
199
+
200
+ ### Example: Build Failure
201
+
202
+ ```json
203
+ {
204
+ "status": "BUILD_FAILURE",
205
+ "source_branch": "task/lane-1-abc123",
206
+ "target_branch": "main",
207
+ "merge_commit": "",
208
+ "conflicts": [],
209
+ "verification": {
210
+ "ran": true,
211
+ "passed": false,
212
+ "output": "src/server.ts:42:17 - error TS2304: Cannot find name 'createApiRouter'"
213
+ }
214
+ }
215
+ ```
@@ -2,7 +2,7 @@
2
2
  name: task-reviewer
3
3
  description: Cross-model code and plan reviewer — provides independent quality assessment
4
4
  tools: read,write,bash,grep,find,ls
5
- model: openai/gpt-5.3-codex
5
+ # model:
6
6
  ---
7
7
  You are an independent code and plan reviewer. You provide quality assessment for
8
8
  task implementations. You have full read access to the codebase and can run commands.
@@ -2,6 +2,7 @@
2
2
  name: task-worker
3
3
  description: Autonomous task execution agent — works on individual steps with checkpoint discipline
4
4
  tools: read,write,edit,bash,grep,find,ls
5
+ # model:
5
6
  ---
6
7
  You are a task execution agent running in a **fresh-context loop**. Each time you
7
8
  are invoked, you have ZERO memory of prior invocations. STATUS.md on disk is your