fix: break delegate→plan-mode→approve infinite loop#42
Conversation
The Stop hook's continuation injection used `reason: "continue"` which the model interpreted as a fresh work request. Combined with orchestrator_stub's "all work MUST go via /workflow-orchestrator:delegate" rule and delegate.md's unconditional EnterPlanMode at Stage 0, this caused an infinite ExitPlanMode → Stop hook → continue → delegate → EnterPlanMode → ExitPlanMode loop after every plan approval. Three-layer fix: - Stop hook continuation message is now explicit anti-redelegation (hooks/stop/python_stop_hook.py). - Orchestrator stub adds an "Exception — continuation after plan approval" clause that overrides the routing rule during in-flight delegation (system-prompts/orchestrator_stub.md). - delegate.md gets a "RE-INVOCATION GUARD" section as the first content after frontmatter, telling the skill to skip Stage 0 if it just exited plan mode (commands/delegate.md). Layered guards because the loop is driven by model interpretation, not deterministic state — any one layer alone could be missed by prompt cache effects or session resumes; together they harden the path. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Review Summary by QodoBreak infinite delegate→plan-mode→approve loop with layered guards
WalkthroughsDescription• Breaks infinite delegate→plan-mode→approve loop via three-layer fix • Stop hook now emits explicit anti-redelegation directive instead of vague "continue" • Orchestrator stub adds exception clause overriding all-work→delegate rule post-approval • Delegate command includes re-invocation guard preventing Stage 0 re-entry after ExitPlanMode Diagramflowchart LR
A["ExitPlanMode"] -->|"Old: vague continue"| B["Stop Hook"]
B -->|"Loops back"| C["Delegate"]
C -->|"Re-enters"| D["EnterPlanMode"]
D -->|"Loops"| A
A -->|"New: explicit directive"| B2["Stop Hook<br/>Anti-redelegation"]
B2 -->|"Skip delegate"| E["Stage 1 Execution"]
C -->|"Guard checks"| F["Skip Stage 0"]
F -->|"Direct to"| E
G["Orchestrator Stub<br/>Exception clause"] -.->|"Overrides routing"| E
File Changes1. hooks/stop/python_stop_hook.py
|
Code Review by Qodo
|
| # This mimics ralph-wiggum's loop mechanism | ||
| output = { | ||
| "decision": "block", | ||
| "reason": "continue", | ||
| "systemMessage": "⚡ Workflow continuation: Proceeding to STAGE 1 execution.", | ||
| "reason": ( | ||
| "PLAN ALREADY APPROVED. Execute Stage 1 NOW directly from the existing " | ||
| "approved plan in context. DO NOT call /workflow-orchestrator:delegate. " | ||
| "DO NOT call EnterPlanMode. DO NOT re-enter plan mode. " | ||
| "Render the dependency graph and start spawning Wave 0 agents." | ||
| ), | ||
| "systemMessage": "⚡ Continuing to STAGE 1 execution (plan already approved).", | ||
| } | ||
| print(json.dumps(output)) # noqa: T201 | ||
| logger.debug("Output block decision with 'continue' reason") |
There was a problem hiding this comment.
1. Continuation sentinel removed 🐞 Bug ≡ Correctness
python_stop_hook's continuation path is documented as injecting a special "continue" sentinel, but the PR replaces the literal "reason": "continue" with a long instruction string while leaving comments/logging that still assume a "continue" reason. If any downstream logic (or Claude Code hook semantics) relies on the sentinel value, workflow auto-continuation may stop working or behave inconsistently.
Agent Prompt
## Issue description
`hooks/stop/python_stop_hook.py` documents and logs that it "injects continue" / outputs a block decision with a "continue" reason, but the emitted JSON now sets `reason` to a long instruction string. If the continuation mechanism depends on the sentinel value (`reason == "continue"`), this change can break workflow continuation.
## Issue Context
This stop-hook path is triggered by `.claude/state/workflow_continuation_needed.json` written by `remind_skill_continuation.py` to force the session to continue after `ExitPlanMode`.
## Fix Focus Areas
- hooks/stop/python_stop_hook.py[152-186]
- hooks/stop/python_stop_hook.py[1-10]
## Suggested fix
- Set `output["reason"]` back to the literal sentinel value (e.g. `"continue"`) to preserve any sentinel-based contract.
- Move the new anti-loop guidance into `systemMessage` (or another non-sentinel field if supported) so the model still receives the explicit instructions.
- Update the surrounding comments/log line so they match the actual output (or vice-versa).
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
Addresses Qodo automated review round 1 on PR #42. ## Finding #2 — stub continuation exception ambiguous (ADDRESSED) `system-prompts/orchestrator_stub.md`: - Narrow the exception trigger to the deterministic "PLAN ALREADY APPROVED" / "continuing to STAGE 1" continuation marker only. Dropped the "OR most recent tool call was ExitPlanMode" clause because it could spuriously match a fresh user prompt arriving right after a prior plan approval. - Explicitly whitelist the tools permitted in the exception path: `Agent`, `TaskCreate`/`TaskUpdate`/`TaskGet`, and `TeamCreate`/ `SendMessage` (for team mode). This removes ambiguity with the stub's `## Rule` section that otherwise restricts tools to Tasks API / AskUserQuestion / delegate. ## Finding #1 — continuation sentinel removed (INTENTIONALLY UNCHANGED) `hooks/stop/python_stop_hook.py` is left as-is. Per the Claude Code Stop hook docs, `reason` is the only field injected into the model's continuation context; `systemMessage` is user-facing only. Moving the anti-redelegation directive into `systemMessage` (as suggested) would make it invisible to the model and defeat Layer 1 of the loop fix. Repo-wide grep also confirms zero downstream readers of `reason` — the literal "continue" was label text, never a parsed sentinel. Full defense-of-design posted as a PR comment on #42. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Thanks for the review! Re: #1 Continuation sentinel removed — the suggested swap would actually defeat Layer 1 of the fix. Per the Claude Code Stop hook docs:
Repo-wide grep also confirms there is no downstream code that parses Layer 1 is intentional: long directive in Re: #2 Stub continuation exception ambiguous — addressed in commit d3d0b6a. Trigger narrowed to the deterministic continuation marker only, and the 🤖 Generated with Claude Code |
|
/agentic_review |
|
Persistent review updated to latest commit d3d0b6a |
|
/agentic_review |
|
Persistent review updated to latest commit d3d0b6a |
Qodo round 2 flagged commands/delegate.md RE-INVOCATION GUARD as too broad: the "most recent tool call was ExitPlanMode" heuristic could misroute a fresh task invocation after a prior plan-mode exit, causing it to skip Stage 0 and re-execute a stale plan. Narrowed to match the Layer 2 fix (d3d0b6a): guard now triggers only on the deterministic Stop-hook continuation marker ("PLAN ALREADY APPROVED" / "continuing to STAGE 1"). Also added explicit tool whitelist (Agent, TaskCreate/TaskUpdate/TaskGet, TeamCreate/SendMessage) for consistency with the orchestrator stub exception path. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
/agentic_review |
|
Persistent review updated to latest commit 2352603 |
| # This mimics ralph-wiggum's loop mechanism | ||
| output = { | ||
| "decision": "block", | ||
| "reason": "continue", | ||
| "systemMessage": "⚡ Workflow continuation: Proceeding to STAGE 1 execution.", | ||
| "reason": ( | ||
| "PLAN ALREADY APPROVED. Execute Stage 1 NOW directly from the existing " | ||
| "approved plan in context. DO NOT call /workflow-orchestrator:delegate. " | ||
| "DO NOT call EnterPlanMode. DO NOT re-enter plan mode. " | ||
| "Render the dependency graph and start spawning Wave 0 agents." | ||
| ), | ||
| "systemMessage": "⚡ Continuing to STAGE 1 execution (plan already approved).", | ||
| } | ||
| print(json.dumps(output)) # noqa: T201 | ||
| logger.debug("Output block decision with 'continue' reason") |
There was a problem hiding this comment.
1. python_stop_hook.py hard-blocks tools 📘 Rule violation ☼ Reliability
The modified stop hook emits a hard-blocking response (decision: block) as part of its continuation behavior. This violates the policy that only python_posttooluse_hook.py may hard-block tool execution.
Agent Prompt
## Issue description
`hooks/stop/python_stop_hook.py` emits a hard-blocking decision (`decision: block`) in the continuation path, but policy allows only `python_posttooluse_hook.py` to hard-block tool execution.
## Issue Context
The PR updated the continuation `reason`/`systemMessage` text but left the stop hook in a hard-blocking mode. Compliance requires all other hooks to be advisory/non-blocking.
## Fix Focus Areas
- hooks/stop/python_stop_hook.py[172-186]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
remind_skill_continuation.pywritesworkflow_continuation_needed.json→ stop hook emitsdecision: block, reason: "continue"→ model sees orchestrator stub rule ("all work MUST go through /workflow-orchestrator:delegate") → re-invokes delegate → re-enters plan mode → regenerates same plan → loops forever.hooks/PostToolUse/remind_skill_continuation.pyunconditionally signals continuation on ExitPlanMode, (2)hooks/stop/python_stop_hook.pyemitted a vague"continue"reason that didn't disambiguate the next action, (3)system-prompts/orchestrator_stub.mdrule + lack of entry guard incommands/delegate.mdcaused the synthetic continuation to re-route through delegate.hooks/stop/python_stop_hook.py): Replace vaguereason: "continue"with explicit anti-redelegation directive ("PLAN ALREADY APPROVED. Execute Stage 1 NOW… DO NOT call /workflow-orchestrator:delegate. DO NOT call EnterPlanMode…").system-prompts/orchestrator_stub.md): Add "Exception — continuation after plan approval" section between## Ruleand## Team Mode, telling the model the all-work-→-delegate rule does NOT apply mid-delegation.commands/delegate.md): Insert "RE-INVOCATION GUARD (READ FIRST)" section immediately after frontmatter, catching the loop at the delegate skill's own entry point.Test plan
git status -sb→ only the three modified files stagedgit diff --stat→ Layer 1/2/3 each show as expecteduvx ruff format --check hooks/stop/python_stop_hook.py→ passuvx ruff check --select F,E711,E712,UP006,UP007,UP035,UP037,T201,S hooks/stop/python_stop_hook.py→ passuvx pyright hooks/stop/python_stop_hook.py→ pass (0 errors)commands/delegate.mdlines 1-30 → guard is FIRST content after frontmattersystem-prompts/orchestrator_stub.md→ Exception section present, after## Rule, before## Team Modehooks/stop/python_stop_hook.pyfor"reason": "continue"→ no match~/dev/agentmem) after reinstalling the plugin — cannot be reproduced inside the session that authored the fix.🤖 Generated with Claude Code