Skip to content

fix: rehydrate execution state from map-plan artifacts#102

Merged
azalio merged 6 commits intomainfrom
fix/map-plan-handoff-contract
Apr 27, 2026
Merged

fix: rehydrate execution state from map-plan artifacts#102
azalio merged 6 commits intomainfrom
fix/map-plan-handoff-contract

Conversation

@azalio
Copy link
Copy Markdown
Owner

@azalio azalio commented Apr 27, 2026

Summary

  • stop /map-plan from instructing users to create a planning-only step_state.json
  • make /map-efficient explicitly rehydrate runtime state from plan artifacts when state is missing or still in planning shape
  • persist recovered aag_contracts and spec constraints during resume_from_plan
  • align Claude and Codex templates/docs with the new handoff contract
  • add regression coverage for the plan → execution transition

Why

/map-plan was producing a planning-only step_state.json contract that did not match runtime expectations in map_orchestrator.py.
That allowed /map-efficient to skip resume_from_plan and, in the worst case, jump straight to ST-002.

This PR makes planning produce only planning artifacts (spec, task_plan, blueprint, manifest), and makes execution rebuild canonical runtime state from those artifacts.

Changes

  • map-plan.md / $map-plan skill:
    • remove planning-time step_state.json creation
    • add explicit handoff note to start execution via resume_from_plan
  • map-efficient.md:
    • detect stale planning-shaped state and reinitialize from plan artifacts
    • avoid treating COMPLETE runtime state as resumable plan state
  • map_orchestrator.py:
    • recover aag_contracts from blueprint.subtasks[].aag_contract
    • persist recovered aag_contracts into runtime StepState
    • parse and persist spec constraints during resume_from_plan
  • map_step_runner.py:
    • treat task_plan + blueprint as sufficient for plan readiness
  • tests:
    • add regressions for stale planning state, resumed AAG contracts, resumed constraints, and command-template guidance

Test Plan

pytest tests/test_map_orchestrator.py tests/test_map_step_runner.py tests/test_command_templates.py tests/test_template_sync.py tests/integration/test_e2e_artifact_contracts.py -q

Result: 357 passed, 5 skipped

Copilot AI review requested due to automatic review settings April 27, 2026 06:24
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Updates the MAP plan → execution handoff contract so execution state is rehydrated from /map-plan artifacts (spec/task plan/blueprint/manifest) rather than relying on a planning-time step_state.json, and adds regression coverage to prevent skipping resume_from_plan.

Changes:

  • Removes planning-time step_state.json creation guidance from /map-plan templates and skills; adds explicit handoff messaging to start execution via resume_from_plan.
  • Updates execution templates to rehydrate runtime state when existing state is missing or “planning-shaped”.
  • Enhances orchestrator resume to recover aag_contracts from blueprint.json subtasks and to parse/persist spec constraints; adjusts plan artifact readiness logic and adds regression tests.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/test_map_step_runner.py Updates/extends tests for new plan artifact readiness semantics (step_state no longer required).
tests/test_map_orchestrator.py Adds regressions for planning-only state behavior and resume_from_plan rehydration (AAG contracts + constraints).
tests/test_command_templates.py Verifies templates reflect the new handoff contract and rehydration detection guidance.
src/mapify_cli/templates/map/scripts/map_step_runner.py Treats task_plan + blueprint as sufficient for plan readiness; step_state becomes optional.
src/mapify_cli/templates/map/scripts/map_orchestrator.py Adds constraint parsing from spec, persists aag_contracts on resume, and rehydrates state from plan artifacts.
src/mapify_cli/templates/commands/map-plan.md Removes planning-time step_state contract; clarifies execution must start via resume_from_plan.
src/mapify_cli/templates/commands/map-efficient.md Adds explicit “rehydrate when stale/missing” detection logic and calls resume_from_plan.
src/mapify_cli/templates/codex/skills/map-plan/SKILL.md Aligns Codex skill with new plan-only artifact contract and execution rehydration.
.codex/skills/map-plan/SKILL.md Mirrors the Codex skill contract updates for plan-only artifacts and resume-based execution init.
.claude/commands/map-plan.md Aligns Claude command template with plan-only artifact contract and explicit execution handoff.
.claude/commands/map-efficient.md Mirrors the new stale/missing state detection + resume_from_plan guidance.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +203 to +213
key, value = line.strip().split(":", 1)
normalized = value.strip()
if normalized in {"null", "None", ""}:
parsed[key] = None
elif key in {"max_files", "max_subtasks", "time_budget"}:
try:
parsed[key] = int(normalized)
except ValueError:
parsed[key] = normalized
else:
parsed[key] = normalized.strip('"\'')
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

_load_constraints_from_spec falls back to storing the raw string when int parsing fails for time_budget/max_* (e.g., quoted numbers like "45" or floats like 45.0). Downstream, the workflow-gate time_budget check silently ignores non-numeric values (TypeError), which effectively disables enforcement. Consider normalizing by stripping quotes before parsing, supporting floats where appropriate, and/or treating non-numeric values as an error (or None) so constraints can’t be bypassed accidentally.

Copilot uses AI. Check for mistakes.
Comment on lines +132 to +138
planning_only_workflow = state.get("workflow") == "map-plan"

should_resume = (
workflow_status in {"", "INITIALIZED"}
or current_phase in {"", "INITIALIZED"}
or (pending_steps == [] and bool(subtask_sequence))
or planning_only_workflow
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

The should_resume heuristic will also return true for a completed runtime state because pending_steps becomes an empty list at completion (see map_orchestrator.validate_step setting current_step_phase to COMPLETE while leaving pending_steps empty). With a plan file present, this would re-run resume_from_plan after completion and overwrite the final runtime state. Consider explicitly excluding COMPLETE (and/or checking current_step_id/current_step_phase == "COMPLETE") from the resume condition, and tightening the pending_steps == [] branch to only match known planning-shaped state (e.g., workflow == "map-plan" and current_phase == "INITIALIZED").

Suggested change
planning_only_workflow = state.get("workflow") == "map-plan"
should_resume = (
workflow_status in {"", "INITIALIZED"}
or current_phase in {"", "INITIALIZED"}
or (pending_steps == [] and bool(subtask_sequence))
or planning_only_workflow
workflow_name = str(state.get("workflow") or "").strip()
is_complete = workflow_status == "COMPLETE" or current_phase == "COMPLETE"
planning_artifact_state = (
workflow_name == "map-plan"
and current_phase in {"", "INITIALIZED"}
and pending_steps == []
and bool(subtask_sequence)
)
should_resume = (
not is_complete
and (
workflow_status in {"", "INITIALIZED"}
or current_phase in {"", "INITIALIZED"}
or planning_artifact_state
)

Copilot uses AI. Check for mistakes.
Comment thread .claude/commands/map-efficient.md Outdated
Comment on lines +133 to +137

should_resume = (
workflow_status in {"", "INITIALIZED"}
or current_phase in {"", "INITIALIZED"}
or (pending_steps == [] and bool(subtask_sequence))
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

Same issue as the template version: pending_steps == [] and bool(subtask_sequence) will also be true for a completed runtime state (where current_step_phase is COMPLETE), causing /map-efficient to re-run resume_from_plan and overwrite the completed execution state whenever a plan file exists. Exclude COMPLETE from the resume heuristic and/or tighten the empty-pending_steps check to only match planning-shaped state.

Suggested change
should_resume = (
workflow_status in {"", "INITIALIZED"}
or current_phase in {"", "INITIALIZED"}
or (pending_steps == [] and bool(subtask_sequence))
planning_shaped_pending_state = (
pending_steps == []
and bool(subtask_sequence)
and current_phase != "COMPLETE"
and (
workflow_status in {"", "INITIALIZED"}
or planning_only_workflow
)
)
should_resume = (
workflow_status in {"", "INITIALIZED"}
or current_phase in {"", "INITIALIZED"}
or planning_shaped_pending_state

Copilot uses AI. Check for mistakes.
Comment on lines +1605 to +1615
try:
src_data = json.loads(source_file.read_text(encoding="utf-8"))
aag_contracts = src_data.get("aag_contracts", {})
if not aag_contracts and isinstance(src_data.get("subtasks"), list):
aag_contracts = {
subtask.get("id"): subtask.get("aag_contract", "")
for subtask in src_data["subtasks"]
if isinstance(subtask, dict)
and subtask.get("id")
and subtask.get("aag_contract")
}
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

resume_from_plan currently prefers reading aag_contracts from an existing step_state.json before blueprint.json. With the new contract, blueprint.json is the canonical planning artifact, while step_state.json may be stale/partial runtime state (or an old planning-shaped file). Consider preferring blueprint.json as the primary source and only falling back to step_state.json when blueprint is missing or doesn’t contain contracts.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 1638 to 1656
@@ -1586,6 +1652,7 @@ def resume_from_plan(branch: str) -> dict:
plan_approved=True,
execution_mode="batch",
workflow_status="IN_PROGRESS",
constraints=constraints,
)
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

resume_from_plan now persists constraints (including time_budget), but the newly created StepState relies on the dataclass default started_at=datetime.now().isoformat() (naive / no timezone). workflow-gate.py’s time_budget enforcement uses datetime.now(timezone.utc) - start, which raises TypeError for naive start and silently skips enforcement. Set started_at explicitly to an unambiguous UTC timestamp (e.g., _utc_timestamp()) when constructing the state (or ensure StepState.started_at is always timezone-aware).

Copilot uses AI. Check for mistakes.
Comment on lines +215 to +218
for raw_line in match.group("body").splitlines():
line = raw_line.split("#", 1)[0].rstrip()
if not line.strip() or ":" not in line:
continue
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

Constraint parsing strips comments via raw_line.split("#", 1), which will corrupt valid quoted YAML values containing # (e.g., scope_glob: "src/#tmp/**") and can lead to silently wrong constraints being persisted. Consider only treating # as a comment delimiter when it occurs outside quotes (or require that values containing # are fully preserved).

Copilot uses AI. Check for mistakes.
@azalio azalio merged commit 149500b into main Apr 27, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants