Skip to content
6 changes: 5 additions & 1 deletion .agents/skills/map-efficient/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ final verifier unless an explicit subagent dispatch is available and useful.

Use [efficient-reference.md](efficient-reference.md) for wave details, retry
recipes, TDD mode, commit policy, and troubleshooting. Read only the referenced
section when the workflow below points to it.
section when the workflow below points to it. Under `isolation_active` (Slice 5a),
the wave-loop creates per-member worktrees, dispatches actor subagents
**sequentially** (one per turn), verifies via `concurrency_ready`, then accepts
atomically via `merge_wave_worktrees`; concurrent fan-out is Slice 5b
(`dispatch_mode==concurrent`).

## Mutation Boundary Constraints

Expand Down
55 changes: 33 additions & 22 deletions .agents/skills/map-efficient/efficient-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,9 @@ wave-loop on every run. The wave-loop engages **only when ALL THREE hold**
**Defaults (canonical MapConfig):** `execution.wave_mode=auto`,
`worktree.isolation=off`. The isolation gate fails by default, so a stock
`mapify init` config always runs the legacy sequential walker. Even when the
wave-loop engages, dispatch stays sequential until concurrency ships (Slice 5+).
wave-loop engages, dispatch remains **sequential** in Slice 5a (`isolation_active=True`,
`dispatch_mode` from `get_wave_step` keyed to `sequential`); concurrent fan-out
is Slice 5b (`dispatch_mode==concurrent`, `concurrency_enabled=True`, not yet shipped).

### Sequential walker

Expand All @@ -136,10 +138,16 @@ Use wave APIs only when the blueprint has multiple ready subtasks whose writes
are low-risk and disjoint, or when the user explicitly requests parallel
execution.

When `worktree.isolation` is enabled and a wave runs in parallel (≥2 disjoint
subtasks), give each subtask its own worktree and accept the whole wave
atomically after all pass Monitor — never merge them one at a time (the first
merge advances HEAD and the next trips `BASE_DIVERGED`):
When `worktree.isolation` is enabled and a wave has ≥2 disjoint subtasks
(`isolation_active=True`), execute the **Slice 5a sequential worktree flow**:

1. **Create** a worktree per wave member via `create_subtask_worktree`.
2. **Dispatch actor subagents sequentially** — one per turn (`HC-3`), each
pinned to its own worktree path. Do NOT dispatch all in one turn (that is
Slice 5b / `dispatch_mode==concurrent`).
3. **Verify** all member worktrees with `concurrency_ready` before merge.
4. **Accept atomically** — never merge one at a time (the first merge advances
HEAD and the next trips `BASE_DIVERGED`):

```bash
python3 .map/scripts/map_step_runner.py merge_wave_worktrees "$ST_A" "$ST_B"
Expand All @@ -150,23 +158,24 @@ to base on any conflict or gate failure (worktrees kept for retry). On a single
subtask's Monitor failure, `discard_subtask_worktree` that subtask and retry it
before calling `merge_wave_worktrees`.

### Concurrent Actor dispatch — GATED EXAMPLE
### Concurrent actor dispatch — **Slice 5b only** (`dispatch_mode == 'concurrent'`) — GATED EXAMPLE

> **IMPORTANT — read before using this example.**
> Concurrent fan-out (dispatching multiple actor subagents in a single turn) is
> enabled **only when concurrency is shipped: Slice 5+ / `concurrency_enabled:
> true` / `parallel_ready` flag set**. In the **current framework**
> `concurrency_enabled` is **False**, so dispatch stays **SEQUENTIAL even when a
> wave has `mode=="parallel"`**. The example below is reference material for when
> that capability ships; do NOT treat it as an active instruction now. Use your
> Codex runtime's own parallel actor-subagent dispatch mechanism — this is the
> provider-neutral shape, not a literal API call.

When concurrency is enabled (Slice 5+ only), a parallel wave with N subtasks
dispatches all N actor subagents in **one turn**:
> **Slice 5b** and is enabled **only when `concurrency_enabled: true` /
> `parallel_ready` flag set / `dispatch_mode == 'concurrent'`**. In the **current
> framework** (`concurrency_enabled=False`, Slice 5a), dispatch stays **SEQUENTIAL
> even when a wave has `mode=="parallel"`** — one actor subagent per turn, each
> pinned to its own worktree. The example below is reference material for when
> Slice 5b ships; do NOT treat it as an active instruction now. Use your runtime's
> own parallel actor-subagent dispatch mechanism — this is the provider-neutral
> shape, not a literal API call.

When Slice 5b concurrency is enabled, a parallel wave with N subtasks dispatches
all N actor subagents in **one turn**:

```text
# CORRECT (Slice 5+ / concurrency_enabled=True only) — one turn, N actor subagents:
# CORRECT (Slice 5b / concurrency_enabled=True / dispatch_mode=='concurrent' only) — one turn, N actor subagents:
dispatch actor subagent -> ST-003 (pinned to its own worktree)
dispatch actor subagent -> ST-004 (pinned to its own worktree)

Expand All @@ -180,12 +189,14 @@ dispatch actor subagent -> ST-004 (pinned to its own worktree)
**`max_actors` cap:** Default 4–8 per wave. Groups larger than `max_actors` are
pre-split into sequential batches before dispatch.

### Anti-patterns
### Anti-patterns — Slice 5b concurrent dispatch only

> These apply **only** under Slice 5b concurrent dispatch (`dispatch_mode == 'concurrent'`). In Slice 5a and the default sequential walker, one actor dispatch per turn **is** the correct behavior.

- One actor dispatch per turn across N turns — serial loop, no concurrency.
- Writing between dispatches (TodoWrite, etc.) — serializes the batch.
- Waiting for one actor result before dispatching the next.
- Mixing `get_next_step` and `get_wave_step` for the same wave.
- One actor dispatch per turn across N turns — serial loop, no concurrency. (Slice 5b only — expected, correct behavior in 5a.)
- Writing between dispatches (TodoWrite, etc.) — serializes the batch. (Slice 5b only.)
- Waiting for one actor result before dispatching the next. (Correct for 5a, wrong for 5b.)
- Mixing `get_next_step` and `get_wave_step` for the same wave. (Applies to both 5a and 5b.)

## TDD Mode

Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/map-efficient/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ else
fi
```

**Execution strategy:** `select_execution_strategy` chooses between the legacy sequential walker and the wave-loop. The wave-loop (`get_wave_step` / `validate_wave_step` / `advance_wave`) engages only when `execution.wave_mode ∈ {on, auto}` AND a color group has ≥2 members; otherwise `get_next_step` (sequential walker) runs. See [efficient-reference.md](efficient-reference.md#wave-execution) for the decision table and full wave loop.
**Execution strategy:** `select_execution_strategy` chooses between the legacy sequential walker and the wave-loop. The wave-loop (`get_wave_step` / `validate_wave_step` / `advance_wave`) engages only when `execution.wave_mode ∈ {on, auto}` AND a color group has ≥2 members; otherwise `get_next_step` (sequential walker) runs. Under `isolation_active` (Slice 5a), the wave-loop creates per-member worktrees, dispatches Actors **sequentially** (one per turn, `HC-3`), verifies via `concurrency_ready`, then accepts atomically via `merge_wave_worktrees`; concurrent fan-out is Slice 5b (`dispatch_mode==concurrent`). See [efficient-reference.md](efficient-reference.md#wave-execution) for the decision table and full wave loop.

**Note on resume:** `resume_from_plan` (Step 0) now auto-invokes `set_waves`
when `blueprint.json` is present, so resumed workflows do not need a manual
Expand Down
53 changes: 32 additions & 21 deletions .claude/skills/map-efficient/efficient-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ including clean passes — must carry concrete evidence references.
| `auto` / `on` | `auto` / `required` | no (all groups size 1) | Legacy sequential walker (`get_next_step`) |
| `auto` / `on` | `auto` / `required` | yes | Wave-loop (`get_wave_step` / `validate_wave_step` / `advance_wave`) |

**Defaults (canonical MapConfig):** `execution.wave_mode=auto`, `worktree.isolation=off`. Because the isolation gate (#2) fails by default, a stock `mapify init` config always runs the legacy sequential walker — byte-identical to pre-Slice-3. Even when the wave-loop does engage, dispatch stays sequential until concurrency ships (Slice 5+, `concurrency_enabled=False`).
**Defaults (canonical MapConfig):** `execution.wave_mode=auto`, `worktree.isolation=off`. Because the isolation gate (#2) fails by default, a stock `mapify init` config always runs the legacy sequential walker — byte-identical to pre-Slice-3. Even when the wave-loop does engage, dispatch remains **sequential** in Slice 5a (`isolation_active=True`, `dispatch_mode` from `get_wave_step` keyed to `sequential`); concurrent fan-out is Slice 5b (`dispatch_mode==concurrent`, `concurrency_enabled=True`, not yet shipped).

### Sequential walker

Expand All @@ -146,22 +146,23 @@ Use `get_next_step` for all sequential (default) execution. One phase at a time,

Use `get_wave_step`, `validate_wave_step`, and `advance_wave` when the wave-loop is active. Do not mix wave APIs with the sequential `get_next_step` cursor for the same wave unless the orchestrator response explicitly tells you to fall back.

Parallel execution is allowed only when a wave has satisfied dependencies, low risk, and disjoint new-file writes, or when the user explicitly requests it. When `worktree.isolation` is on and a wave runs in parallel, each subtask gets its own worktree and the wave is accepted atomically via `merge_wave_worktrees` — see [Parallel waves](#worktree-isolation) under Worktree isolation.
When the wave-loop engages AND `isolation_active` is true (`worktree.isolation` ∈ {`auto`, `required`}), the Slice 5a flow applies: (a) create a worktree per wave member via `create_subtask_worktree`; (b) dispatch the member Actors **sequentially** — one per turn, each pinned to its own worktree path (`HC-3`); (c) call `concurrency_ready` (ST-003) to verify all member worktrees before merge; (d) accept the whole wave atomically via `merge_wave_worktrees` — never one-at-a-time, with whole-wave rollback on any failure. See [Parallel waves](#worktree-isolation) under Worktree isolation for the full protocol. Concurrent fan-out (dispatching all Actors in one message) is Slice 5b (`dispatch_mode==concurrent`) and is not yet active.

### Concurrent Actor dispatch — GATED EXAMPLE
### Concurrent Actor dispatch — **Slice 5b only** (`dispatch_mode == 'concurrent'`) — GATED EXAMPLE

> **IMPORTANT — read before using this example.**
> Concurrent fan-out (emitting multiple `Task(actor)` calls in a single message) is
> enabled **only when concurrency is shipped: Slice 5+ / `concurrency_enabled: true` /
> `parallel_ready` flag set**. In the **current framework** `concurrency_enabled` is
> **False**, so dispatch stays **SEQUENTIAL even when a wave has `mode=="parallel"`**.
> The example below is reference material for when that capability ships; do NOT
> treat it as an active instruction now.
> **Slice 5b** and is enabled **only when `concurrency_enabled: true` /
> `parallel_ready` flag set / `dispatch_mode == 'concurrent'`**. In the **current
> framework** (`concurrency_enabled=False`, Slice 5a), dispatch stays **SEQUENTIAL
> even when a wave has `mode=="parallel"`** — one Actor per turn, each pinned to
> its own worktree. The example below is reference material for when Slice 5b ships;
> do NOT treat it as an active instruction now.

When concurrency is enabled (Slice 5+ only), a parallel wave with N subtasks dispatches all N Actors in **one message** with N `Task` calls — not one per turn:
When Slice 5b concurrency is enabled, a parallel wave with N subtasks dispatches all N Actors in **one message** with N `Task` calls — not one per turn:

```text
# CORRECT (Slice 5+ / concurrency_enabled=True only) — N Task calls in one message:
# CORRECT (Slice 5b / concurrency_enabled=True / dispatch_mode=='concurrent' only) — N Task calls in one message:
Task(
subagent_type="actor",
description="Implement ST-003",
Expand All @@ -184,12 +185,14 @@ Task(

**`max_actors` cap:** Default 4–8 concurrent actors per wave. Groups larger than `max_actors` are pre-split into sequential batches of `max_actors` before dispatch; do not emit more than `max_actors` Task calls in a single message.

### Anti-patterns (wave execution)
### Anti-patterns — Slice 5b concurrent dispatch only

- **One Task per turn across N turns** — serial actor loop that happens to use wave state; does not achieve concurrency.
- **TodoWrite between actor dispatches** — a TodoWrite call between Task calls serializes the batch; emit all Task calls in one message.
- **Waiting for one actor result before dispatching the next** — correct for sequential, wrong for concurrent waves.
- **Mixing `get_next_step` and `get_wave_step` for the same wave** — corrupts the state-machine cursor.
> These apply **only** under Slice 5b concurrent dispatch (`dispatch_mode == 'concurrent'`). In Slice 5a and the default sequential walker, one Task per turn **is** the correct behavior — the first three below are NOT anti-patterns there.

- **One Task per turn across N turns** — serial actor loop that happens to use wave state; does not achieve concurrency. (Slice 5b only — this is the expected, correct behavior in 5a.)
- **TodoWrite between actor dispatches** — a TodoWrite call between Task calls serializes the batch; emit all Task calls in one message. (Slice 5b only.)
- **Waiting for one actor result before dispatching the next** — correct for sequential dispatch (5a), wrong for concurrent waves (5b).
- **Mixing `get_next_step` and `get_wave_step` for the same wave** — corrupts the state-machine cursor. (Applies to both 5a and 5b.)

### Actor-boundary prompt template (worktree-isolated subtasks)

Expand Down Expand Up @@ -653,12 +656,20 @@ with `worktree_isolation_status`.
### Parallel waves (≥2 worktree-isolated subtasks) — #284 Phase 2

When `get_wave_step` returns `mode:"parallel"` (a wave with ≥2 disjoint-file
subtasks) AND isolation is enabled, give EACH subtask its own worktree and
dispatch the Actors concurrently (separate Task agents, each pinned to its own
`$WT_PATH`). Do NOT merge them one at a time: every worktree was cut off the same
HEAD, so the first `merge_subtask_worktree` advances the working branch and the
next trips `BASE_DIVERGED`. Accept the whole wave atomically instead — only after
EVERY subtask in the wave has passed Monitor (+ Evaluator):
subtasks) AND `isolation_active` is true, execute the **Slice 5a sequential
worktree flow**:

1. **Create** a worktree per wave member: `create_subtask_worktree` for each.
2. **Dispatch Actors sequentially** — one per turn (`HC-3`), each pinned to its
own `$WT_PATH`. Do NOT dispatch all in one message (that is Slice 5b).
3. **Verify** all member worktrees with `concurrency_ready` (ST-003) before merge.
4. **Accept atomically** via `merge_wave_worktrees` after every subtask passes
Monitor (+ Evaluator) — never merge one at a time.

Do NOT merge them one at a time: every worktree was cut off the same HEAD, so
the first `merge_subtask_worktree` advances the working branch and the next trips
`BASE_DIVERGED`. Accept the whole wave atomically instead — only after EVERY
subtask in the wave has passed Monitor (+ Evaluator):
Comment on lines +665 to +672

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Drop the Evaluator gate from this Slice 5a merge recipe.

map-efficient documents Monitor as the terminal validator in normal execution, so requiring Monitor (+ Evaluator) here contradicts the skill’s own phase model and blocks the wave-accept path on a phase operators never run.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/skills/map-efficient/efficient-reference.md around lines 665 - 672,
Remove the Evaluator requirement from the Slice 5a merge recipe so it matches
the skill’s phase model. Update the wording around the merge instructions in the
affected section to require only Monitor before calling merge_wave_worktrees,
and keep the guidance consistent with concurrency_ready, merge_subtask_worktree,
and merge_wave_worktrees without mentioning Evaluator in the normal path.


```bash
python3 .map/scripts/map_step_runner.py merge_wave_worktrees "$ST_A" "$ST_B" "$ST_C"
Expand Down
41 changes: 38 additions & 3 deletions .map/scripts/map_orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,11 @@ def _extract_subtask_ids_from_plan_artifacts(
# concurrent Task dispatch is actually implemented and safe to enable.
WAVE_CONCURRENCY_ENABLED = False

# Stable reason codes for get_wave_step return sites (ST-002).
WAVE_REASON_NO_WAVES = "no_waves"
WAVE_REASON_WAVE_COMPLETE = "wave_complete"
WAVE_REASON_DISPATCH_SEQUENTIAL = "dispatch_sequential_5a"


def _read_map_config_scalars(project_dir: Path) -> dict[str, str]:
"""Read top-level scalar values from .map/config.yaml without dependencies."""
Expand Down Expand Up @@ -2298,13 +2303,26 @@ def get_wave_step(branch: str) -> dict:
state_file = Path(f".map/{branch}/step_state.json")
state = StepState.load(state_file)

# Compute structured dispatch signal fields (ST-002).
dispatch_mode = "concurrent" if WAVE_CONCURRENCY_ENABLED else "sequential"
try:
from map_step_runner import ( # pyright: ignore[reportMissingImports]
_worktree_isolation_mode,
)
isolation_active = _worktree_isolation_mode(Path(".")) != "off"
except ImportError:
isolation_active = False

if not state.execution_waves:
return {
"mode": "sequential",
"wave_index": 0,
"subtasks": [],
"is_complete": True,
"concurrency_enabled": WAVE_CONCURRENCY_ENABLED,
"concurrency_enabled": dispatch_mode == "concurrent",
"dispatch_mode": dispatch_mode,
"isolation_active": isolation_active,
"reason": WAVE_REASON_NO_WAVES,
"message": "No execution waves configured. Use sequential mode.",
}

Expand All @@ -2314,7 +2332,10 @@ def get_wave_step(branch: str) -> dict:
"wave_index": state.current_wave_index,
"subtasks": [],
"is_complete": True,
"concurrency_enabled": WAVE_CONCURRENCY_ENABLED,
"concurrency_enabled": dispatch_mode == "concurrent",
"dispatch_mode": dispatch_mode,
"isolation_active": isolation_active,
"reason": WAVE_REASON_WAVE_COMPLETE,
}

wave = state.execution_waves[state.current_wave_index]
Expand Down Expand Up @@ -2353,7 +2374,10 @@ def get_wave_step(branch: str) -> dict:
"is_complete": False,
# concurrency_enabled=False: even when mode=="parallel" (width>=2 wave),
# dispatch is strictly sequential this slice. Slice 5 flips WAVE_CONCURRENCY_ENABLED.
"concurrency_enabled": WAVE_CONCURRENCY_ENABLED,
"concurrency_enabled": dispatch_mode == "concurrent",
"dispatch_mode": dispatch_mode,
"isolation_active": isolation_active,
"reason": WAVE_REASON_DISPATCH_SEQUENTIAL,
}


Expand Down Expand Up @@ -2507,12 +2531,18 @@ def select_execution_strategy(
reason = "no color-group with width>=2 → sequential (all width-1 waves)"
strategy = "sequential"

concurrency_allowed = (
strategy == "wave_loop"
and isolation_mode in {"auto", "required"}
and has_parallel_groups
)
return {
"strategy": strategy,
"wave_mode": wave_mode,
"worktree_isolation": isolation_mode,
"has_parallel_groups": has_parallel_groups,
"reason": reason,
"concurrency_allowed": concurrency_allowed,
}


Expand Down Expand Up @@ -4418,6 +4448,7 @@ def main():
"get_wave_step",
"validate_wave_step",
"advance_wave",
"select_execution_strategy",
"resume_single_subtask",
"get_plan_progress",
"monitor_failed",
Expand Down Expand Up @@ -4804,6 +4835,10 @@ def main():
result = get_wave_step(branch)
print(json.dumps(result, indent=2))

elif args.command == "select_execution_strategy":
result = select_execution_strategy(branch)
print(json.dumps(result, indent=2))

elif args.command == "validate_wave_step":
if not args.task_or_step:
print(
Expand Down
Loading
Loading