taskplane 0.28.7 → 0.29.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.
@@ -4,6 +4,10 @@
4
4
  */
5
5
  import { join } from "path";
6
6
  import type { ExitClassification, TaskExitDiagnostic } from "./diagnostics.js";
7
+ // TP-189 (Cluster B): single source of truth for the worker user-tools
8
+ // default literal. The constants module is import-free so this does NOT
9
+ // create a cycle (types.ts -> tool-allowlist-constants.ts is a leaf).
10
+ import { DEFAULT_WORKER_USER_TOOLS } from "./tool-allowlist-constants.ts";
7
11
 
8
12
  // ── Types ────────────────────────────────────────────────────────────
9
13
 
@@ -390,11 +394,12 @@ export const DEFAULT_ORCHESTRATOR_CONFIG: OrchestratorConfig = {
390
394
  },
391
395
  merge: {
392
396
  model: "",
393
- // NOTE (TP-184): Mirrors `DEFAULT_WORKER_USER_TOOLS` in
394
- // `agent-host.ts`. Kept as a literal here because types.ts anchors
395
- // the module-import graph (agent-host.ts imports from types.ts), so
396
- // importing the constant the other direction would create a cycle.
397
- tools: "read,write,edit,bash,grep,find,ls",
397
+ // TP-189 (Cluster B): merge default sourced from the import-free
398
+ // `tool-allowlist-constants.ts` module. The previous concern about
399
+ // importing from `agent-host.ts` (which DOES depend on types.ts and
400
+ // would create a cycle) no longer applies because the constant
401
+ // lives in a leaf module that imports nothing.
402
+ tools: DEFAULT_WORKER_USER_TOOLS,
398
403
  thinking: "off",
399
404
  verify: [],
400
405
  order: "fewest-files-first",
@@ -2189,6 +2194,30 @@ export interface SupervisorAlert {
2189
2194
  */
2190
2195
  export type SupervisorAlertCallback = (alert: SupervisorAlert) => void;
2191
2196
 
2197
+ /**
2198
+ * Information about a lane that has just reached a terminal state.
2199
+ *
2200
+ * Emitted at the no-progress kill and hard-fail decision points so the
2201
+ * supervisor process can mark the lane as terminated and drop any further
2202
+ * alerts queued for it (see {@link LaneTerminatedCallback}).
2203
+ *
2204
+ * @since TP-187 (#538)
2205
+ */
2206
+ export interface LaneTerminatedInfo {
2207
+ laneNumber: number;
2208
+ agentId: string;
2209
+ batchId: string;
2210
+ terminatedAt: number;
2211
+ reason: "no-progress-kill" | "hard-fail" | "supervisor-takeover";
2212
+ }
2213
+
2214
+ /**
2215
+ * Callback invoked when a lane reaches a terminal state.
2216
+ *
2217
+ * @since TP-187 (#538)
2218
+ */
2219
+ export type LaneTerminatedCallback = (info: LaneTerminatedInfo) => void;
2220
+
2192
2221
  /**
2193
2222
  * Build a batch progress snapshot from runtime state.
2194
2223
  *
@@ -3,7 +3,7 @@
3
3
  * @module orch/worktree
4
4
  */
5
5
  import { existsSync, mkdirSync, readdirSync, realpathSync, rmdirSync, rmSync } from "fs";
6
- import { execSync } from "child_process";
6
+ import { execSync, execFileSync } from "child_process";
7
7
  import { join, basename, resolve } from "path";
8
8
 
9
9
  import { execLog } from "./execution.ts";
@@ -639,6 +639,57 @@ export function isRetriableRemoveError(stderr: string): boolean {
639
639
  return false;
640
640
  }
641
641
 
642
+ /**
643
+ * Detect Windows MAX_PATH ("Filename too long") errors from `git worktree remove`.
644
+ *
645
+ * On Windows with default `core.longpaths = false`, git refuses to delete
646
+ * paths that exceed MAX_PATH (260 characters). Deep `node_modules` trees
647
+ * commonly trip this. Native `cmd` `rd /s /q` uses a different deletion
648
+ * code path (NT object namespace, longer path tolerance) and usually
649
+ * succeeds where git fails.
650
+ *
651
+ * @param stderr - Error output from `git worktree remove`
652
+ * @returns true if the failure looks like the Windows MAX_PATH case
653
+ * @since TP-188 (#543)
654
+ */
655
+ export function isWindowsMaxPathError(stderr: string): boolean {
656
+ if (process.platform !== "win32") return false;
657
+ return /filename too long/i.test(stderr);
658
+ }
659
+
660
+ /**
661
+ * Run `cmd /c rd /s /q <path>` to recursively delete a directory on Windows.
662
+ *
663
+ * Used as a fallback after `git worktree remove` fails with the Windows
664
+ * MAX_PATH ("Filename too long") error. Caller must ensure platform is win32
665
+ * and the path is absolute. Path separators are normalized to backslashes
666
+ * because cmd's `rd` is more reliable with native Windows paths.
667
+ *
668
+ * @param absolutePath - Absolute path to remove (forward or back slashes accepted)
669
+ * @returns { ok, stdout, stderr }
670
+ * @since TP-188 (#543)
671
+ */
672
+ export function runWindowsCmdRd(
673
+ absolutePath: string,
674
+ ): { ok: boolean; stdout: string; stderr: string } {
675
+ const winPath = absolutePath.replace(/\//g, "\\");
676
+ try {
677
+ const stdout = execFileSync("cmd", ["/c", "rd", "/s", "/q", winPath], {
678
+ encoding: "utf-8",
679
+ timeout: 60_000,
680
+ stdio: ["pipe", "pipe", "pipe"],
681
+ }).toString().trim();
682
+ return { ok: true, stdout, stderr: "" };
683
+ } catch (err: unknown) {
684
+ const e = err as { stdout?: string; stderr?: string; message?: string };
685
+ return {
686
+ ok: false,
687
+ stdout: (e.stdout ?? "").toString().trim(),
688
+ stderr: (e.stderr ?? e.message ?? "unknown error").toString().trim(),
689
+ };
690
+ }
691
+ }
692
+
642
693
  /**
643
694
  * Remove a git worktree and clean up its associated branch.
644
695
  *
@@ -733,6 +784,50 @@ export function removeWorktree(
733
784
 
734
785
  lastError = removeResult.stderr;
735
786
 
787
+ // ── Windows MAX_PATH fallback (#543) ────────────────────────
788
+ // On Windows, `git worktree remove` fails with "Filename too long"
789
+ // when the worktree contains deep `node_modules` trees (most
790
+ // non-trivial Node projects) and `core.longpaths = false` (default).
791
+ // `cmd /c rd /s /q <path>` uses a different deletion code path
792
+ // that tolerates long paths better. Try it ONCE before classifying
793
+ // the error as terminal/retriable so other error classes still
794
+ // surface unchanged.
795
+ if (isWindowsMaxPathError(lastError)) {
796
+ execLog(
797
+ "cleanup",
798
+ "worktree",
799
+ `Windows MAX_PATH detected — falling back to cmd "rd /s /q"`,
800
+ { path: worktreePath, attempt },
801
+ );
802
+ const fallback = runWindowsCmdRd(worktreePath);
803
+ if (fallback.ok) {
804
+ execLog(
805
+ "cleanup",
806
+ "worktree",
807
+ `cmd "rd /s /q" fallback succeeded; pruning git worktree state`,
808
+ { path: worktreePath },
809
+ );
810
+ // The on-disk tree is gone; git's bookkeeping still has a
811
+ // stale entry. Prune so isRegisteredWorktree() returns false
812
+ // during post-removal verification below.
813
+ runGit(["worktree", "prune"], repoRoot);
814
+ break;
815
+ }
816
+ // Fallback also failed — enrich error so the operator sees both
817
+ // attempts, then fall through to the existing terminal/retry
818
+ // classification (which will throw because "Filename too long"
819
+ // is non-retriable per isRetriableRemoveError).
820
+ execLog(
821
+ "cleanup",
822
+ "worktree",
823
+ `cmd "rd /s /q" fallback failed`,
824
+ { path: worktreePath, error: fallback.stderr.slice(0, 200) },
825
+ );
826
+ lastError =
827
+ `git worktree remove failed: ${lastError}; ` +
828
+ `cmd rd /s /q fallback failed: ${fallback.stderr}`;
829
+ }
830
+
736
831
  // Check if error is terminal (non-retriable)
737
832
  if (!isRetriableRemoveError(lastError)) {
738
833
  throw new WorktreeError(
@@ -1806,7 +1901,11 @@ export function runPreflight(config: OrchestratorConfig, repoRoot?: string): Pre
1806
1901
  switch (piResult.errorKind) {
1807
1902
  case "not-found":
1808
1903
  message = "Pi not found on PATH";
1809
- hint = "Install Pi: npm install -g @mariozechner/pi-coding-agent";
1904
+ // Issue #560: Pi was renamed from @mariozechner to @earendil-works
1905
+ // in v0.74.0. Recommend the new scope for new installs; the legacy
1906
+ // scope still resolves at runtime via Pi's bundled aliasing if a
1907
+ // transitional install has it.
1908
+ hint = "Install Pi: npm install -g @earendil-works/pi-coding-agent (legacy: @mariozechner/pi-coding-agent)";
1810
1909
  break;
1811
1910
  case "timeout":
1812
1911
  message = `Pi did not respond within ${PI_PREFLIGHT_TIMEOUT_MS / 1000}s (retried once)`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskplane",
3
- "version": "0.28.7",
3
+ "version": "0.29.0",
4
4
  "description": "AI agent orchestration for pi — parallel task execution with checkpoint discipline",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -153,6 +153,43 @@ Individual steps can override the task-level review:
153
153
  > **Review override: code review** — This step touches authorization.
154
154
  ```
155
155
 
156
+ ### Per-Step Reviews vs. Consolidated Reviews (Checkpoint Markers)
157
+
158
+ A second axis sits alongside the Review Level: **how many** reviews fire
159
+ for a given level. PROMPT authors should make this choice deliberately.
160
+
161
+ **Default — per-step reviews:** at Review Level ≥ 1, the worker fires a
162
+ plan review BEFORE each implementation step and (at Level ≥ 2) a code
163
+ review AFTER. A 5-implementation-step Level 2 task therefore fires
164
+ ~5 plan + ~5 code = ~10 reviews. This is the right default for tasks
165
+ where each step is an independent piece of work (e.g., a multi-cluster
166
+ polish bundle, a multi-feature sprint).
167
+
168
+ **Opt-in — consolidated via checkpoint markers:** a PROMPT can include
169
+ `**Plan-review checkpoint**` or `**Code review checkpoint**` markers in
170
+ specific steps. The worker treats those markers as instructions to fire
171
+ the corresponding review at *that* step only, instead of per-step. A
172
+ single-deliverable task that decomposes into 1 design step + 3 mechanical
173
+ implementation steps + 1 verify-everything step might mark the design
174
+ step as the plan checkpoint and the verify step as the code checkpoint,
175
+ for a total of 2 reviews instead of ~8.
176
+
177
+ **When to use which:**
178
+
179
+ - **Per-step (default):** independent multi-feature work, polish bundles,
180
+ refactor sweeps. Per-cluster review feedback is more useful than a
181
+ consolidated review across unrelated changes.
182
+ - **Consolidated (checkpoint markers):** single-deliverable tasks where
183
+ the steps are mechanical applications of one design decision. TP-186
184
+ (the `review_step` death-spiral fix) is a real example: 1 prompt-design
185
+ deliverable + 3 mechanical implementation steps + 1 code-review-
186
+ everything step → 2 reviews total instead of ~8.
187
+
188
+ **How to choose at PROMPT-authoring time:** ask "would the reviewer
189
+ benefit from seeing each step in isolation, or only the whole picture?"
190
+ If each step touches a different concern, per-step. If every step is
191
+ the same change applied to a different file, consolidate.
192
+
156
193
  ---
157
194
 
158
195
  ## Task Sizing
@@ -143,6 +143,15 @@ You can invoke these tools directly — no need to ask the operator or use slash
143
143
  - **orch_pause()** — Pause the running batch (current tasks finish, no new tasks start)
144
144
  - **orch_resume(force?)** — Resume a paused or interrupted batch. Use `force=true` for stuck batches.
145
145
  - **orch_abort(hard?)** — Abort the running batch. Use `hard=true` for immediate kill.
146
+ - **supervisor_takeover(reason)** — **Non-destructive escape hatch.** Pause the
147
+ wave, drain all per-agent on-disk outboxes, and suppress in-transit zombie
148
+ alerts from already-running lanes. Worktrees, branches, batch state, and
149
+ sessions are preserved. Distinct from `orch_abort`, which kills sessions and
150
+ deletes state. Use this when the batch is producing alert spam or has hit a
151
+ death-spiral pattern but you may still want to resume the same batch later.
152
+ After takeover, call `orch_status()` to inspect, then either
153
+ `orch_resume(force=true)` to continue (alert suppression is lifted
154
+ automatically) or `orch_abort()` to escalate to destructive shutdown.
146
155
  - **orch_integrate(mode?, force?, branch?)** — Integrate completed batch into working branch.
147
156
  Modes: `"fast-forward"` (default), `"merge"`, `"pr"`.
148
157
 
@@ -153,12 +162,64 @@ Use tools **proactively** when the situation calls for it:
153
162
  - Operator asks "how's it going?" → call `orch_status()` first, then summarize
154
163
  - Batch paused due to a failure you diagnosed and fixed → call `orch_resume()`
155
164
  - Batch completed successfully → offer to call `orch_integrate()` (fast-forward is default and cleanest; use `mode="merge"` if diverged, `mode="pr"` only if remotes exist and branch is protected)
156
- - Batch is stuck or failing repeatedly → call `orch_status()` to diagnose, then `orch_abort()` if needed
165
+ - Batch is stuck, producing alert spam, or hitting a death-spiral → call `orch_status()` to diagnose, then **prefer `supervisor_takeover(reason)`** to park the batch non-destructively (worktrees + state preserved; resume with `orch_resume(force=true)` afterward). Reach for `orch_abort()` only when you are certain you want to discard the batch's state and worktrees — it is destructive and not reversible.
157
166
  - Need to investigate before more tasks launch → call `orch_pause()` first
158
167
 
159
168
  These tools are preferred over reading batch-state.json directly because they handle
160
169
  disk fallback, in-memory state, and all edge cases automatically.
161
170
 
171
+ ## Worker exit-intercept replies (text-reply parser semantics)
172
+
173
+ When a worker lane is about to exit without making progress, the lane-runner
174
+ fires an alert (`worker-exit-intercept`) and waits up to **60 seconds** for
175
+ you to reply via the worker's mailbox inbox (e.g., via `send_agent_message`).
176
+
177
+ Replies fall into two categories. The lane-runner classifies them by **shape**,
178
+ not by intent:
179
+
180
+ ### Close directives
181
+
182
+ These close the worker session without re-prompting. They MUST be:
183
+
184
+ 1. **Short** — the entire reply is **under 30 characters**, AND
185
+ 2. **Either an exact match for a close keyword OR a close keyword followed by
186
+ `:`, ` ` (space), `.`, or ` -` (space-dash).**
187
+
188
+ Close keywords: `skip`, `let it fail`, `close`, `abort`, `stop`.
189
+
190
+ Examples that close the session:
191
+
192
+ - `skip`
193
+ - `let it fail`
194
+ - `stop.`
195
+ - `skip - blocker logged`
196
+
197
+ Examples that do NOT close the session (treated as instructional re-prompts):
198
+
199
+ - `Stop trying that approach — use the alternate path described in CONTEXT.md`
200
+ (longer than 30 chars, so it is a re-prompt, not a stop directive)
201
+ - `let it fail because the dependency is missing` (longer than 30 chars)
202
+ - `Skip the file-system check and proceed with the in-memory test` (longer
203
+ than 30 chars)
204
+
205
+ ### Instructional replies
206
+
207
+ Anything that is not a close directive is treated as a re-prompt: the worker
208
+ resumes with your reply text as additional instructions for the next iteration.
209
+ This is the right shape for steering messages.
210
+
211
+ ### Practical rule of thumb
212
+
213
+ - To **close** a stuck lane: send a one-word reply (`skip`, `stop`, `abort`).
214
+ - To **steer** a stuck lane: send a multi-sentence message with concrete
215
+ instructions. Do NOT prefix instructions with one of the close keywords —
216
+ if your message starts with `stop` or `abort` and is short, the worker will
217
+ exit instead of taking your instructions.
218
+
219
+ Replies that arrive after the 60-second timeout are ignored; the lane proceeds
220
+ with its corrective re-spawn behavior. After three iterations without progress
221
+ the lane is killed regardless of replies.
222
+
162
223
  ## Startup Checklist
163
224
 
164
225
  Now that you've activated:
@@ -50,6 +50,74 @@ You handle a single review request and then exit.
50
50
  Do NOT just respond with text — the orchestrator reads the OUTPUT FILE to get
51
51
  your verdict. If you don't write the file, your review is lost.
52
52
 
53
+ ## Quality-check verification (code reviews only)
54
+
55
+ **This section applies to code reviews only.** For plan reviews, skip this
56
+ section entirely — there is no code to type-check or lint yet.
57
+
58
+ Before returning a code-review verdict, run the project's declared
59
+ typecheck / lint / format-check commands against the post-change tree. A
60
+ behavioural-correctness APPROVE is **invalidated** by failing quality checks.
61
+
62
+ The reviewer's tool allowlist already includes `bash`, so you can invoke these
63
+ commands directly — no special tooling is required.
64
+
65
+ ### How to discover the commands
66
+
67
+ 1. **Project config first.** Read `.pi/taskplane-config.json` (or the legacy
68
+ `.pi/task-runner.yaml` / `.pi/task-runner.json` fallbacks) and look at
69
+ `taskRunner.testing.commands` — a `Record<string, string>` mapping a
70
+ command name (e.g. `typecheck`, `lint`, `format:check`) to a
71
+ shell command. Run any command whose key matches one of
72
+ `typecheck` / `tsc` / `types` / `lint` / `format:check`.
73
+ **Prefer `format:check` over `format`** — the latter typically rewrites
74
+ files in place, which would mutate the working tree the reviewer is
75
+ evaluating. If only a mutating `format` script is available in either
76
+ source, skip it and note this in the Summary; do not run mutating
77
+ commands from the reviewer.
78
+ 2. **Fallback to `package.json` scripts.** If step 1 did not yield any
79
+ relevant commands — either because `taskRunner.testing.commands` is
80
+ absent OR because it exists but contains no keys matching the
81
+ typecheck/lint/format-check set — read `package.json` and run any of
82
+ these scripts that exist, in this order:
83
+ `npm run typecheck`, `npm run lint`, `npm run format:check`.
84
+ Skip a script if `package.json#scripts` does not declare it — do not
85
+ invent commands.
86
+ 3. **Skip silently** if neither source yields a relevant command. Do not fail
87
+ the review just because the project has no quality-check pipeline
88
+ configured. Note this in the Summary so the operator knows quality checks
89
+ were not exercised.
90
+
91
+ Do NOT run the project's full test suite from this section — that is the
92
+ worker's Testing & Verification step. The quality checks here are
93
+ **fast static checks** (typecheck, lint, format) that are cheap to run and
94
+ high-signal for catching regressions the behavioural diff review would miss.
95
+
96
+ ### What to do with the results
97
+
98
+ - **All quality checks pass** → proceed to behavioural code review as normal.
99
+ - **A quality check fails** → surface each failing command as an entry in
100
+ **Issues Found** with severity `important`. Include:
101
+ - The command that failed (e.g. `npm run typecheck`)
102
+ - The first few lines of the failing output (file/line locations are
103
+ most useful)
104
+ - A concrete suggested fix where the failure makes one obvious
105
+ - **Verdict downgrade rule:** If quality checks fail, the verdict is
106
+ **REVISE** — even if the behavioural code review would otherwise have
107
+ been APPROVE. Quality-check failures are blocking by definition: they
108
+ would surface at the worker's Testing & Verification step and force a
109
+ redo of the entire review cycle, so it is strictly cheaper to surface
110
+ them here.
111
+
112
+ ### Worked example (Issues Found entry)
113
+
114
+ ```
115
+ 1. **[npm run typecheck:1] [important]** — 5 strict-mode errors in
116
+ tests/foo.test.ts. Sample: "Argument of type 'undefined' is not
117
+ assignable to parameter of type 'string'" at line 42. Fix: narrow
118
+ `getThing()` return type or assert non-null at call site.
119
+ ```
120
+
53
121
  ## Verdict Criteria
54
122
 
55
123
  - **APPROVE** — Step will achieve its stated outcomes. Minor suggestions belong
@@ -33,9 +33,22 @@ visibility into your progress. If you batch updates, the dashboard shows
33
33
  3. **Hydrate if needed** (see STATUS.md Hydration below)
34
34
  4. Within that step, find the **first unchecked checkbox** (`- [ ]`)
35
35
  5. Resume from there — do NOT redo checked items (`- [x]`)
36
- 6. When a step's items are all checked, proceed to the next incomplete step
37
- 7. If all steps are complete, update STATUS.md **Status** field to `✅ Complete`
38
- and **Current Step** to the last step name this is your final action
36
+ 6. When a step's checkbox items are all checked, the next move depends on
37
+ the task's Review Level:
38
+ - **Review Level 0 or 1** (no code review): the step is done. Commit
39
+ the implementation and proceed to the next incomplete step.
40
+ - **Review Level 2 or 3** (code review required): the step is NOT
41
+ done yet. Commit the implementation, call
42
+ `review_step(step=N, type="code")`, and only flip the step's
43
+ `**Status:**` heading to `✅ Complete` AFTER the reviewer returns
44
+ APPROVE. See **Order of Operations for steps with code review**
45
+ below for the full sequence and the recovery recipe if the order
46
+ gets violated.
47
+ 7. If all steps are complete, update the top-of-file STATUS.md **Status**
48
+ field to `✅ Complete` and **Current Step** to the last step name —
49
+ this is your final action. (The top-of-file Status is the task-level
50
+ field; per-step `**Status:** ✅ Complete` headings are governed by
51
+ the Order of Operations rule.)
39
52
 
40
53
  ## CRITICAL: Do NOT Create .DONE Files
41
54
 
@@ -61,6 +74,29 @@ There is NO other reason to exit. Do not exit after completing a step to
61
74
  "hand off" to the next iteration. Do not exit to report progress. Do not
62
75
  exit because you've been working for a while. Just keep going.
63
76
 
77
+ ### ⚠️ MANDATORY: If you DO exit-with-no-progress, state the reason
78
+
79
+ If you genuinely must exit an iteration without checking any new boxes (no
80
+ blocker logged, no soft progress), the lane-runner will intercept and ask
81
+ the supervisor for guidance. The alert sent to the supervisor includes a
82
+ `Worker said:` field populated from your most recent assistant message.
83
+
84
+ **You MUST emit a one-sentence assistant message stating the specific reason
85
+ before exiting.** Examples of acceptable reasons:
86
+
87
+ - "Stuck on TS error in lane-runner.ts:691 — emitAlert types mismatched, need
88
+ to check SupervisorAlertContext shape."
89
+ - "Tests for the new helper need fixtures that don't exist; cannot proceed
90
+ without the supervisor pointing me at the right pattern."
91
+ - "The reviewer's REVISE feedback contradicts the TP-187 design; need
92
+ clarification on whether wave-plan reconstruction is in scope."
93
+
94
+ Empty/silent exits are still intercepted, but the supervisor sees `Worker
95
+ said: ""` (or a fallback to your most-recent visible assistant message)
96
+ which is much harder to act on. Always articulate the blocker before
97
+ exiting — it is the difference between getting useful steering and burning
98
+ an iteration on a generic re-prompt.
99
+
64
100
  ## CRITICAL: Never Narrate What You Plan To Do — Just Do It
65
101
 
66
102
  **YOUR #1 FAILURE MODE:** Producing a message like "Now let me fix this:" or
@@ -126,14 +162,30 @@ orchestrator and you will be re-spawned to do it again.
126
162
  ### Git commits (after completing a STEP)
127
163
 
128
164
  Git commits happen at **step boundaries**, not after every checkbox. When all
129
- checkboxes in a step are checked off:
165
+ checkboxes in a step are checked off, commit the implementation:
130
166
 
131
167
  ```bash
132
- git add -A && git commit -m "feat(TASK-ID): complete Step N — description"
168
+ git add -A && git commit -m "feat(TASK-ID): step N implementation"
133
169
  ```
134
170
 
171
+ For **Review Level 0 or 1** tasks, this commit completes the step — the next
172
+ thing you do is move to step N+1.
173
+
174
+ For **Review Level 2 or 3** tasks, this commit is the *implementation* commit;
175
+ the step is not done yet. After committing, call `review_step(type="code")`,
176
+ then — once the reviewer returns APPROVE — flip the step's `**Status:**`
177
+ heading to `✅ Complete` and commit that status update separately:
178
+
179
+ ```bash
180
+ git commit -am "chore(TASK-ID): step N complete (code review APPROVE)"
181
+ ```
182
+
183
+ See **Order of Operations for steps with code review** below for the full
184
+ sequence and the recovery recipe if the order is violated.
185
+
135
186
  This keeps the git history meaningful — one coherent commit per step instead of
136
- dozens of micro-commits that nobody reads.
187
+ dozens of micro-commits that nobody reads, with an explicit review-gating
188
+ commit when applicable.
137
189
 
138
190
  **Exceptions** — commit immediately (before step completion) in these cases:
139
191
  - **Hydration:** After expanding STATUS.md with new checkboxes, commit before