Skip to content

fix(orchestrator): atomic workflow completion + canonical reopen gate#106

Open
azalio wants to merge 2 commits intomainfrom
fix/mark-workflow-complete-atomic
Open

fix(orchestrator): atomic workflow completion + canonical reopen gate#106
azalio wants to merge 2 commits intomainfrom
fix/mark-workflow-complete-atomic

Conversation

@azalio
Copy link
Copy Markdown
Owner

@azalio azalio commented May 7, 2026

Summary

  • mark_workflow_complete(branch) — new orchestrator API that atomically sets workflow_status, current_step_id, current_step_phase, and a new completed_at field; refuses while pending_steps is non-empty.
  • map-check/SKILL.md: replaced the historical jq '.current_state = "WORKFLOW_COMPLETE"' mutation with the new API + explicit "never edit state directly" warning.
  • reopen_for_fixes now gates on the canonical signal workflow_status == "WORKFLOW_COMPLETE" (with fallback to legacy current_step_id / current_step_phase so already-corrupted state files can still be reopened).

Why

A user hit this on a real branch: /map-check finalised the workflow via jq, which silently bypassed current_step_phase. The field stayed on "ACTOR" from the last subtask's Actor cycle. When /map-review later tried python3 .map/scripts/map_orchestrator.py reopen_for_fixes, it was refused with "Workflow is in phase 'ACTOR', not COMPLETE" even though the workflow was clearly complete (workflow_status = "WORKFLOW_COMPLETE", current_step_id = "COMPLETE").

Two architectural rules from architecture-patterns.md apply:

  • Orchestrator Prompts Must Prohibit Direct State File Modification (2026-04-11)
  • State Machine Transition Completeness: Reset All Sub-State Atomically (2026-04-11)

Test plan

  • tests/test_map_orchestrator.py — new TestMarkWorkflowComplete class (5 tests): atomic transition, reject when pending, no state file, completed_at round-trip, end-to-end mark → reopen.
  • Regression test test_accepts_canonical_workflow_status_with_stale_phase reproduces the STACKLAND-1591 case (stale current_step_phase="ACTOR" + canonical workflow_status="WORKFLOW_COMPLETE" → reopen succeeds).
  • Updated test_rejects_non_complete_phasetest_rejects_in_progress_workflow to match the new contract.
  • ruff check src/ tests/ — clean.
  • mypy src/ — clean.
  • pytest --ignore=tests/integration/test_e2e_claude_sdk.py — 1051 passed, 2 skipped (SDK e2e excluded; needs API key, unrelated).
  • make sync-templates.claude/ and src/mapify_cli/templates/ are byte-identical (verified via diff).

map-check used `jq` to set `current_state="WORKFLOW_COMPLETE"` directly on
step_state.json. The mutation skipped `current_step_phase`, which stayed on
"ACTOR" from the last subtask's edit. `reopen_for_fixes` then refused the
post-/map-review transition because it gated on `current_step_phase != "COMPLETE"`.

- Add `mark_workflow_complete(branch)` orchestrator API that atomically sets
  `workflow_status`, `current_step_id`, `current_step_phase`, and a new
  `completed_at` field; refuses while `pending_steps` is non-empty.
- Replace the `jq` mutation in map-check/SKILL.md with the new API.
- Make `reopen_for_fixes` accept the canonical `workflow_status ==
  "WORKFLOW_COMPLETE"` signal (with fallback to legacy fields so existing
  state files marked via the old `jq` path can still be reopened).
- Regression + unit tests cover the stale-phase scenario and the full
  mark → reopen lifecycle.
Copilot AI review requested due to automatic review settings May 7, 2026 15:00
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

This PR introduces an orchestrator-level, atomic “workflow complete” transition and updates /map-check to use it, while making reopen_for_fixes accept the canonical completion signal (workflow_status == "WORKFLOW_COMPLETE") with legacy fallbacks to recover from partially-mutated state.

Changes:

  • Added mark_workflow_complete(branch) to atomically set completion fields and refuse completion when pending_steps is non-empty.
  • Updated reopen_for_fixes gating to rely on canonical workflow_status (with fallbacks for legacy/corrupted state files).
  • Updated /map-check skill docs to prohibit direct state mutation and call the new orchestrator command; added/updated tests for these behaviors.

Reviewed changes

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

Show a summary per file
File Description
src/mapify_cli/templates/map/scripts/map_orchestrator.py Adds completed_at, mark_workflow_complete, and canonical completion detection used by reopen_for_fixes.
.map/scripts/map_orchestrator.py Mirrors the same orchestrator changes in the source-of-truth script copy.
tests/test_map_orchestrator.py Adds coverage for atomic completion, completed_at persistence, and reopen gating regression.
src/mapify_cli/templates/skills/map-check/SKILL.md Switches from jq state mutation to calling mark_workflow_complete; adds explicit warning.
.claude/skills/map-check/SKILL.md Mirrors the same /map-check documentation updates for the Claude skill source.

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

state.workflow_status = "WORKFLOW_COMPLETE"
state.current_step_id = "COMPLETE"
state.current_step_phase = "COMPLETE"
state.completed_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
Comment on lines +1310 to 1316
if not _is_workflow_complete(state):
return {
"status": "error",
"message": (
f"Workflow is in phase '{state.current_step_phase}', not COMPLETE. "
f"Workflow is in phase '{state.current_step_phase}' "
f"(workflow_status='{state.workflow_status}'), not COMPLETE. "
"Use monitor_failed for non-COMPLETE retry."
Comment thread .map/scripts/map_orchestrator.py Outdated
state.workflow_status = "WORKFLOW_COMPLETE"
state.current_step_id = "COMPLETE"
state.current_step_phase = "COMPLETE"
state.completed_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
Comment on lines +1310 to 1316
if not _is_workflow_complete(state):
return {
"status": "error",
"message": (
f"Workflow is in phase '{state.current_step_phase}', not COMPLETE. "
f"Workflow is in phase '{state.current_step_phase}' "
f"(workflow_status='{state.workflow_status}'), not COMPLETE. "
"Use monitor_failed for non-COMPLETE retry."
- mark_workflow_complete: use shared _utc_timestamp() helper instead of
  hand-rolled strftime to keep RFC3339 formatting consistent with
  contract_ready_subtasks[*].ready_at and other timestamped fields.
- reopen_for_fixes: reset workflow_status="IN_PROGRESS" and clear
  completed_at when reopening. Without this, post-reopen state still
  reads workflow_status="WORKFLOW_COMPLETE" while pending_steps and
  current_step_phase=ACTOR are reset — exactly the kind of partial
  transition this PR was opened to eliminate. Adds regression test.
@azalio
Copy link
Copy Markdown
Owner Author

azalio commented May 7, 2026

@copilot-pull-request-reviewer please re-review — addressed the two findings from your previous pass in afc5ec4 (use _utc_timestamp() helper, atomic reset of workflow_status+completed_at on reopen).

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 5 out of 5 changed files in this pull request and generated no new comments.

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