# Ship — Learned Rules Archive

audience: AI coding agents. Consult the index below BEFORE the matching phase; read only matching entries. Rules that fire ≥2× get promoted into SKILL.md process steps; `learn-from-mistakes` appends new rules here.

Index by phase:
- **Worktree/build env:** image-pipeline-dependency-unverified, worktree-stale-node-modules-after-merge, worktree-no-node-modules-blocks-typecheck, astro-check-must-run-from-apps-web, worktree-missing-gitignored-build-scripts, scripts-sh-gitignored-no-commit, worktree-pnpm-tsc-counts-must-match-main
- **Prompt authoring:** verify-component-api-before-subagent-prompt, dead-import-after-block-removal, rtl-physical-margin-in-tsx, implementer-adds-index-without-reading-existing-ddl, prop-sweep-leaves-unused-hook, same-named-wrapper-grep-import-path, design-system-entry-before-component-deletion
- **Parallel waves:** implementer-main-repo-contamination, parallel-wave-typecheck-mid-flight-stale, parallel-wave-controller-must-commit-before-next-wave, controller-commit-must-cd-to-worktree, parallel-wave-stage-from-git-status-not-plan
- **Merge/land:** merge-theirs-drops-imports-in-uu-conflicts, merge-from-main-not-worktree, merge-conflict-resolution-missing-vars, worktree-deploy-relative-path-overwrites-feature, wave2-build-ops-must-use-worktree-paths
- **Review/verification:** build-exit-0-does-not-verify-output, reviewer-ignores-spec-architecture-decisions, spec-review-dynamic-feature-in-static-diff, test-row-time-window-too-short, playwright-stale-server-empty-css-vars, playwright-verification-script-spawn-agent, dev-server-interactive-output-timeout, rtk-git-diff-name-only-truncated, astro-trailingslash-never-breaks-typed-urls

### verify-component-api-before-subagent-prompt | fired:1 | 2026-05-23
Subagent prompt for CartCheckout.tsx specified `<InlineNotice>` with children (no children prop) and `Button variant="outline"` (not a valid variant) → 3 type errors post-dispatch, controller had to fix manually.
Prevent: when writing subagent prompts that include exact JSX, read target component source (`Read` the .tsx) to verify prop types and valid variants before embedding code in prompt. Never guess component APIs from memory.

### image-pipeline-dependency-unverified | fired:1 | 2026-05-24
ImageCard built srcset using `-600w.webp`/`-1200w.webp`/`-2400w.webp` paths. Script `process-gallery-images.mjs` never ran → `gallery-sources/` empty → all images 404 in browser.
Prevent: when component src path depends on a pre-build pipeline (sharp, imagemin, etc.), verify output files exist (`ls public/assets/...`) before committing component code that references them. If pipeline not run, run it or add original-src fallback first.

### astro-trailingslash-never-breaks-typed-urls | fired:1 | 2026-05-24
Set `trailingSlash: 'never'` → `/en/` returned 404 because users naturally type or browser-appends trailing slash. Had to fix post-report.
Prevent: default Astro static sites to `trailingSlash: 'ignore'` — accepts both `/en` and `/en/`. Only use `'never'` or `'always'` when deployment platform enforces it (e.g. Cloudflare Pages redirect rules).

### worktree-stale-node-modules-after-merge | fired:1 | 2026-05-24
Merged `feat/astro-6-upgrade` (package.json changed) into `feat/site-audit-fixes` worktree. Verification ran build with Astro 5.18.1 (old node_modules) despite package.json saying `"astro": "6.3.7"`. Wrong Astro version silently used.
Prevent: after any merge that modifies package.json (or package-lock.json) into a worktree, immediately run `npm install` (or equivalent) before any build/verification step. Check installed version: `node -e "console.log(require('./node_modules/astro/package.json').version)"`.

### merge-theirs-drops-imports-in-uu-conflicts | fired:1 | 2026-05-24
`git merge -X theirs feat/site-audit-fixes` resolved UU conflicts in `src/pages/[lang]/*.astro` by taking branch versions, but resulting files had missing imports (Breadcrumb, buildBreadcrumbSchema, SectionHeader). Caused runtime "X is not defined" errors in 4 pages.
Prevent: after any merge with UU conflicts in src/pages/, run `npx astro build 2>&1 | grep "is not defined"` before committing. UU resolution can silently produce files with missing imports even with -X theirs.

### build-exit-0-does-not-verify-output | fired:1 | 2026-05-24
Implementer ran `npx astro build`, got exit 0, reported "19 pages" — but stale Astro 5 dist was present. Actual fresh build (after rm -rf dist) produced 0 CSS and 1 page due to Tailwind/rolldown incompatibility. Stale dist masked complete failure.
Prevent: verification builds for major upgrades MUST include `rm -rf dist && <build command> && find dist -name '*.html' | wc -l`. Exit 0 alone is not sufficient — always count HTML files in a freshly-emptied dist.

### implementer-main-repo-contamination | fired:1 | 2026-05-25
Subagent wrote `contact.astro` to main repo `src/` path instead of `WORKTREE_PATH` → master working tree dirty, blocked `finishing-a-development-branch` merge and required manual `git checkout --` restoration.
Prevent: after each implementer completes, run `git diff HEAD --name-only` from git root — any `src/` file appearing means subagent wrote to main repo, not worktree. Add to every implementer prompt: "Before writing ANY file, confirm `pwd` is inside WORKTREE_PATH. cd WORKTREE_PATH explicitly as first step."

### dead-import-after-block-removal | fired:1 | 2026-05-25
Implementer removed caption overlay div from ImageCard.astro but left `import Text` which was only used by that div → quality review caught dead import, extra fix needed.
Prevent: when implementer prompt includes removing a UI block/div, add explicit instruction: "After deletion, grep the file for every component/symbol imported — remove any import whose only usage was inside the deleted block."

### rtl-physical-margin-in-tsx | fired:1 | 2026-05-29
Implementer used `mr-2` on flag span instead of logical `me-2` in AudienceTab.tsx, violating Hard Rule 3. Caught by quality reviewer, needed extra fix commit.
Prevent: add explicit RTL reminder to every implementer prompt for .tsx files: "Use logical margin/padding (me-/ms-/pe-/ps-/start-/end-). Never mr-/ml-/pr-/pl-." Quality reviewer still catches it but implementer must avoid first.

### scripts-sh-gitignored-no-commit | fired:1 | 2026-05-31
Implementer tried to commit `scripts/warmup.sh` → blocked. Shell scripts in `scripts/` (deploy.sh, warmup.sh, check.sh) are gitignored dev tooling; only `.ts`/`.mjs` build helpers tracked.
Prevent: before dispatching implementer with commit instructions for any `scripts/*.sh` file, run `git check-ignore scripts/<file>`. If gitignored, prompt must say "write locally, do NOT commit".

### reviewer-ignores-spec-architecture-decisions | fired:1 | 2026-06-01
Final reviewer flagged PhonePreviewSkeleton as FINAL_FAIL ("never moved to components/ui/") — spec's own Architecture Decisions section explicitly stated "extract to sibling file inside vendor-add-deal/steps/, NOT to components/ui/primitives/". Reviewer invented a requirement that contradicted the spec.
Prevent: final reviewer prompt must include instruction: "Before flagging any location/architecture as spec violation, read the spec's Architecture Decisions section. A decision there overrides any implied convention."

### implementer-adds-index-without-reading-existing-ddl | fired:1 | 2026-06-01
T22 implementer added `idx_sessions_token` UNIQUE on `token_hash` to a `### Required Indexes` section without reading the existing DDL block above it. The DDL already had both a `UNIQUE` column constraint AND a non-unique `idx_sessions_hash` on the same column — creating a contradictory duplicate. Caught by final reviewer; required extra fix commit.
Prevent: when implementer prompt asks to add indexes/constraints to a spec that contains a DDL `CREATE TABLE` block, prompt must include: "Read the full DDL schema section first. If an index or constraint already covers the same column, annotate or supersede it — never add a second index on the same column without noting the existing one."

### test-row-time-window-too-short | fired:1 | 2026-06-01
Verification test row set `last_refreshed_at = now() + interval '5 minutes'`. Multi-attempt debug session ran >12 min → condition expired → grace SELECT always missed → T6a returned 302 despite correct code. Wasted full debug cycle investigating wrong root cause (version mismatch, code bugs).
Prevent: time-sensitive test rows (grace windows, TTLs, expiry conditions) must use `+ interval '1 hour'` or longer. Never use short offsets (<10 min) for rows used across multiple debug attempts.

### worktree-deploy-relative-path-overwrites-feature | fired:1 | 2026-06-01
Deployed from worktree (correct, had grace code). Later debug attempt ran `wrangler deploy --config dist/server/wrangler.json` from main repo CWD → deployed main's `dist/` (no grace code) → overwrote feature. Spent session debugging stale code.
Prevent: always use absolute worktree path: `wrangler deploy --config /home/user/Projects/multideal/.worktrees/<branch>/apps/web/dist/server/wrangler.json`. If version ID from `wrangler deployments list` doesn't match expected, check if later deploy overwrote it before any code debugging.

### worktree-no-node-modules-blocks-typecheck | fired:1 | 2026-06-04
Worktree `apps/web/` had no `node_modules` (gitignored) → `astro check` failed with "Cannot find module 'astro/config'". Had to symlink before typecheck could run.
Prevent: after creating worktree, before any typecheck, run: `ln -s /home/user/Projects/multideal/apps/web/node_modules WORKTREE/apps/web/node_modules`. Then cd into `WORKTREE/apps/web` and run `/home/user/Projects/multideal/apps/web/node_modules/.bin/astro check`.

### astro-check-must-run-from-apps-web | fired:1 | 2026-06-04
Running `astro check` from worktree root (not `apps/web/`) produced 185 wrong errors ("Cannot find module '@/layouts/BaseLayout.astro'") — wrong root → stale/garbage result. First typecheck appeared to show 1 error but was actually against wrong directory.
Prevent: always `cd WORKTREE/apps/web` before `astro check`. Verify result file count is ~2600+ (full project). Count < 100 files → wrong CWD, re-run from correct directory.

### parallel-wave-typecheck-mid-flight-stale | fired:1 | 2026-06-04
9 parallel subagents each ran `pnpm --filter web typecheck` during their own execution while siblings were still writing files → each checked a different mid-flight snapshot → results were contradictory and all meaningless. Required one authoritative post-wave check by the controller.
Prevent: in parallel wave prompts, remove the per-subagent typecheck step entirely. After ALL parallel agents complete (controller step), run ONE authoritative `./node_modules/.bin/astro check` from `WORKTREE/apps/web`. Only that result matters.

### merge-from-main-not-worktree | fired:1 | 2026-06-04
`git merge feat/branch` run from inside worktree (CWD = `.worktrees/feat/branch/`) → merging branch into itself → "Already up to date" → push showed no new commits on main.
Prevent: always run `git -C /path/to/main/repo merge ...` or `cd` to the main repo root before merging. Verify with `git -C <main-repo> log --oneline main..feat/branch` BEFORE merging — if commits show, merge hasn't happened yet.

### merge-conflict-resolution-missing-vars | fired:1 | 2026-06-04
Resolved merge conflict in `earnings.astro` by taking branch body but left HEAD's import block unchanged. Branch body referenced `tVendorDashboard` variable that HEAD's frontmatter didn't declare → eslint blocked commit.
Prevent: after resolving any merge conflict in `.astro` files, grep the resolved file for every variable used in the template (`grep '\.astro' -e 'tXxx\|varName'`). Cross-check against frontmatter declarations. Run `eslint --fix <file>` before committing — catches undefined variable errors before the lint-staged hook fires.

### prop-sweep-leaves-unused-hook | fired:1 | 2026-06-04
Wave 3 subagent stripped `pageTitle={t('...')}` from ~20 VendorShell callers but left `const t = useT(...)` unused in 2 files (DraftsList.tsx, VendorEarningsPage.tsx). Subagent reported "zero errors"; TS6133 warnings appeared; controller had to fix manually.
Prevent: subagent prompt for any prop-removal sweep must include: "After removing the prop attribute, check if any `const t = useT(...)` / `const { t }` call in the same component now has zero usages. If so, remove the hook call and its import."

### same-named-wrapper-grep-import-path | fired:1 | 2026-06-05
Two files named `VendorWelcomeWrapper.tsx` existed: `features/vendor-dashboard/` (dead code) and `features/vendor-welcome/` (live, imported by `dashboard.astro`). Implementer modified the dead file; all wave 2 changes landed in unused code.
Prevent: before dispatching any task targeting a file by name (especially `*Wrapper.tsx`, `*Provider.tsx`, `*Container.tsx`), run `grep -rn "import.*FileName\|from.*FileName" src/pages/ src/layouts/` to confirm which file the page actually imports. Two copies = only one is live.

### design-system-entry-before-component-deletion | fired:1 | 2026-06-04
Wave 4 planned to delete VendorTopbar files, but `Domain.tsx` ComponentEntry + import for VendorTopbar was not in the Wave 3 subagent prompt. Controller had to catch and manually edit Domain.tsx before committing Wave 3.
Prevent: when a wave deletes a component, before dispatching the deletion wave, grep `features/design-system/sections/09-domain/Domain.tsx` for an import and ComponentEntry for that component. If found, add Domain.tsx cleanup to the PRECEDING wave's subagent prompt — never let deletion outpace Domain.tsx.

### rtk-git-diff-name-only-truncated | fired:1 | 2026-06-04
`git diff main..branch --name-only` returned empty output via output-filter proxy (then RTK; git now routes through `ft` via bash-gate) — filter reduced the changed-file list to nothing, making controller believe the branch had no diff vs main (it had 15 changed files).
Prevent: never trust an empty filtered `git diff --name-only` result. Rerun with `FT_FULL=1` prefix, or verify with `git diff <sha1> <sha2> -- <specific-path>` / `git ls-tree -r <commit> --name-only | grep <pattern>`. Empty `--name-only` output = filter truncated, not "no changes".

### worktree-missing-gitignored-build-scripts | fired:1 | 2026-06-04
`pnpm build` in worktree failed: `sh: cpu-limit.sh: not found`. `scripts/cpu-limit.sh` gitignored + absent from worktree. Required manual `cp` from main repo before build could proceed.
Prevent: after creating worktree, before build, copy gitignored build scripts: `grep "scripts/" WORKTREE/apps/web/package.json` to find references, then `cp /home/user/Projects/multideal/apps/web/scripts/<file> WORKTREE/apps/web/scripts/<file> && chmod +x ...` for each missing one.

### wave2-build-ops-must-use-worktree-paths | fired:1 | 2026-06-05
Plan Wave 2 (typecheck/build/deploy) used relative `cd apps/web` and main-repo `wrangler.json` path → would have deployed unchanged main code while all changes were in worktree. Advisor blocked before execution.
Prevent: all Wave 2 commands must use absolute worktree path: `cd /home/user/Projects/multideal/.worktrees/<branch>/apps/web` for typecheck/build; `wrangler deploy --config /home/user/Projects/multideal/.worktrees/<branch>/apps/web/dist/server/wrangler.json` for deploy. Verify by checking the wrangler.json path starts with `.worktrees/`, not `multideal/apps/web/dist/`.

### playwright-stale-server-empty-css-vars | fired:1 | 2026-06-05
Ran `pnpm test:e2e` while old preview server was running on port 4321 (`reuseExistingServer: true`). Server served pre-fix CSS → CSS vars returned `""` → 6/7 tests failed. Fresh kill + rebuild fixed all.
Prevent: before running playwright verification from a worktree, kill any server on the configured port: `lsof -ti:<port> | xargs kill -9 2>/dev/null`. Then run `pnpm build` explicitly from the worktree before `pnpm test:e2e`. Never trust a reused server after source changes.

### parallel-wave-controller-must-commit-before-next-wave | fired:1 | 2026-06-05
Wave 1 passed spec+quality review; controller dispatched Wave 2 without committing Wave 1 files. Wave 2 agent (Task 4) committed its own files alongside uncommitted Wave 1 `queries.ts` changes in one commit. `translator.ts` (Wave 1) was then committed after Wave 2, inverting commit order.
Prevent: after ALL tasks in a parallel wave pass spec+quality review, controller MUST `git add <all wave files> && SKIP_SIMPLE_GIT_HOOKS=1 git commit` before dispatching the next wave. Never dispatch Wave N+1 with uncommitted files from Wave N on disk.

### controller-commit-must-cd-to-worktree | fired:1 | 2026-06-06
Controller ran `git add ... && SKIP_SIMPLE_GIT_HOOKS=1 git commit` from main repo root (`/home/user/Projects/multideal`) while all wave changes were in the worktree → "nothing to commit, working tree clean". Required re-run from worktree path.
Prevent: every controller `git add`/`git commit` during a wave MUST be prefixed `cd /home/user/Projects/multideal/.worktrees/<branch> &&`. Never rely on shell CWD being the worktree — hooks and tool calls can reset it to the main repo root.

### parallel-wave-stage-from-git-status-not-plan | fired:1 | 2026-06-06
Plan's hardcoded `git add` list omitted `lib/i18n/he.ts`/`en.ts` (namespace registration files not listed in the plan's Wave 3 task). Would have silently dropped namespace wiring if staged verbatim.
Prevent: for controller wave commit, ALWAYS run `git -C <worktree> status --short` and stage from the ACTUAL modified/untracked set. Never copy the plan's file list verbatim into `git add`. Plan lists intent; `git status` lists reality.

### spec-review-dynamic-feature-in-static-diff | fired:1 | 2026-06-06
Spec said sidebar nav item "with unread badge (poll every 60s)". Implementer added only the static nav item (4-line diff). Spec review passed without catching badge omission — badge requires useQuery + conditional render = ~15 lines minimum.
Prevent: when spec says "with badge/count/indicator", check the diff line count for that component. A dynamic badge (polling fetch + conditional render) can never be implemented in ≤5 lines. If diff is too small for the stated feature, flag badge as missing before approving.

### worktree-pnpm-tsc-counts-must-match-main | fired:1 | 2026-06-10
Symlinked root+apps node_modules into pnpm-monorepo worktree; `packages/*/node_modules` still missing → tsc reported 3514 errors vs 7 real in main repo. Implementer judged them "real" (TS7006/TS2322 shapes, not TS2307) and wrote a 1704-entry garbage ratchet baseline.
Prevent: never write a baseline/snapshot from worktree typecheck output. Before accepting any worktree tsc result, run the same `tsc -p` in the MAIN repo and compare error counts — mismatch >2x = environment garbage, rerun in main. Error shape (no TS2307) does NOT prove errors are real.

### playwright-verification-script-spawn-agent | fired:1 | 2026-06-12
Controller tried to write multi-hundred-line Playwright script via bash heredoc and Python one-liner → both failed: heredoc delimiter issues with single quotes inside the block, Python string escaping broke on nested JS double-quotes. Spent 5+ tool calls before spawning an Agent that wrote and ran the script cleanly in 1 call.
Prevent: when writing a non-trivial Playwright (or any JS) verification script to /tmp, spawn an Agent with the full spec — never use heredoc or `python3 -c "script = '...'"` for scripts containing nested quotes. Agent handles the quoting context natively.

### dev-server-interactive-output-timeout | fired:1 | 2026-06-12
`npm run dev -- --port N &` followed by `sleep N && echo done` in a single Bash call → exit 144 (tool timeout). Interactive TTY output from Vite's "ready" banner blocked the call.
Prevent: always start dev servers with full output redirect: `node_modules/.bin/vite --port N > /tmp/vite.log 2>&1 &`, then `sleep 4 && cat /tmp/vite.log` as a separate call to confirm startup. Never use `npm run dev &` directly — npm wrapper emits interactive output that causes tool timeout.
