# Anti-patterns

Canonical owner for anti-pattern guidance across all workflow families.

## Core anti-patterns

1. **Skipping fan-out/fan-in for non-trivial changes**: Do not implement directly without refinement when scope exceeds light-mode thresholds.
2. **Routing review-only work through local_implementation**: Review-only comparison, synthesis, or consolidation must use `refiner` agent, not `dev-loop` + `local_implementation`.
3. **Thin placeholder PR descriptions**: Always include change summary, scope/context, acceptance criteria, definition of done, non-goals, and `Closes #N` (tracker-backed; issue-less lightweight and plan-file promotion PRs instead follow [merge precondition 7](merge-preconditions.md#required-before-merge)).
4. **Merging directly to main without PR**: Use PR-based remote loop when practical.
5. **Duplicate worktree paths**: See `WORKTREE-DEDUPE` in [Worktree usage guidance](./worktree-guidance.md#coordination-and-collision-checks).
6. **Main-checkout mutation**: See `WORKTREE-DEFAULT-USE` in [Worktree usage guidance](./worktree-guidance.md#default-rule-use-a-worktree-for-mutating-local-work). A specific slip inside this family — an absolute-path `Edit`/`Write` that lands on the MAIN checkout while a worktree cycle is active (silently lost from the branch, as on #1973) — is caught at tool-call time by `WORKTREE-WRONG-CHECKOUT-GUARD` (the PreToolUse `Edit`/`Write` hook), see [Worktree usage guidance](./worktree-guidance.md#wrong-checkout-file-mutation-guard).
7. **Spelunking tooling internals instead of using the public surface**: Do not read installed package internals, scan tooling source, or run ad-hoc scripts to understand a tool's behavior. Use the CLI, its `--help` subcommands, and `skills/docs/`. Read tool source only when the task is to inspect or change that tool, or when a concrete failure path is inside it and no public CLI/docs path exists. Once a failure is concrete, search changed files for the exact pattern — don't run duplicate broad searches.
8. **Hand-editing the queue file when a board is configured**: When a GitHub Projects board is configured (`tracker.board.number`/`tracker.board.title` in `.devloops`), the board is the authoritative queue MEMBERSHIP and ordering source — not just status. Add work via the board (`dev-loops queue add ... --next-up` to land directly in the normative `Next Up` pickup queue; default lands in Backlog, which is unprioritized intake and never auto-picked), not by hand-editing `.pi/dev-loop-queue.json`; the queue runner reconciles board `Next Up` items into queue entries before each run. See the board setup and queue workflow in [GitHub Projects Queue Contract](./projects-queue-contract.md#setup).
9. <!-- rule: ANTIPATTERN-FANIN-WAIT --> `ANTIPATTERN-FANIN-WAIT` — **Hand-rolling a fan-in wait by polling a subagent transcript or parsing tool JSON with `node -e`/`python3`**: When awaiting fan-out reviewers (gate `draft_gate`/`pre_approval_gate` sub-loops, or any Agent-tool fan-out), NEVER tail/parse another agent's JSONL transcript, NEVER use `node -e`/`python3` to parse tool/subagent JSON, and NEVER `sleep`-poll in a shell loop for completion. These improvisations breach the [internal-tooling-only rule (issue #982)](retrospective-checkpoint-contract.md#internal-tooling-only-rule-issue-982--now-advisory) (no `node -e`/`python3` parsing of tool JSON) and the no-shell-polling rule, and a single improvised wait adds a non-empty entry to the advisory `retrospectiveFindings.rawCallViolations` array (issue #1077: reported to the conductor, **never blocking**). **Sanctioned wait:** rely on the harness completion notification, or read each reviewer's findings artifact at its deterministic output path (see [Checkpoint Review Chain Contract](./gate-review-sub-loop-contract.md) Phase 2), then join via the sanctioned fan-in CLI `dev-loops gate consolidate-fanin` (backed by `consolidateFanin` from `@dev-loops/core/loop/gate-fanin`; Phase 3). See also the dev-loop SKILL "Bounded async task contract".
10. **Ad hoc `gh api graphql` review-thread queries or `gh pr checks`/shell-pipe CI polling**: Do not hand-write a GraphQL query to enumerate review threads, and do not wrap `gh pr checks` in an `awk`/`grep`/`until`-based shell loop to wait for CI. Enumerate threads (with the ids `reply-resolve-review-thread.mjs` needs) via `scripts/github/list-review-threads.mjs`; block on current-head CI settling via `scripts/github/wait-pr-checks.mjs` (or `dev-loops loop watch-ci` for the JSON-status variant); read the aggregate loop/CI/thread state via `detect-copilot-loop-state.mjs` instead of re-deriving it from raw `gh` output.
11. **`git stash` in this repo's shared-`.git` worktree layout**: See `WORKTREE-NO-STASH` in [Worktree usage guidance](./worktree-guidance.md#never-git-stash-in-a-shared-git-layout).
12. <!-- rule: END-TURN-AND-AWAIT-WAKE --> `END-TURN-AND-AWAIT-WAKE` — **Ending a turn to await a nested child's completion**: An async dev-loop parent that ends its turn after dispatching a nested child (a judge, fixer, or reviewer this run started) is TERMINAL — the child's completion never wakes it, and the chain silently dead-ends. Observed evidence (2026-08-31): a unit-1 reviewer completed and wrote its findings artifact while its parent, having ended its turn to await it, stayed terminal and never resumed. Sanctioned alternatives: a **blocking join** (the parent stays alive and awaits the child directly — `async: false` dispatch, or a single `bg_wait` nonBlocking subscription) or `contact_supervisor` BEFORE ending a turn. "End turn and await wake" is structurally unsound and MUST NOT be used to await a nested child this run dispatched. See the dev-loop SKILL's guard rules and [dev-loop agent](../../agents/dev-loop.md#subagent-delegation) for the actionable dispatch-shape pin.
13. <!-- rule: SILENT-STDERR-PROBE --> `SILENT-STDERR-PROBE` — **Swallowing a path/existence probe's error** (`2>/dev/null` or any equivalent error-swallow) on a probe used to justify a citation: it hides a typo'd or absolute-path mistake and produces a confabulated citation — a path reported as verified when the probe silently failed instead. Every cited path MUST be verified to exist, and a probe that could fail must surface its error rather than redirect it away.
14. <!-- rule: STICKY-PROVIDER-PIN --> `STICKY-PROVIDER-PIN` — **Pinning provider choice to a fallback after one transient failure**: provider choice is a PER-DISPATCH decision. A transient failure (e.g. one `429`) on one dispatch MUST NOT pin subsequent dispatches to a fallback provider for the rest of the session; re-evaluate provider per dispatch once the failing dispatch's cap window has passed. See `GATE-EXEC-DISPATCH-RETRY-BACKOFF` in [Gate Review Sub-Loop Contract](./gate-review-sub-loop-contract.md) for the retry/backoff mechanics this pairs with.
15. <!-- rule: RAW-GH-PR-READY-BYPASS --> `RAW-GH-PR-READY-BYPASS` — **Flipping a PR from draft to ready via a raw `gh pr ready` call or the GitHub UI's "Ready for review" button**: both bypass the caller-side gate-authored-thread guard that lives only inside `scripts/github/ready-for-review.mjs` (the #1584/#1585 "don't go ready dirty" invariant), letting a PR reach ready with gate-authored review threads still unresolved. Always run `scripts/github/ready-for-review.mjs` — the mandatory path — for the draft→ready transition; never call `gh pr ready` directly and never use the UI button. If a PR reaches ready this way anyway, the loop still self-corrects: the loop's own PR-state detection (`detect-copilot-loop-state.mjs` / `detect-pr-gate-coordination-state.mjs`) routes a non-draft PR carrying unresolved gate-authored threads back to the fixer/disposition path on the next iteration instead of forward to `pre_approval_gate`, and the pre-merge gate's existing unresolved-thread re-assertion is the terminal backstop for any path that never re-enters the loop (issue #1915).
16. <!-- rule: RAW-GH-PR-MERGE-BYPASS --> `RAW-GH-PR-MERGE-BYPASS` — **Merging a PR via a raw `gh pr merge` call**: it bypasses the sanctioned merge wrapper `scripts/github/merge-pr.mjs`, which runs the FULL merge-precondition set fail-closed (conflict-free, clean current-head draft_gate + pre_approval_gate with fan-out provenance, CI green, zero unresolved threads, a non-stale/non-foreign runner lock, no title marker, satisfied size-budget human-approval) and requires a verified `--human-approved-by <login>`. Always route a merge through `node scripts/github/merge-pr.mjs --repo <owner/name> --pr <n> --human-approved-by <login>`; never call `gh pr merge` directly. The raw call is a recorded raw-`gh` violation (`scripts/loop/check-retro-tooling.mjs` flags it now that the wrapper exists), and the PreToolUse Bash gate (`decideBashGate`) denies a raw `gh pr merge` outright as defense-in-depth, naming the wrapper.

## Light mode exception

Small scoped changes under the configured `localImplementation.lightMode` thresholds (owned by `LOCAL-LIGHT-MODE-CONFIG-SURFACE` in [Local Implementation](../local-implementation/SKILL.md)) may skip fan-out/fan-in but must still run validation and a single review pass.

## Cross-references

- [Structural quality](structural-quality.md)
- [Validation policy](validation-policy.md)
