work
This skill should be used when executing work plans efficiently while maintaining quality and finishing features.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
This skill should be used when executing work plans efficiently while maintaining quality and finishing features.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
This skill should be used when auditing the recurring per-Anthropic-model-release checklist (model IDs, claude-code-action pin freshness, pricing drift, tier-map re-evaluation): it auto-fixes stale model-ID swaps into a CI-gated PR and flags the rest.
This skill should be used when performing exhaustive code reviews using multi-agent analysis, ultra-thinking, and worktrees.
This skill should be used when designing agent-native applications where agents are first-class citizens: architecting autonomous agents, creating MCP tools, building apps where features are agent-driven outcomes.
This skill should be used when working with DSPy.rb, a Ruby framework for type-safe, composable LLM applications.
This skill provides a promptfoo eval harness that measures whether a Soleur skill or agent edit actually improves behavior, comparing a skill arm against a baseline control arm.
This skill should be used when resolving all TODO comments in the codebase using parallel processing. It analyzes dependencies, creates a resolution plan with a mermaid flow diagram, and spawns parallel resolver agents.
| name | work |
| description | This skill should be used when executing work plans efficiently while maintaining quality and finishing features. |
You are the implementation orchestrator for standalone /work and one-shot Step 3:
/review → /compound → /ship.## Work Phase Complete as a turn boundary when you own the pipeline (see Invocation Mode below)./review, /compound, /ship via slash commands — never hand-roll ship steps.See plugins/soleur/lib/workflow-fidelity.ts (IMPLEMENTATION_TAIL) and Phase 4 Invocation Mode below.
Execute a work plan efficiently while maintaining quality and finishing features.
This command takes a work document (plan, specification, or todo file) and executes it systematically. The focus is on shipping complete features by understanding requirements quickly, following existing patterns, and maintaining quality throughout.
If $ARGUMENTS contains --headless, set HEADLESS_MODE=true. Strip --headless from $ARGUMENTS before processing the remainder as a plan path. Pipeline mode (file path detection) already covers all prompt bypasses for work's own prompts — --headless is only needed for forwarding to child skills in Phase 4.
<input_document> #$ARGUMENTS </input_document>
<decision_gate> API budget. This skill executes a work plan iteratively across many phases. Tier A (Agent Teams) carries ~7x per-task token cost; Tier B (Subagent Fan-Out) is moderate; Tier C is single-agent. Total cost scales with plan length, chosen tier, and per-task RED/GREEN/REFACTOR cycles. Soleur does not bill or proxy these calls — Anthropic does, against the key in your session. The Soleur LICENSE (BSL 1.1) disclaims warranty for runtime cost; you operate this loop against your own budget.
The tier offer fires inline at the right phase. Decline if running an unfamiliar plan against a tight budget. </decision_gate>
Load project conventions:
# Load project conventions
if [[ -f "CLAUDE.md" ]]; then
cat CLAUDE.md
fi
Clean up merged worktrees (silent, runs in background):
Navigate to the repository root, then run bash ${CLAUDE_PLUGIN_ROOT:-./plugins/soleur}/skills/git-worktree/scripts/worktree-manager.sh cleanup-merged. Report cleanup results: how many worktrees were cleaned up, which branches remain active.
Check for knowledge-base directory and load context:
Check if knowledge-base/ directory exists. If it does:
git branch --show-current to get the current branch namefeat-, read knowledge-base/project/specs/<branch-name>/tasks.md if it existsIf knowledge-base/ exists:
Read CLAUDE.md if it exists - apply project conventions during implementation
If # Project Constitution heading is NOT already in context, read knowledge-base/project/constitution.md - apply principles during implementation. Skip if already loaded (e.g., from a preceding /soleur:plan).
Detect feature from current branch (feat-<name> pattern)
Read knowledge-base/project/specs/feat-<name>/tasks.md if it exists - use as work checklist alongside TodoWrite
4.5. Read lane: from spec.md if present. Guard file existence first:
spec_path="knowledge-base/project/specs/feat-${branch_name}/spec.md"
if [[ -f "$spec_path" ]]; then
LANE=$(awk '/^lane:/ { gsub(/^lane:[[:space:]]*"?|"?$/, ""); print; exit }' "$spec_path")
case "$LANE" in
single-domain|cross-domain|procedural) ;;
"") LANE="" ;; # legacy spec; silent skip in announce
*) echo "work: invalid lane value '$LANE' in spec; ignoring."; LANE="" ;;
esac
fi
Lane is non-binding in skill logic — work code does not branch on LANE. Operators MAY use the announced lane as a heuristic when picking work Tier 0/A/B/C in Phase 2; binding behavior is deferred per Non-Goal #2.
Announce: "Loaded constitution and tasks for \feat-`"— append" (lane=)"whenLANE` is non-empty.
If knowledge-base/ does NOT exist:
Run these checks before proceeding to Phase 1. A FAIL blocks execution with a remediation message. A WARN displays and continues. If all checks pass, proceed silently.
Environment checks:
git branch --show-current. If the result is empty (detached HEAD), FAIL: "Detached HEAD state -- checkout a feature branch or create a worktree." If the result is the default branch (main or master), FAIL: "On default branch -- create a worktree before starting work. Run: bash ${CLAUDE_PLUGIN_ROOT:-./plugins/soleur}/skills/git-worktree/scripts/worktree-manager.sh feature <name>"pwd. If the path does NOT contain .worktrees/, WARN: "Not in a worktree directory. You can create one via git-worktree skill in Phase 1."git status --short. If output is non-empty, WARN: "Uncommitted changes detected. Consider committing or stashing before starting new work."git stash (the hr-never-git-stash-in-worktrees hook denies even the read-only git stash list). Use git rev-parse --verify --quiet refs/stash — a zero exit means a stash exists; WARN: "Stashed changes found. Review stash list to avoid forgotten work." A non-zero exit means no stash; continue silently.Scope checks:
.md or starts with a path-like pattern), verify it exists and is readable. If not, FAIL: "Plan file not found at the specified path." If the input appears to be a text description rather than a file path, WARN: "Input appears to be a description, not a file path. Scope validation limited."git diff --name-only HEAD...origin/main to identify files that diverged between this branch and main. If output is non-empty, WARN: "Branch has diverged from main in [N] files: [file list]. Consider merging main before starting." If the git command fails (e.g., offline, no remote), skip this check silently. For plans that edit AGENTS.* (high-collision file class), plugins/soleur/skills/ship/SKILL.md (Phase 5.5 gates), OR any path under docs/legal/** / knowledge-base/legal/** (legal-doc cross-document gate; weekly compliance PRs collide on the same 4-file set), FAIL HARD instead of WARN — fetch + rebase BEFORE Phase 1 (git fetch origin main && git rebase origin/main); sibling PRs landing mid-session reliably obsolete plan-quoted budget baselines and trim-target line numbers. Applying-then-rebasing duplicates sibling work and requires full reassessment. See knowledge-base/project/learnings/best-practices/2026-05-20-rebase-before-applying-agents-md-plan-edits.md and knowledge-base/project/learnings/2026-05-25-closed-field-list-must-classify-at-value-shape-not-column-name.md §Session Errors #5 (PR #4351 — 10 commits behind including #4353 legal-doc lockstep; caught at review time, not Phase 0.5).## Domain Review or ## UX Review heading (both are accepted for backward compatibility). If NEITHER heading found: scan the plan content for UI file patterns (page.tsx, layout.tsx, template.tsx, .jsx, .vue, .svelte, .astro, +page.svelte, app/, pages/, components/, layouts/, routes/). If UI patterns found, WARN: "Plan references UI files but has no Domain Review section. Consider running /soleur:plan to add domain review before implementing." If either heading IS present: pass silently.Design artifact checks:
git ls-files '*.pen' '*.fig' '*.sketch' | grep -i "<feature-name>" and check knowledge-base/product/design/ for related files. If design artifacts exist AND the current tasks include UI/page implementation (patterns: .njk, .html, .tsx, .jsx, .vue, .svelte, pages/, components/, layouts/): store the artifact paths as DESIGN_ARTIFACTS for use in Phase 2.Specialist review checks:
If a plan file was provided (check 5 passed) and a ## Domain Review section exists with a ### Product/UX Gate subsection: check whether domain leader assessments recommended specialists (copywriter, ux-design-lead, conversion-optimizer) that are NEITHER listed in **Agents invoked:** NOR in **Skipped specialists:**. If the **Decision:** field says reviewed (partial), WARN: "Domain review was partial — some specialist agents failed. Review the Domain Review section before proceeding." If any recommended specialist is missing from both fields: Interactive mode: FAIL with message listing the missing specialists and options: (a) "Run <specialist> now" — invoke the specialist agent directly, update the plan file's **Agents invoked:** field, then continue; (b) "Skip with justification" — prompt for reason, add to the plan file's **Skipped specialists:** field, then continue. Pipeline mode (headless/one-shot): auto-invoke each missing specialist agent. If the agent succeeds, add to **Agents invoked:**. If it fails, add to **Skipped specialists:** with note (auto-skipped — agent unavailable in pipeline) and WARN. Do not FAIL in pipeline mode. If all recommended specialists are accounted for (in **Agents invoked:** or **Skipped specialists:**): pass silently.
UX-skip-on-UI-plan hard gate (within check 9): Determine "UI plan" by matching the plan's ## Files to Create AND ## Files to Edit against the shared UI-surface term list + glob superset (plugins/soleur/skills/brainstorm/references/ui-surface-terms.md) — NOT just *.tsx/*.jsx. The superset includes *.njk, *.html, *.vue, *.svelte, *.astro, and email templates, so an Eleventy/Svelte/email UI surface does not slip the gate (wg-ui-feature-requires-pen-wireframe). On a UI plan, FAIL when EITHER: (a) ux-design-lead appears in **Skipped specialists:**, OR (b) the plan has no ### Product/UX Gate subsection at all (the gate was never run). Message: "Plan touches a UI surface but has no committed .pen (ux-design-lead skipped or gate never ran). Invoke the specialist or provide an explicit override naming the specific UI surfaces being shipped without review." This overrides the "all accounted for → pass silently" branch. A documented skip — or absent gate — on a UI plan is a process gap, not process compliance. See knowledge-base/project/learnings/workflow-patterns/2026-05-26-ux-design-review-skip-must-fail-hard-on-ui-plans.md.
UX artifact commit checkpoint (after each specialist in check 9): After each specialist agent completes successfully (interactive "Run specialist now" or pipeline auto-invoke), commit the output:
git status --short to discover new/modified files from the specialistgit add <discovered files>git commit -m "wip: <specialist-name> artifacts for <feature-name>"Each specialist gets its own commit so partial progress is preserved if a later specialist fails. Do not commit on specialist failure.
On FAIL: Display the failure message with remediation steps and stop. Do not proceed to Phase 1.
On WARN only: Display all warnings together and proceed to Phase 1.
On all pass: Proceed silently to Phase 1.
Pipeline detection: If $ARGUMENTS contains a file path (ends in .md or matches a path-like pattern), this skill is running in pipeline mode (invoked by one-shot or another orchestrator). In pipeline mode, skip all interactive approval gates and proceed directly. If $ARGUMENTS is empty or a plain text description, this is interactive mode — keep the approval gates below.
Read Plan and Clarify
bun test … reports X, wc -c < AGENTS.md = N, "cumulative ~Y words; ~Z headroom", git ls-files | wc -l), re-run the measurement at /work start before depending on it. Plans authored hours-or-days earlier observe a moving target; parallel branches landing in main invalidate the measurement. PR #3501 plan claimed ~186 word headroom against an actual 15 and required inline trim of the gate description. See knowledge-base/project/learnings/2026-05-10-handshake-schema-drift-and-stale-precondition-budgets.md.--help — the unit is the highest-risk part. When the plan names a CLI/config flag value (--postgres-conn-max-idle-time 30, a timeout, a size, a percent), resolve the pinned version and read that version's flag registration for type + unit + default + validation before writing it — a wrong unit (minutes/seconds/ms, bytes/KB, count/%) passes typecheck AND the binary's own validation, so only the source catches it. The plan is authoritative for intent (e.g. "drain idle conns fast"), never the literal value. Why: #6258 — the plan's SECS=30 for --postgres-conn-max-idle-time was seconds-intent, but the inngest v1.19.4 flag is an IntFlag in MINUTES (default 5) → 30 = 30 min, defeating the drain; corrected to 1. See knowledge-base/project/learnings/best-practices/2026-07-09-plan-quoted-tool-flag-value-and-unit-are-claims-verify-against-pinned-source.md.notFound(), the 404 response, the empty-state component) and walk the redirect/call chain to the exact determining condition, confirming or falsifying each plan hypothesis against code. Code-tracing is a valid substitute for a plan-prescribed live repro when the repro needs hard-to-synthesize state. Verify route classifications by reading the route's exported HTTP methods (a "read route" with no GET is a write route). Why: #4543 follow-up — the plan's two 404 hypotheses + its "redirect lands on /dashboard" reconciliation were all wrong; a 4-file trace (invite-actions → settings/team/page.notFound() → null-org resolver → accept-invite never set active workspace) found the real cause. See knowledge-base/project/learnings/bug-fixes/2026-06-01-symptom-root-cause-trace-the-actual-redirect-not-the-plan-hypothesis.md..git probe (FR2/FR3) would have skipped the in-dispatch ensureWorkspaceRepoCloned self-heal, dead-ending connected-repo resume; reverted + descoped. See knowledge-base/project/learnings/best-practices/2026-06-14-short-circuit-guard-must-sit-after-the-recovery-it-gates.md.UNREMOVABLE emit was nested under the regular-lock staleness gate, so a fresh non-regular config.lock slipped straight into the doomed git config EEXIST write; 3 review agents converged on a gap noticed at write-time. See knowledge-base/project/learnings/best-practices/2026-07-02-fail-loud-guard-must-not-nest-under-a-different-state-class-gate.md.session-state.md ### Decisions entry is INTENT, not an accomplishment — verify each against the live artifact before treating it as done. The section is written in the past tense by a session that was still mid-flight, so a decision reading "met the intent via a correcting comment on #N" / "filed the follow-up" / "re-titled the issue" records what the author RESOLVED to do, and the step is exactly what a mid-task death (API timeout, compaction, crash) leaves undone — the resume then inherits the claim without the act. Cheapest gate: for every Decisions line naming an outward-facing artifact, probe it (gh issue view N --json comments --jq '[.comments[]|select(.body|test("<cite>"))]|length', gh issue view N --json title, ls <path>) and re-open it in the task list on a zero. This is the outward-facing sibling of the artifact rule below (which covers FILES the contaminated session wrote); a GitHub comment leaves no working-tree trace at all, so nothing else surfaces its absence. Why: #6497 — session-state.md recorded a #6416 correcting comment as met; gh issue view 6416 showed ZERO comments citing #6497, and the resumed session nearly shipped the claim in its PR body. See knowledge-base/project/learnings/2026-07-16-a-mutation-battery-only-covers-what-you-mutate.md.session-state.md documents that the originating session's tool layer was contaminated/degraded (batched output, warnings prepended to Read results), the contamination taints every FILE that session wrote, not just the status claims it retracted ("applied"/"GREEN"). Re-derive each artifact from its authoritative source: read the real migration/file it claims to mirror and rewrite it as a verbatim delta (diff to prove byte-identity), then re-establish status from scratch (apply + verify the live DB; re-run RED→GREEN). For SQL, a misread RETURNS type is a free tripwire — CREATE OR REPLACE cannot change a function's return type, so any "applied GREEN" against a return-type-changed body is self-evidently false. Why: #4709 — 089 was authored against a misread of 088 (RETURNS void vs integer, dropped NULL-auth + 22023 gates); caught only by reading the real 088 body. See knowledge-base/project/learnings/2026-06-01-resumed-session-artifacts-from-contaminated-tool-layer-are-unverified.md.grep -cE '^[[:space:]]+-target=' <workflow>, wc -l <list>, etc.) and copy the integer into every comment/AC reference. Plan-prose mental tallies drift by ±1-2 during expansion; multi-agent review reliably catches it (P3 polish) but the inline grep at write-time is free. Why: PR #4122 — workflow header carried "68 explicit targets" from plan §Phase 0.3 mental tally while grep returned 67; caught by code-quality-analyst + pattern-recognition-specialist at review. See knowledge-base/project/learnings/best-practices/2026-05-20-plan-time-pr-vs-issue-disambiguation-and-self-derived-counts.md.<predicate>") reads like rigour and is not: the prose leaves per-line-vs-per-match, unique-vs-all, marker position, and language-correct-markers free, each worth 10–30%. A number pinned to prose rots exactly like a citation pinned to a line number — both name a referent without fixing it — so the fix is the same: anchor on content (the command), not on a description. If the figure is load-bearing, also show the conclusion survives the plausible range. Why: #6517/PR #6527 — a reviewer measured and adopted "403, where site = a line in a code file bearing a comment marker AND citing <path>.<src-ext>:<N>" as canonical; at /work, 18 faithful readings of that exact prose returned 319–583 and never 403 (two independent resolvers also disagreed on the sibling counts). Shipped as ~360 + a git grep … | wc -l; the conclusion was invariant across the whole range (0.17–0.31%), which is why the false precision survived a 5-agent panel — nothing depended on it. See knowledge-base/project/learnings/2026-07-16-advisory-first-precedent-is-a-claim-to-measure-and-a-coordinate-citation-carries-no-claim.md Session Errors #3.grep -cE '^- \*\*\([a-z]\)\*\* ' (or equivalent, using [a-z] NOT [a-N]) across each file BEFORE the first letter-inserting Edit. Counts AND letter-sets must match; anything else means the plan's lockstep claim is paraphrase-from-stale-read. PR #3755 (#3708): plan asserted DPD §(a)-(k) lockstep; canonical actually had §(l) DSAR; AC1 caught the §(l) collision but the cheaper gate is re-lockstep at /work-start. See knowledge-base/project/learnings/2026-05-14-discrete-enumeration-relockstep-and-pr-introduced-asymmetry.md.### N.M-numbered section, the Edit anchor MUST target the LAST ### N.M block before the desired slot (not the lexically-adjacent one). Confirm with grep -nE '^### [0-9]+\.[0-9]+ ' <file> | tail -3; the new M must be greater than the picked anchor's M. PR #3755 (#3708): §5.10 was anchored on §5.9-Resend's block start and landed BEFORE §5.9. One-grep prevention. Same learning file.assertWriteScope for cross-tenant integrity, GDPR write-boundary checks), enumerate ALL write sites where the property applies — not just diff sites. Run git grep -nE '\.from\("<table>"\)\.insert\(' <scope> (or the equivalent for the boundary type) at Phase 0 and verify every match is sentinel-gated, then file follow-up tasks for any uncovered sites BEFORE entering Phase 1. See knowledge-base/project/learnings/2026-05-12-type-widening-cascades-and-write-boundary-sentinels.md; hard rule hr-write-boundary-sentinel-sweep-all-write-sites. Why: PR-A2 #3603 — sentinel placed at assistant-row write but not user-row write at cc-dispatcher.ts:1008; same service-role-bypass surface.unknown/any/jsonb boundary (compiler cannot enforce optionality at the consumer), git grep -nE '<field-name-pattern>' apps/ across every consumer and verify each respects the new optionality. For Message-class fields the canonical grep is git grep -nE '\bmessage\.usage\.(input_tokens|output_tokens|completed_actions)\b' apps/ (adapt per field family). See learning 2026-05-12-type-widening-cascades-and-write-boundary-sentinels.md; hard rule hr-type-widening-cross-consumer-grep. Why: PR-A2 #3603.workspacePathForWorkspaceId, an active-workspace resolver, any join(root, id) path builder), the guard THROWS on every test fixture that fabricated that id with a short non-UUID literal. Before sizing the change as "1 file", grep the fixture surface — git grep -nE '"(user-1|ws-[A-Za-z0-9]|owner-[A-Z]|[a-z]+-workspace)"' apps/web-platform/test/ — and trace which hits reach the guarded function; size the fixture sweep at plan time, not at GREEN. The fix is realistic UUID fixtures (cq-test-fixtures-synthesized-only), never a looser guard. Why: #5344 — a "1 source + 1 test file" estimate broke ~34 fixture files. See knowledge-base/project/learnings/2026-06-15-id-shape-guard-test-fixture-blast-radius-and-syntactic-sast.md.git grep -nE '<pattern>' <scope> | grep -vE '<safe-form>' | wc -l = 0), run the grep ONCE at Phase 0 to enumerate the authoritative work-list (capture it via targets=$(mktemp) and write the hits there — never a fixed /tmp name a sibling session would clobber), fix each line, then re-run the grep after each batch. The plan's narrative enumeration of "files X, Y, Z" is a starting hypothesis; the grep result is the work-list. Same applies to regex widenings: enumerate the configs/verbs/paths invoked by the IN-SCOPE runbooks, not the configs the incident occurred against — the incident is one data point; the trap-class config-set is the full union of every config the runbooks touch. Why: PR #4031 — initial sweep handled 9 named runbook hits but missed 2 buried in deeper sections of the same files; widened regex covered (prd|prd_terraform|dev|ci) per the plan's leak-footprint enumeration but missed prd_orchestration which 2 in-scope runbooks operate against. Pattern-recognition + security-sentinel caught both at multi-agent review. See knowledge-base/project/learnings/best-practices/2026-05-18-sweep-class-fixes-grep-enumerated-not-intuited.md.--postgres-uri and never parses redis). Run grep -cF -- "$tok" "$file" for every (file, token) pair at Phase 0 and let the matrix define the per-file token set; asserting the full set on a file that uses a subset false-fails a correct codebase. The plan is authoritative for the guard's intent, never its exact token/file set (same class as hr-when-a-plan-specifies-relative-paths-e-g). Corollary: plan-quoted AC verify commands are preconditions to re-derive, not facts — a git diff origin/main | grep -c 'ssh ' proxy false-positives on self-referential prose + branch-divergence in cited-but-unedited files; scope it to the changed code files. Why: #5553 — the drift-guard spec required 4 tokens on all 3 ExecStart parsers, but inngest-wiped-volume-verify.sh references only 2; scoped per-file at /work. See knowledge-base/project/learnings/best-practices/2026-06-18-cross-file-drift-guard-verify-per-file-token-usage.md.soleur:engineering:cto agent (Agent tool, subagent_type: soleur:engineering:cto). Do NOT surface it to the operator via AskUserQuestion: the operator is non-technical, and architecture is the CTO's call. Reserve operator escalation for product / scope / preference decisions (what to build, never how). Hand the CTO the discovered evidence (file:line), the candidate options with trade-offs, and the plan's brand_survival_threshold; then implement exactly what it returns and record the decision + rejected alternatives in an ADR (/soleur:architecture). This is a routing rule, not an approval gate — it fires in pipeline mode too. Why: #5325 — /work found the plan's action_sends-reuse mechanism (deepen P0-1) was structurally blocked (NOT NULL message_id FK with no agent-path message id; UI-only scope_grants creation); the substrate choice (dedicated WORM table vs gate-only vs full reuse) is an architecture decision that was first (wrongly) offered to the operator before being routed to the cto agent, which ruled. See knowledge-base/project/learnings/workflow-patterns/2026-06-15-architectural-fork-decisions-route-to-cto-not-operator.md.knowledge-base/project/specs/<branch>/decision-challenges.md (append; alongside session-state.md) — never a mid-pipeline pause. ship Phase 6 renders that artifact into the PR body and files the action-required issue the operator actually sees. The one exception to no-pause: a security/feasibility regression halts terminally before merge (see the reference doc).Setup Environment
First, check the current branch by running git branch --show-current. Then determine the default branch by running git symbolic-ref refs/remotes/origin/HEAD and extracting the branch name. If that fails, check whether origin/main exists (fallback to master).
If already on a feature branch (not the default branch):
[current_branch], or create a new branch?"If on the default branch, you MUST create a worktree before proceeding. Never edit files on the default branch -- parallel agents cause silent merge conflicts, and this repo uses core.bare=true where git pull and git checkout are unavailable.
Create a worktree for the new feature:
SOLEUR_SKILL_NAME=work SOLEUR_EXPECTED_DURATION_MIN=240 \
bash ${CLAUDE_PLUGIN_ROOT:-./plugins/soleur}/skills/git-worktree/scripts/worktree-manager.sh --yes create feature-branch-name
Then cd into the worktree path printed by the script. The worktree manager handles bare-repo detection, branch creation from latest origin/main, .env copying, and dependency installation. The env vars wire a session lease so sibling cleanup-merged invocations refuse to reap this worktree.
Phase Exit (release lease). At the end of the workflow — after /soleur:ship returns OR if you exit without shipping — release the lease so a sibling cleanup-merged can reap the worktree once it's actually merged:
bash .claude/hooks/lib/session-state.sh release_lease "$(basename "$PWD")"
The release is a no-op if the lease was already removed by the multi-signal trap (EXIT/INT/TERM/HUP fires on abnormal exit). Stale leases get swept after 24 hours regardless.
Use a meaningful name based on the work (e.g., feat-user-authentication, fix-email-validation).
Create Todo List (TDD-First Structure)
Structure tasks as RED/GREEN/REFACTOR units, not as "implement everything, then test":
blockedBy dependency (GREEN blocked by RED)Anti-pattern to avoid: Creating a task list like [implement A, implement B, implement C, ..., write tests, lint]. This structure guarantees TDD violation because the agent executes tasks in order. The correct structure is [RED: test A, GREEN: implement A, RED: test B, GREEN: implement B, ..., lint].
Post-creation validation (HARD GATE): After creating all tasks, scan the task list for any non-exempt implementation task (GREEN) that does NOT have a corresponding RED test task in its blockedBy list. If found, restructure the task list before proceeding. Do not start Phase 2 with an invalid task structure. Why: In PR #2428, the agent created flat tasks ("Fix X", "Write tests") and started implementation before tests — the user had to intervene and force a restructure. The anti-pattern instruction was not enough without a validation gate.
Output discipline (all tiers). Long execution phases blow the response-token ceiling and truncate mid-pipeline, losing the thread. Keep inline output bounded: when a command's output is large (full diffs, build logs, test dumps), write it to a file and reference the path rather than pasting it — never echo a diff over ~200 lines inline (d=$(mktemp -t <task>.XXXXXXXX.diff); git diff > "$d"; echo "DIFF=$d" then summarize, citing the path). After each task or logical unit, emit a one-line ## Work Phase <N> complete checkpoint marker so an interrupted run has a clear resume point. This complements hard rule hr-never-run-commands-with-unbounded-output (which forbids unbounded commands); this is about bounding your own narration of them.
Execution Mode Selection (HARD GATE — must complete before executing ANY task)
Do NOT execute any task before completing this analysis. Analyze independence first, select the execution tier, then begin. Starting sequential execution "because the first tasks feel simple" is a workflow violation — it forfeits parallelization savings on the remaining tasks.
Before starting the sequential task loop, check for parallelization opportunities:
Step 0: Tier 0 pre-check (Lifecycle Parallelism)
Read the plan. Apply a single judgment: "Does this plan have distinct code and test workstreams that can be assigned to separate agents with non-overlapping file scopes?"
Read plugins/soleur/skills/work/references/work-lifecycle-parallel.md now for the full Tier 0 protocol (offer/auto-select, generate contract, spawn 2 agents, collect/commit, test-fix-loop, docs). If Tier 0 executes, proceed directly to Phase 3 after completing Step 06 of the protocol. If declined, fall through to Step 1.
Step 1: Analyze independence
Read the TaskList. Identify tasks that have no blockedBy dependencies and reference
different files or modules (no obvious file overlap). Count the independent tasks.
If fewer than 3 independent tasks exist, skip to Tier C: Sequential below.
If 3+ independent tasks exist, proceed through the tiers in order (A, then B, then C). Each tier either executes or falls through to the next.
Pipeline mode override: If running in pipeline mode (plan file argument detected in Phase 1), auto-select Tier 0 if eligible (Step 0 above). If Tier 0 is ineligible, skip Tier A entirely and auto-accept Tier B without prompting. Do not present "Run as Agent Team?" or "Run in parallel?" questions -- proceed directly to Step B2 of the Subagent Fan-Out protocol if 3+ independent tasks exist, otherwise fall through to Tier C.
Tier A: Agent Teams (highest capability, ~7x token cost)
Read plugins/soleur/skills/work/references/work-agent-teams.md now for the full Agent Teams protocol (offer, activate, spawn teammates, monitor/commit/shutdown). If declined or failed, fall through to Tier B.
Tier B: Subagent Fan-Out (fire-and-gather, moderate cost)
Read plugins/soleur/skills/work/references/work-subagent-fanout.md now for the full Subagent Fan-Out protocol (offer, group/spawn, collect/integrate). If declined, fall through to Tier C.
Tier C: Sequential (default)
Proceed to the task execution loop below.
Task Execution Loop
Design Artifact Gate (before first UI task): If DESIGN_ARTIFACTS was set in Phase 0.5, spawn the ux-design-lead agent with the artifact paths and ask it to produce an implementation brief (see ux-design-lead "Wireframe-to-Implementation Handoff" workflow). The brief is a structured description of every section, its content, and its layout — this becomes the binding input for all UI tasks. Do not write any markup until the brief is received.
UX artifact commit checkpoint (after Design Artifact Gate): After the implementation brief is received, commit before proceeding to UI tasks:
git status --short to discover the implementation brief and any generated design filesgit add <discovered files>git commit -m "wip: UX implementation brief for <feature-name>"This checkpoint ensures the implementation brief survives session crashes.
For each task in priority order:
while (tasks remain):
- Mark task as in_progress in TodoWrite
- Read any referenced files from the plan
- If task creates UI/pages: verify implementation brief exists (HARD GATE)
- TDD GATE: (see below)
- Look for similar patterns in codebase
- RED: Write failing test(s) for this task's acceptance criteria
- GREEN: Write minimum code to make the test(s) pass
- REFACTOR: Improve code while keeping tests green
- Run full test suite after changes
- Mark task as completed in TodoWrite
- Mark off the corresponding checkbox in the plan file ([ ] → [x])
- Evaluate for incremental commit (see below)
No mid-plan pause gates (HARD GATE). A multi-phase plan
(tasks.md Phase 0 through Phase N) is a SINGLE execution unit.
Do NOT insert "Pause for review or continue?" prompts between
phases. Do NOT end a turn after one phase commits with "Continue
into Phase N+1 next turn?". The skill's Phase 4 handoff is the
only sanctioned stopping point — until then, chain straight
through every phase the plan defines, including phases the plan
labels "Pre-merge verification" or "Post-merge (operator)" if
they're automatable per the next gate. Why: the founder is a
solo operator; every "continue or pause?" is a context switch
that defeats the entire point of a multi-phase plan. Pipeline
mode (file-path arg in Phase 1) means pipeline mode for the WHOLE
plan, not per-phase.
Operator-step automation gate (HARD GATE). Before treating
any task in tasks.md as "operator-driven" (apply migration,
verify pg_cron, verify Storage bucket, run end-to-end smoke,
gh pr ready, gh pr merge --auto), check whether it is
automatable via a loaded MCP server or CLI:
cron.job queries + Storage bucket
existence + RLS spot-checks → mcp__plugin_supabase_supabase__*
with Doppler DATABASE_URL_POOLER fallback when MCP is
unavailable — see "Supabase fallback chain" below. When a
migration needs a SECURITY DEFINER RPC (e.g. to bypass an RLS /
column-grant restriction), start from
sql-security-definer-rpc-scaffold.sql
— it encodes the search_path pin + 4-role REVOKE + auth.uid()
authorization pin that test/migration-rpc-grants.test.ts enforces.gh pr ready / gh pr merge --squash --auto / gh issue close
→ Bash via gh CLImcp__playwright__*)mcp__plugin_soleur_cloudflare__*mcp__plugin_soleur_stripe__*If automatable, EXECUTE it inline as part of the work pipeline —
never list it back to the operator. The /ship skill already
handles gh pr ready + auto-merge + migration verification (see
plugins/soleur/skills/ship/SKILL.md); chain to /soleur:ship
at Phase 4 and let it run. For migration apply to dev (vs
verify), invoke mcp__plugin_supabase_supabase__apply_migration
inline at the phase where the migration lands, not as a
post-merge todo. Why: see ship/SKILL.md:1027 ("Every 'please
run this manually' is a context switch") and ship/SKILL.md:1177
(PR #1375 — migration verification was left as a manual
"post-merge todo" instead of being executed; deployed code
expected the new schema and broke). Same class as the
Playwright-first audit in Phase 4: if a tool exists, use it.
Supabase fallback chain (when MCP OAuth fails). The Supabase
MCP OAuth flow at https://api.supabase.com/v1/oauth/authorize
intermittently rejects valid URLs at the dashboard auth_id
handoff (cause: external — Supabase-side). When that happens, do
NOT fall back to "paste this SQL into the dashboard SQL editor"
handoff — that's a manual-step rationalisation that violates
hr-never-label-any-step-as-manual-without. Instead walk down the
hr-exhaust-all-automated-options-before priority chain:
(1) Doppler DATABASE_URL_POOLER — already provisioned for every
env; the migration apply path. (2) Verify the project ref in the
URL matches the plan's stated dev/prd refs — Doppler is the
source of truth (plan-quoted project refs are preconditions to
verify, never facts; the plan can drift). (3) Rewrite the URL's
port :6543 → :5432 so the pooler runs in session mode (multi-
statement DDL works; transaction mode rejects with SQLSTATE 42601
"cannot insert multiple commands into a prepared statement").
(4) Apply via pg (node-pg, bun-installed in /tmp if missing)
wrapped in BEGIN; <migration>; COMMIT;. The direct DB host
db.<ref>.supabase.co:5432 is IPv6-only and typically
unreachable from operator/CI networks; the pooler is IPv4.
(5) Post-apply, verify schema via the same connection — RLS
enabled, policy_count, trigger names, RPC signatures + SECURITY
DEFINER flag, UNIQUE constraints. Write the verification artifact
to knowledge-base/project/specs/feat-<name>/migration-checklist.md.
Why: PR #3853 / #3205 — Supabase MCP OAuth was rejecting URLs
at the auth_id handoff; the agent first proposed "paste SQL into
dashboard" (manual-step violation), then pivoted to Playwright-
first audit on dashboard navigation (correct), then discovered
Doppler had the working DATABASE_URL_POOLER and applied via
pg directly — the path it should have taken at step 1.
Session stickiness: once the MCP OAuth handoff has failed even
once in the current session, treat Doppler DATABASE_URL_POOLER as
the default for ALL subsequent Supabase operations this session — do
not re-attempt the OAuth flow per-operation. Re-probing a known-flaky
external auth each time is the wasted-cycle trap; the fallback is not
slower once you are already authenticated to Doppler.
Pre-apply collision check (always, even on first attempt).
Before invoking pg apply (or supabase migration up) against any
shared env, run git fetch origin main && git ls-tree origin/main -- apps/web-platform/supabase/migrations/ | awk '{print $4}' | grep -oE '^[0-9]{3}_[^.]+' | sort -u. For each LOCAL migration
file the branch introduces, assert no DIFFERENT filename with the
same 3-digit prefix exists in that list. A collision means a
sibling PR is landing the same number window; renumber FIRST,
then apply under the final filename. Why: PR #4225 — applied
053–057 in the morning; PR #4251 landed 054_schema_migrations_ content_sha.sql 10 hours later and main's CI drift probe flagged
the entire branch; the recovery (renumber 054→058, 055→059, 056→060,
057→061 + reconcile public._schema_migrations on both dev + prd
via git hash-object content_sha) took ~30 min and could have been
zero-cost if the operator had grepped origin/main first.
The collision window extends through /ship, not just work-time. This
check at work-start is necessary but NOT sufficient: a sibling migration can
land on main DURING the (often 30–90 min) ship phase — especially under a
fast-moving-main burst where /ship Phase 7 performs repeated git merge origin/main auto-syncs on OPEN BEHIND. Each sync that pulls in a sibling
supabase/migrations/NNN_*.sql sharing your prefix is a silent collision the
BEHIND loop pushes straight to CI (where the migration drift/shape gate fails,
~16 min later). After ANY ship-time sync whose merge output lists
supabase/migrations/, re-run the prefix check above and renumber-during-ship
(git mv both up/down + update every in-repo reference: migration headers,
code comments, plan/tasks/learning) BEFORE the next push. Why: PR #5760 —
114_disk_io_top_wal_statements (a #5739-sibling) landed mid-ship; my
114_prune_cron_job_run_details collided and surfaced only at CI after ~6
auto-syncs; recovery was a renumber to 115. See
knowledge-base/project/learnings/workflow-patterns/2026-06-30-migration-number-collision-mid-pipeline.md.
Tracking row in the SAME transaction as the migration body.
The project's canonical apps/web-platform/scripts/run-migrations.sh
writes INSERT INTO public._schema_migrations (filename, content_sha) VALUES ('<basename>', '<git-hash-object>') in the same transaction
as the migration SQL. The Doppler+pg fallback MUST mirror this —
bare BEGIN; <migration>; COMMIT; produces a phantom-applied state
where the schema reflects the migration but _schema_migrations
does not, and the next deploy attempts re-apply (failing on
non-idempotent statements like CREATE TRIGGER). The reconciliation
pattern (UPSERT with ON CONFLICT (filename) DO UPDATE SET content_sha = EXCLUDED.content_sha) is the recovery shape — but
doing it inline is cheaper.
PostgREST schema cache reload via session-mode pooler does NOT
work. NOTIFY pgrst, 'reload schema' over a :5432 pooler
connection does not reach PostgREST's LISTEN (PgBouncer
multiplexes; LISTEN/NOTIFY channel scope is bound to backend
process identity, not session). 90 attempts over 5 minutes
returned PGRST205. After a direct-pg apply: either wait for the
natural ~10-min schema poll cycle, OR use the Supabase Management
API to restart PostgREST. The direct DB host
(db.<ref>.supabase.co:5432) is IPv6-only and typically
unreachable from operator networks, so the canonical "NOTIFY via
direct connection" workaround documented upstream isn't available.
Storage-bucket migrations: down.sql cannot DELETE storage tables;
column-takeover proof is permissive-vs-restrictive, not name-count.
Supabase installs a platform BEFORE DELETE trigger (protect_objects_delete
→ storage.protect_delete()) that blocks direct DELETE FROM storage.objects
AND storage.buckets ("Direct deletion from storage tables is not allowed").
So a bucket migration's down.sql reverts only SQL-droppable objects
(policies → function → column) — NOT the bucket/objects (Storage-API/operator
teardown; 019/042 precedent ship none; 071's DELETE FROM storage.buckets is
a dormant bug). Runtime cleanup uses service.storage.from(b).remove([...])
(allowed). And when verifying "no client can write column X" (read-proxy
trust), assert no PERMISSIVE INSERT/UPDATE/DELETE/ALL policy (a
RESTRICTIVE FOR ALL like workspaces_jti_not_denied only denies, never
grants) + a behavioral authenticated UPDATE affecting 0 rows. The
pooler also presents a self-signed CA chain → transient node-pg verify
scripts use ssl:{rejectUnauthorized:false} (dev-only, mirrors
run-migrations.sh sslmode=require; no committed code disables TLS verify).
See knowledge-base/project/learnings/2026-06-04-supabase-bucket-migration-down-and-rls-takeover-proof.md (#4916).
TDD Gate (HARD GATE): Before writing ANY implementation code for a task, determine if the task has testable behavior:
Emit rule-application telemetry (records that the TDD gate was reached — see AGENTS.md cq-write-failing-tests-before):
source "$(git rev-parse --show-toplevel)/.claude/hooks/lib/incidents.sh" && \
emit_incident cq-write-failing-tests-before applied \
"Write failing tests BEFORE implementation code whe"
count === 2 while two slots are held) in addition to the final-state assertion. A test that passes identically with and without the primitive isn't testing the primitive. See knowledge-base/project/learnings/test-failures/2026-04-18-red-verification-must-distinguish-gated-from-ungated.md. Test-environment fidelity: if the SUT's buggy code lives behind a guard (if [[ -d "$X" ]], if (cache.has(key)), etc.), the harness MUST seed the precondition the guard requires — otherwise both buggy and fixed paths short-circuit identically and any negative-space assertion passes vacuously. See knowledge-base/project/learnings/test-failures/2026-04-22-red-test-must-simulate-suts-preconditions.md. Early-exit shadowing: if the SUT has a guarded fast path (substring strip like replaceAll(arg, ""), cache-hit, env-flag short-circuit) that handles a superset of inputs the slow path under test handles, RED inputs MUST choose identities ONLY the slow path can produce. Sharing a fixture across the fast/slow boundary lets the fast path scrub first and the regex/branch under test never fires — the assertion passes without testing the fix. Add an invariant guard test asserting the fast/slow fixtures do not collide. See knowledge-base/project/learnings/2026-05-04-vacuous-red-via-shared-fixture-and-toolchain-pinning.md. In-component state machines (RTL): when the gate-under-test is component-local state (useState/useRef/useReducer), drive the SUT through state transitions with result.rerender(<C />) — never unmount() + fresh render(). Remount resets the in-component bookkeeping that IS the gate, producing vacuous green. See knowledge-base/project/learnings/test-failures/2026-05-11-rerender-not-remount-for-in-component-state-machine-tests.md. Laundered-target resolvability (normalizer/strip/prefix-mangle security fixes): a regression guard for an anchored-strip / path-canonicalization / allowlist-key fix is vacuous unless the fixture makes the LAUNDERED (mis-normalized) target resolvable to an observable effect — if the downstream gate rejects it for an unrelated reason (nonexistent skill/row/file), the test passes identically with and without the fix. Litmus: under the buggy impl, does this input produce a DIFFERENT output than under the correct one? See knowledge-base/project/learnings/test-failures/2026-07-05-security-fix-regression-guard-must-make-the-laundered-target-resolvable.md. Fixture-space cardinality (ask this per contract sentence, and note that your own passing mutation battery cannot answer it): for each property the test claims, name the SET it quantifies over and count how many distinct members the FIXTURE instantiates — one member is a sample, not a proof, and code-mutation coverage does not detect a fixture-space gap. Three shapes recur: (a) a temporal contract (a wait/retry/debounce/convergence) sampled only by STATIC fixtures probes t=0 and t=∞ but never the transition — the case the guard exists for — so deleting the loop's break stays green; drive it with a stateful stub that changes on the Nth invocation and assert the success arm was reached VIA the loop; (b) a bidirectional guard (-w, an ordering, a comparison) must be fixtured in the direction where the weaker implementation gives a FALSE POSITIVE, not the direction that fails either way; (c) a stub that ignores argv, or a sleep/clock stubbed to a no-op, silently voids the call-shape and budget contracts — validate "$*" in the stub and COUNT the stubbed calls against the design's bound. Why: #6441 — a 7-mutation battery reported 7/7 RED while nine unimagined mutations (loop-break deleted, grep -qwF→-qF, addr show→link show, bound 30→1, sleep 2→600) all survived. See knowledge-base/project/learnings/2026-07-19-my-own-mutation-battery-was-the-false-confidence.md.Skipping this gate — writing implementation before tests — is a workflow violation equivalent to committing directly to main. The rationalization "this is simple enough to not need test-first" is exactly the reasoning TDD is designed to prevent.
When adding MCP tools to an existing registration block in agent-runner.ts, verify each tool's prerequisites are independent of the block's guard condition. Write a test that validates the new tool works WITHOUT the existing block's prerequisites (e.g., Plausible tools work without GitHub installation).
When adding route handler tests that require vi.mock(), create a separate test file from existing unit tests that import the real module. Vitest hoists all vi.mock() calls to the top of the file, clobbering real imports for the entire file regardless of describe block scope.
When creating test files with vi.mock() factories that reference shared variables, use vi.hoisted() from the start -- vitest hoists vi.mock to the top of the file before const/let declarations execute.
When a NEW shared module will be imported (directly or transitively) by files that already have test suites mocking a node builtin (vi.mock("node:child_process") with spawn-only factories is the common case), do NOT destructure that builtin's exports at module top level (promisify(execFile) crashes EVERY sibling suite at import). Lazy-import inside the function that uses it. Why: #5091 — _cron-safe-commit.ts's top-level promisify(execFile) broke 28 cron-bug-fixer tests at module load. See knowledge-base/project/learnings/2026-06-10-bot-cron-safe-commit-substrate-symlink-removal.md.
Before adding a vi.mock("<module>") to an EXISTING test file, grep the file for a pre-existing mock of the same module (grep -n 'vi.mock' <file> | grep <module-basename>) and wire your spy into that block instead. Vitest registers one mock per resolved module per file; a duplicate does not error — it silently picks one, and your hoisted spy captures zero calls. Why: PR #5090 — a new egress-posture-log spy was added as a second @/server/logger mock while the factory test already mocked it ~150 lines down; cost 4 debug cycles. See knowledge-base/project/learnings/bug-fixes/2026-06-10-sandbox-network-plane-not-token-plane-error-shape-triage.md.
A WHOLESALE vi.mock("<module>", () => ({...})) replaces the ENTIRE module, dropping every export the factory omits — so a module with multiple named exports (@/server/logger exposes default AND createChildLogger; observability, db-helper, supabase wrappers similarly) breaks any REAL sibling in the SUT's import graph that consumes a different export. Default to vi.mock(spec, async (importOriginal) => ({ ...await importOriginal(), <override> })); reserve wholesale factories for modules you fully replace — or skip the mock entirely if the thing under test already mocks the export's consumer. Detection is free: run the FULL test file (never -t "<new test>" alone) — the RED run surfaces unexpected sibling failures naming the missing export. Why: #5689 — a wholesale @/server/logger mock dropped createChildLogger (used by probe-octokit.ts via _cron-shared), breaking 10 unrelated arm-2 tests. See knowledge-base/project/learnings/test-failures/2026-06-29-wholesale-module-mock-drops-named-exports-needed-by-transitive-siblings.md.
A partial vi.mock(spec, async (importOriginal) => ({ ...actual, B: spy })) override only changes what importERS see — it does NOT intercept a call made by a REAL sibling function A (kept via ...actual) to B within the same module; A references B through the module's internal lexical binding, not the export object. Symptom: the spy reports 0 calls even though the path clearly runs B. To observe B while keeping A real, mock the deeper boundary B itself crosses (fetch, the DB client, child_process) and assert there. Decision rule: mock the seam the unit under test does not own. Why: #5728 — overriding postSentryHeartbeat didn't intercept the real finalizeOutputAwareHeartbeat's internal call; fixed by keeping it real + stubbing fetch + asserting the POST URL. See knowledge-base/project/learnings/test-failures/2026-06-30-partial-module-mock-does-not-intercept-intra-module-calls.md.
When mocking child_process.spawn, fetch, or any constructor returning an event-emitter-like object, use mockImplementation(() => factory(...)) rather than mockReturnValue(factory(...)). mockReturnValue evaluates the factory eagerly at test-setup time; any queueMicrotask / setTimeout / setImmediate scheduled inside the factory fires BEFORE the SUT attaches its listeners, producing empty event data or an "uncaught error" test timeout. See knowledge-base/project/learnings/test-failures/2026-04-17-vitest-mockReturnValue-eager-factory-async-event-race.md.
A vi.fn(() => value) mock declared with a ZERO-parameter implementation cannot be invoked via a (...args) => mock(...args) forwarder (the standard vi.mock factory shape) — tsc rejects the spread with TS2556 "A spread argument must either have a tuple type or be passed to a rest parameter", even though the vitest run is GREEN (vitest type-checks test files lazily). Give the impl a rest param: vi.fn((..._args: unknown[]) => value), matching sibling vi.fn() mocks. Only a standalone ./node_modules/.bin/tsc --noEmit catches it. Why: #5817 — execFileSyncMock = vi.fn(() => Buffer.from("")) passed 36/36 tests but failed tsc. See knowledge-base/project/learnings/test-failures/2026-07-01-vitest-zero-arg-mock-cannot-take-spread-suite-green-tsc-red.md.
When the SUT awaits something (mkdtemp, a config read, a lock) BEFORE it calls the mocked spawn/fetch and attaches listeners, emit the child's close/error/data events from INSIDE the spawn mock (spawnMock.mockImplementation(() => { queueMicrotask(emit); return child; })), NOT from a sibling top-level queueMicrotask in the test body. A test-level microtask scheduled right after calling the SUT fires during the pre-spawn await gap — before listeners exist — so the settle-once promise never resolves and the test times out (16s). The emit must be scheduled relative to when spawn is actually invoked. Why: PR #4970 — adding await mkdtemp before spawn in c4-render.ts timed out 6 tests until the emit moved inside the mock; see knowledge-base/project/learnings/best-practices/2026-06-05-external-cli-exit-0-is-not-proof-validate-the-artifact.md.
When using vi.doMock("specifier", () => { throw new Error("X") }) to simulate a module-init failure, do NOT assert on the inner error message via the SUT's caller. Vitest wraps factory throws with its own synthetic Error ("[vitest] There was an error when mocking a module...") and the inner string is unobservable. Assert on the SUT's observable contract (return shape, observability mirror call) instead — the throw is a trigger, not a contract. See knowledge-base/project/learnings/2026-05-07-vitest-domock-factory-throw-wrapped-message.md.
To prove a cache-hit skips work (not just that the response status is correct), wrap the real implementation in a spy via vi.importActual rather than stubbing the return value: vi.mock("@/module", async () => { const actual = await vi.importActual(...); return { ...actual, expensiveFn: (...args) => { spy(...args); return actual.expensiveFn(...args); } }). Stubbed returns break any downstream behavior that depends on the real output (hash-match, SQL row shape, etc.); wrapping preserves the contract while exposing call counts for assertions like expect(spy).toHaveBeenCalledTimes(1) across a HEAD+GET sequence. Why: In PR #2515, verifying that HEAD populates shareHashVerdictCache so a follow-up GET skips the SHA-256 drain required counting hashStream calls, not stubbing its return — a stubbed return would have broken the post-drain hash-equality check and masked the very regression the test was meant to catch.
When testing decorative images (alt="") with happy-dom, use container.querySelector instead of screen.getAllByRole("img", { hidden: true }) -- happy-dom excludes presentational elements from role queries even with hidden: true.
When asserting against a conditional render branch in a component test, grep the test file's vi.mock(...) factories for the inputs the branch reads and confirm the mock returns values that activate the target branch. Mocks that simplify (e.g., getDisplayName: (id) => id.toUpperCase()) often skip production branches like leader.title.includes(displayName) — assertions on the skipped branch fail for non-bug reasons. Why: PR #3427 — see knowledge-base/project/learnings/2026-05-07-test-assertion-must-verify-mock-activates-branch.md.
A wait-on-ABSENCE (await vi.waitFor(() => expect(queryByTestId(x)).toBeNull())) is vacuous — it passes on the FIRST tick, before the async work resolves, so it never proves "absent AFTER the state commit." Anchor the wait on a positive settle signal (e.g., a .finally(() => { settled = true; }) flag on the mocked response body), then assert absence. Also: vitest's vi.waitFor and RTL's waitFor/findBy* have independent 1 s defaults and independent config surfaces — a global RTL asyncUtilTimeout bump does not touch vi.waitFor call sites. Why: #5113 — see knowledge-base/project/learnings/test-failures/2026-06-10-parallel-load-flake-two-mechanisms-and-vacuous-absence-waits.md.
An intermittent absence-wait that times out at the FULL (explicit) timeout is a component/state RACE, not a timeout-floor problem — raising the timeout cannot fix it. Discriminator: if the failing vi.waitFor site already carries an explicit { timeout }, the floor is irrelevant; trace the component's effect ordering. A passive effect that resets state on EVERY render where a condition holds (if (cond) setX(false)) rather than on a prev→curr transition races any user action that should win (React runs passive effects AFTER commit, so it can land after the click and undo it) — gate such effects on the transition via a prevValue ref. Why: #5796 — see knowledge-base/project/learnings/test-failures/2026-06-30-vi-waitfor-floor-vs-component-rearm-race.md.
When testing a fallback ladder or mode option (primary-then-degrade, retry-then-cache, mergeMode direct→arm-auto-merge), assert the FIRST rung was attempted (the primary call fired), not just the fallback's effect — an effect-only assertion passes identically against an option-ignoring implementation whose default path yields the same end state. Why: PR #5133 — two mergeMode-direct fallback tests passed against the pre-#5111 helper; see knowledge-base/project/learnings/2026-06-11-pipeline-consolidation-behavior-preserving-migration-traps.md.
When adding sessionStorage usage to React components, ensure the component's test file includes sessionStorage.clear() in its beforeEach block. Shared jsdom environments leak sessionStorage between tests, causing ordering-dependent failures.
When adding a React-context-dependent hook (useTheme, useRouter, any provider-gated hook) OR a new provider import to a SHARED component, grep test/ for every file that renders that component DIRECTLY (not via a vi.mock of its module) and add the provider stub in the SAME commit. tsc and the component's own test pass; sibling direct-render tests fail at RUNTIME with <hook> must be used inside <Provider>. Why: PR #5217 — C4Canvas gained useTheme(); c4-fullscreen.test.tsx (the only direct <C4Canvas> renderer) broke 8 tests until stubs for theme-provider + @mantine/core were added. See knowledge-base/project/learnings/2026-06-12-likec4-mantine-color-scheme-seam-and-vendored-theme-preservation.md.
To reproduce a provider's SSR-hydration "no-bootstrap" state in jsdom (lazy useState initializer landed on a server fallback like "system" WHILE durable storage holds the real value AND the DOM attribute is absent), do NOT use a Storage.prototype.getItem call-count spy — it bleeds across tests in the shared jsdom worker (passes in isolation, fails in-suite) and a leftover DOM attribute pollutes later inits. Instead use REAL localStorage (empty at init) and write the stored value from inside the matchMedia.matches getter (fires during the resolved-state initializer — after both init storage reads, before the first-mount effect); scrub the attribute + clear storage inside the mount helper and cleanup() in afterEach. Pair it with a precondition self-check (if (!released) throw) so a future init refactor that stops touching matchMedia fails as a clear FIXTURE error, not a phantom SUT regression. A naive client-only mount masks the bug (initializer reaches the durable value directly → vacuous green). Why: PR #5312 — see knowledge-base/project/learnings/test-failures/2026-06-15-ssr-hydration-no-bootstrap-theme-test-gate.md.
When asserting on vi.getTimerCount(), remember that vi.useFakeTimers() mocks every timer-like API by default — including requestAnimationFrame, setImmediate, queueMicrotask, requestIdleCallback. The count is a SUM across all fake timer types, not just setTimeout. Prefer stability assertions (count before N extra calls === count after) over magnitude assertions (count === 1) so refactors that add a well-behaved rAF or microtask don't falsely read as leaks. See knowledge-base/project/learnings/test-failures/2026-04-17-vitest-getTimerCount-counts-requestAnimationFrame.md.
When a component exports an interface that a test harness consumes (e.g., ChatInputQuoteHandle), have the test import it via type X = ExportedInterface — never shadow with a local duplicate. Duplicate interfaces silently drift when the exported type gains a method; the tsc --noEmit failure surfaces only at build time.
When adding a new npm dependency, check the installed major version (node -e "console.log(require('<pkg>/package.json').version)") and read the type definitions before using API from docs or training data. Library APIs change across major versions (e.g., react-resizable-panels v4 uses Group/Separator/orientation/useDefaultLayout, not v2's PanelGroup/PanelResizeHandle/direction/autoSaveId).
For sizing APIs from third-party libraries, always pass explicit units as strings (e.g., "18%", "100px", "1rem") rather than bare numbers. Docstrings may claim a default unit but runtime parsers often treat numbers as pixels. Why: react-resizable-panels v4 doc said "Percentage of the parent Group (0..100)" for numeric sizes, but the runtime treated 18 as 18px, producing a ~18px-wide sidebar in production. Explicit units make intent visible at the call site and survive library version upgrades.
Test environment setup: If the project's test runner cannot run the type of test needed (e.g., React component tests require jsdom but vitest is configured for node), set up the test environment BEFORE starting the task. This is part of RED — the test infrastructure must exist for the test to fail properly.
await import() for all subsequent dependencies — static ES imports are hoisted before any imperative code, causing libraries like @testing-library/react to initialize without DOM globals. See knowledge-base/project/learnings/test-failures/2026-04-03-bun-test-dom-preload-execution-order.md.*/ inside a /* … */ / /** … */ block comment — it closes the comment early. The trap is documenting a regex that ends in */ (/--[^\n]*/g, foo/**/*): esbuild/tsc then parses the trailing prose as code and reports Expected ";" but found <token> at a line deep inside the docstring (a red herring — the real cause is the stray */ upstream). Describe the regex in prose or use a // line comment; grep -nF '*/' <file> after authoring confirms every hit is real code. Why: #5920 — a */g in a JSDoc comment broke collection of byok-rpc-body-markers.test.ts. See knowledge-base/project/learnings/build-errors/2026-07-03-jsdoc-block-comment-closed-early-by-regex-star-slash.md.pdfjs-dist, sharp, puppeteer, playwright, @xenova/transformers, onnxruntime), pre-warm the module in beforeAll(async () => { await import("<module>"); }, 30_000). The cold-start cost (~5-10s on CI runners) otherwise lands on the first it() and blows the default 5s vitest timeout — the second test in the same file runs at warm ~9ms because subsequent calls hit the module cache. Cheapest detection: git grep -lE '(pdfjs-dist|sharp|puppeteer|playwright|@xenova/transformers|onnxruntime)' -- '*.test.ts' and check for sibling beforeAll. Why: PR #3681 pdf-text-extract.test.ts cold-start flake (7s vs 9ms, #3687)./tmp/). Playwright MCP restricts file access to the repo root. When Google Search Console offers Cloudflare auto-verification, prefer "Any DNS provider" manual flow — the popup OAuth flow opens an external tab that crashes the Playwright browser context.browser_evaluate(filename: ...) from the FIRST attempt — the return value otherwise enters the conversation transcript and the token is leaked even after revocation. AND the filename parameter JSON-encodes the result (surrounding quotes), so the canonical pipe is python3 -c "import sys,json; sys.stdout.write(json.loads(open('<path>').read()))" | doppler secrets set <KEY> --no-interactive. Validate via the vendor's API (HTTP 200 + length check) before shredding the file — some vendors silently tolerate quoted tokens via Authorization: Bearer "abc", but Terraform's HCL parser does not. For ●●●-masked UI tokens (Doppler personal tokens), click the in-page copy button via browser_evaluate, then xclip -selection clipboard -o > <path>; clear with xclip -i </dev/null. Doppler TF var storage convention: drop the TF_VAR_ prefix from the secret name — --name-transformer tf-var ADDS the prefix at injection time (DOPPLER_TOKEN_TF → TF_VAR_doppler_token_tf; storing the already-prefixed TF_VAR_DOPPLER_TOKEN_TF produces TF_VAR_tf_var_doppler_token_tf). See 2026-03-21-doppler-tf-var-naming-alignment.md. Why: PR #3973 (#3960) — full pattern + recovery flow at 2026-05-18-vendor-token-mint-and-oci-image-content-carrier-patterns.md.Write whose hook output emits a warning (security, style, rule), immediately Read the file to verify the full content landed. PreToolUse hooks that print error output but return non-blocking status can still cause partial writes — detecting this only when tests fail wastes a debug round. See knowledge-base/project/learnings/2026-04-15-kb-share-binary-files-lifecycle.md.readFileSync(path) + expect(src).toMatch(...)) as a negative-space regression gate after an extraction, put them in a standalone *.test.ts file — never add them to an existing test file that already mocks node:fs or node:path. The existing vi.mock("node:fs", ...) factory likely omits readFileSync, and the new test will fail at collection with "No readFileSync export is defined" before any assertion runs. Also trim the gate to only the assertion that cannot be expressed behaviorally — usually the negative "symbol-not-present" check. Positive assertions (import regex, await-call regex) duplicate coverage that mock-based behavioral tests already provide and are brittle to barrel re-exports, aliases, and whitespace. See knowledge-base/project/learnings/best-practices/2026-04-17-regex-on-source-delegation-tests-trim-to-negative-space.md.readFileSync + toContain/toMatch over .tf/.yml/.ts), and the obvious correction — slice a narrower region — FAILS when a file puts explanatory comments INSIDE the construct: there is then no scope that holds the config but no prose. Anchor on something a comment cannot produce (^\s*key\s*= — a comment line starts with #; a call shape Fn\(\s*arg), never a bare word. Treat every toContain of a token that also appears in a nearby comment as guilty until mutation-tested, and give every slice helper an explicit lower bound plus an indexOf === -1 guard (slice(-1) yields the last character, so .not.toMatch() against it always passes). Why: #6456 shipped FOUR — /value\s*=\s*2/ matched its own "WHY value = 2 AND NOT 3" comment; toContain("IssueOwners") matched an in-body comment while the entire actions_v2 block was deleted (the rule then paged nobody — the outcome the test was named for) and stayed 10/10 green; a boundless scopeResource swallowed the next resource's comment so a GROUPING-anchor check was satisfied by the pointer to the paragraph it was meant to find. All four were read-and-believed; only mutation caught them. Count failures from the runner's summary line after stripping ANSI — grep -cE '^\s+×' always returns 0. See knowledge-base/project/learnings/2026-07-15-narrowing-is-not-anchoring-and-a-documented-class-recurred-four-times-in-one-pr.md..sh/.test.sh body-grep gate, an AC grep -n … | head -1 order check), anchor it on the syntactic write/call construct (rest/v1/<table>, a function-call shape) — NEVER a bare token (<table>, <flag-name>) that the same file also names in a COMMENT or header-inventory. A body-grep sees comments too, so the moment a task requires both a "must / must-not contain X" assertion AND documenting X in a comment, they collide: a negative ! grep -qE 'X' false-FAILs on the explanatory comment, and a grep -n X | head -1 order check returns the comment line, not the code. Reword forbidden-literal comments to drop the literal. Same class as the source-reading-regex rule above, for bash. Why: PR for #5501 — seed-live-verify-user.sh's user_session_state upsert: the test's ! grep '/rpc/set_current_workspace_id' tripped on a comment naming the RPC, and the AC3 bare grep user_session_state matched the new header-inventory line. See knowledge-base/project/learnings/test-failures/2026-06-17-grep-assertion-over-script-body-false-matches-own-comments.md.