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
15 changes: 15 additions & 0 deletions architecture.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,21 @@
"signature": "(text: Optional[str]) -> Optional[str]",
"returns": "Optional[str]"
},
{
"name": "extract_step_report",
"signature": "(text: Optional[str]) -> Optional[str]",
"returns": "Optional[str]"
},
{
"name": "normalize_step_comments_state",
"signature": "(raw: Any) -> Set[int]",
"returns": "Set[int]"
},
{
"name": "post_step_comment_once",
"signature": "(*, repo_owner: str, repo_name: str, issue_number: int, step_num: int, body: str, posted_steps: Set[int], cwd: Path) -> bool",
"returns": "bool"
},
{
"name": "_sanitize_comment_body",
"signature": "(body: Optional[str], max_chars: int = 25_000) -> str",
Expand Down
105 changes: 104 additions & 1 deletion pdd/agentic_change_orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
post_step_comment,
set_agentic_progress,
clear_agentic_progress,
extract_step_report,
normalize_step_comments_state,
post_step_comment_once,
)
from pdd.load_prompt_template import load_prompt_template
from pdd.sync_order import (
Expand Down Expand Up @@ -1417,7 +1420,10 @@ def run_agentic_change_orchestrator(
model_used = "unknown"
github_comment_id = None
worktree_path = None


step_comments_set: Set[int] = normalize_step_comments_state(state.get("step_comments"))
state["step_comments"] = sorted(step_comments_set)

pddrc_context = _load_pddrc_context(cwd)

context = {
Expand Down Expand Up @@ -1694,6 +1700,7 @@ def run_agentic_change_orchestrator(
output=step_output, cwd=cwd,
)
state["step_outputs"][str(step_num)] = f"FAILED: {step_output}"
state["step_comments"] = sorted(step_comments_set)
save_workflow_state(cwd, issue_number, "change", state, state_dir, repo_owner, repo_name, use_github_state, github_comment_id)
return False, f"Aborting: {consecutive_provider_failures} consecutive steps failed — agent providers unavailable", total_cost, model_used, []
else:
Expand All @@ -1717,6 +1724,7 @@ def run_agentic_change_orchestrator(
refreshed = _fetch_issue_updated_at(repo_owner, repo_name, issue_number)
if refreshed:
state["issue_updated_at"] = refreshed
state["step_comments"] = sorted(step_comments_set)
save_workflow_state(cwd, issue_number, "change", state, state_dir, repo_owner, repo_name, use_github_state, github_comment_id)
return False, f"Stopped at step {step_num}: {stop_reason}", total_cost, model_used, changed_files
console.print(f"[yellow]Warning: Step {step_num} reported failure but continuing...[/yellow]")
Expand All @@ -1739,6 +1747,7 @@ def run_agentic_change_orchestrator(
refreshed = _fetch_issue_updated_at(repo_owner, repo_name, issue_number)
if refreshed:
state["issue_updated_at"] = refreshed
state["step_comments"] = sorted(step_comments_set)
save_workflow_state(cwd, issue_number, "change", state, state_dir, repo_owner, repo_name, use_github_state, github_comment_id)
return False, f"Stopped at step {step_num}: {stop_reason}", total_cost, model_used, changed_files

Expand Down Expand Up @@ -1771,6 +1780,7 @@ def run_agentic_change_orchestrator(
# Issue #467: Mark as FAILED instead of using step_num - 1
state["step_outputs"][str(step_num)] = f"FAILED: {step_output}"
# Don't advance last_completed_step — keep it at its current value
state["step_comments"] = sorted(step_comments_set)
save_workflow_state(cwd, issue_number, "change", state, state_dir, repo_owner, repo_name, use_github_state, github_comment_id)
return False, "Stopped at step 9: Implementation produced no file changes", total_cost, model_used, []

Expand Down Expand Up @@ -1925,6 +1935,28 @@ def run_agentic_change_orchestrator(
consecutive_provider_failures = 0
state["step_outputs"][str(step_num)] = step_output
state["last_completed_step"] = step_num
try:
report_body = extract_step_report(step_output)
if not report_body:
report_body = (
f"_Step {step_num} completed; no `<step_report>` block "
"returned by agent. Raw output retained in workflow state._"
)
comment_body = (
f"## Step {step_num}/13: {description}\n\n{report_body}"
)
post_step_comment_once(
repo_owner=repo_owner,
repo_name=repo_name,
issue_number=issue_number,
step_num=step_num,
body=comment_body,
posted_steps=step_comments_set,
cwd=current_work_dir,
)
except Exception as exc: # pylint: disable=broad-except
console.print(f"[yellow]post_step_comment_once failed: {exc}[/yellow]")
state["step_comments"] = sorted(step_comments_set)
else:
state["step_outputs"][str(step_num)] = f"FAILED: {step_output}"

Expand Down Expand Up @@ -1970,6 +2002,33 @@ def run_agentic_change_orchestrator(
instruction=s11_prompt, cwd=current_work_dir, verbose=verbose, quiet=quiet, timeout=timeout11, label=f"step11_iter{review_iteration}", max_retries=DEFAULT_MAX_RETRIES, reasoning_time=reasoning_time,
)
total_cost += s11_cost; model_used = s11_model; state["total_cost"] = total_cost
# Round-2 of Greg's review: gate the trusted Step 11 post on
# s11_success. A failed task (e.g. provider exhaustion) still
# returns step output, and posting a "completed" fallback would
# both mislead the user and mark this iteration's composite key
# (review_iteration*100 + 11) as already-posted in
# state["step_comments"]. On a later resume/retry the real
# successful Step 11 report would be silently deduped away.
if s11_success:
try:
s11_report = extract_step_report(s11_output)
if not s11_report:
s11_report = (
"_Step 11 completed; no `<step_report>` block returned "
"by agent. Raw output retained in workflow state._"
)
post_step_comment_once(
repo_owner=repo_owner,
repo_name=repo_name,
issue_number=issue_number,
step_num=review_iteration * 100 + 11,
body=f"## Step 11/13: Review (iteration {review_iteration})\n\n{s11_report}",
posted_steps=step_comments_set,
cwd=current_work_dir,
)
state["step_comments"] = sorted(step_comments_set)
except Exception as exc: # pylint: disable=broad-except
console.print(f"[yellow]post_step_comment_once failed: {exc}[/yellow]")
if _review_loop_no_issues(s11_output):
if not quiet: console.print(" -> No issues found. Proceeding to PR.")
context["step11_output"] = s11_output; break
Expand All @@ -1989,6 +2048,30 @@ def run_agentic_change_orchestrator(
total_cost += s12_cost; model_used = s12_model; state["total_cost"] = total_cost
previous_fixes += f"\n\nIteration {review_iteration}:\n{s12_output}"
state["previous_fixes"] = previous_fixes
# Round-2 of Greg's review: gate the trusted Step 12 post on
# s12_success. Same reasoning as Step 11 above — a failed fix
# task must not burn the composite key, otherwise a later
# successful retry of the same iteration would be deduped.
if s12_success:
try:
s12_report = extract_step_report(s12_output)
if not s12_report:
s12_report = (
"_Step 12 completed; no `<step_report>` block returned "
"by agent. Raw output retained in workflow state._"
)
post_step_comment_once(
repo_owner=repo_owner,
repo_name=repo_name,
issue_number=issue_number,
step_num=review_iteration * 100 + 12,
body=f"## Step 12/13: Fix (iteration {review_iteration})\n\n{s12_report}",
posted_steps=step_comments_set,
cwd=current_work_dir,
)
state["step_comments"] = sorted(step_comments_set)
except Exception as exc: # pylint: disable=broad-except
console.print(f"[yellow]post_step_comment_once failed: {exc}[/yellow]")
save_result = save_workflow_state(cwd, issue_number, "change", state, state_dir, repo_owner, repo_name, use_github_state, github_comment_id)
if save_result: github_comment_id = save_result; state["github_comment_id"] = github_comment_id
if review_iteration >= MAX_REVIEW_ITERATIONS:
Expand Down Expand Up @@ -2097,8 +2180,28 @@ def run_agentic_change_orchestrator(
if not s13_success:
post_step_comment(repo_owner, repo_name, issue_number, 13, 13, "Create PR and link to issue", s13_output, cwd)
console.print("[red]Step 13 (PR Creation) failed.[/red]")
state["step_comments"] = sorted(step_comments_set)
save_workflow_state(cwd, issue_number, "change", state, state_dir, repo_owner, repo_name, use_github_state, github_comment_id)
return False, "PR Creation failed", total_cost, model_used, changed_files
try:
s13_report = extract_step_report(s13_output)
if not s13_report:
s13_report = (
"_Step 13 completed; no `<step_report>` block returned "
"by agent. Raw output retained in workflow state._"
)
post_step_comment_once(
repo_owner=repo_owner,
repo_name=repo_name,
issue_number=issue_number,
step_num=13,
body=f"## Step 13/13: Create PR and link to issue\n\n{s13_report}",
posted_steps=step_comments_set,
cwd=current_work_dir,
)
state["step_comments"] = sorted(step_comments_set)
except Exception as exc: # pylint: disable=broad-except
console.print(f"[yellow]post_step_comment_once failed: {exc}[/yellow]")
pr_url = "Unknown"; url_match = re.search(r"https://github.com/\S+/pull/\d+", s13_output)
if url_match: pr_url = url_match.group(0)
if not quiet:
Expand Down
67 changes: 61 additions & 6 deletions pdd/agentic_checkup_orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,20 @@
import shutil
import subprocess
from pathlib import Path
from typing import Dict, List, Optional, Tuple, Union
from typing import Dict, List, Optional, Set, Tuple, Union

from rich.console import Console

from .agentic_common import (
DEFAULT_MAX_RETRIES,
_sanitize_comment_body,
clear_workflow_state,
extract_step_report,
load_workflow_state,
normalize_step_comments_state,
post_pr_comment,
post_step_comment,
post_step_comment_once,
run_agentic_task,
save_workflow_state,
substitute_template_variables,
Expand Down Expand Up @@ -1029,6 +1032,11 @@ def run_agentic_checkup_orchestrator(
if changed_files:
context["files_to_stage"] = ", ".join(changed_files)

if state is None:
step_comments_set: Set[int] = set()
else:
step_comments_set = normalize_step_comments_state(state.get("step_comments"))

# Step definitions (step 6 split into 6.1/6.2/6.3 sub-steps).
steps: List[Tuple[Union[int, float], str, str]] = [
(1, "discover", "Discovering project structure and tech stack"),
Expand Down Expand Up @@ -1069,6 +1077,7 @@ def _save_state() -> None:
pr_owner=pr_owner,
pr_repo=pr_repo,
pr_head_sha=current_pr_head_sha if pr_mode else None,
step_comments=sorted(step_comments_set),
)
github_comment_id = save_workflow_state(
cwd=cwd, issue_number=issue_number, workflow_type="checkup",
Expand All @@ -1078,6 +1087,43 @@ def _save_state() -> None:
github_comment_id=github_comment_id,
)

def _step_comment_key(step_num: Union[int, float], iteration: int = 1) -> int:
"""Project (step_num, iteration) -> deterministic non-negative int.

Encoding ``iteration * 10000 + int(round(step_num * 10))`` handles
fractional steps (6.1 -> 61) and the iterated fix-verify loop.
"""
return iteration * 10000 + int(round(float(step_num) * 10))

def _maybe_post_step_comment(
step_num: Union[int, float],
description: str,
step_output: str,
iteration: int = 1,
) -> None:
"""Post a trusted per-step success comment; log-and-continue on error."""
try:
report_body = extract_step_report(step_output)
if not report_body:
report_body = (
f"_Step {step_num} completed; no `<step_report>` block "
"returned by agent. Raw output retained in workflow state._"
)
comment_body = (
f"## Step {step_num}/{TOTAL_STEPS}: {description}\n\n{report_body}"
)
post_step_comment_once(
repo_owner=repo_owner,
repo_name=repo_name,
issue_number=issue_number,
step_num=_step_comment_key(step_num, iteration),
body=comment_body,
posted_steps=step_comments_set,
cwd=current_cwd,
)
except Exception as exc: # pylint: disable=broad-except
console.print(f"[yellow]post_step_comment_once failed: {exc}[/yellow]")

def _is_provider_failure(output: str) -> bool:
return "All agent providers failed" in output

Expand All @@ -1087,6 +1133,8 @@ def _handle_step_result(
output: str,
cost: float,
model: str,
description: str = "",
iteration: int = 1,
) -> Optional[Tuple[bool, str, float, str]]:
"""Process a step result — update context, save state.

Expand Down Expand Up @@ -1123,6 +1171,8 @@ def _handle_step_result(
step_outputs[step_key] = output
last_completed_step_to_save = step_num
consecutive_provider_failures = 0
if description:
_maybe_post_step_comment(step_num, description, output, iteration)
else:
step_outputs[step_key] = f"FAILED: {output}"
if _is_provider_failure(output):
Expand Down Expand Up @@ -1352,7 +1402,7 @@ def _post_pr_mode_final_report(final_step7_output: str) -> str:

success, output, cost, model = result

abort = _handle_step_result(step_num, success, output, cost, model)
abort = _handle_step_result(step_num, success, output, cost, model, description=description)
if abort is not None:
return abort

Expand Down Expand Up @@ -1395,7 +1445,7 @@ def _post_pr_mode_final_report(final_step7_output: str) -> str:
return (False, f"Missing prompt template: {template_name}", total_cost, last_model_used)

success, output, cost, model = result
abort = _handle_step_result(step_num, success, output, cost, model)
abort = _handle_step_result(step_num, success, output, cost, model, description=description)
if abort is not None:
return abort
if not success and _is_provider_failure(output):
Expand Down Expand Up @@ -1444,7 +1494,7 @@ def _post_pr_mode_final_report(final_step7_output: str) -> str:

success, output, cost, model = result
nofix_step7_output = output
abort = _handle_step_result(7, success, output, cost, model)
abort = _handle_step_result(7, success, output, cost, model, description=desc7)
if abort is not None:
return abort
else:
Expand Down Expand Up @@ -1585,7 +1635,10 @@ def _post_pr_mode_final_report(final_step7_output: str) -> str:
return (False, f"Missing prompt template: {tmpl_name}", total_cost, last_model_used)

success, output, cost, model = result
abort = _handle_step_result(step_num, success, output, cost, model)
abort = _handle_step_result(
step_num, success, output, cost, model,
description=description, iteration=fix_verify_iteration,
)
if abort is not None:
return abort

Expand Down Expand Up @@ -1854,7 +1907,7 @@ def _post_pr_mode_final_report(final_step7_output: str) -> str:
return (False, f"Missing prompt template: {template_name}", total_cost, last_model_used)

success, output, cost, model = result
abort = _handle_step_result(8, success, output, cost, model)
abort = _handle_step_result(8, success, output, cost, model, description=desc8)
if abort is not None:
return abort

Expand Down Expand Up @@ -1903,6 +1956,7 @@ def _build_state(
pr_owner: Optional[str] = None,
pr_repo: Optional[str] = None,
pr_head_sha: Optional[str] = None,
step_comments: Optional[List[int]] = None,
) -> Dict:
"""Build a serialisable state dict for persistence.

Expand Down Expand Up @@ -1938,4 +1992,5 @@ def _build_state(
"pr_owner": pr_owner,
"pr_repo": pr_repo,
"pr_head_sha": pr_head_sha,
"step_comments": list(step_comments) if step_comments else [],
}
Loading
Loading