Skip to content
Closed
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
2 changes: 1 addition & 1 deletion desloppify/app/commands/plan/override/resolve_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ def _finalize_workflow_resolution(
note: str | None,
) -> None:
purge_ids(plan, synthetic_ids)
step_messages = auto_complete_steps(plan)
step_messages = auto_complete_steps(plan, resolved_ids=synthetic_ids)
for message in step_messages:
print(colorize(message, "green"))
append_log_entry(plan, "done", issue_ids=synthetic_ids, actor="user", note=note)
Expand Down
2 changes: 1 addition & 1 deletion desloppify/app/commands/resolve/living_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ def update_living_plan_after_resolve(
completed_clusters = _completed_cluster_names(plan, all_resolved)
phase_before = current_lifecycle_phase(plan)
purged = purge_ids(plan, all_resolved)
step_messages = auto_complete_steps(plan)
step_messages = auto_complete_steps(plan, resolved_ids=all_resolved)
for msg in step_messages:
print(colorize(msg, "green"))
append_log_entry(
Expand Down
13 changes: 12 additions & 1 deletion desloppify/engine/_plan/step_completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@
from __future__ import annotations


def auto_complete_steps(plan: dict) -> list[str]:
def auto_complete_steps(
plan: dict, resolved_ids: list[str] | None = None
) -> list[str]:
"""Mark steps done when all their issue_refs are no longer in the queue.

When ``resolved_ids`` is provided, only consider steps linked to this
resolution operation.
Returns list of human-readable messages for completed steps.
"""
messages: list[str] = []
queue_set = set(plan.get("queue_order", []))
resolved_set = set(resolved_ids or [])

for name, cluster in plan.get("clusters", {}).items():
for i, step in enumerate(cluster.get("action_steps") or []):
Expand All @@ -18,6 +23,12 @@ def auto_complete_steps(plan: dict) -> list[str]:
refs = step.get("issue_refs", [])
if not refs:
continue
if resolved_ids is not None and not any(
resolved_id.endswith(ref) or resolved_id == ref
for resolved_id in resolved_set
for ref in refs
):
continue
# Match by suffix: ref "abc123" matches "review::path::abc123"
all_gone = all(
not any(qid.endswith(ref) or qid == ref for qid in queue_set)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ def test_override_resolve_cmd_handles_synthetic_only_resolution(
monkeypatch.setattr(
resolve_workflow_mod,
"auto_complete_steps",
lambda _plan: ["step complete"],
lambda _plan, resolved_ids=None: ["step complete"],
)
monkeypatch.setattr(
resolve_workflow_mod,
Expand Down
20 changes: 15 additions & 5 deletions desloppify/tests/commands/resolve/test_living_plan_direct.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ def test_update_living_plan_after_resolve_fixed_flow(monkeypatch, capsys) -> Non
monkeypatch.setattr(living_plan_mod, "load_plan", lambda _p=None: plan)
monkeypatch.setattr(living_plan_mod, "purge_ids", lambda _plan, _ids: 1)
monkeypatch.setattr(
living_plan_mod, "auto_complete_steps", lambda _plan: ["step complete"]
living_plan_mod,
"auto_complete_steps",
lambda _plan, resolved_ids=None: ["step complete"],
)
monkeypatch.setattr(
living_plan_mod, "append_log_entry", lambda *_a, **_k: calls.append("log")
Expand Down Expand Up @@ -123,7 +125,9 @@ def test_update_living_plan_after_resolve_marks_all_completed_clusters_done(
monkeypatch.setattr(living_plan_mod, "has_living_plan", lambda _p=None: True)
monkeypatch.setattr(living_plan_mod, "load_plan", lambda _p=None: plan)
monkeypatch.setattr(living_plan_mod, "purge_ids", lambda _plan, _ids: 2)
monkeypatch.setattr(living_plan_mod, "auto_complete_steps", lambda _plan: [])
monkeypatch.setattr(
living_plan_mod, "auto_complete_steps", lambda _plan, resolved_ids=None: []
)
monkeypatch.setattr(
living_plan_mod,
"append_log_entry",
Expand Down Expand Up @@ -174,7 +178,9 @@ def _purge(_plan, _ids):
return 1

monkeypatch.setattr(living_plan_mod, "purge_ids", _purge)
monkeypatch.setattr(living_plan_mod, "auto_complete_steps", lambda _plan: [])
monkeypatch.setattr(
living_plan_mod, "auto_complete_steps", lambda _plan, resolved_ids=None: []
)
monkeypatch.setattr(living_plan_mod, "append_log_entry", lambda *_a, **_k: None)
monkeypatch.setattr(
living_plan_mod, "add_uncommitted_issues", lambda *_a, **_k: None
Expand Down Expand Up @@ -226,7 +232,9 @@ def test_update_living_plan_after_resolve_skips_reconcile_without_state(
monkeypatch.setattr(living_plan_mod, "has_living_plan", lambda _p=None: True)
monkeypatch.setattr(living_plan_mod, "load_plan", lambda _p=None: plan)
monkeypatch.setattr(living_plan_mod, "purge_ids", lambda _plan, _ids: 1)
monkeypatch.setattr(living_plan_mod, "auto_complete_steps", lambda _plan: [])
monkeypatch.setattr(
living_plan_mod, "auto_complete_steps", lambda _plan, resolved_ids=None: []
)
monkeypatch.setattr(living_plan_mod, "append_log_entry", lambda *_a, **_k: None)
monkeypatch.setattr(
living_plan_mod, "add_uncommitted_issues", lambda *_a, **_k: None
Expand Down Expand Up @@ -270,7 +278,9 @@ def _purge(_plan, _ids):
return 1

monkeypatch.setattr(living_plan_mod, "purge_ids", _purge)
monkeypatch.setattr(living_plan_mod, "auto_complete_steps", lambda _plan: [])
monkeypatch.setattr(
living_plan_mod, "auto_complete_steps", lambda _plan, resolved_ids=None: []
)
monkeypatch.setattr(living_plan_mod, "append_log_entry", lambda *_a, **_k: None)
monkeypatch.setattr(
living_plan_mod, "add_uncommitted_issues", lambda *_a, **_k: None
Expand Down
24 changes: 24 additions & 0 deletions desloppify/tests/plan/test_step_completion_direct.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,27 @@ def test_auto_complete_steps_ignores_done_steps_and_invalid_step_shapes() -> Non

assert messages == []
assert plan["clusters"]["epic/mixed"]["action_steps"][0]["done"] is True


def test_auto_complete_steps_ignores_unrelated_stale_refs() -> None:
plan = {
"queue_order": [],
"clusters": {
"target": {
"action_steps": [
{"title": "Target step", "issue_refs": ["target-id"]},
]
},
"unrelated": {
"action_steps": [
{"title": "Unrelated step", "issue_refs": ["stale-id"]},
]
},
},
}

messages = auto_complete_steps(plan, resolved_ids=["target-id"])

assert plan["clusters"]["target"]["action_steps"][0]["done"] is True
assert plan["clusters"]["unrelated"]["action_steps"][0].get("done") is not True
assert messages == [" Step 1 of 'target' auto-completed: Target step"]