Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 52 additions & 10 deletions .map/scripts/map_orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,24 @@ def _extract_subtask_ids_from_plan_artifacts(
# Current wave is width-1 even though a later wave is parallel; dispatch sequentially
# for this wave — not an error, just the natural plan structure.
WAVE_REASON_CURRENT_WAVE_SEQUENTIAL = "current_wave_sequential"
# Kill-switch reason: MAP_EFFICIENT_SEQUENTIAL_ONLY=1 forces the full legacy sequential path.
WAVE_REASON_SEQUENTIAL_ONLY_ENV = "sequential_only_env"

# Truthy string values for MAP_EFFICIENT_SEQUENTIAL_ONLY env kill-switch.
_SEQUENTIAL_ONLY_TRUTHY = frozenset({"1", "true", "yes", "y", "on"})


def _sequential_only_env() -> bool:
"""Return True when MAP_EFFICIENT_SEQUENTIAL_ONLY is set to a truthy value.

Truthy: {"1", "true", "yes", "y", "on"} (case-insensitive).
When True, forces the full legacy sequential path regardless of config —
no wave-loop, no worktrees, no concurrent dispatch. This is the global
kill-switch / off-ramp introduced in Slice 6 (byte-identical to pre-5a legacy).
Never raises.
"""
val = os.environ.get("MAP_EFFICIENT_SEQUENTIAL_ONLY", "")
return val.strip().lower() in _SEQUENTIAL_ONLY_TRUTHY


class DispatchGateError(RuntimeError):
Expand Down Expand Up @@ -2498,10 +2516,14 @@ def select_execution_strategy(
"""Determine whether to use wave_loop or legacy sequential walker.

Predicate: wave_loop IFF wave_mode in {on, auto} AND worktree.isolation != 'off'
AND any color-group has width >= 2. This mirrors the canonical MapConfig gating
(#305): execution_wave_mode defaults to 'auto' but the wave-loop stays dormant
because worktree.isolation defaults to 'off', so default config always returns
'sequential' and the legacy get_next_step path is byte-identical (HC-1).
AND any color-group has width >= 2.

Slice 6: worktree.isolation defaults to 'auto' and concurrent_dispatch defaults
to True, so a parallel-ready plan now selects the wave-loop by default.

Kill-switch: MAP_EFFICIENT_SEQUENTIAL_ONLY=1 (checked FIRST) forces the full
legacy sequential path regardless of config — byte-identical to pre-5a legacy.
Per-repo opt-out: set `worktree.isolation: off` in .map/config.yaml.

Args:
branch: Git branch name (sanitized)
Expand All @@ -2515,11 +2537,23 @@ def select_execution_strategy(
"worktree_isolation": "off" | "auto" | "required",
"has_parallel_groups": bool,
"reason": str,
"concurrency_allowed": bool,
}
"""
if project_dir is None:
project_dir = Path(".")

# Kill-switch: MAP_EFFICIENT_SEQUENTIAL_ONLY=1 forces legacy sequential regardless of config.
if _sequential_only_env():
return {
"strategy": "sequential",
"wave_mode": "off",
"worktree_isolation": "off",
"has_parallel_groups": False,
"reason": WAVE_REASON_SEQUENTIAL_ONLY_ENV,
"concurrency_allowed": False,
}

try:
from map_step_runner import ( # pyright: ignore[reportMissingImports]
_execution_wave_mode,
Expand Down Expand Up @@ -2607,18 +2641,26 @@ def compute_dispatch_gate(
if project_dir is None:
project_dir = Path(".")

# Step 1: short-circuit on flag=false — first gate check after parameter
# normalization. No concurrency probe, no select_execution_strategy call, no
# _worktree_isolation_mode/concurrency_ready call, no dispatcher import runs on
# this path (HC-1 byte-identity). The project_dir None-guard above is a safe
# default-arg normalization, not a concurrency primitive.
# Kill-switch FIRST: MAP_EFFICIENT_SEQUENTIAL_ONLY=1 forces legacy sequential path.
# No concurrency probe, no config read, no import of any concurrency primitive.
if _sequential_only_env():
return {
"dispatch_mode": "sequential",
"reason": WAVE_REASON_SEQUENTIAL_ONLY_ENV,
}

# Step 1: short-circuit on flag=false — first gate check after kill-switch and
# parameter normalization. No concurrency probe, no select_execution_strategy call,
# no _worktree_isolation_mode/concurrency_ready call, no dispatcher import runs on
# this path. The project_dir None-guard above is a safe default-arg normalization,
# not a concurrency primitive.
try:
from map_step_runner import ( # pyright: ignore[reportMissingImports]
_concurrent_dispatch_enabled,
)
flag_on = _concurrent_dispatch_enabled(project_dir)
except ImportError:
flag_on = False
flag_on = True # default ON (Slice 6) when runner unavailable

if not flag_on:
return {
Expand Down
87 changes: 54 additions & 33 deletions .map/scripts/map_step_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,24 +153,26 @@ def _map_config_int(project_dir: Path, key: str, default: int) -> int:


def _concurrent_dispatch_enabled(project_dir: Path) -> bool:
"""Return True when execution.concurrent_dispatch is explicitly enabled.
"""Return True when execution.concurrent_dispatch is enabled (default ON, Slice 6).

Mirrors the _wt_isolation_enabled pattern. Default is False (off) so the
sequential path stays byte-identical to Slice 5a by default (HC-1). Never
raises.
Default is True — Slice 6 flipped from False. Disable via
MAP_EFFICIENT_SEQUENTIAL_ONLY=1 (global kill-switch) or set
`execution.concurrent_dispatch: false` in .map/config.yaml.
Mirrors the canonical MapConfig default (config/project_config.py). Never raises.
"""
raw = _map_config_str(project_dir, "execution.concurrent_dispatch", "false")
raw = _map_config_str(project_dir, "execution.concurrent_dispatch", "true")
return raw.strip().lower() in _CONCURRENT_DISPATCH_TRUTHY


def _execution_wave_mode(project_dir: Path) -> str:
"""Return the execution.wave_mode setting: 'off' | 'auto' | 'on'.

Default + enum mirror the canonical MapConfig schema (config/project_config.py):
absent/unknown/garbage normalises to 'auto'. 'auto' is still behavior-neutral
by default because the wave-loop only engages when worktree.isolation != 'off'
(which itself defaults to 'off') AND a color group has >=2 members — see
select_execution_strategy. Never raises.
absent/unknown/garbage normalises to 'auto'. As of the Slice 6 flip,
worktree.isolation also defaults to 'auto', so the wave-loop IS engaged by
default for a parallel-ready plan (it engages when worktree.isolation != 'off'
AND a color group has >=2 members) — see select_execution_strategy. Disable
via MAP_EFFICIENT_SEQUENTIAL_ONLY=1 or the per-repo opt-out keys. Never raises.
"""
raw = _map_config_str(project_dir, "execution.wave_mode", "auto")
return raw if raw in _WAVE_MODE_VALID else "auto"
Expand Down Expand Up @@ -15381,25 +15383,37 @@ def _wt_force_remove(path: Path, branch_ref: str) -> None:
})

_WT_ISOLATION_VALID = frozenset({"off", "auto", "required"})
# Legacy YAML booleans that map to 'off' (explicit per-repo disable)
_WT_ISOLATION_FALSY = frozenset({"false", "0", "no", "n"})


def _worktree_isolation_mode(project_dir: Path) -> str:
"""Return the worktree.isolation setting: 'off' | 'auto' | 'required'.

Accepts the new enum strings directly (case-insensitive).
Legacy boolean compat: boolish-truthy (true/1/yes) → 'required';
boolish-false (false/0/no/'') → 'off'.
Absent key → 'off' (today's behavior). Any unknown/garbage → 'off'.
boolish-false (false/0/no) → 'off'.
Absent key → 'auto' (default ON, Slice 6). Any unknown/garbage → 'auto'.
Disable via MAP_EFFICIENT_SEQUENTIAL_ONLY=1 (global kill-switch) or set
`worktree.isolation: off` in .map/config.yaml.
Mirrors the canonical MapConfig default (config/project_config.py).
Never raises.
"""
raw = _map_config_str(project_dir, "worktree.isolation", "")
normalized = raw.strip().lower()
if normalized in _WT_ISOLATION_VALID:
return normalized
# Legacy boolean compat
# Legacy boolean compat: truthy → 'required'
if _parse_boolish(normalized):
return "required"
return "off"
# Legacy boolean compat: explicit falsy (false/0/no/n) → 'off' (per-repo disable)
if normalized in _WT_ISOLATION_FALSY:
return "off"
# Absent key → default "auto" (Slice 6 flip from "off")
if not normalized:
return "auto"
# Unknown/garbage → safe default 'auto' (degrade gracefully)
return "auto"


def _wt_isolation_enabled(project_dir: Path) -> bool:
Expand All @@ -15408,19 +15422,18 @@ def _wt_isolation_enabled(project_dir: Path) -> bool:
Handles the enum migration (#303): the old boolean ``false``/``true`` raw
strings (YAML 1.1 booleans are written as ``false``/``true`` when read
line-by-line) still work. New enum values:
- ``off`` -> False (disabled — default)
- ``auto`` -> False (Slice 0: sequential everywhere; parallel dispatch
lands in Slice 5; the call site in create_subtask_worktree
already degrades to sequential when this returns False)
- ``off`` -> False (disabled)
- ``auto`` -> True (default ON, Slice 6; degrades gracefully when git
worktrees are unavailable)
- ``required`` -> True (hard-fail on unavailability, same as old ``true``)

Canonical enum vocabulary + default live in MapConfig
(config/project_config.py). `_worktree_isolation_mode` above mirrors the
same `worktree.isolation` key for probe/fallback paths that need the full
enum value (auto vs required), not just the enabled boolean.
"""
val = _map_config_str(project_dir, "worktree.isolation", "off").strip().lower()
return val in {"required", "true", "yes", "y", "1", "on"}
mode = _worktree_isolation_mode(project_dir)
return mode in {"auto", "required", "true", "yes", "y", "1", "on"}


def _wt_max_deletions(project_dir: Path) -> int:
Expand Down Expand Up @@ -15544,8 +15557,9 @@ def _wt_parse_name_status(text: str) -> tuple[list[str], list[str], int]:
def _worktree_probe(project_dir: Path) -> dict[str, object]:
"""Safe detached worktree probe. Cached per session keyed on toplevel.

HC-1 dormancy contract: when worktree.isolation is 'off' (the default),
this function returns immediately WITHOUT running any git command.
HC-1 dormancy contract: when worktree.isolation is 'off' (the per-repo
opt-out or MAP_EFFICIENT_SEQUENTIAL_ONLY off-ramp), this function returns
immediately WITHOUT running any git command.

When mode is 'auto' or 'required':
* Resolves the storage root via _wt_storage_root() (never .map/worktrees/).
Expand Down Expand Up @@ -15626,7 +15640,7 @@ def _worktree_probe(project_dir: Path) -> dict[str, object]:
def _require_clean_merge_target(project_dir: Path) -> dict[str, object]:
"""Check that the main checkout is clean enough for a worktree merge.

Dormant when worktree.isolation is 'off' (default — HC-1).
Dormant when worktree.isolation is 'off' (per-repo opt-out / kill-switch — HC-1).
When the check is active AND require_clean_merge_target config is True
(default True), runs `git status --porcelain` and returns:
* {"status":"ok","ok":True} when clean (excluding MAP runtime state)
Expand Down Expand Up @@ -15671,8 +15685,8 @@ def _require_clean_merge_target(project_dir: Path) -> dict[str, object]:
def resolve_worktree_isolation(project_dir: Path) -> dict[str, object]:
"""Classify the current environment and decide the execution decision.

HC-1 dormancy: when isolation is 'off' (the default), returns immediately
without running any git command.
HC-1 dormancy: when isolation is 'off' (per-repo opt-out / kill-switch),
returns immediately without running any git command.

Return schema
-------------
Expand Down Expand Up @@ -17460,10 +17474,13 @@ def run_concurrent_wave(
spawn actor agents — the skill emits N Task blocks to start actors (ST-007).
Its responsibilities are:

1. **Default-off guard** (HC-1, defense-in-depth): if ``concurrent_dispatch``
is not explicitly enabled in config, return ``CONCURRENT_DISPATCH_DISABLED``
immediately so a direct CLI or coordinator call cannot trigger concurrent
merging under the default-off configuration.
1. **Disabled-dispatch guard** (defense-in-depth): if ``concurrent_dispatch``
is disabled — via the per-repo opt-out (``execution.concurrent_dispatch:
false``) or the ``MAP_EFFICIENT_SEQUENTIAL_ONLY`` kill-switch — return
``CONCURRENT_DISPATCH_DISABLED`` immediately so a direct CLI or coordinator
call cannot trigger concurrent merging when the operator has opted out.
(As of the Slice 6 flip the default is enabled; this guard catches the
explicit off-ramps.)

2. **Batch-split**: read ``max_actors`` from config (via ``_max_actors()``),
clamp to [1, 8], then split the sorted ``group_ids`` into sequential
Expand Down Expand Up @@ -17531,14 +17548,18 @@ def run_concurrent_wave(
pd = project_dir or _wt_project_dir() or Path(".")
branch_name = branch or get_branch_name()

# F6: Default-off guard (HC-1, defense-in-depth). A direct CLI call or
# coordinator misconfiguration cannot trigger concurrent merging unless the
# flag is explicitly set. This is a second line of defense after compute_dispatch_gate.
# F6: Disabled-dispatch guard (defense-in-depth). A direct CLI call or
# coordinator misconfiguration cannot trigger concurrent merging when the
# operator has opted out. Second line of defense after compute_dispatch_gate
# (which also honors the MAP_EFFICIENT_SEQUENTIAL_ONLY kill-switch upstream).
if not _concurrent_dispatch_enabled(pd):
return _wt_error(
"CONCURRENT_DISPATCH_DISABLED",
"run_concurrent_wave requires execution.concurrent_dispatch=true in config; "
"default is off (HC-1). Set the flag explicitly to enable concurrent dispatch.",
"run_concurrent_wave: concurrent dispatch is disabled by this repo's "
"config (execution.concurrent_dispatch: false). It defaults to enabled "
"since the Slice 6 flip — remove that key or set it to true to enable "
"concurrent dispatch (or it may be the MAP_EFFICIENT_SEQUENTIAL_ONLY "
"kill-switch, which forces sequential everywhere).",
)

# Deterministic sorted list — order-of-call must not vary group membership.
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed
- **Parallel execution defaults flipped ON (`worktree.isolation` off→auto, `execution.concurrent_dispatch` false→true, Slice 6 of #303).** Concurrent wave execution is now **ON by default** for repositories that are git repos with a parallel-ready plan (>=2 independent subtasks in a wave). Off-ramps (either is sufficient): (1) **global kill-switch** — set `MAP_EFFICIENT_SEQUENTIAL_ONLY=1` in your environment; forces the full legacy sequential path, byte-identical to pre-5a behavior, regardless of config; (2) **per-repo opt-out** — set `worktree.isolation: off` and/or `execution.concurrent_dispatch: false` in `.map/config.yaml`. The `auto` isolation mode degrades gracefully to sequential with a warning when git worktrees are unavailable (non-git repo, shallow clone, detached HEAD). Default `worktree.isolation` `MapConfig` value: `"off"` → `"auto"`; `concurrent_dispatch` `MapConfig` value: `False` → `True`; matching defaults in the step-runner config readers (`_worktree_isolation_mode`, `_concurrent_dispatch_enabled`). The `select_execution_strategy` and `compute_dispatch_gate` functions now check the kill-switch as their **first** gate (before any config read or concurrency probe), backed by a new shared `_sequential_only_env()` helper and a stable `WAVE_REASON_SEQUENTIAL_ONLY_ENV` reason code.

### Added
- **Concurrent Actor dispatch for parallel waves (`execution.concurrent_dispatch`, part of #303 Slice 5b).** Activates same-turn concurrent dispatch of Actor subagents within a parallel wave, previously scheduled but always run sequentially. Flag-gated (`execution.concurrent_dispatch: false` by default) so the default code-path is byte-identical to Slice 5a; defaults flip only in Slice 6. Key components: `compute_dispatch_gate` (strict conjunction — `concurrent_dispatch` AND `concurrency_allowed` AND `concurrency_ready` AND `isolation != off`; hard-aborts with a `ConfigError`-equivalent on any config contradiction, fail-closed rather than degrading silently), `run_concurrent_wave` (splits the wave into sub-batches of `execution.max_actors` and dispatches each sub-batch; atomic per-sub-batch merge via `merge_wave_worktrees`), `abort_wave_group` (whole-group rollback — reverts every worktree in the group back to wave base, bounded by `execution.max_wave_retries`), `record_dispatch_actual` (clock-free phantom-parallelism classifier using `max_in_flight` replay — worktree SHA proves isolation but NOT concurrency; emits `phantom_parallel` evidence when actors ran but no concurrent overlap is detectable). Test harness uses barrier-based determinism (no wall-clock sleeps); HC-1 leak-guard suite validates no cross-subtask state leaks under concurrent dispatch. Council review split this into 5a (infrastructure) and 5b (activation); this entry covers 5b.
- **`deferred_nondeterministic` wired into the core Monitor verdict path (completes #252).** The flaky-triage primitives (`run_/record_/validate_flaky_test_triage`) and the `defer_flaky_subtask` close+advance command already existed, but were **disjoint** from the Monitor verdict path: Monitor could only emit `valid:true`/`valid:false`, had no field to signal a flaky defer, and `validate_step 2.4` could only pass or hard-stop — so a confirmed flake forced an out-of-band manual `defer_flaky_subtask`. The third Monitor outcome is now part of the structured verdict. (1) The Monitor schema gains an OPTIONAL structured `disposition: {kind, check_id}` field (`kind` enum currently `{deferred_nondeterministic}`), absent for normal verdicts, with guidance to emit it on confirmed mixed pass/fail evidence instead of demanding a fake Actor fix. (2) `validate_step 2.4 --disposition deferred_nondeterministic --check-id <id> --monitor-envelope -` routes to the existing `defer_flaky_subtask` **in-process** (the single owner of the close+advance transaction), placed BEFORE the recommendation gates so a defer carrying `recommendation=needs_investigation` is not hard-stopped. (3) **Anti-gaming** (llm-council-reviewed, conv `d3ddca63`): the deferral is honored ONLY when the Monitor envelope structurally backs it — `valid:false`, non-empty `failed_checks`, and a structured `disposition` whose kind + `check_id` match the flags — AND the sidecar holds mixed pass/fail evidence for that `check_id` (re-validated from disk by `defer_flaky_subtask`). A Monitor cannot dodge a real deterministic failure or a green check by merely claiming "flaky"; `recommendation in {revise, block}` together with a disposition is rejected as a contradiction. (Note: the Monitor schema's `failed_checks` lists failed quality *dimensions*, a different namespace from a flaky check id, so the binding is "Monitor admits a dimension failure + dispositions match" rather than "check_id ∈ failed_checks".) (4) **Verdict vs routing:** a deferred run returns `valid:false` + `deferred:true` + `non_green_outcome:true` (a deferral is NOT green — it is a routing decision, not a clean pass); the CLI exits `0` on a deferral (not a hard-stop) and `1` only on a true invalid verdict. A single source-of-truth `MONITOR_DISPOSITIONS` policy dict drives the routing, the CLI `--disposition` surface, and a drift-guard test (the Monitor prompt must name every supported disposition). Closes the last core slice of #252.
Expand Down
Loading
Loading