diff --git a/apps/decodex/src/agent/tracker_tool_bridge/tests/mutation/dispatch.rs b/apps/decodex/src/agent/tracker_tool_bridge/tests/mutation/dispatch.rs index 847892483..3eeb6fe7f 100644 --- a/apps/decodex/src/agent/tracker_tool_bridge/tests/mutation/dispatch.rs +++ b/apps/decodex/src/agent/tracker_tool_bridge/tests/mutation/dispatch.rs @@ -1023,7 +1023,7 @@ fn rejects_manual_attention_comments_with_private_or_unsupported_fields() { } #[test] -fn rejects_manual_attention_comment_with_runtime_owned_retryable_error_class() { +fn rejects_manual_attention_comment_with_runtime_owned_error_class() { for error_class in [ "retryable_execution_failure", "repo_gate_verify_failed", @@ -1072,7 +1072,7 @@ fn rejects_manual_attention_comment_with_runtime_owned_retryable_error_class() { assert!(matches!( response.content_items.as_slice(), [DynamicToolContentItem::InputText { text }] - if text.contains("cannot use runtime-owned retryable error class") + if text.contains("cannot use runtime-owned error class") && text.contains(error_class) )); } diff --git a/apps/decodex/src/agent/tracker_tool_bridge/tools.rs b/apps/decodex/src/agent/tracker_tool_bridge/tools.rs index 9c47bdc0f..10d6224c2 100644 --- a/apps/decodex/src/agent/tracker_tool_bridge/tools.rs +++ b/apps/decodex/src/agent/tracker_tool_bridge/tools.rs @@ -2214,7 +2214,7 @@ fn validate_manual_attention_error_class(error_class: &str) -> Result<(), String if is_runtime_owned_manual_attention_error_class(error_class) { return Err(format!( - "`{ISSUE_COMMENT_TOOL_NAME}` kind `{COMMENT_KIND_MANUAL_ATTENTION}` cannot use runtime-owned retryable error class `{error_class}`; keep repairing or retrying the lane, or use a human-owned blocker class only when automation cannot clear the blocker." + "`{ISSUE_COMMENT_TOOL_NAME}` kind `{COMMENT_KIND_MANUAL_ATTENTION}` cannot use runtime-owned error class `{error_class}`; keep repairing, retrying, or letting Decodex retain the lane, and use a human-owned blocker class only when automation cannot clear the blocker." )); } diff --git a/apps/decodex/src/orchestrator/daemon.rs b/apps/decodex/src/orchestrator/daemon.rs index d00653399..00032bef4 100644 --- a/apps/decodex/src/orchestrator/daemon.rs +++ b/apps/decodex/src/orchestrator/daemon.rs @@ -7,6 +7,12 @@ enum RetryEntryRetentionDecision { Block, } +enum ChildExitPhaseGoalRecovery { + None, + Continuation(PhaseGoalRecoveryContinuation), + Terminalized, +} + struct ChildExitRetrySchedule<'a> { issue_id: &'a str, run_id: &'a str, @@ -1045,7 +1051,7 @@ where } fn schedule_retry_after_child_exit( - context: ChildExitRetryContext<'_, T>, + mut context: ChildExitRetryContext<'_, T>, child: ChildRunRef<'_>, #[cfg(test)] _retry_project_slug: &str, @@ -1087,8 +1093,7 @@ where return Ok(()); }; - let continuation_pending = - exit_status.success() && run_attempt.status() == CONTINUATION_PENDING_RUN_STATUS; + let continuation_pending = exit_status.success() && run_attempt.status() == CONTINUATION_PENDING_RUN_STATUS; if !exit_status.success() && run_attempt.status() != "failed" { clear_retry_schedule_and_release(context.retry_queue, context.state_store, issue_id)?; @@ -1110,16 +1115,18 @@ where return Ok(()); } - let recovered_phase_goal_continuation = if !exit_status.success() { - maybe_recover_child_exit_active_phase_goal_continuation( - &context, - &issue, - child, - initial_issue_state, - dispatch_mode, - )? - } else { - None + let recovered_phase_goal_continuation = match recover_child_exit_phase_goal( + &mut context, + &issue, + child, + issue_id, + initial_issue_state, + dispatch_mode, + exit_status.success(), + )? { + ChildExitPhaseGoalRecovery::None => None, + ChildExitPhaseGoalRecovery::Continuation(recovery) => Some(recovery), + ChildExitPhaseGoalRecovery::Terminalized => return Ok(()), }; let (kind, attempt, continuation_initial_issue_state) = if continuation_pending { ( @@ -1219,13 +1226,44 @@ fn queue_child_exit_retry( Ok(()) } +fn recover_child_exit_phase_goal( + context: &mut ChildExitRetryContext<'_, T>, + issue: &TrackerIssue, + child: ChildRunRef<'_>, + issue_id: &str, + initial_issue_state: &str, + dispatch_mode: IssueDispatchMode, + exit_success: bool, +) -> Result +where + T: IssueTracker, +{ + if exit_success { + return Ok(ChildExitPhaseGoalRecovery::None); + } + + let recovery = maybe_recover_child_exit_active_phase_goal_continuation( + context, + issue, + child, + initial_issue_state, + dispatch_mode, + )?; + + if matches!(recovery, ChildExitPhaseGoalRecovery::Terminalized) { + clear_retry_schedule_and_release(context.retry_queue, context.state_store, issue_id)?; + } + + Ok(recovery) +} + fn maybe_recover_child_exit_active_phase_goal_continuation( context: &ChildExitRetryContext<'_, T>, issue: &TrackerIssue, child: ChildRunRef<'_>, initial_issue_state: &str, dispatch_mode: IssueDispatchMode, -) -> Result> +) -> Result where T: IssueTracker, { @@ -1242,13 +1280,28 @@ where run_id: child.run_id.to_owned(), retry_budget_base: 0, }; - let recovery = recover_active_phase_goal_continuation( + let recovery = match recover_active_phase_goal_continuation( context.project, context.workflow, context.state_store, &issue_run, "child_exit_failed", - )?; + ) { + Ok(recovery) => recovery, + Err(error) if run_failure_requires_terminal_attention(&error) => { + handle_failure( + context.tracker, + context.project, + context.workflow, + context.state_store, + &issue_run, + &error, + )?; + + return Ok(ChildExitPhaseGoalRecovery::Terminalized); + }, + Err(error) => return Err(error), + }; if let Some(recovery) = &recovery { tracing::warn!( @@ -1263,7 +1316,9 @@ where ); } - Ok(recovery) + Ok(recovery.map_or(ChildExitPhaseGoalRecovery::None, |recovery| { + ChildExitPhaseGoalRecovery::Continuation(recovery) + })) } fn child_exit_retry_retention_decision( diff --git a/apps/decodex/src/orchestrator/git_ops.rs b/apps/decodex/src/orchestrator/git_ops.rs index 67c7effe8..b8777db2b 100644 --- a/apps/decodex/src/orchestrator/git_ops.rs +++ b/apps/decodex/src/orchestrator/git_ops.rs @@ -41,8 +41,8 @@ mod repo_gate_failure { fn disposition(self) -> RepoGateFailureDisposition { match self { Self::CanonicalizeCommandFailed - | Self::VerifyCommandFailed - | Self::TrackedRewritesLeft => RepoGateFailureDisposition::ContinueRepair, + | Self::VerifyCommandFailed => RepoGateFailureDisposition::ContinueRepair, + Self::TrackedRewritesLeft => RepoGateFailureDisposition::NeedsHumanAttention, Self::GitLockContention => RepoGateFailureDisposition::RetryAfterBackoff, Self::CommandSpawnFailed | Self::CleanlinessCheckFailed => { RepoGateFailureDisposition::NeedsHumanAttention @@ -59,7 +59,7 @@ mod repo_gate_failure { "additional agent repair is required before repo verification can pass; decodex will retry automatically" }, Self::TrackedRewritesLeft => { - "additional agent repair is required to reconcile repo-gate tracked rewrites before handoff; decodex will retry automatically" + "automatic retry is stopped because the repo gate left tracked rewrites after completing; inspect the retained worktree manually" }, Self::GitLockContention => { "another Git process appears to hold `.git/index.lock`; decodex will wait briefly, refresh lane state, and retry automatically" @@ -82,7 +82,7 @@ mod repo_gate_failure { "inspect the worktree, repair the repo verification failure manually, {recovery_gate}" ), Self::TrackedRewritesLeft => format!( - "inspect the worktree, reconcile the tracked rewrites left by the repo gate manually, {recovery_gate}" + "inspect the retained worktree, decide whether the tracked rewrites are in scope, then finish validation and PR handoff or reset the patch manually, {recovery_gate}" ), Self::GitLockContention => format!( "inspect the worktree for an active or stale `.git/index.lock` holder, clear the Git lock contention manually, {recovery_gate}" diff --git a/apps/decodex/src/orchestrator/prompting.rs b/apps/decodex/src/orchestrator/prompting.rs index b84b5d47e..bcd7bae1c 100644 --- a/apps/decodex/src/orchestrator/prompting.rs +++ b/apps/decodex/src/orchestrator/prompting.rs @@ -184,7 +184,7 @@ where ); let tracker_contract = match issue_run.dispatch_mode { IssueDispatchMode::ReviewRepair => format!( - "Tracker tool contract\n- You own issue-scoped tracker writes for `{issue}` on retained PR `{pr_url}`.\n- This run resumes an existing `{success}` lane. Do not move the issue back to `{in_progress}` and do not call `{review_handoff_tool}`.\n- Update `{progress_checkpoint_tool}` whenever the execution phase, focus, next action, blockers, evidence, or verification state changes materially.\n{decodex_review_guidance}- For each actionable review item on `{pr_url}`, including non-thread review summaries, validate the claim against the codebase, tests, and requirements before changing code, and keep pushback or clarification threads open until the repaired head is ready.\n- If this run was triggered by retained landing fallback, handle only the implementation-shaped blocker such as branch sync, conflict resolution, ambiguous mergeability, or repository-specific recovery. Do not merge or land the PR yourself.\n{repair_architecture_guidance}- Repair the current PR head on branch `{branch}`, run the repository validation needed to justify the repaired head, and push the repaired head.\n- Treat failures from repo-native `canonicalize_commands`, `verify_commands`, or tracked rewrites left by that repo gate as continued repair by default: keep fixing the lane and rerun the gate instead of taking `manual_attention` unless the blocker is clearly toolchain, environment, or operator-owned.\n{github_review_guidance}- After the repaired head is pushed, reply in-thread for every addressed comment and resolve only the GitHub review threads whose fixes landed and verified on the repaired head.\n{completion_guidance}- {manual_attention_guidance}\n{retained_tail_guidance}- Do not report the run as complete or treat `{progress_checkpoint_tool}` as terminal completion until `{terminal_finalize_tool}` succeeds.{continuation_guidance}\n- Never write to any other issue.", + "Tracker tool contract\n- You own issue-scoped tracker writes for `{issue}` on retained PR `{pr_url}`.\n- This run resumes an existing `{success}` lane. Do not move the issue back to `{in_progress}` and do not call `{review_handoff_tool}`.\n- Update `{progress_checkpoint_tool}` whenever the execution phase, focus, next action, blockers, evidence, or verification state changes materially.\n{decodex_review_guidance}- For each actionable review item on `{pr_url}`, including non-thread review summaries, validate the claim against the codebase, tests, and requirements before changing code, and keep pushback or clarification threads open until the repaired head is ready.\n- If this run was triggered by retained landing fallback, handle only the implementation-shaped blocker such as branch sync, conflict resolution, ambiguous mergeability, or repository-specific recovery. Do not merge or land the PR yourself.\n{repair_architecture_guidance}- Repair the current PR head on branch `{branch}`, run the repository validation needed to justify the repaired head, and push the repaired head.\n- Treat repo-native `canonicalize_commands` and `verify_commands` failures as continued repair: keep fixing the lane and rerun the gate. If the repo gate completes but leaves tracked rewrites, do not infer file semantics or widen scope; leave the retained worktree for operator review unless the issue-owned fix already makes the gate clean.\n{github_review_guidance}- After the repaired head is pushed, reply in-thread for every addressed comment and resolve only the GitHub review threads whose fixes landed and verified on the repaired head.\n{completion_guidance}- {manual_attention_guidance}\n{retained_tail_guidance}- Do not report the run as complete or treat `{progress_checkpoint_tool}` as terminal completion until `{terminal_finalize_tool}` succeeds.{continuation_guidance}\n- Never write to any other issue.", issue = issue_run.issue.identifier, pr_url = recorded_pr_url.unwrap_or("(missing review handoff marker)"), progress_checkpoint_tool = ISSUE_PROGRESS_CHECKPOINT_TOOL_NAME, @@ -218,7 +218,7 @@ where continuation_guidance = continuation_guidance, ), _ => format!( - "Tracker tool contract\n- You own issue-scoped tracker writes for `{issue}`.\n- At the start of execution, call `{transition_tool}` to move the issue to `{in_progress}`. Decodex already records the run-start Linear ledger, so do not add a separate start comment.\n- Update `{progress_checkpoint_tool}` whenever the execution phase, focus, next action, blockers, evidence, or verification state changes materially.\n{decodex_review_guidance}- Treat failures from repo-native `canonicalize_commands`, `verify_commands`, or tracked rewrites left by that repo gate as continued repair by default: keep fixing the lane and rerun the gate instead of taking `manual_attention` unless the blocker is clearly toolchain, environment, or operator-owned.\n- When the implementation is ready, commit the lane, push branch `{branch}`, and create or update a non-draft PR titled `{pr_title}` for that branch.\n{completion_guidance}- {manual_attention_guidance}\n- Do not move the issue directly to `{success}` with `{transition_tool}`. `decodex` will complete the success writeback only after its own validation passes.\n- Do not report the run as complete or treat `{progress_checkpoint_tool}` as terminal completion until `{terminal_finalize_tool}` succeeds.{continuation_guidance}\n- Never write to any other issue.", + "Tracker tool contract\n- You own issue-scoped tracker writes for `{issue}`.\n- At the start of execution, call `{transition_tool}` to move the issue to `{in_progress}`. Decodex already records the run-start Linear ledger, so do not add a separate start comment.\n- Update `{progress_checkpoint_tool}` whenever the execution phase, focus, next action, blockers, evidence, or verification state changes materially.\n{decodex_review_guidance}- Treat repo-native `canonicalize_commands` and `verify_commands` failures as continued repair: keep fixing the lane and rerun the gate. If the repo gate completes but leaves tracked rewrites, do not infer file semantics or widen scope; leave the retained worktree for operator review unless the issue-owned fix already makes the gate clean.\n- When the implementation is ready, commit the lane, push branch `{branch}`, and create or update a non-draft PR titled `{pr_title}` for that branch.\n{completion_guidance}- {manual_attention_guidance}\n- Do not move the issue directly to `{success}` with `{transition_tool}`. `decodex` will complete the success writeback only after its own validation passes.\n- Do not report the run as complete or treat `{progress_checkpoint_tool}` as terminal completion until `{terminal_finalize_tool}` succeeds.{continuation_guidance}\n- Never write to any other issue.", issue = issue_run.issue.identifier, transition_tool = ISSUE_TRANSITION_TOOL_NAME, progress_checkpoint_tool = ISSUE_PROGRESS_CHECKPOINT_TOOL_NAME, @@ -287,7 +287,7 @@ where match issue_run.dispatch_mode { IssueDispatchMode::ReviewRepair => format!( - "Continue retained review repair for Linear issue {identifier}: {title}\n\nDescription:\n{description}\n\nCurrent PR:\n- `{pr_url}`\n\nExecution checklist:\n- Resume from the current branch and PR state in this worktree. Do not move the issue back to `{in_progress}`.\n- Update `{progress_checkpoint_tool}` whenever the execution phase, focus, next action, blockers, evidence, or verification state changes materially.\n{decodex_review_guidance}- Read the current review feedback on `{pr_url}`, including non-thread review summaries, validate each actionable claim against the codebase, tests, and requirements, fix only the verified issues on branch `{branch}`, and keep scope limited to the outstanding retained repair.\n- If the lane is here because retained landing was not a deterministic clean path, handle only the branch sync, conflict resolution, ambiguous mergeability, or repository-specific recovery needed to make the PR clean again. Do not merge or land the PR yourself.\n- Leave pushback or clarification threads open until the repaired head is ready.\n{repair_architecture_guidance}- Treat failures from repo-native `canonicalize_commands`, `verify_commands`, or tracked rewrites left by that repo gate as continued repair by default: keep fixing the lane and rerun the gate instead of taking `manual_attention` unless the blocker is clearly toolchain, environment, or operator-owned.\n- Run the repository validation needed to justify the repaired head.\n- Commit the repair and push the same branch.\n{github_review_guidance}- After the repaired head is pushed, reply in-thread for every addressed comment and resolve only the GitHub review threads whose fixes landed and verified on the repaired head.\n{completion_guidance}- {manual_attention_guidance}\n- Keep the issue in `{success}` and do not treat `{progress_checkpoint_tool}` as terminal completion until `{terminal_finalize_tool}` succeeds.{continuation_guidance}", + "Continue retained review repair for Linear issue {identifier}: {title}\n\nDescription:\n{description}\n\nCurrent PR:\n- `{pr_url}`\n\nExecution checklist:\n- Resume from the current branch and PR state in this worktree. Do not move the issue back to `{in_progress}`.\n- Update `{progress_checkpoint_tool}` whenever the execution phase, focus, next action, blockers, evidence, or verification state changes materially.\n{decodex_review_guidance}- Read the current review feedback on `{pr_url}`, including non-thread review summaries, validate each actionable claim against the codebase, tests, and requirements, fix only the verified issues on branch `{branch}`, and keep scope limited to the outstanding retained repair.\n- If the lane is here because retained landing was not a deterministic clean path, handle only the branch sync, conflict resolution, ambiguous mergeability, or repository-specific recovery needed to make the PR clean again. Do not merge or land the PR yourself.\n- Leave pushback or clarification threads open until the repaired head is ready.\n{repair_architecture_guidance}- Treat repo-native `canonicalize_commands` and `verify_commands` failures as continued repair: keep fixing the lane and rerun the gate. If the repo gate completes but leaves tracked rewrites, do not infer file semantics or widen scope; leave the retained worktree for operator review unless the issue-owned fix already makes the gate clean.\n- Run the repository validation needed to justify the repaired head.\n- Commit the repair and push the same branch.\n{github_review_guidance}- After the repaired head is pushed, reply in-thread for every addressed comment and resolve only the GitHub review threads whose fixes landed and verified on the repaired head.\n{completion_guidance}- {manual_attention_guidance}\n- Keep the issue in `{success}` and do not treat `{progress_checkpoint_tool}` as terminal completion until `{terminal_finalize_tool}` succeeds.{continuation_guidance}", identifier = issue.identifier, title = issue.title, description = description, @@ -321,7 +321,7 @@ where continuation_guidance = continuation_guidance, ), _ => format!( - "Resolve Linear issue {identifier}: {title}\n\nDescription:\n{description}\n\n{recovery_context}Execution checklist:\n- Move the issue to `{in_progress}` with `{transition_tool}`. Decodex already records the run-start Linear ledger, so do not leave a separate start comment.\n- Update `{progress_checkpoint_tool}` whenever the execution phase, focus, next action, blockers, evidence, or verification state changes materially.\n- Keep discovery bounded to the minimal implementation files needed for this issue; defer broader docs or upstream reading unless a concrete ambiguity blocks the change.\n- Implement the fix in the current worktree.\n{decodex_review_guidance}- Treat failures from repo-native `canonicalize_commands`, `verify_commands`, or tracked rewrites left by that repo gate as continued repair by default: keep fixing the lane and rerun the gate instead of taking `manual_attention` unless the blocker is clearly toolchain, environment, or operator-owned.\n- Run the repository validation needed to justify a reviewable PR.\n- Commit the lane, push branch `{branch}`, and create or update a non-draft PR titled `{pr_title}` for that branch.\n{completion_guidance}- {manual_attention_guidance}\n- Do not move the issue directly to `{success}` with `{transition_tool}`; `decodex` will finish that writeback after its own validation passes.\n- Do not report the run as complete or treat `{progress_checkpoint_tool}` as terminal completion until `{terminal_finalize_tool}` succeeds.{continuation_guidance}", + "Resolve Linear issue {identifier}: {title}\n\nDescription:\n{description}\n\n{recovery_context}Execution checklist:\n- Move the issue to `{in_progress}` with `{transition_tool}`. Decodex already records the run-start Linear ledger, so do not leave a separate start comment.\n- Update `{progress_checkpoint_tool}` whenever the execution phase, focus, next action, blockers, evidence, or verification state changes materially.\n- Keep discovery bounded to the minimal implementation files needed for this issue; defer broader docs or upstream reading unless a concrete ambiguity blocks the change.\n- Implement the fix in the current worktree.\n{decodex_review_guidance}- Treat repo-native `canonicalize_commands` and `verify_commands` failures as continued repair: keep fixing the lane and rerun the gate. If the repo gate completes but leaves tracked rewrites, do not infer file semantics or widen scope; leave the retained worktree for operator review unless the issue-owned fix already makes the gate clean.\n- Run the repository validation needed to justify a reviewable PR.\n- Commit the lane, push branch `{branch}`, and create or update a non-draft PR titled `{pr_title}` for that branch.\n{completion_guidance}- {manual_attention_guidance}\n- Do not move the issue directly to `{success}` with `{transition_tool}`; `decodex` will finish that writeback after its own validation passes.\n- Do not report the run as complete or treat `{progress_checkpoint_tool}` as terminal completion until `{terminal_finalize_tool}` succeeds.{continuation_guidance}", identifier = issue.identifier, title = issue.title, description = description, @@ -371,7 +371,7 @@ fn build_continuation_user_input( match dispatch_mode { IssueDispatchMode::ReviewRepair => format!( - "Continue retained review repair for Linear issue {identifier} in the current thread and worktree.\n\nContinuation checklist:\n- Resume from the current repository state and outstanding review feedback or retained landing fallback on `{pr_url}`.\n- Keep changes scoped to the same retained review lane and do not move the issue out of `{success}`.\n{decodex_review_guidance}- Validate each actionable review claim against the codebase, tests, and requirements before changing code, and keep pushback or clarification threads open until the repaired head is ready.\n- If the blocker is landing fallback, repair only the branch sync, conflict, ambiguous mergeability, or repository-specific recovery issue; do not merge or land the PR yourself.\n- Treat failures from repo-native `canonicalize_commands`, `verify_commands`, or tracked rewrites left by that repo gate as continued repair by default: keep fixing the lane and rerun the gate instead of taking `manual_attention` unless the blocker is clearly toolchain, environment, or operator-owned.\n- If the repaired head is ready, push it.\n{github_review_guidance}- After the repaired head is pushed, reply in-thread for every addressed comment and resolve only the GitHub review threads whose fixes landed and verified on the repaired head.\n{completion_guidance}- {manual_attention_guidance}\n- If more work still remains after this turn, you may end the turn without terminal finalization and Decodex will decide whether to continue.", + "Continue retained review repair for Linear issue {identifier} in the current thread and worktree.\n\nContinuation checklist:\n- Resume from the current repository state and outstanding review feedback or retained landing fallback on `{pr_url}`.\n- Keep changes scoped to the same retained review lane and do not move the issue out of `{success}`.\n{decodex_review_guidance}- Validate each actionable review claim against the codebase, tests, and requirements before changing code, and keep pushback or clarification threads open until the repaired head is ready.\n- If the blocker is landing fallback, repair only the branch sync, conflict, ambiguous mergeability, or repository-specific recovery issue; do not merge or land the PR yourself.\n- Treat repo-native `canonicalize_commands` and `verify_commands` failures as continued repair: keep fixing the lane and rerun the gate. If the repo gate completes but leaves tracked rewrites, do not infer file semantics or widen scope; leave the retained worktree for operator review unless the issue-owned fix already makes the gate clean.\n- If the repaired head is ready, push it.\n{github_review_guidance}- After the repaired head is pushed, reply in-thread for every addressed comment and resolve only the GitHub review threads whose fixes landed and verified on the repaired head.\n{completion_guidance}- {manual_attention_guidance}\n- If more work still remains after this turn, you may end the turn without terminal finalization and Decodex will decide whether to continue.", identifier = issue.identifier, pr_url = recorded_pr_url.unwrap_or("(missing review handoff marker)"), success = success_state, @@ -397,7 +397,7 @@ fn build_continuation_user_input( manual_attention_guidance = repair_manual_attention_guidance, ), _ => format!( - "Continue working on Linear issue {identifier} in the current thread and worktree.\n\nContinuation checklist:\n- Resume from the current repository state instead of restarting broad discovery.\n- Keep changes scoped to the same issue lane.\n{decodex_review_guidance}- Treat failures from repo-native `canonicalize_commands`, `verify_commands`, or tracked rewrites left by that repo gate as continued repair by default: keep fixing the lane and rerun the gate instead of taking `manual_attention` unless the blocker is clearly toolchain, environment, or operator-owned.\n{completion_guidance}- {manual_attention_guidance}\n- If more work still remains after this turn, you may end the turn without terminal finalization and Decodex will decide whether to continue.", + "Continue working on Linear issue {identifier} in the current thread and worktree.\n\nContinuation checklist:\n- Resume from the current repository state instead of restarting broad discovery.\n- Keep changes scoped to the same issue lane.\n{decodex_review_guidance}- Treat repo-native `canonicalize_commands` and `verify_commands` failures as continued repair: keep fixing the lane and rerun the gate. If the repo gate completes but leaves tracked rewrites, do not infer file semantics or widen scope; leave the retained worktree for operator review unless the issue-owned fix already makes the gate clean.\n{completion_guidance}- {manual_attention_guidance}\n- If more work still remains after this turn, you may end the turn without terminal finalization and Decodex will decide whether to continue.", identifier = issue.identifier, manual_attention_guidance = handoff_manual_attention_guidance, decodex_review_guidance = build_handoff_continuation_review_guidance( diff --git a/apps/decodex/src/orchestrator/tests/intake/workflow_reload.rs b/apps/decodex/src/orchestrator/tests/intake/workflow_reload.rs index 9470d94a7..b00e23e0a 100644 --- a/apps/decodex/src/orchestrator/tests/intake/workflow_reload.rs +++ b/apps/decodex/src/orchestrator/tests/intake/workflow_reload.rs @@ -181,7 +181,7 @@ fn expected_developer_instructions( )); sections.push(format!( - "Tracker tool contract\n- You own issue-scoped tracker writes for `{issue}`.\n- At the start of execution, call `{transition_tool}` to move the issue to `{in_progress}`. Decodex already records the run-start Linear ledger, so do not add a separate start comment.\n- Update `{progress_checkpoint_tool}` whenever the execution phase, focus, next action, blockers, evidence, or verification state changes materially.\n- Self Check: Review your work repeatedly and fix any logic bugs until no new issues are found.\n- Decodex Review: request an independent fresh-context read-only review pass for the actual current diff and branch state. The reviewer must not edit files, push, land, or mutate tracker state.\n- Use the repo-native bounded review method from `WORKFLOW.md`: run the requirements pass and the adversarial reviewer pass against the current `HEAD`, including regression risk, missing tests, docs/config drift, migration fallout, operator-facing fallout, and mismatch with the accepted Loop/Decision Contract.\n- Validate reviewer comments before repair. Record accepted findings separately from rejected or non-actionable comments, fix only the smallest coherent owned batch, rerun verification, and re-read `HEAD` before deciding the normalized review status.\n- Every time the Decodex Review pass produces a result for the current head, call `{review_checkpoint_tool}` with reviewer `independent_fresh_context`, that normalized status, the exact current `HEAD` SHA, concise evidence, checklist notes, and structured accepted/rejected findings.\n- Treat failures from repo-native `canonicalize_commands`, `verify_commands`, or tracked rewrites left by that repo gate as continued repair by default: keep fixing the lane and rerun the gate instead of taking `manual_attention` unless the blocker is clearly toolchain, environment, or operator-owned.\n- When the implementation is ready, commit the lane, push branch `{branch}`, and create or update a non-draft PR titled `{pr_title}` for that branch.\n- Call `{review_handoff_tool}` only after the latest `{review_checkpoint_tool}` for this handoff phase and current `HEAD` is `clean`. Then call `{terminal_finalize_tool}` with path `review_handoff`.\n- If you determine the issue needs human attention, request label `{needs_attention}` with `{label_tool}`; that records manual-attention label intent only. Then call `issue_comment` with kind `manual_attention` and structured public fields (`error_class`, `next_action`, `blockers`, `evidence`; include `failed_command` and `raw_error` only when public-safe). Decodex applies the actual label only after that manual_attention comment validates. Use a human-owned blocker class; do not use runtime-owned retry/repair classes such as app-server timeout, transport, turn, dynamic-tool, or usage-limit failures; stalled-run detection; phase-goal terminal-path misses; repo-gate canonicalize, verify, tracked-rewrite, or git-lock failures; or generic retryable execution failures. Then call `{terminal_finalize_tool}` with path `manual_attention`. Do not speculate about capabilities you did not directly verify. Do not call `{review_handoff_tool}` in that case; `decodex` will stop the lane as a human-required failure without automatic retry.\n- Do not move the issue directly to `{success}` with `{transition_tool}`. `decodex` will complete the success writeback only after its own validation passes.\n- Do not report the run as complete or treat `{progress_checkpoint_tool}` as terminal completion until `{terminal_finalize_tool}` succeeds.{continuation_guidance}\n- Never write to any other issue.", + "Tracker tool contract\n- You own issue-scoped tracker writes for `{issue}`.\n- At the start of execution, call `{transition_tool}` to move the issue to `{in_progress}`. Decodex already records the run-start Linear ledger, so do not add a separate start comment.\n- Update `{progress_checkpoint_tool}` whenever the execution phase, focus, next action, blockers, evidence, or verification state changes materially.\n- Self Check: Review your work repeatedly and fix any logic bugs until no new issues are found.\n- Decodex Review: request an independent fresh-context read-only review pass for the actual current diff and branch state. The reviewer must not edit files, push, land, or mutate tracker state.\n- Use the repo-native bounded review method from `WORKFLOW.md`: run the requirements pass and the adversarial reviewer pass against the current `HEAD`, including regression risk, missing tests, docs/config drift, migration fallout, operator-facing fallout, and mismatch with the accepted Loop/Decision Contract.\n- Validate reviewer comments before repair. Record accepted findings separately from rejected or non-actionable comments, fix only the smallest coherent owned batch, rerun verification, and re-read `HEAD` before deciding the normalized review status.\n- Every time the Decodex Review pass produces a result for the current head, call `{review_checkpoint_tool}` with reviewer `independent_fresh_context`, that normalized status, the exact current `HEAD` SHA, concise evidence, checklist notes, and structured accepted/rejected findings.\n- Treat repo-native `canonicalize_commands` and `verify_commands` failures as continued repair: keep fixing the lane and rerun the gate. If the repo gate completes but leaves tracked rewrites, do not infer file semantics or widen scope; leave the retained worktree for operator review unless the issue-owned fix already makes the gate clean.\n- When the implementation is ready, commit the lane, push branch `{branch}`, and create or update a non-draft PR titled `{pr_title}` for that branch.\n- Call `{review_handoff_tool}` only after the latest `{review_checkpoint_tool}` for this handoff phase and current `HEAD` is `clean`. Then call `{terminal_finalize_tool}` with path `review_handoff`.\n- If you determine the issue needs human attention, request label `{needs_attention}` with `{label_tool}`; that records manual-attention label intent only. Then call `issue_comment` with kind `manual_attention` and structured public fields (`error_class`, `next_action`, `blockers`, `evidence`; include `failed_command` and `raw_error` only when public-safe). Decodex applies the actual label only after that manual_attention comment validates. Use a human-owned blocker class; do not use runtime-owned retry/repair classes such as app-server timeout, transport, turn, dynamic-tool, or usage-limit failures; stalled-run detection; phase-goal terminal-path misses; repo-gate canonicalize, verify, tracked-rewrite, or git-lock failures; or generic retryable execution failures. Then call `{terminal_finalize_tool}` with path `manual_attention`. Do not speculate about capabilities you did not directly verify. Do not call `{review_handoff_tool}` in that case; `decodex` will stop the lane as a human-required failure without automatic retry.\n- Do not move the issue directly to `{success}` with `{transition_tool}`. `decodex` will complete the success writeback only after its own validation passes.\n- Do not report the run as complete or treat `{progress_checkpoint_tool}` as terminal completion until `{terminal_finalize_tool}` succeeds.{continuation_guidance}\n- Never write to any other issue.", issue = issue_run.issue.identifier, transition_tool = ISSUE_TRANSITION_TOOL_NAME, label_tool = ISSUE_LABEL_ADD_TOOL_NAME, diff --git a/apps/decodex/src/orchestrator/tests/recovery/terminal_failures.rs b/apps/decodex/src/orchestrator/tests/recovery/terminal_failures.rs index 426d1a0f5..3744a87cf 100644 --- a/apps/decodex/src/orchestrator/tests/recovery/terminal_failures.rs +++ b/apps/decodex/src/orchestrator/tests/recovery/terminal_failures.rs @@ -236,6 +236,84 @@ fn terminal_failure_with_retained_tracked_changes_records_retained_partial_progr ); } +#[test] +fn repo_gate_tracked_rewrites_left_records_retained_partial_progress_without_retry() { + let (_temp_dir, config, workflow) = temp_project_layout(); + let active_label = tracker::automation_active_label(TEST_SERVICE_ID); + let issue = sample_issue("In Progress", &[active_label.as_str()]); + let tracker = FakeTracker::new(vec![issue.clone()]); + let state_store = StateStore::open_in_memory().expect("state store should open"); + let worktree_path = config.worktree_root().join("PUB-102"); + + git_status_success( + config.repo_root(), + &["worktree", "add", "-b", "x/pubfi-pub-102", ".worktrees/PUB-102", "main"], + ); + + fs::write(worktree_path.join("README.md"), "repo gate left tracked rewrites\n") + .expect("tracked worktree file should change"); + + let issue_run = IssueRunPlan { + issue: issue.clone(), + issue_state: String::from("In Progress"), + initial_issue_state: String::from("Todo"), + worktree: WorktreeSpec { + branch_name: String::from("x/pubfi-pub-102"), + issue_identifier: issue.identifier.clone(), + path: worktree_path, + reused_existing: true, + }, + retry_project_slug: issue.project_slug.clone().expect("sample issue should carry a project slug"), + dispatch_mode: IssueDispatchMode::Normal, + attempt_number: 1, + run_id: String::from("pub-102-attempt-1-123"), + retry_budget_base: 0, + }; + let error = Report::new(RepoGateFailure::new( + RepoGateFailureKind::TrackedRewritesLeft, + String::from("Repo gate verification left tracked-file rewrites."), + )); + + state_store + .record_run_attempt(&issue_run.run_id, &issue.id, issue_run.attempt_number, "failed") + .expect("run attempt should record"); + + orchestrator::handle_failure(&tracker, &config, &workflow, &state_store, &issue_run, &error) + .expect("tracked repo-gate rewrites should retain partial progress"); + + let comments = tracker.comments.borrow(); + + assert!(comments.iter().any(|comment| { + comment.contains("decodex retained partial progress and needs attention") + && comment.contains("partial_progress_retained") + && comment.contains("finish validation and PR handoff or reset the patch manually") + })); + assert!( + comments + .iter() + .all(|comment| !comment.contains("decodex run failed and will retry")), + "tracked repo-gate rewrites should not continue automatic retry" + ); + + let ledger_event = comments + .iter() + .find_map(|comment| records::parse_linear_execution_event_record(comment)) + .expect("retained partial progress should write a Linear execution event"); + + assert_eq!(ledger_event.event_type, "needs_attention"); + assert_eq!(ledger_event.error_class.as_deref(), Some("partial_progress_retained")); + assert_eq!(ledger_event.terminal_path.as_deref(), Some("retained_partial_progress")); + assert!( + ledger_event + .evidence + .as_deref() + .is_some_and(|evidence| evidence + .iter() + .any(|item| item.contains("Source failure class `repo_gate_tracked_rewrites_left`"))), + "retained progress evidence should preserve the source repo-gate failure class" + ); +} + #[test] fn retryable_runtime_failure_with_retained_tracked_changes_retries_before_attention() { let (_temp_dir, config, workflow) = temp_project_layout(); diff --git a/apps/decodex/src/orchestrator/tests/retry/scheduling.rs b/apps/decodex/src/orchestrator/tests/retry/scheduling.rs index 8216dd808..531d2b494 100644 --- a/apps/decodex/src/orchestrator/tests/retry/scheduling.rs +++ b/apps/decodex/src/orchestrator/tests/retry/scheduling.rs @@ -820,7 +820,7 @@ fn schedule_retry_after_child_exit_records_continuation_retry_for_clean_exit() { } #[test] -fn schedule_retry_after_child_exit_recovers_active_phase_goal_before_terminal_attention() { +fn schedule_retry_after_child_exit_terminalizes_active_phase_goal_tracked_rewrites() { let (_temp_dir, config, workflow) = temp_project_layout(); let issue = sample_service_owned_issue("In Progress"); let tracker = @@ -883,9 +883,8 @@ fn schedule_retry_after_child_exit_recovers_active_phase_goal_before_terminal_at IssueDispatchMode::Retry, exit_status, ) - .expect("active phase goal should recover into continuation"); + .expect("active phase goal tracked rewrites should terminalize cleanly"); - let entry = retry_queue.entries.get(&issue.id).expect("retry entry should exist for the issue"); let run_attempt = state_store .run_attempt(run_id) .expect("run attempt lookup should succeed") @@ -893,14 +892,20 @@ fn schedule_retry_after_child_exit_recovers_active_phase_goal_before_terminal_at let events = state_store .list_private_execution_events(TEST_SERVICE_ID, &issue.id, run_id, 3) .expect("private events should load"); + let comments = tracker.comments.borrow(); - assert_eq!(entry.kind, orchestrator::RetryKind::Continuation); - assert_eq!(entry.attempt, 3); - assert_eq!(run_attempt.status(), CONTINUATION_PENDING_RUN_STATUS); - assert!(tracker.comments.borrow().is_empty()); + assert!(!retry_queue.entries.contains_key(&issue.id)); + assert_eq!(run_attempt.status(), "failed"); assert!(events.iter().any(|event| { - event.event_type() == "phase_goal_recovery" - && event.payload()["payload"]["sourceErrorClass"] == "child_exit_failed" + event.event_type() == "phase_goal_transition" + && event.payload()["signal"] == "validation_fail" + && event.payload()["payload"]["disposition"] == "needs_human_attention" + })); + assert!(events.iter().all(|event| event.event_type() != "phase_goal_recovery")); + assert!(comments.iter().any(|comment| { + comment.contains("decodex retained partial progress and needs attention") + && comment.contains("partial_progress_retained") + && comment.contains("Source failure class `repo_gate_tracked_rewrites_left`") })); } diff --git a/apps/decodex/src/orchestrator/tests/runtime/repo_gate.rs b/apps/decodex/src/orchestrator/tests/runtime/repo_gate.rs index 01e3bcf60..8473f1efb 100644 --- a/apps/decodex/src/orchestrator/tests/runtime/repo_gate.rs +++ b/apps/decodex/src/orchestrator/tests/runtime/repo_gate.rs @@ -22,7 +22,7 @@ fn repo_gate_rejects_dirty_tracked_files_left_by_canonicalize_commands() { assert_eq!(repo_gate_failure.error_class(), "repo_gate_tracked_rewrites_left"); assert_eq!( repo_gate_failure.disposition(), - orchestrator::RepoGateFailureDisposition::ContinueRepair + orchestrator::RepoGateFailureDisposition::NeedsHumanAttention ); assert_eq!(tracked_contents, "after\n"); assert!(tracked_status.contains("tracked.txt")); @@ -187,7 +187,7 @@ fn phase_goal_completion_runs_repo_gate_and_persists_handoff_phase() { } #[test] -fn active_phase_goal_failure_recovery_continues_to_repair_instead_of_attention() { +fn active_phase_goal_tracked_rewrites_stop_instead_of_repair_continuation() { let (_temp_dir, config, workflow) = temp_project_layout(); let repo_root = config.repo_root(); let issue = sample_issue("In Progress", &[tracker::automation_active_label(TEST_SERVICE_ID).as_str()]); @@ -231,33 +231,33 @@ fn active_phase_goal_failure_recovery_continues_to_repair_instead_of_attention() ) .expect("phase goal event should record"); - let summary = orchestrator::maybe_continue_after_active_phase_goal_recovery( + let error = orchestrator::maybe_continue_after_active_phase_goal_recovery( &config, &workflow, &state_store, &issue_run, &Report::msg("app server transport closed after local verification"), ) - .expect("phase goal recovery should not fail") - .expect("dirty active phase goal should recover to continuation"); + .expect_err("tracked repo-gate rewrites should stop phase-goal continuation"); let events = state_store .list_private_execution_events(TEST_SERVICE_ID, &issue.id, &issue_run.run_id, 1) .expect("private events should load"); + let repo_gate_failure = error + .downcast_ref::() + .expect("phase goal recovery should preserve repo-gate failure"); - assert!(summary.continuation_pending); + assert_eq!(repo_gate_failure.error_class(), "repo_gate_tracked_rewrites_left"); + assert_eq!( + repo_gate_failure.disposition(), + orchestrator::RepoGateFailureDisposition::NeedsHumanAttention + ); assert!(events.iter().any(|event| { event.event_type() == "phase_goal_transition" && event.payload()["signal"] == "validation_fail" - && event.payload()["payload"]["disposition"] == "continue_repair" - })); - assert!(events.iter().any(|event| { - event.event_type() == "phase_goal_next" - && event.payload()["phase"] == "repair_validation_failures" - })); - assert!(events.iter().any(|event| { - event.event_type() == "phase_goal_recovery" - && event.payload()["payload"]["nextPhase"] == "repair_validation_failures" + && event.payload()["payload"]["disposition"] == "needs_human_attention" })); + assert!(events.iter().all(|event| event.event_type() != "phase_goal_next")); + assert!(events.iter().all(|event| event.event_type() != "phase_goal_recovery")); } #[test] diff --git a/docs/spec/runtime.md b/docs/spec/runtime.md index 2a5ab4ff8..21c9bac40 100644 --- a/docs/spec/runtime.md +++ b/docs/spec/runtime.md @@ -417,10 +417,10 @@ When `decodex` runs the repo-native gate during `validating`, it must preserve t - `canonicalize_commands` non-zero exit: continued repair in the retained lane - `verify_commands` non-zero exit: continued repair in the retained lane -- repo gate leaves tracked-file rewrites behind after its commands complete: continued repair in the retained lane +- repo gate leaves tracked-file rewrites behind after its commands complete: retained partial progress that requires operator attention - repo-gate command spawn failures or tracked-file cleanliness inspection failures: human-attention failure path immediately -The continued-repair classes above are ordinary bounded churn: the coding agent should keep repairing code and rerun the repo gate rather than requesting `manual_attention` just because the gate has not passed yet. Human-attention exits remain reserved for environment, toolchain, or operator-owned blockers that the coding agent cannot clear from the retained worktree alone. +The continued-repair classes above are ordinary bounded churn: the coding agent should keep repairing code and rerun the repo gate rather than requesting `manual_attention` just because the gate has not passed yet. A tracked-rewrite residue after the gate commands have completed is different: Decodex must not infer repository file meaning, generated-artifact ownership, or fixture policy. It must preserve the retained worktree, write `error_class = "partial_progress_retained"` with the repo-gate source class in evidence, and stop until an operator decides whether to finish validation and handoff or reset the patch. Human-attention exits also remain reserved for environment, toolchain, or operator-owned blockers that the coding agent cannot clear from the retained worktree alone. Continued repair is still bounded by loop guardrails. For each retryable failure, Decodex records local `loop_guardrail_checkpoint` evidence and updates the @@ -432,8 +432,10 @@ the bounded recovery budget remains, Decodex records `architecture_recovery_star and schedules a materially different recovery strategy. Otherwise it records a terminal recovery reason such as `contract_boundary_required`, `external_dependency_required`, or `architecture_recovery_exhausted` and routes the -lane through the human-required failure path. Repo-gate failures record both -`validation_repeat` and `remaining_delta_unchanged` observations. Retryable failures +lane through the human-required failure path. Retryable repo-gate command failures +record both `validation_repeat` and `remaining_delta_unchanged` observations. +Tracked rewrites left by a completed repo gate bypass architecture recovery and are +retained for operator convergence instead. Retryable failures record `no_effective_diff` only when the retained worktree has no effective source delta: no tracked status/diff against `HEAD` and no ordinary untracked files after excluding Decodex runtime artifacts such as `.decodex-run-activity` and diff --git a/docs/spec/workflow-file.md b/docs/spec/workflow-file.md index c4ebf6dc6..28b9f9ee9 100644 --- a/docs/spec/workflow-file.md +++ b/docs/spec/workflow-file.md @@ -259,9 +259,9 @@ The runtime-owned failure model for this repo gate must distinguish at least the - a `verify_commands` entry failed - the repo gate completed its commands but left tracked-file rewrites behind -Those three classes are explicit continued-repair outcomes in normal retained-lane policy: the retained lane stays in implementation or review-repair flow until the coding agent repairs the worktree and reruns the gate, or until retry policy is exhausted. They are not, by themselves, automatic human-attention exits. +The first two classes are explicit continued-repair outcomes in normal retained-lane policy: the retained lane stays in implementation or review-repair flow until the coding agent repairs the worktree and reruns the gate, or until retry policy is exhausted. The tracked-rewrite residue class is a retained-partial-progress stop. Decodex must preserve the source repo-gate class as evidence, but it must not add project-specific artifact, generated-file, fixture, or snapshot semantics to decide which tracked rewrites are acceptable. -Human-attention exits are reserved for repo-gate failures that the coding agent cannot reasonably repair from the worktree alone, such as command-spawn failures, missing runtime prerequisites, or inability to inspect tracked-file cleanliness. When the runtime takes that path, prompts and tracker comments should preserve the repo-gate failure class instead of collapsing it into vague generic wording. +Human-attention exits are reserved for repo-gate failures that the coding agent cannot reasonably repair from the worktree alone, such as command-spawn failures, missing runtime prerequisites, inability to inspect tracked-file cleanliness, or tracked rewrites left after the gate completed. When the runtime takes that path, prompts and tracker comments should preserve the repo-gate source failure class instead of collapsing it into vague generic wording. Landing policy is no longer repository-configurable in the machine-readable workflow contract for this repo surface. For retained review landing, `decodex` applies a fixed strict policy: require green checks, require an up-to-date base branch, preserve commit-level history, use merge commits, and never squash or rebase.