taskplane 0.26.1 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1359,6 +1359,9 @@ export function serializeBatchState(
1359
1359
  endedAt: state.endedAt,
1360
1360
  currentWaveIndex: state.currentWaveIndex,
1361
1361
  totalWaves: state.totalWaves,
1362
+ // TP-166: Persist task-level wave metadata for correct display after resume
1363
+ ...(state.taskLevelWaveCount != null ? { taskLevelWaveCount: state.taskLevelWaveCount } : {}),
1364
+ ...(state.roundToTaskWave != null ? { roundToTaskWave: [...state.roundToTaskWave] } : {}),
1362
1365
  wavePlan,
1363
1366
  lanes: laneRecords,
1364
1367
  tasks: taskRecords,
@@ -7,7 +7,7 @@ import { join } from "path";
7
7
 
8
8
  import { assembleDiagnosticInput, emitDiagnosticReports } from "./diagnostic-reports.ts";
9
9
  import { runDiscovery } from "./discovery.ts";
10
- import { executeOrchBatch } from "./engine.ts";
10
+ import { executeOrchBatch, resolveDisplayWaveNumber } from "./engine.ts";
11
11
  import { buildReviewerEnv, computeTransitiveDependents, execLog, executeLaneV2, executeWave, resolveCanonicalTaskPaths } from "./execution.ts";
12
12
  import type { MonitorUpdateCallback, RuntimeBackend } from "./execution.ts";
13
13
  import { selectRuntimeBackend } from "./engine.ts";
@@ -158,9 +158,14 @@ export function reconstructAllocatedLanes(
158
158
  if (persistedTask?.resolvedRepoId !== undefined) {
159
159
  taskStub.resolvedRepoId = persistedTask.resolvedRepoId;
160
160
  }
161
- if (persistedTask?.taskFolder) {
162
- taskStub.taskFolder = persistedTask.taskFolder;
163
- }
161
+ // TP-169: Always set taskFolder on stub, even if empty string.
162
+ // Previously, the falsy check `if (persistedTask?.taskFolder)` skipped
163
+ // empty-string values, leaving taskFolder as `undefined` on the stub.
164
+ // This caused crashes in buildExecutionUnit and merge code when
165
+ // accessing `task.task.taskFolder` on dynamically-expanded segments
166
+ // whose persisted records had taskFolder="" (the default from
167
+ // serializeBatchState before enrichment).
168
+ taskStub.taskFolder = persistedTask?.taskFolder ?? "";
164
169
  if ((persistedTask as any)?.packetRepoId !== undefined) {
165
170
  (taskStub as any).packetRepoId = (persistedTask as any).packetRepoId;
166
171
  }
@@ -1324,6 +1329,10 @@ export async function resumeOrchBatch(
1324
1329
  // Preserve pauseSignal if already set during "launching" phase (TP-040)
1325
1330
  if (!batchState.pauseSignal?.paused) batchState.pauseSignal = { paused: false };
1326
1331
  batchState.totalWaves = persistedState.totalWaves;
1332
+ // TP-166: Restore task-level wave metadata for correct display.
1333
+ // Normalize: fall back to totalWaves for pre-TP-166 state files.
1334
+ batchState.taskLevelWaveCount = persistedState.taskLevelWaveCount ?? persistedState.totalWaves;
1335
+ batchState.roundToTaskWave = persistedState.roundToTaskWave ? [...persistedState.roundToTaskWave] : undefined;
1327
1336
  batchState.totalTasks = persistedState.totalTasks;
1328
1337
  batchState.succeededTasks = resumePoint.completedTaskIds.length;
1329
1338
  batchState.failedTasks = resumePoint.failedTaskIds.length;
@@ -1372,6 +1381,42 @@ export async function resumeOrchBatch(
1372
1381
  batchState._extraFields = persistedState._extraFields;
1373
1382
  }
1374
1383
 
1384
+ // ── 6b. TP-169: Verify orch branch exists in all workspace repos ────
1385
+ // During the original batch start, the orch branch was created in every
1386
+ // workspace repo. On resume, we verify it still exists. If it's missing
1387
+ // in any repo (e.g., deleted by user, corrupted), re-create it from the
1388
+ // repo's current branch so that worktree creation doesn't silently fall
1389
+ // back to the base branch, bypassing orch branch isolation.
1390
+ if (workspaceConfig && batchState.orchBranch) {
1391
+ for (const [repoId, repoConf] of workspaceConfig.repos) {
1392
+ const rRoot = repoConf.path;
1393
+ const check = runGit(["rev-parse", "--verify", `refs/heads/${batchState.orchBranch}`], rRoot);
1394
+ if (!check.ok) {
1395
+ // Orch branch missing in this repo — re-create from current HEAD
1396
+ const repoBranch = getCurrentBranch(rRoot) || "HEAD";
1397
+ const createRes = runGit(["branch", batchState.orchBranch, repoBranch], rRoot);
1398
+ if (createRes.ok) {
1399
+ execLog("resume", batchState.batchId, `re-created missing orch branch in ${repoId}`, {
1400
+ orchBranch: batchState.orchBranch,
1401
+ base: repoBranch,
1402
+ });
1403
+ onNotify(
1404
+ `⚠️ Orch branch "${batchState.orchBranch}" was missing in repo "${repoId}" — re-created from ${repoBranch}`,
1405
+ "warning",
1406
+ );
1407
+ } else {
1408
+ const errMsg = `Failed to re-create orch branch "${batchState.orchBranch}" in repo "${repoId}": ${createRes.stderr}. ` +
1409
+ `Cannot resume without orch branch isolation.`;
1410
+ execLog("resume", batchState.batchId, errMsg, {
1411
+ orchBranch: batchState.orchBranch,
1412
+ error: createRes.stderr,
1413
+ });
1414
+ throw new Error(errMsg);
1415
+ }
1416
+ }
1417
+ }
1418
+ }
1419
+
1375
1420
  // ── 7. Re-run discovery for ParsedTask metadata ──────────────
1376
1421
  // We need fresh ParsedTask data (taskFolder, promptPath) for execution.
1377
1422
  // Use "all" to discover all areas.
@@ -1767,12 +1812,17 @@ export async function resumeOrchBatch(
1767
1812
  persistedState.tasks.map((task) => [task.taskId, task.status] as const),
1768
1813
  );
1769
1814
 
1815
+ // TP-166: Use task-level wave metadata for correct display.
1816
+ const roundToTaskWave = batchState.roundToTaskWave;
1817
+ const taskLevelWaveCount = batchState.taskLevelWaveCount;
1818
+
1770
1819
  for (let waveIdx = resumePoint.resumeWaveIndex; waveIdx < wavePlan.length; waveIdx++) {
1771
1820
  // Check pause signal
1772
1821
  if (batchState.pauseSignal.paused) {
1773
1822
  batchState.phase = "paused";
1774
1823
  persistRuntimeState("pause-before-wave", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, stateRoot);
1775
- onNotify(`⏸️ Batch paused before wave ${waveIdx + 1}.`, "warning");
1824
+ const { displayWave: pauseWave } = resolveDisplayWaveNumber(waveIdx, roundToTaskWave, taskLevelWaveCount);
1825
+ onNotify(`⏸️ Batch paused before wave ${pauseWave}.`, "warning");
1776
1826
  break;
1777
1827
  }
1778
1828
 
@@ -1807,7 +1857,7 @@ export async function resumeOrchBatch(
1807
1857
  // All tasks are terminal but the merge may have failed/been interrupted.
1808
1858
  if (resumePoint.mergeRetryWaveIndexes.includes(waveIdx)) {
1809
1859
  execLog("resume", batchState.batchId, `wave ${waveIdx + 1}: all tasks done but merge needs retry`);
1810
- onNotify(`🔀 Wave ${waveIdx + 1}: retrying merge (tasks already complete, merge was missing/failed)`, "info");
1860
+ onNotify(`🔀 Wave ${resolveDisplayWaveNumber(waveIdx, roundToTaskWave, taskLevelWaveCount).displayWave}: retrying merge (tasks already complete, merge was missing/failed)`, "info");
1811
1861
 
1812
1862
  // Reconstruct lanes for this wave from persisted state
1813
1863
  const waveTaskIds = new Set(wavePlan[waveIdx]);
@@ -1922,7 +1972,7 @@ export async function resumeOrchBatch(
1922
1972
  batchState.mergeResults.push(mergeRetryResult);
1923
1973
 
1924
1974
  if (mergeRetryResult.status === "succeeded") {
1925
- onNotify(`✅ Wave ${waveIdx + 1} merge retry succeeded`, "info");
1975
+ onNotify(`✅ Wave ${resolveDisplayWaveNumber(waveIdx, roundToTaskWave, taskLevelWaveCount).displayWave} merge retry succeeded`, "info");
1926
1976
  // Clean up merged branches
1927
1977
  for (const lr of mergeRetryResult.laneResults) {
1928
1978
  if (!lr.error && (lr.result?.status === "SUCCESS" || lr.result?.status === "CONFLICT_RESOLVED")) {
@@ -1932,7 +1982,7 @@ export async function resumeOrchBatch(
1932
1982
  }
1933
1983
  } else {
1934
1984
  onNotify(
1935
- `⚠️ Wave ${waveIdx + 1} merge retry ${mergeRetryResult.status}: ${mergeRetryResult.failureReason || "unknown"}`,
1985
+ `⚠️ Wave ${resolveDisplayWaveNumber(waveIdx, roundToTaskWave, taskLevelWaveCount).displayWave} merge retry ${mergeRetryResult.status}: ${mergeRetryResult.failureReason || "unknown"}`,
1936
1986
  "warning",
1937
1987
  );
1938
1988
  // Apply merge failure policy (same as normal wave merge failure)
@@ -1954,10 +2004,13 @@ export async function resumeOrchBatch(
1954
2004
  continue;
1955
2005
  }
1956
2006
 
1957
- onNotify(
1958
- ORCH_MESSAGES.orchWaveStart(waveIdx + 1, wavePlan.length, waveTasks.length, Math.min(waveTasks.length, orchConfig.orchestrator.max_lanes)),
1959
- "info",
1960
- );
2007
+ {
2008
+ const { displayWave, displayTotal } = resolveDisplayWaveNumber(waveIdx, roundToTaskWave, taskLevelWaveCount);
2009
+ onNotify(
2010
+ ORCH_MESSAGES.orchWaveStart(displayWave, displayTotal, waveTasks.length, Math.min(waveTasks.length, orchConfig.orchestrator.max_lanes)),
2011
+ "info",
2012
+ );
2013
+ }
1961
2014
 
1962
2015
  const handleResumeMonitorUpdate: MonitorUpdateCallback = (monitorState) => {
1963
2016
  const changed = syncTaskOutcomesFromMonitor(monitorState, allTaskOutcomes);
@@ -2064,7 +2117,7 @@ export async function resumeOrchBatch(
2064
2117
  frontierSummary +
2065
2118
  ` Lane: ${laneForTask?.laneId ?? "unknown"} (lane ${laneForTask?.laneNumber ?? "?"})\n` +
2066
2119
  ` Partial progress preserved: ${hasPartialProgress ? "yes" : "no"}\n` +
2067
- ` Batch: wave ${waveIdx + 1}/${batchState.totalWaves}, ` +
2120
+ ` Batch: wave ${resolveDisplayWaveNumber(waveIdx, roundToTaskWave, taskLevelWaveCount).displayWave}/${taskLevelWaveCount ?? batchState.totalWaves}, ` +
2068
2121
  `${batchState.succeededTasks} succeeded, ${batchState.failedTasks} failed\n\n` +
2069
2122
  `Available actions:\n` +
2070
2123
  ` - orch_status() to inspect current state\n` +
@@ -2088,16 +2141,19 @@ export async function resumeOrchBatch(
2088
2141
  persistRuntimeState("wave-execution-complete", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, stateRoot);
2089
2142
 
2090
2143
  const elapsedSec = Math.round((waveResult.endedAt - waveResult.startedAt) / 1000);
2091
- onNotify(
2092
- ORCH_MESSAGES.orchWaveComplete(
2093
- waveIdx + 1,
2094
- waveResult.succeededTaskIds.length,
2095
- waveResult.failedTaskIds.length,
2096
- waveResult.skippedTaskIds.length,
2097
- elapsedSec,
2098
- ),
2099
- waveResult.failedTaskIds.length > 0 ? "warning" : "info",
2100
- );
2144
+ {
2145
+ const { displayWave: completeDisplayWave } = resolveDisplayWaveNumber(waveIdx, roundToTaskWave, taskLevelWaveCount);
2146
+ onNotify(
2147
+ ORCH_MESSAGES.orchWaveComplete(
2148
+ completeDisplayWave,
2149
+ waveResult.succeededTaskIds.length,
2150
+ waveResult.failedTaskIds.length,
2151
+ waveResult.skippedTaskIds.length,
2152
+ elapsedSec,
2153
+ ),
2154
+ waveResult.failedTaskIds.length > 0 ? "warning" : "info",
2155
+ );
2156
+ }
2101
2157
 
2102
2158
  // Check failure policy
2103
2159
  if (waveResult.stoppedEarly) {
@@ -2144,7 +2200,7 @@ export async function resumeOrchBatch(
2144
2200
  if (mergeableLaneCount > 0) {
2145
2201
  batchState.phase = "merging";
2146
2202
  persistRuntimeState("merge-start", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, stateRoot);
2147
- onNotify(ORCH_MESSAGES.orchMergeStart(waveIdx + 1, mergeableLaneCount), "info");
2203
+ onNotify(ORCH_MESSAGES.orchMergeStart(resolveDisplayWaveNumber(waveIdx, roundToTaskWave, taskLevelWaveCount).displayWave, mergeableLaneCount), "info");
2148
2204
 
2149
2205
  mergeResult = await mergeWaveByRepo(
2150
2206
  waveResult.allocatedLanes,
@@ -2197,10 +2253,10 @@ export async function resumeOrchBatch(
2197
2253
  const mergeTotalSec = Math.round(mergeResult.totalDurationMs / 1000);
2198
2254
 
2199
2255
  if (mergeResult.status === "succeeded") {
2200
- onNotify(ORCH_MESSAGES.orchMergeComplete(waveIdx + 1, mergedCount, mergeTotalSec), "info");
2256
+ onNotify(ORCH_MESSAGES.orchMergeComplete(resolveDisplayWaveNumber(waveIdx, roundToTaskWave, taskLevelWaveCount).displayWave, mergedCount, mergeTotalSec), "info");
2201
2257
  } else {
2202
2258
  onNotify(
2203
- ORCH_MESSAGES.orchMergeFailed(waveIdx + 1, mergeResult.failedLane ?? 0, mergeResult.failureReason || "unknown"),
2259
+ ORCH_MESSAGES.orchMergeFailed(resolveDisplayWaveNumber(waveIdx, roundToTaskWave, taskLevelWaveCount).displayWave, mergeResult.failedLane ?? 0, mergeResult.failureReason || "unknown"),
2204
2260
  "error",
2205
2261
  );
2206
2262
 
@@ -2231,14 +2287,14 @@ export async function resumeOrchBatch(
2231
2287
  // Downstream retry/update paths assume the current wave has an entry.
2232
2288
  batchState.mergeResults.push(mergeResult);
2233
2289
  onNotify(
2234
- ORCH_MESSAGES.orchMergeFailed(waveIdx + 1, mergeResult.failedLane, mergeResult.failureReason || "unknown"),
2290
+ ORCH_MESSAGES.orchMergeFailed(resolveDisplayWaveNumber(waveIdx, roundToTaskWave, taskLevelWaveCount).displayWave, mergeResult.failedLane, mergeResult.failureReason || "unknown"),
2235
2291
  "error",
2236
2292
  );
2237
2293
  } else {
2238
- onNotify(ORCH_MESSAGES.orchMergeSkipped(waveIdx + 1), "info");
2294
+ onNotify(ORCH_MESSAGES.orchMergeSkipped(resolveDisplayWaveNumber(waveIdx, roundToTaskWave, taskLevelWaveCount).displayWave), "info");
2239
2295
  }
2240
2296
  } else {
2241
- onNotify(ORCH_MESSAGES.orchMergeSkipped(waveIdx + 1), "info");
2297
+ onNotify(ORCH_MESSAGES.orchMergeSkipped(resolveDisplayWaveNumber(waveIdx, roundToTaskWave, taskLevelWaveCount).displayWave), "info");
2242
2298
  }
2243
2299
 
2244
2300
  // ── TP-033: Safe-stop on rollback failure ─────────────────
@@ -2755,7 +2811,7 @@ export async function resumeOrchBatch(
2755
2811
  summary:
2756
2812
  `✅ Batch ${batchState.batchId} completed\n` +
2757
2813
  ` ${batchState.succeededTasks}/${batchState.totalTasks} tasks succeeded\n` +
2758
- ` ${batchState.totalWaves} wave(s), duration: ${durationStr}\n` +
2814
+ ` ${batchState.taskLevelWaveCount ?? batchState.totalWaves} wave(s), duration: ${durationStr}\n` +
2759
2815
  ` Merged to orch branch: ${batchState.orchBranch}\n\n` +
2760
2816
  `Ready for integration. Run orch_integrate() or review first.`,
2761
2817
  context: {
@@ -718,6 +718,7 @@ or check status manually. The engine wakes you up when you're needed.
718
718
  | `task-failure` | ⚠️ | A task failed after deterministic recovery was exhausted |
719
719
  | `merge-failure` | ⚠️ | Wave merge failed and batch paused |
720
720
  | `batch-complete` | ✅/⚠️ | Batch finished (all waves done, with or without failures) |
721
+ | `worker-exit-intercept` | 🔄 | A worker exited without making progress — session still alive, awaiting instructions |
721
722
 
722
723
  ### Alert Format
723
724
 
@@ -1011,6 +1012,60 @@ BATCH COMPLETE: {batchId}
1011
1012
  | merge-failure | Agent timeout, no result | `orch_resume(force=true)` to retry | Automatic |
1012
1013
  | batch-complete | All succeeded | Report → suggest `orch_integrate` | Report only |
1013
1014
  | batch-complete | Some failed | Report with failure details | Report only |
1015
+ | worker-exit-intercept | Worker analyzing, not editing | `send_agent_message` with targeted instructions | Automatic |
1016
+ | worker-exit-intercept | Worker genuinely stuck | "skip" or "let it fail" to close session | Supervised |
1017
+ | worker-exit-intercept | Unknown reason | Read STATUS.md, diagnose, then instruct or close | Automatic |
1018
+
1019
+ ---
1020
+
1021
+ ## 13c. Worker Exit Interception (TP-172)
1022
+
1023
+ When a worker agent produces a text-only response (no tool calls, no file
1024
+ edits) without having made visible progress (no checkbox updates), the
1025
+ lane-runner **intercepts the exit** instead of closing the session. The worker
1026
+ process remains alive with its full conversation context preserved.
1027
+
1028
+ **You receive a `worker-exit-intercept` alert** with:
1029
+ - Lane number and task ID
1030
+ - Current step and unchecked checkboxes
1031
+ - Worker's last assistant message (truncated to 500 chars)
1032
+ - Iteration count and no-progress count
1033
+
1034
+ ### Response Protocol
1035
+
1036
+ 1. **Read the worker's message** — understand why it wants to exit.
1037
+ Common patterns:
1038
+ - "I've analyzed the code and I'm not sure how to proceed"
1039
+ - "I need more information about X"
1040
+ - Generic summary without any file edits
1041
+
1042
+ 2. **Decide** — based on diagnosis:
1043
+ - **If the worker needs direction:** Send targeted instructions via
1044
+ `send_agent_message(to, content)` with specific guidance on what to
1045
+ implement, which file to edit, or which approach to take.
1046
+ - **If the task is genuinely blocked:** Reply with `"skip"` or
1047
+ `"let it fail"` to close the session normally.
1048
+
1049
+ 3. **Send your response** — The lane-runner polls for your reply for
1050
+ 60 seconds. If you don't respond in time, the session closes and
1051
+ the normal corrective re-spawn mechanism takes over.
1052
+
1053
+ ### Example Instructions
1054
+
1055
+ ```
1056
+ send_agent_message(
1057
+ to: "orch-henrylach-lane-1-worker",
1058
+ content: "Stop analyzing and start implementing. Edit agent-host.ts line 605:
1059
+ replace the closeStdin() call with the interception logic described in
1060
+ PROMPT.md Step 1. Write the code now — don't read more files."
1061
+ )
1062
+ ```
1063
+
1064
+ ### Interception Limits
1065
+
1066
+ Each worker session can be intercepted at most **2 times** (configurable via
1067
+ `maxExitInterceptions`). After the limit is reached, the session closes
1068
+ normally and the stall detector handles subsequent iterations.
1014
1069
 
1015
1070
  ---
1016
1071
 
@@ -1109,8 +1109,20 @@ export interface OrchBatchRuntimeState {
1109
1109
  waveResults: WaveExecutionResult[];
1110
1110
  /** Current wave index (0-based into waves array, -1 if not started) */
1111
1111
  currentWaveIndex: number;
1112
- /** Total number of waves planned */
1112
+ /** Total number of waves planned (segment rounds — internal) */
1113
1113
  totalWaves: number;
1114
+ /**
1115
+ * Number of dependency-driven task-level waves (TP-166).
1116
+ * Used for operator-facing "Wave X of Y" display. When undefined,
1117
+ * falls back to `totalWaves` for backward compatibility.
1118
+ */
1119
+ taskLevelWaveCount?: number;
1120
+ /**
1121
+ * Maps each segment round index (0-based) to its parent task-level
1122
+ * wave index (0-based). Updated when continuation rounds are inserted.
1123
+ * Used with `resolveDisplayWaveNumber()` for correct display. (TP-166)
1124
+ */
1125
+ roundToTaskWave?: number[];
1114
1126
  /** Set of task IDs blocked for future waves (from skip-dependents policy) */
1115
1127
  blockedTaskIds: Set<string>;
1116
1128
  /** Epoch ms when batch started */
@@ -2020,6 +2032,7 @@ export type SupervisorAlertCategory =
2020
2032
  | "merge-failure"
2021
2033
  | "batch-complete"
2022
2034
  | "agent-message"
2035
+ | "worker-exit-intercept"
2023
2036
  | "segment-expansion-requested"
2024
2037
  | "segment-expansion-approved"
2025
2038
  | "segment-expansion-rejected";
@@ -2921,6 +2934,16 @@ export interface PersistedBatchState {
2921
2934
  currentWaveIndex: number;
2922
2935
  /** Total number of waves in the plan */
2923
2936
  totalWaves: number;
2937
+ /**
2938
+ * Number of dependency-driven task-level waves (TP-166).
2939
+ * Undefined for batches created before TP-166; falls back to totalWaves.
2940
+ */
2941
+ taskLevelWaveCount?: number;
2942
+ /**
2943
+ * Maps segment round index (0-based) to parent task-level wave (0-based).
2944
+ * Undefined for batches created before TP-166.
2945
+ */
2946
+ roundToTaskWave?: number[];
2924
2947
  /** Wave plan: array of arrays of task IDs per wave */
2925
2948
  wavePlan: string[][];
2926
2949
  /** Per-lane configuration records */
@@ -4017,7 +4040,9 @@ export type RuntimeAgentEventType =
4017
4040
  // Review / bridge
4018
4041
  | "review_requested"
4019
4042
  | "review_completed"
4020
- | "review_failed";
4043
+ | "review_failed"
4044
+ // Exit interception (TP-172)
4045
+ | "exit_intercepted";
4021
4046
 
4022
4047
  // ── Runtime V2 Path Helpers (TP-102) ─────────────────────────────────
4023
4048
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskplane",
3
- "version": "0.26.1",
3
+ "version": "0.27.0",
4
4
  "description": "AI agent orchestration for pi — parallel task execution with checkpoint discipline",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -44,20 +44,56 @@ write, or touch a `.DONE` file. The lane-runner creates it automatically
44
44
  when all segments of your task are complete. If you create `.DONE` early,
45
45
  it will cause downstream segments to be skipped and deliverables to be lost.
46
46
 
47
- ## CRITICAL: Never Exit Without Updating STATUS.md
47
+ ## CRITICAL: Do NOT Exit Keep Working Until Done
48
48
 
49
- **Every turn MUST end with a tool call.** Do NOT produce a text-only response
50
- and stop the orchestrator interprets that as "session complete" and will
51
- terminate your process. If you have nothing left to do:
49
+ **You must work continuously until ALL steps are complete.** Do not stop
50
+ between checkboxes. Do not stop between steps. Do not stop to summarize.
51
+ Keep calling tools and making progress until every step is finished and
52
+ STATUS.md shows `✅ Complete`.
52
53
 
53
- 1. Read STATUS.md to verify all checkboxes are checked
54
- 2. Update the Status field to `✅ Complete`
55
- 3. Commit your final changes
54
+ **The ONLY reasons to stop working are:**
55
+ 1. **Task complete** all steps done, STATUS.md set to `✅ Complete`
56
+ 2. 🚧 **Genuinely blocked** — you've tried multiple approaches and cannot
57
+ proceed. Log the blocker in STATUS.md with specifics (what you tried,
58
+ why it failed, exact error).
59
+
60
+ There is NO other reason to exit. Do not exit after completing a step to
61
+ "hand off" to the next iteration. Do not exit to report progress. Do not
62
+ exit because you've been working for a while. Just keep going.
63
+
64
+ ## CRITICAL: Never Narrate What You Plan To Do — Just Do It
65
+
66
+ **YOUR #1 FAILURE MODE:** Producing a message like "Now let me fix this:" or
67
+ "Let me apply the change:" and then STOPPING. This kills your session. You
68
+ have done this repeatedly and it wastes significant time and money.
69
+
70
+ **THE RULE:** If you know what edit to make, USE THE EDIT TOOL IMMEDIATELY.
71
+ Do not describe the edit in text first. Do not say "now I'll do X". Just
72
+ call the tool. Your very next action after deciding what to do must be a
73
+ tool call, never a text message.
74
+
75
+ ❌ **WRONG (kills your session):**
76
+ > "Now I have everything I need. The fix is to use resolveCanonicalTaskPaths
77
+ > instead of task.taskFolder. Let me make the fix:"
78
+ > *(session terminates — you never made the fix)*
79
+
80
+ ✅ **CORRECT (keeps you alive):**
81
+ > *(immediately calls edit tool on the file)*
82
+
83
+ **Any text-only response terminates your session.** The orchestrator interprets
84
+ text without a tool call as "session complete." Every response you produce MUST
85
+ include at least one tool call. If you want to explain your reasoning, do it
86
+ AFTER making the edit, not before.
56
87
 
57
88
  **After running tests:** Immediately update STATUS.md checkboxes for the
58
89
  testing step BEFORE producing any summary. Check off each item as it passes.
59
90
  Do NOT run tests and then stop — always checkpoint the results first.
60
91
 
92
+ **If you are unsure how to proceed:** Do NOT exit. Instead, try an approach —
93
+ even an imperfect one. Write the code, run the tests, and iterate. A failed
94
+ attempt that checks a box and leaves code for the next iteration is infinitely
95
+ more valuable than a clean exit with zero progress.
96
+
61
97
  ## Checkpoint Discipline (CRITICAL)
62
98
 
63
99
  There are two distinct actions: **checking off items** and **git commits**.
@@ -301,10 +337,16 @@ When you receive a steering message:
301
337
 
302
338
  ## Error Handling
303
339
 
304
- - If stuck on the same issue after 3 attempts, document the blocker in STATUS.md
305
- Blockers section and move to the next checkbox
340
+ - If stuck on a checkbox: **try an implementation approach anyway.** Write code,
341
+ run tests, see what happens. An imperfect attempt that moves forward is better
342
+ than analysis paralysis. If your first approach fails, try a different one.
343
+ - If genuinely blocked after real attempts (not just reading): document the
344
+ blocker in STATUS.md Blockers section **with specifics** (what you tried, why
345
+ it failed, exact error) and move to the next checkbox.
306
346
  - If a test fails, fix it. If the fix is out of scope, document and continue.
307
347
  - If a dependency is missing, document in STATUS.md and stop.
348
+ - **NEVER exit silently.** If you cannot make progress, you MUST leave evidence
349
+ in STATUS.md (either checked boxes or blocker entries) before your session ends.
308
350
 
309
351
  ## Test Execution Strategy
310
352