sanook-cli 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (235) hide show
  1. package/.env.example +19 -0
  2. package/CHANGELOG.md +144 -0
  3. package/README.md +153 -20
  4. package/README.th.md +136 -0
  5. package/dist/agentContext.js +4 -0
  6. package/dist/approval.js +6 -0
  7. package/dist/bin.js +394 -51
  8. package/dist/brain.js +92 -59
  9. package/dist/brand.js +47 -0
  10. package/dist/checkpoint.js +37 -0
  11. package/dist/commands.js +86 -6
  12. package/dist/compaction.js +76 -5
  13. package/dist/config.js +100 -12
  14. package/dist/cost.js +60 -3
  15. package/dist/doctor.js +92 -0
  16. package/dist/gateway/auth.js +2 -2
  17. package/dist/gateway/ledger.js +2 -2
  18. package/dist/gateway/scheduler.js +1 -0
  19. package/dist/gateway/serve.js +6 -4
  20. package/dist/gateway/server.js +10 -2
  21. package/dist/git.js +11 -2
  22. package/dist/hooks.js +43 -17
  23. package/dist/knowledge.js +48 -49
  24. package/dist/loop.js +182 -66
  25. package/dist/lsp/client.js +173 -0
  26. package/dist/lsp/framing.js +56 -0
  27. package/dist/lsp/index.js +138 -0
  28. package/dist/lsp/servers.js +82 -0
  29. package/dist/mcp-server.js +244 -0
  30. package/dist/mcp.js +184 -29
  31. package/dist/memory-store.js +559 -0
  32. package/dist/memory.js +143 -29
  33. package/dist/orchestrate.js +150 -0
  34. package/dist/providers/codex.js +2 -2
  35. package/dist/providers/keys.js +3 -2
  36. package/dist/providers/registry.js +133 -1
  37. package/dist/repomap.js +93 -0
  38. package/dist/search/chunk.js +158 -0
  39. package/dist/search/embed-store.js +187 -0
  40. package/dist/search/engine.js +203 -0
  41. package/dist/search/fuse.js +35 -0
  42. package/dist/search/index-core.js +187 -0
  43. package/dist/search/indexer.js +241 -0
  44. package/dist/search/store.js +77 -0
  45. package/dist/session.js +42 -8
  46. package/dist/skill-install.js +10 -10
  47. package/dist/skills.js +12 -9
  48. package/dist/summarize.js +31 -0
  49. package/dist/tools/bash.js +21 -2
  50. package/dist/tools/diagnostics.js +41 -0
  51. package/dist/tools/edit.js +29 -7
  52. package/dist/tools/index.js +8 -1
  53. package/dist/tools/list.js +7 -2
  54. package/dist/tools/permission.js +90 -9
  55. package/dist/tools/read.js +23 -4
  56. package/dist/tools/remember.js +1 -1
  57. package/dist/tools/sandbox.js +61 -0
  58. package/dist/tools/search.js +105 -4
  59. package/dist/tools/task.js +195 -29
  60. package/dist/tools/timeout.js +35 -0
  61. package/dist/tools/util.js +10 -0
  62. package/dist/tools/write.js +6 -4
  63. package/dist/trust.js +89 -0
  64. package/dist/ui/app.js +218 -27
  65. package/dist/ui/banner.js +4 -9
  66. package/dist/ui/history.js +30 -0
  67. package/dist/ui/mentions.js +44 -0
  68. package/dist/ui/setup.js +6 -5
  69. package/dist/ui/useEditor.js +83 -0
  70. package/dist/update.js +114 -0
  71. package/dist/worktree.js +173 -0
  72. package/package.json +11 -5
  73. package/scripts/postinstall.mjs +33 -0
  74. package/second-brain/.agents/_Index.md +30 -0
  75. package/second-brain/.agents/skills/_Index.md +30 -0
  76. package/second-brain/.agents/workflows/_Index.md +30 -0
  77. package/second-brain/AGENTS.md +4 -4
  78. package/second-brain/Acceptance/_Index.md +30 -0
  79. package/second-brain/Acceptance/golden-case-template.md +39 -0
  80. package/second-brain/Areas/_Index.md +30 -0
  81. package/second-brain/Bugs/System-OS/_Index.md +30 -0
  82. package/second-brain/Bugs/_Index.md +30 -0
  83. package/second-brain/CLAUDE.md +4 -1
  84. package/second-brain/Checklists/_Index.md +30 -0
  85. package/second-brain/Checklists/preflight-postflight-template.md +29 -0
  86. package/second-brain/Distillations/_Index.md +30 -0
  87. package/second-brain/Entities/_Index.md +30 -0
  88. package/second-brain/Entities/entity-template.md +33 -0
  89. package/second-brain/Evals/_Index.md +30 -0
  90. package/second-brain/Evals/correction-pairs.md +24 -0
  91. package/second-brain/Evals/failure-taxonomy.md +24 -0
  92. package/second-brain/Evals/golden-set.md +25 -0
  93. package/second-brain/Evals/quality-ledger.md +23 -0
  94. package/second-brain/Evals/self-eval-rubric.md +23 -0
  95. package/second-brain/GEMINI.md +4 -4
  96. package/second-brain/Goals/_Index.md +30 -0
  97. package/second-brain/Handoffs/_Index.md +30 -0
  98. package/second-brain/Home.md +7 -0
  99. package/second-brain/Intake/Raw Sources/_Index.md +30 -0
  100. package/second-brain/Intake/_Index.md +30 -0
  101. package/second-brain/Intake/_Quarantine/_Index.md +30 -0
  102. package/second-brain/Learning/_Index.md +30 -0
  103. package/second-brain/Playbooks/_Index.md +30 -0
  104. package/second-brain/Playbooks/playbook-template.md +23 -0
  105. package/second-brain/Projects/_Index.md +30 -0
  106. package/second-brain/Prompts/_Index.md +30 -0
  107. package/second-brain/README.md +2 -1
  108. package/second-brain/Research/_Index.md +30 -0
  109. package/second-brain/Retrospectives/_Index.md +30 -0
  110. package/second-brain/Reviews/_Index.md +30 -0
  111. package/second-brain/Runbooks/_Index.md +30 -0
  112. package/second-brain/Runbooks/eval-loop.md +24 -0
  113. package/second-brain/Sessions/_Index.md +30 -0
  114. package/second-brain/Shared/AI-Context-Index.md +20 -0
  115. package/second-brain/Shared/AI-Threads/_Index.md +30 -0
  116. package/second-brain/Shared/Archive/_Index.md +30 -0
  117. package/second-brain/Shared/Assets/_Index.md +30 -0
  118. package/second-brain/Shared/Context-Packs/_Index.md +30 -0
  119. package/second-brain/Shared/Context7-Docs/_Index.md +30 -0
  120. package/second-brain/Shared/Coordination/NOW.md +28 -0
  121. package/second-brain/Shared/Coordination/_Index.md +30 -0
  122. package/second-brain/Shared/Coordination/agent-registry.md +24 -0
  123. package/second-brain/Shared/Coordination/task-board/_Index.md +30 -0
  124. package/second-brain/Shared/Coordination/task-board/task-template.md +43 -0
  125. package/second-brain/Shared/Coordination/task-board.md +32 -0
  126. package/second-brain/Shared/Core-Facts/_Index.md +30 -0
  127. package/second-brain/Shared/Decision-Memory/_Index.md +30 -0
  128. package/second-brain/Shared/Glossary/_Index.md +30 -0
  129. package/second-brain/Shared/Memory-Inbox/_Index.md +30 -0
  130. package/second-brain/Shared/Operating-State/_Index.md +30 -0
  131. package/second-brain/Shared/Prompting/_Index.md +30 -0
  132. package/second-brain/Shared/Provenance/_Index.md +30 -0
  133. package/second-brain/Shared/Rules/_Index.md +30 -0
  134. package/second-brain/Shared/Rules/contextual-note-rule.md +30 -0
  135. package/second-brain/Shared/Rules/frontmatter-standard.md +10 -0
  136. package/second-brain/Shared/Rules/memory-write-protocol.md +28 -0
  137. package/second-brain/Shared/Rules/procedural-runbook-header.md +40 -0
  138. package/second-brain/Shared/Rules/review-and-staleness-policy.md +22 -0
  139. package/second-brain/Shared/Rules/rules-formatting.md +34 -0
  140. package/second-brain/Shared/Scripts/_Index.md +30 -0
  141. package/second-brain/Shared/Scripts-Archive/_Index.md +30 -0
  142. package/second-brain/Shared/Tech-Standards/_Index.md +30 -0
  143. package/second-brain/Shared/Tech-Standards/verification-standard.md +40 -0
  144. package/second-brain/Shared/User-Memory/_Index.md +30 -0
  145. package/second-brain/Shared/User-Persona/_Index.md +30 -0
  146. package/second-brain/Shared/User-Persona/owner-profile.md +25 -0
  147. package/second-brain/Shared/Working-Memory/_Index.md +30 -0
  148. package/second-brain/Shared/_Index.md +30 -0
  149. package/second-brain/Shared/mcp-servers/_Index.md +30 -0
  150. package/second-brain/Skills/_Index.md +30 -0
  151. package/second-brain/Templates/_Index.md +30 -0
  152. package/second-brain/Templates/bug.md +2 -0
  153. package/second-brain/Templates/handoff.md +2 -0
  154. package/second-brain/Templates/session.md +2 -0
  155. package/second-brain/Tools/_Index.md +30 -0
  156. package/second-brain/Traces/_Index.md +30 -0
  157. package/second-brain/Vault Structure Map.md +33 -1
  158. package/second-brain/copilot/_Index.md +30 -0
  159. package/skills/audit-license-compliance/SKILL.md +117 -0
  160. package/skills/author-codemod/SKILL.md +110 -0
  161. package/skills/build-audit-logging/SKILL.md +112 -0
  162. package/skills/build-cdc-streaming-pipeline/SKILL.md +123 -0
  163. package/skills/build-cli-tool/SKILL.md +108 -0
  164. package/skills/build-data-table/SKILL.md +141 -0
  165. package/skills/build-native-mobile-ui/SKILL.md +154 -0
  166. package/skills/build-offline-first-sync/SKILL.md +118 -0
  167. package/skills/build-realtime-channel/SKILL.md +122 -0
  168. package/skills/build-vector-search/SKILL.md +131 -0
  169. package/skills/compose-local-dev-stack/SKILL.md +149 -0
  170. package/skills/configure-bundler-build/SKILL.md +166 -0
  171. package/skills/configure-dns-tls/SKILL.md +142 -0
  172. package/skills/configure-reverse-proxy-lb/SKILL.md +129 -0
  173. package/skills/configure-security-headers-csp/SKILL.md +122 -0
  174. package/skills/contract-testing/SKILL.md +140 -0
  175. package/skills/datetime-timezone-correctness/SKILL.md +125 -0
  176. package/skills/debug-ci-pipeline-failure/SKILL.md +134 -0
  177. package/skills/debug-flaky-tests/SKILL.md +128 -0
  178. package/skills/defend-llm-prompt-injection/SKILL.md +110 -0
  179. package/skills/deliver-webhooks/SKILL.md +116 -0
  180. package/skills/design-api-pagination/SKILL.md +144 -0
  181. package/skills/design-authorization-model/SKILL.md +119 -0
  182. package/skills/design-backup-dr-recovery/SKILL.md +113 -0
  183. package/skills/design-event-sourcing-cqrs/SKILL.md +143 -0
  184. package/skills/design-multi-tenancy/SKILL.md +100 -0
  185. package/skills/design-protobuf-grpc-service/SKILL.md +146 -0
  186. package/skills/design-relational-schema/SKILL.md +129 -0
  187. package/skills/design-search-index-infra/SKILL.md +151 -0
  188. package/skills/design-state-machine/SKILL.md +108 -0
  189. package/skills/design-token-system/SKILL.md +109 -0
  190. package/skills/distributed-locks-leases/SKILL.md +120 -0
  191. package/skills/encrypt-sensitive-data/SKILL.md +148 -0
  192. package/skills/feature-flags-rollout/SKILL.md +130 -0
  193. package/skills/file-upload-object-storage/SKILL.md +107 -0
  194. package/skills/fuzz-dynamic-security-test/SKILL.md +111 -0
  195. package/skills/harden-llm-app-reliability/SKILL.md +126 -0
  196. package/skills/i18n-localization-setup/SKILL.md +113 -0
  197. package/skills/idempotency-keys/SKILL.md +107 -0
  198. package/skills/implement-push-notifications/SKILL.md +142 -0
  199. package/skills/ingest-webhook-secure/SKILL.md +120 -0
  200. package/skills/integrate-oauth-oidc/SKILL.md +126 -0
  201. package/skills/load-stress-test/SKILL.md +129 -0
  202. package/skills/map-privacy-data-gdpr/SKILL.md +146 -0
  203. package/skills/model-nosql-data/SKILL.md +118 -0
  204. package/skills/money-decimal-arithmetic/SKILL.md +123 -0
  205. package/skills/monitor-ml-drift/SKILL.md +109 -0
  206. package/skills/numeric-precision-units/SKILL.md +144 -0
  207. package/skills/optimize-llm-cost-latency/SKILL.md +103 -0
  208. package/skills/optimize-react-rerenders/SKILL.md +124 -0
  209. package/skills/orchestrate-agent-workflow/SKILL.md +100 -0
  210. package/skills/payments-billing-integration/SKILL.md +114 -0
  211. package/skills/pin-toolchain-versions/SKILL.md +116 -0
  212. package/skills/plan-strangler-migration/SKILL.md +95 -0
  213. package/skills/property-based-testing/SKILL.md +108 -0
  214. package/skills/publish-package-registry/SKILL.md +130 -0
  215. package/skills/recover-git-state/SKILL.md +119 -0
  216. package/skills/remediate-web-vulnerabilities/SKILL.md +125 -0
  217. package/skills/resilience-timeouts-retries/SKILL.md +104 -0
  218. package/skills/resolve-merge-rebase-conflict/SKILL.md +97 -0
  219. package/skills/rewrite-git-history/SKILL.md +109 -0
  220. package/skills/scaffold-cross-platform-app/SKILL.md +137 -0
  221. package/skills/schema-evolution-compatibility/SKILL.md +121 -0
  222. package/skills/send-transactional-email/SKILL.md +126 -0
  223. package/skills/serve-deploy-ml-model/SKILL.md +107 -0
  224. package/skills/setup-cdn-edge-waf/SKILL.md +107 -0
  225. package/skills/setup-devcontainer-env/SKILL.md +131 -0
  226. package/skills/setup-lint-format-precommit/SKILL.md +140 -0
  227. package/skills/setup-monorepo-tooling/SKILL.md +125 -0
  228. package/skills/ship-mobile-app-store-release/SKILL.md +137 -0
  229. package/skills/structured-output-llm/SKILL.md +86 -0
  230. package/skills/supply-chain-sbom-provenance/SKILL.md +120 -0
  231. package/skills/test-data-factories/SKILL.md +158 -0
  232. package/skills/threat-model-stride/SKILL.md +123 -0
  233. package/skills/train-evaluate-ml-model/SKILL.md +109 -0
  234. package/skills/unicode-text-correctness/SKILL.md +109 -0
  235. package/skills/visual-regression-testing/SKILL.md +120 -0
@@ -0,0 +1,107 @@
1
+ ---
2
+ name: setup-cdn-edge-waf
3
+ description: Configures CDN/edge delivery and a WAF — cache keys and Cache-Control/Surrogate-Control, stale-while-revalidate, tag/path purge wired into deploy, origin shielding with request collapsing, edge TLS + HTTP/3, and a managed OWASP ruleset with edge rate-limiting and bot/DDoS mitigation rolled out detect-then-enforce — to raise cache hit-ratio and absorb attacks before the origin.
4
+ when_to_use: Serving static assets or APIs through a CDN, raising cache hit-ratio, or adding edge security (WAF, DDoS, bot mitigation). Distinct from caching-strategy (app/Redis caching), configure-reverse-proxy-lb (the origin proxy itself), rate-limiting (origin app limits), and configure-dns-tls (the cert/DNS records the CDN sits on top of).
5
+ ---
6
+
7
+ ## When to Use
8
+
9
+ Reach for this skill when the work is at the **edge in front of the origin** — caching, TLS termination, and attack absorption at the POP, not inside your app:
10
+
11
+ - "Put the static site / assets / images behind a CDN and stop hitting origin"
12
+ - "Our cache hit-ratio is low / everything is `MISS` / origin is melting under read traffic"
13
+ - "Cache this API response at the edge but purge it the instant we deploy / the record changes"
14
+ - "Add a WAF / block SQLi+XSS / OWASP ruleset / managed rules"
15
+ - "We're getting scraped / credential-stuffed / L7 DDoS'd — mitigate at the edge before it reaches us"
16
+ - "Enable HTTP/3, edge TLS, origin shielding, request collapsing"
17
+
18
+ NOT this skill:
19
+ - Cache-aside / write-through / Redis TTLs *inside the application* → caching-strategy
20
+ - Configuring the origin reverse proxy / load balancer (nginx/Envoy/HAProxy, upstream pools, health checks) → configure-reverse-proxy-lb
21
+ - Per-user/per-key request quotas enforced *in the origin app* (429 + Retry-After business logic) → rate-limiting
22
+ - Issuing the cert, DNS records, HSTS, CAA that the CDN hostname rides on → configure-dns-tls
23
+ - Fixing the actual SQLi/XSS/SSRF in code (the WAF is a shield, not a patch) → remediate-web-vulnerabilities
24
+ - Actively pen-testing the app to find injection bugs → fuzz-dynamic-security-test
25
+
26
+ ## Steps
27
+
28
+ 1. **Classify every route into a cache class first — caching policy follows the class, not the URL.** Decide this before touching any config.
29
+
30
+ | Class | Example | Cache-Control | Edge TTL | Cache key includes |
31
+ |---|---|---|---|---|
32
+ | Immutable static | `/_next/static/*.js`, hashed assets | `public, max-age=31536000, immutable` | 1y | path only |
33
+ | Versioned media | `/img/logo.png` | `public, max-age=86400, stale-while-revalidate=604800` | 1d | path only |
34
+ | HTML (anon) | `/`, `/blog/x` | `public, max-age=0, s-maxage=300, stale-while-revalidate=86400` | 5m (edge), 0 (browser) | path + `Vary` allow-list |
35
+ | Cacheable API (GET) | `/api/products` | `public, s-maxage=60, stale-while-revalidate=300` | 60s | path + canonical query + auth-tier |
36
+ | Private / auth | `/account`, `POST` | `private, no-store` | bypass | never cache |
37
+
38
+ Split `max-age` (browser) from `s-maxage`/`Surrogate-Control` (edge) so you can hold a long edge TTL while browsers revalidate. Origin sets `Surrogate-Control: max-age=...` (CDN strips it before responding); browser-facing `Cache-Control` carries the public value.
39
+
40
+ 2. **Engineer the cache key — this is where hit-ratio is won or lost.** Default to: `scheme + host + path + sorted-allowlisted-query`.
41
+ - **Strip tracking/garbage query params** (`utm_*`, `fbclid`, `gclid`, `ref`, session ids) from the key — otherwise every share link is a `MISS`. Allow-list the params that actually change the response; drop everything else.
42
+ - **Sort query params** so `?a=1&b=2` and `?b=2&a=1` collapse to one entry.
43
+ - **`Vary` only on what truly changes the body** — usually `Accept-Encoding` (or let the CDN normalize it) and at most a derived `X-Device-Type` (mobile/desktop) or auth-tier cookie you set. **Never `Vary: Cookie` or `Vary: User-Agent`** — that fragments the cache into near-uniqueness and kills hit-ratio.
44
+ - For cacheable APIs, normalize the auth dimension to a *tier* (e.g. `anon` vs `member`), not the raw token — fold the token down to a coarse bucket in an edge function before keying.
45
+
46
+ 3. **Add `stale-while-revalidate` and `stale-if-error` everywhere cacheable.** SWR serves the stale object instantly and revalidates in the background — no user waits on origin. `stale-if-error` keeps the site up when origin 5xx's. Without these, every TTL expiry is a latency spike and a thundering-herd to origin.
47
+
48
+ 4. **Wire purge into deploy — by tag/path, never blanket.** A full "purge everything" after each deploy nukes hit-ratio to ~0 and stampedes origin. Tag objects with surrogate keys at the origin and purge those keys:
49
+
50
+ ```http
51
+ # origin response tags the object
52
+ Surrogate-Key: product-42 catalog homepage
53
+
54
+ # deploy/webhook purges only the affected keys (Fastly example)
55
+ curl -X POST -H "Fastly-Key: $FASTLY_API_TOKEN" \
56
+ https://api.fastly.com/service/$SVC/purge/product-42
57
+ ```
58
+ - Static assets are **content-hashed** (`app.4f2a.js`) → never purge them, just deploy new filenames; old ones expire naturally.
59
+ - Purge HTML/API by surrogate key on the entity that changed (`product-42`), driven from the same event that writes the DB — not on a timer.
60
+ - Reserve path-based soft purge for the rare untagged route. Blanket purge is a break-glass action, not a deploy step.
61
+
62
+ 5. **Turn on origin shielding + request collapsing.** Pin a single shield POP between all edge POPs and origin so a global cache `MISS` hits origin once, not once-per-POP. Request collapsing (a.k.a. coalescing) dedupes concurrent `MISS`es for the same key into **one** origin fetch while the rest wait — this is your built-in cache-stampede guard at the edge. Confirm both are enabled; they are the difference between a viral event being absorbed vs. a self-inflicted DDoS on origin.
63
+
64
+ 6. **Terminate TLS at the edge and enable HTTP/3.** Edge TLS (TLS 1.3) + `Alt-Svc: h3` / HTTP/3 (QUIC) cuts handshake RTTs and head-of-line blocking on lossy/mobile networks. Keep **origin** connections on TLS too (full/strict, validated cert) — edge-to-origin in cleartext is a classic foot-gun. Force HTTPS with a 308 redirect at the edge; let configure-dns-tls own the cert issuance/HSTS/CAA underneath.
65
+
66
+ 7. **Enable the WAF in detect/log mode first, then enforce.** Order matters — flipping a managed ruleset straight to block will false-positive real traffic and page you at 2am.
67
+ - Turn on the **managed OWASP Core Rule Set** (SQLi, XSS, RCE, LFI/RFI, protocol anomalies) in **count/log mode**. Watch for 24–72h.
68
+ - Triage the log: tune or exception the rules that hit legitimate traffic (rich-text editors, `<`/`'` in JSON bodies, signed webhooks). Adjust the **anomaly/paranoia threshold** rather than disabling whole rule families.
69
+ - **Then flip to block.** Add **custom rules** for your app's known-bad shapes (admin paths from non-office IPs, deprecated API versions, oversized bodies). Example custom rule + edge rate-limit (Cloudflare ruleset-expression syntax):
70
+
71
+ ```
72
+ # block /admin from outside the office, enforce mode
73
+ (http.request.uri.path matches "^/admin" and not ip.src in $office_ips) -> block
74
+
75
+ # edge rate-limit /login: >10 req/min/IP -> managed challenge (never reaches origin)
76
+ when (http.request.uri.path eq "/login")
77
+ rate_limit { characteristics = ["ip.src"]; period = 60; requests = 10; action = "managed_challenge" }
78
+ ```
79
+ - **Edge rate-limiting** for brute-force/scraping (e.g. `/login` > N/min/IP → challenge or 429) — this is the *edge* sibling of origin `rate-limiting`; do coarse volumetric/abuse limits here, fine per-key quotas in the app.
80
+ - **Bot + DDoS mitigation:** enable managed bot rules + L3/4 and L7 DDoS protection; serve a **JS/managed challenge** (not a hard block) to suspected bots so false positives self-recover. Geo/IP allow/deny rules only where you have a real basis (block sanctioned regions, allow-list admin office IPs) — geo-blocking is blunt and breaks VPN users, so prefer challenges.
81
+
82
+ ## Common Errors
83
+
84
+ - **Blanket purge on every deploy.** Drops global hit-ratio to zero and stampedes origin each release. Tag with surrogate keys and purge only what changed; let hashed assets expire on their own.
85
+ - **Tracking params in the cache key.** `?utm_source=...` makes every shared link a unique `MISS`. Strip the allow-list's complement before keying.
86
+ - **`Vary: Cookie` (or `User-Agent`).** Fragments the cache to near-uniqueness — effectively no caching. Normalize to a coarse derived dimension (auth-tier, device-class) and `Vary` on that.
87
+ - **One TTL for both browser and edge.** Using `max-age` alone means you can't hold a long edge TTL without baking it into browsers. Separate `s-maxage`/`Surrogate-Control` (edge) from `max-age` (browser).
88
+ - **No `stale-while-revalidate`.** Every expiry becomes a synchronous origin round-trip and a latency spike; add SWR + `stale-if-error`.
89
+ - **No origin shield / no request collapsing.** A cold key fetches origin once *per POP* and concurrent misses each hit origin — a viral object DDoSes you. Enable both.
90
+ - **Cleartext edge-to-origin.** TLS terminates at the edge but the origin pull is HTTP — a MITM goldmine. Use full/strict mode with a validated origin cert.
91
+ - **WAF flipped straight to block.** Guaranteed false positives on launch. Run count/log mode 24–72h, tune, then enforce.
92
+ - **Hard-blocking suspected bots.** A false positive locks out a real user with no recovery. Serve a managed/JS challenge instead so legit clients pass automatically.
93
+ - **Caching `Set-Cookie` / personalized HTML.** Leaks one user's session or data to the next requester. Force `private, no-store` on any response carrying `Set-Cookie` or auth-specific content, and strip `Set-Cookie` from cacheable responses.
94
+ - **Treating the WAF as the fix.** A blocked payload is still an unpatched bug; an attacker who bypasses one rule still wins. Fix the vuln in code (remediate-web-vulnerabilities) — the WAF only buys time.
95
+
96
+ ## Verify
97
+
98
+ 1. **Hit-ratio rises, origin load drops.** Pull the CDN analytics cache hit-ratio before/after; it should climb (static well above 95%, cacheable HTML/API materially up). Confirm origin request rate / bandwidth fell by a corresponding amount — the whole point.
99
+ 2. **Edge actually served it.** `curl -sI https://host/path` shows the CDN cache header (`x-cache: HIT`, `cf-cache-status: HIT`, or `x-served-by` with `cache-*-HIT`) and the expected `Cache-Control`/`age`. A second request after a `MISS` must return `HIT`.
100
+ 3. **Key normalization works.** Request `?utm_source=x` and the bare URL → both `HIT` the same object (same `age`/etag), proving tracking params are stripped and params are sorted.
101
+ 4. **Purge actually invalidates.** Cache an object (`HIT`), change the entity, fire the surrogate-key/path purge, re-request → `MISS` then fresh content. A purge that doesn't flip `HIT`→`MISS` is broken.
102
+ 5. **SWR serves stale instantly.** After TTL expiry, the first request returns immediately (stale, background revalidate) rather than blocking on origin; latency stays flat across the expiry boundary.
103
+ 6. **TLS + HTTP/3 negotiated.** `curl --http3 -sI https://host` succeeds and `Alt-Svc: h3` is advertised; TLS 1.3 on edge *and* validated TLS on the origin pull (no cleartext hop).
104
+ 7. **WAF blocks a test attack.** Send a benign canary payload (`?q=' OR 1=1--`, a reflected-XSS probe) → blocked (403/challenge) with a rule id in the WAF log. Confirm in the same window that real traffic shows ~0 false positives (block-mode log clean of legit requests).
105
+ 8. **Edge rate-limit / bot rule fires.** Burst `/login` past the threshold from one IP → challenge/429 at the edge (request never reaches origin logs). A known-good client sails through.
106
+
107
+ Done = cache hit-ratio is up and measured origin load is down, `HIT`/purge/SWR behave exactly as configured, edge+origin TLS with HTTP/3 are verified, and the WAF blocks a canary attack with zero false positives on real traffic (managed rules in enforce mode, not count).
@@ -0,0 +1,131 @@
1
+ ---
2
+ name: setup-devcontainer-env
3
+ description: Builds a reproducible Dev Container workspace from a devcontainer.json — pinned base image, language toolchains via devcontainer features, editor config, postCreate provisioning, runtime-injected dev secrets, dependency cache volumes, and parity with the CI base image.
4
+ when_to_use: User wants a clone-and-reopen reproducible workspace, to kill "works on my machine", to onboard a dev fast, or to make a repo Codespaces-ready. Not the production runtime image (dockerfile-optimize), not the multi-service backing stack (compose-local-dev-stack), not host-level version pinning without containers (pin-toolchain-versions).
5
+ ---
6
+
7
+ ## When to Use
8
+
9
+ - "Set up a dev container so everyone gets the same Node/Python/Go and tools"
10
+ - "New hire spends a day installing deps — make it `clone → reopen in container → it just works`"
11
+ - "Make this repo open in GitHub Codespaces"
12
+ - "Our CI uses a different image than our laptops and that's why builds diverge"
13
+ - "I want a disposable, sandboxed workspace I can blow away and rebuild"
14
+
15
+ NOT this skill:
16
+ - Optimizing the **production runtime** image (multi-stage, distroless, smaller layers) → dockerfile-optimize
17
+ - Standing up **backing services** (Postgres + Redis + a queue) the app talks to → compose-local-dev-stack
18
+ - Pinning tool versions on the **host without containers** (asdf/mise/.tool-versions) → pin-toolchain-versions
19
+ - Secrets in CI/infra, Vault, rotation, leak scanning → secrets-management
20
+ - The CI **workflow** itself (jobs, caching, OIDC) → cicd-pipeline-author
21
+
22
+ A Dev Container is the **development** environment (editor, debuggers, source bind-mounted, runs as you). Keep it distinct from the lean image you ship.
23
+
24
+ ## Steps
25
+
26
+ 1. **Pick the container source — one of three, decided up front.** Put `.devcontainer/devcontainer.json` at repo root (or `.devcontainer/<name>/devcontainer.json` for multiple).
27
+
28
+ | Source key | Use when | Tradeoff |
29
+ |---|---|---|
30
+ | `image` | Toolchain is standard, get a workspace in seconds | Customize only via features + postCreate, no custom layers |
31
+ | `build.dockerfile` | You need OS packages/layers not covered by features, **or want parity with your CI image** | Slower first build; you maintain a Dockerfile |
32
+ | `dockerComposeFile` + `service` | The dev workspace must join a multi-container stack (db, redis) | Heaviest; stack ownership belongs to compose-local-dev-stack — reference it, don't rebuild it |
33
+
34
+ **Default: `build.dockerfile`** pointing at a thin dev Dockerfile that `FROM`s the *same pinned base as CI*. That single decision is what kills environment drift.
35
+
36
+ 2. **Pin the base by digest, never `latest`.** A floating tag means a rebuild three months later silently bumps glibc/openssl and re-breaks "works on my machine". Use the prebuilt devcontainer bases (they ship a non-root `vscode` user + common CLIs) and pin them:
37
+
38
+ ```dockerfile
39
+ # .devcontainer/Dockerfile
40
+ FROM mcr.microsoft.com/devcontainers/base:ubuntu-24.04@sha256:<digest>
41
+ # OS packages features can't provide — keep this list tiny
42
+ RUN apt-get update && apt-get install -y --no-install-recommends \
43
+ postgresql-client \
44
+ && rm -rf /var/lib/apt/lists/*
45
+ ```
46
+ Refresh the digest deliberately (a dependency-upgrade pass), not by accident.
47
+
48
+ 3. **Install toolchains via `features`, not ad-hoc `apt`/`curl|sh`.** Features are versioned, composable, cache well, and stay portable to Codespaces. Pin each feature to an exact toolchain version — `latest` here reintroduces the drift you just removed.
49
+
50
+ ```jsonc
51
+ {
52
+ "name": "app-dev",
53
+ "build": { "dockerfile": "Dockerfile" },
54
+ "features": {
55
+ "ghcr.io/devcontainers/features/node:1": { "version": "20.17.0" },
56
+ "ghcr.io/devcontainers/features/python:1": { "version": "3.12" },
57
+ "ghcr.io/devcontainers/features/github-cli:1": {}
58
+ },
59
+ "remoteUser": "vscode"
60
+ }
61
+ ```
62
+ Keep feature versions in lockstep with `.tool-versions`/CI so host, container, and CI agree. Reserve the Dockerfile for things no feature provides (step 2).
63
+
64
+ 4. **Wire the editor, ports, and lifecycle hooks in the same file.**
65
+
66
+ ```jsonc
67
+ {
68
+ "customizations": {
69
+ "vscode": {
70
+ "extensions": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "ms-python.python"],
71
+ "settings": { "editor.formatOnSave": true, "terminal.integrated.defaultProfile.linux": "zsh" }
72
+ }
73
+ },
74
+ "forwardPorts": [3000, 5432],
75
+ "portsAttributes": { "3000": { "label": "web", "onAutoForward": "notify" } },
76
+ "onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}",
77
+ "postCreateCommand": "npm ci && npm run build",
78
+ "postStartCommand": "npm run db:migrate"
79
+ }
80
+ ```
81
+ - `postCreateCommand` — runs **once** after the container is created. Put idempotent provisioning here: install deps, build, seed. Make it a script (`.devcontainer/post-create.sh`, `set -euo pipefail`) once it grows past one command.
82
+ - `postStartCommand` — runs **every start**. Cheap/repeatable only (migrations, service warmup). Never put `npm ci` here.
83
+ - `onCreateCommand` — earliest hook, before secrets/mounts; use for the `safe.directory` fix and base setup.
84
+
85
+ 5. **Run as a non-root user; only `root` when a hook truly needs it.** Set `"remoteUser": "vscode"` so bind-mounted files you create are owned by your host UID, not root. If a feature/base lacks the user, create it in the Dockerfile (`useradd -m vscode`). Need root for one provisioning step? Use `"onCreateCommand"` with `sudo`, then drop back — don't run the whole container as root.
86
+
87
+ 6. **Cache dependencies in named volumes so rebuilds aren't from-scratch.** Bind-mounting the workspace is automatic; the slow part is re-downloading deps. Mount package caches (not `node_modules` inside the workspace — that fights the bind mount):
88
+
89
+ ```jsonc
90
+ "mounts": [
91
+ "source=app-pnpm-store,target=/home/vscode/.local/share/pnpm/store,type=volume",
92
+ "source=app-pip-cache,target=/home/vscode/.cache/pip,type=volume"
93
+ ]
94
+ ```
95
+ A fresh `postCreateCommand` then restores from a warm cache instead of the network. For `node_modules`, prefer a volume at the workspace's `node_modules` path so it doesn't sync over the bind mount on macOS/Windows.
96
+
97
+ 7. **Inject dev secrets at runtime — never bake them into the image.** A secret in a Dockerfile layer is in the image history forever and leaks to anyone who pulls it.
98
+ - Provide a committed `.env.example`; have `postCreateCommand` copy it to a gitignored `.env` the app reads.
99
+ - For values devs supply, use `"remoteEnv"` referencing host env vars (`"remoteEnv": { "GH_TOKEN": "${localEnv:GH_TOKEN}" }`), or `runArgs: ["--env-file", ".devcontainer/devcontainer.env"]` (gitignored).
100
+ - In **Codespaces**, real secrets come from repo/Codespaces secrets and appear as env vars — your code must read from env either way, with no host-path assumptions.
101
+ These are *dev-only* credentials (local DB password, sandbox API key). Real production secrets and rotation belong to secrets-management.
102
+
103
+ 8. **Achieve CI parity by sharing the base, not eyeballing versions.** Point the dev Dockerfile's `FROM` (or a shared build stage) at the **exact pinned image your CI runs on**, and keep feature/toolchain versions equal to CI's. Build the image once and let both consume it; if CI builds its own, diff the resolved versions (`node -v`, `python --version`) in step 9. Parity verified by sameness of inputs beats "looks close".
104
+
105
+ 9. **Verify on a truly clean machine state (see Verify).** The only honest test of reproducibility is a fresh clone + rebuilt container with no host toolchain, including a Codespaces or `--no-cache` rebuild.
106
+
107
+ ## Common Errors
108
+
109
+ - **`latest`/floating tags on the base image or features.** Looks reproducible until a rebuild months later pulls a new toolchain and breaks the build. Pin the base by `@sha256:` digest and every feature to an exact version.
110
+ - **Installing toolchains with `RUN curl ... | sh` in the Dockerfile.** Unpinned, unportable, and re-downloads on every cache miss. Use a versioned `feature` instead; reserve the Dockerfile for OS packages features can't provide.
111
+ - **Baking secrets/tokens into image layers** (`ENV API_KEY=...` or `COPY .env`). They persist in image history and leak on push. Inject at runtime via `remoteEnv`/`--env-file`/Codespaces secrets; commit only `.env.example`.
112
+ - **Running the container as root.** Files you create on the bind-mounted workspace become root-owned and uneditable from the host. Set `remoteUser` to a non-root user matching your UID.
113
+ - **Heavy work in `postStartCommand`.** It runs on every start, so `npm ci`/full builds make every "reopen" crawl. Heavy/once-only provisioning → `postCreateCommand`; only cheap idempotent steps → `postStartCommand`.
114
+ - **Non-idempotent `postCreateCommand`.** Re-running on rebuild double-seeds the DB or fails on existing rows. Guard with `IF NOT EXISTS`/`--if-not-exists`/existence checks so a rebuild is clean.
115
+ - **`node_modules` (or other native deps) on the host bind mount.** Native binaries built for the host OS break in the Linux container, and sync is slow on macOS/Windows. Put `node_modules` in a named volume at the workspace path.
116
+ - **Dev container drifting from CI.** Different base, different Node minor → the classic green-locally/red-in-CI. Derive both from one pinned base and keep feature versions identical; diff resolved versions in verification.
117
+ - **Testing the rebuild on a machine that still has the host toolchain installed.** It masks "the container forgot to install X" because the host silently provides it. Validate where no host toolchain exists (Codespaces, a clean VM, or a `--no-cache` rebuild).
118
+ - **Editing `devcontainer.json` and expecting a running container to pick it up.** Lifecycle/feature changes only apply on **Rebuild Container**, not reload-window. Always rebuild after config changes.
119
+
120
+ ## Verify
121
+
122
+ 1. **Fresh clone, cold open:** Clone into a directory with no prior build, "Reopen in Container" → container builds, `postCreateCommand` runs to completion (exit 0), and the app builds/starts with **zero host toolchain installed** (uninstall or run in a clean VM).
123
+ 2. **Pins are real:** `grep` the Dockerfile and `devcontainer.json` — base is `@sha256:`, every feature has an explicit `version`, no `:latest`. No raw `curl|sh` toolchain installs.
124
+ 3. **No baked secrets:** `docker history --no-trunc <image>` shows no tokens/keys/`.env`; the running app reads secrets from env/`.env` injected at runtime. `.env` is gitignored; `.env.example` is committed.
125
+ 4. **Non-root + ownership:** `whoami` inside ≠ `root`; a file created in the workspace is owned by your host user, editable from the host.
126
+ 5. **CI parity:** Resolved versions inside the container (`node -v`, `python --version`, `go version`) match the CI job's exactly. The dev base and CI base resolve to the same digest (or documented-equal).
127
+ 6. **Cache works:** Rebuild the container; `postCreateCommand` restores deps from the cache volume without re-downloading the full dependency set (visibly faster than the first build).
128
+ 7. **Idempotent rebuild:** Run "Rebuild Container" twice — second `postCreateCommand` succeeds with no duplicate seeds/errors.
129
+ 8. **Codespaces (if targeted):** The repo opens in a Codespace and reaches the same working state with no local-filesystem/host assumptions.
130
+
131
+ Done = a fresh clone with no host toolchain reaches a building, running app via reopen-in-container; every base/feature is version-pinned; no secret is in any image layer; the container runs non-root; and resolved toolchain versions match CI exactly.
@@ -0,0 +1,140 @@
1
+ ---
2
+ name: setup-lint-format-precommit
3
+ description: Stands up a lint, format, and pre-commit toolchain (Biome or ESLint flat config + Prettier, or ruff + ruff format) with .editorconfig, fast staged-only git hooks (husky+lint-staged or the pre-commit framework), and a check-only CI gate (lint --max-warnings=0 + format --check, no auto-fix), plus a one-shot reformat of a dirty repo hidden behind .git-blame-ignore-revs.
4
+ when_to_use: A repo has no/inconsistent linting or formatting, style churn floods diffs, or commits bypass checks and you're adding enforced gates. Distinct from type-safety-strict (type-checker strictness, not style), code-review (human correctness review), refactor-cleanup (behavior-preserving cleanups, not gates), and pin-toolchain-versions (pinning the binaries the gate runs).
5
+ ---
6
+
7
+ ## When to Use
8
+
9
+ - "Set up ESLint/Biome/ruff + a formatter for this repo"
10
+ - "Add a pre-commit hook so unformatted/lint-failing code can't get committed"
11
+ - "Whitespace/quote/import-order churn is polluting every diff — kill it"
12
+ - "CI should fail on lint warnings and unformatted files"
13
+ - "We have config but it's slow, inconsistent across machines, or people `--no-verify` past it"
14
+
15
+ NOT this skill:
16
+ - Making the **type checker** strict (`strict: true`, removing `any`, `mypy --strict`) → type-safety-strict
17
+ - Judging whether the code is **correct** (logic bugs, edge cases) → code-review
18
+ - Behavior-preserving **cleanups/renames/dedup** of working code → refactor-cleanup
19
+ - Pinning Node/Python/tool **versions** so everyone runs the same binaries → pin-toolchain-versions
20
+ - A **monorepo's** cross-package task wiring/caching/affected-only runs → setup-monorepo-tooling
21
+
22
+ ## Steps
23
+
24
+ 1. **Pick the toolchain by ecosystem — never run a linter *and* a formatter that fight over the same rules.** The linter checks logic/correctness rules; the formatter owns whitespace/quotes/commas. Never leave ESLint stylistic/formatting rules on alongside Prettier.
25
+
26
+ | Stack | Default | Why |
27
+ |---|---|---|
28
+ | JS/TS, want speed + one tool | **Biome** (`biome lint` + `biome format`) | One Rust binary, no plugin graph, ~10–100x faster, lint+format+import-sort in one config |
29
+ | JS/TS, need plugin ecosystem (React, a11y, import, custom) | **ESLint flat config (`eslint.config.js`) + Prettier** | Plugin coverage Biome lacks; Prettier owns formatting so disable ESLint stylistic rules |
30
+ | Python | **ruff** (`ruff check`) **+ `ruff format`** | Replaces flake8+isort+black+pyupgrade in one tool; `ruff format` is black-compatible |
31
+
32
+ Default to **Biome** for greenfield JS/TS; switch to **ESLint flat + Prettier** only when a required plugin (e.g. `eslint-plugin-jsx-a11y`, `eslint-plugin-import`) has no Biome equivalent; **ruff + ruff format** for Python. Don't reach for ESLint legacy `.eslintrc` — flat config is the only supported format on ESLint v9+.
33
+
34
+ 2. **Write minimal config that extends a shared base — don't hand-roll a 200-rule file.** Turn on the recommended preset, override only the handful you actually disagree with.
35
+
36
+ Biome (`biome.json`):
37
+ ```json
38
+ {
39
+ "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
40
+ "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true },
41
+ "linter": { "enabled": true, "rules": { "recommended": true } },
42
+ "formatter": { "enabled": true, "indentStyle": "space", "indentWidth": 2 },
43
+ "organizeImports": { "enabled": true }
44
+ }
45
+ ```
46
+ ESLint flat (`eslint.config.js`) — keep Prettier as the formatter, use `eslint-config-prettier` to switch off every conflicting ESLint rule:
47
+ ```js
48
+ import js from "@eslint/js";
49
+ import prettier from "eslint-config-prettier";
50
+ export default [
51
+ js.configs.recommended,
52
+ prettier, // MUST be last — disables all stylistic rules so Prettier wins
53
+ { ignores: ["dist/", "build/", "coverage/", "node_modules/"] },
54
+ ];
55
+ ```
56
+ ruff (`pyproject.toml`):
57
+ ```toml
58
+ [tool.ruff]
59
+ line-length = 100
60
+ [tool.ruff.lint]
61
+ select = ["E", "F", "I", "UP", "B"] # pycodestyle, pyflakes, isort, pyupgrade, bugbear
62
+ ```
63
+
64
+ 3. **Add `.editorconfig` + format-on-save so the editor stops introducing churn at the source.** One `.editorconfig` at the repo root makes every editor agree on charset/EOL/indent before a linter ever runs:
65
+ ```ini
66
+ root = true
67
+ [*]
68
+ charset = utf-8
69
+ end_of_line = lf
70
+ insert_final_newline = true
71
+ trim_trailing_whitespace = true
72
+ indent_style = space
73
+ indent_size = 2
74
+ ```
75
+ Commit `.vscode/settings.json` with `"editor.formatOnSave": true` and `"editor.defaultFormatter"` set to the chosen tool (`biomejs.biome`, `esbenp.prettier-vscode`, or `charliermarsh.ruff`) so the gate rarely fires in the first place.
76
+
77
+ 4. **Wire the pre-commit hook to run on *changed files only* — keep it under a few seconds or people will `--no-verify`.** For JS/TS use **husky + lint-staged** (lints/formats only staged files); for Python or polyglot repos use the **pre-commit framework**.
78
+
79
+ husky + lint-staged:
80
+ ```bash
81
+ npm i -D husky lint-staged && npx husky init
82
+ printf 'npx lint-staged\n' > .husky/pre-commit
83
+ ```
84
+ `package.json` — lint-staged runs on the staged paths and re-stages what it fixes:
85
+ ```json
86
+ "lint-staged": {
87
+ "*.{js,ts,jsx,tsx}": ["biome check --write --no-errors-on-unmatched"],
88
+ "*.{json,css,md}": ["biome format --write --no-errors-on-unmatched"]
89
+ }
90
+ ```
91
+ pre-commit framework (`.pre-commit-config.yaml`) — pin hook revs, then `pre-commit install`:
92
+ ```yaml
93
+ repos:
94
+ - repo: https://github.com/astral-sh/ruff-pre-commit
95
+ rev: v0.6.9
96
+ hooks:
97
+ - id: ruff # lint
98
+ args: [--fix]
99
+ - id: ruff-format
100
+ ```
101
+ The hook may auto-fix locally; CI must not (step 5). Never run a full-repo lint in the hook — staged-only is what keeps it fast.
102
+
103
+ 5. **Make CI the real gate: check, never fix.** The hook is bypassable (`--no-verify`, unconfigured machines, IDE off); CI is the wall. Run lint with **zero-tolerance** and format in **check** mode so CI fails on drift instead of silently rewriting it:
104
+ - Biome: `biome ci .` (lint + format check + import-order in one CI-tuned command)
105
+ - ESLint + Prettier: `eslint . --max-warnings=0` **and** `prettier --check .`
106
+ - ruff: `ruff check --output-format=github .` **and** `ruff format --check .`
107
+
108
+ `--max-warnings=0` makes warnings fail the build (otherwise they rot into hundreds). `--check`/`ci` exits non-zero on any unformatted file and prints the diff — **never** `--write`/`--fix` in CI, which would push autofixes nobody reviewed. Run on the same tool versions the hook uses (a lockfile + pinned hook revs), or CI and local disagree — coordinate with pin-toolchain-versions if the binaries float.
109
+
110
+ 6. **Migrate a dirty repo in one isolated reformat commit, then hide it from blame.** Don't fold the format sweep into a feature PR — it buries the real change. Run the formatter across the whole tree once, commit alone, then add that commit to `.git-blame-ignore-revs` so `git blame` skips it:
111
+ ```bash
112
+ biome format --write . || (prettier --write . ; ruff format .)
113
+ git commit -am "style: format entire repo (no behavior change)"
114
+ git rev-parse HEAD >> .git-blame-ignore-revs
115
+ git config blame.ignoreRevsFile .git-blame-ignore-revs # local; GitHub/GitLab honor the committed file
116
+ ```
117
+ Sanity-check the sweep changed only formatting: `git show --stat` should be whitespace/quotes only. After this commit the CI `--check` gate passes on a clean tree, so every later PR is gated against an already-formatted baseline.
118
+
119
+ ## Common Errors
120
+
121
+ - **Linter and formatter fighting.** ESLint stylistic rules (or `airbnb` quote/semi rules) vs Prettier produce an infinite "fixed by one, broken by the other" loop. Add `eslint-config-prettier` **last** in the flat config to disable every conflicting rule; let the formatter own formatting.
122
+ - **Hook lints the whole repo.** A pre-commit that runs `eslint .` takes 30s+ and gets `--no-verify`'d into uselessness. Use lint-staged / pre-commit's built-in file filtering so it only touches staged paths.
123
+ - **CI auto-fixes instead of checking.** `eslint --fix` / `prettier --write` / `ruff --fix` in CI either commits unreviewed changes or, on a read-only checkout, masks failures. CI must use `--check`/`ci`/`--max-warnings=0` and fail, not mutate.
124
+ - **Warnings allowed in CI.** Without `--max-warnings=0`, warnings accumulate into noise nobody reads. Treat warnings as errors in CI; downgrade a rule to `off` deliberately if you truly don't want it.
125
+ - **Format sweep mixed into a feature PR.** Reviewers can't see the real diff and `git blame` points everything at you. Reformat in its own commit and register it in `.git-blame-ignore-revs`.
126
+ - **No `.editorconfig` / format-on-save.** Editors keep reintroducing CRLF/tabs/trailing whitespace, so the hook fires on every commit. Fix it at the editor with a committed `.editorconfig` + `formatOnSave`.
127
+ - **Legacy `.eslintrc` with new ESLint.** ESLint v9 defaults to flat config; a leftover `.eslintrc.json` is silently ignored or errors. Migrate to `eslint.config.js`.
128
+ - **Unpinned hook/tool versions.** `pre-commit autoupdate` or a floating `biome`/`eslint` makes CI and local disagree and breaks reproducibly-later. Pin hook `rev`s and lock tool versions (a lockfile, or coordinate with pin-toolchain-versions).
129
+ - **Ignoring generated/vendored dirs not configured.** Linting `dist/`, `build/`, `coverage/`, `.next/`, migrations, or snapshots floods output and slows everything. Set ignores in config (and `useIgnoreFile`/`.eslintignore`-equivalent) so they're skipped everywhere — hook and CI alike.
130
+
131
+ ## Verify
132
+
133
+ 1. **Bad file is caught by the formatter check:** create a deliberately mangled file (wrong indent, double→single quotes, no final newline). `biome ci .` / `prettier --check .` / `ruff format --check .` exits non-zero and names that file.
134
+ 2. **Bad file is caught by the linter:** add an unused import / `==` where a rule forbids it. `eslint . --max-warnings=0` / `biome lint .` / `ruff check .` exits non-zero.
135
+ 3. **Hook blocks the commit:** `git add` the bad file and `git commit` — the commit is rejected (or the file is auto-fixed and you must re-stage), proving the hook runs on staged files.
136
+ 4. **Hook is fast:** time a commit touching one file — pre-commit completes in a few seconds, not tens (proves it's staged-only, not whole-repo).
137
+ 5. **CI gate fails on drift:** push the bad file (or run the CI command locally) → the lint/format job is red; fix it → green. Confirm CI uses `--check`/`--max-warnings=0`, never `--write`/`--fix`.
138
+ 6. **Clean baseline:** on a freshly formatted tree, the full CI command exits 0 with no changes — the reformat commit landed and is in `.git-blame-ignore-revs` (`git blame` skips it).
139
+
140
+ Done = a deliberately bad file is rejected by **both** the local pre-commit hook (in seconds, staged-only) and the CI gate (lint `--max-warnings=0` + format `--check`, no auto-fix), the formatter and linter don't fight, and the existing tree is already clean behind a single blame-ignored reformat commit.
@@ -0,0 +1,125 @@
1
+ ---
2
+ name: setup-monorepo-tooling
3
+ description: Sets up and tunes a monorepo — pnpm-workspace layout, an acyclic internal package graph wired with the workspace: protocol, a cached task pipeline (Turborepo/Nx/Bazel) with dependsOn + inputs/outputs and remote cache, affected/changed-only CI runs, shared extended base configs, and Changesets versioning — so CI rebuilds only what changed.
4
+ when_to_use: Converting to or fixing a monorepo — CI rebuilds everything, circular package deps, drifting per-package tooling, duplicated shared code. NOT the CI workflow file itself (cicd-pipeline-author), one package's bundler/build config (configure-bundler-build), or the npm publish step (publish-package-registry).
5
+ ---
6
+
7
+ ## When to Use
8
+
9
+ Reach for this skill when the problem is **repo-level orchestration and package boundaries**, not one package's build:
10
+
11
+ - "Our CI rebuilds/tests every package on every PR even when one file changed"
12
+ - "Convert this repo into a monorepo" / "split this app into packages"
13
+ - "We have a circular dependency between two internal packages"
14
+ - "Each package has its own drifting tsconfig/eslint — unify them"
15
+ - "Stop copy-pasting this util across three apps — make it a shared package"
16
+ - "Turbo/Nx cache never hits" or "the affected graph is wrong"
17
+
18
+ NOT this skill:
19
+ - Writing the GitHub Actions / GitLab CI workflow file itself → cicd-pipeline-author (this skill defines the `turbo run` command it calls, not the YAML)
20
+ - One package's bundler/output config (tsup/Vite/esbuild, dual ESM+CJS, externals) → configure-bundler-build
21
+ - Actually publishing a package to npm (auth, provenance, `npm publish`) → publish-package-registry (this skill sets up Changesets version PRs; that skill does the registry push)
22
+ - Repo-wide lint/format/pre-commit hook *content* → setup-lint-format-precommit
23
+ - Pinning the Node/pnpm/toolchain versions themselves → pin-toolchain-versions
24
+
25
+ ## Steps
26
+
27
+ 1. **Pick the orchestrator by scale — default to pnpm workspaces + Turborepo.** Don't reach for Nx or Bazel unless a row below forces it.
28
+
29
+ | Tool | Use when | Cost / friction |
30
+ |---|---|---|
31
+ | **pnpm workspaces + Turborepo** | JS/TS monorepo, you want caching + affected runs with near-zero config — **the default** | Tiny `turbo.json`; no codegen, no plugins |
32
+ | Nx | You need code generators/scaffolding, an enforced module-boundary lint rule, or rich project-graph tooling | Heavier config, `nx.json` + project.json or inferred targets, more to learn |
33
+ | Bazel / Buck2 | Polyglot at scale (JS + Go + Java + protos), hermetic builds, thousands of targets | High: BUILD files everywhere, steep ramp — only worth it at large org scale |
34
+ | Lerna (alone) | — | Legacy; for new repos use pnpm + Turbo/Changesets instead |
35
+
36
+ Use **pnpm** as the package manager regardless (strict, fast, disk-efficient, first-class `workspace:` protocol). Commit `pnpm-lock.yaml`.
37
+
38
+ 2. **Lay out the workspace and declare it.** `apps/*` = deployables (not published), `packages/*` = shared libs (publishable or internal). Root is private.
39
+
40
+ ```yaml
41
+ # pnpm-workspace.yaml
42
+ packages:
43
+ - "apps/*"
44
+ - "packages/*"
45
+ ```
46
+ ```jsonc
47
+ // package.json (root) — root is NOT published
48
+ { "name": "@acme/root", "private": true, "packageManager": "pnpm@9.12.0" }
49
+ ```
50
+ Name every internal package under one scope: `@acme/ui`, `@acme/config`, `@acme/api-client`. The scope makes ownership and the dependency graph legible at a glance.
51
+
52
+ 3. **Wire internal deps with the `workspace:` protocol — never a version range.** In a consumer's `package.json`:
53
+ ```jsonc
54
+ "dependencies": { "@acme/ui": "workspace:*" }
55
+ ```
56
+ `workspace:*` symlinks the local source so changes are picked up instantly; at publish time Changesets/pnpm rewrites it to the real version. Run `pnpm install` from the root once — it links everything. A range like `"^1.0.0"` instead would silently pull the *registry* copy, defeating the monorepo.
57
+
58
+ 4. **Keep the package graph acyclic and explicit.** Cycles break topological build order and caching. Enforce it, don't hope:
59
+ - **Direction:** `apps → packages → packages`. Apps depend on packages; packages never depend on apps. Leaf utils depend on nothing internal.
60
+ - Every cross-package import must correspond to a declared `dependency` in that package's `package.json` — no reaching into a sibling's `../other-pkg/src`. Set `eslint-plugin-import/no-relative-packages` (or Nx's `enforce-module-boundaries`) to ban it.
61
+ - Detect cycles in CI: `pnpm dlx madge --circular --extensions ts,tsx packages apps` must exit 0. Turbo also errors on a cyclic task graph.
62
+
63
+ 5. **Define the task pipeline with dependsOn + inputs/outputs — this is what makes caching work.** A task's cache key = its declared `inputs` + its dependencies' outputs; get these wrong and you get false hits (stale) or zero hits (no speedup).
64
+
65
+ ```jsonc
66
+ // turbo.json
67
+ {
68
+ "$schema": "https://turbo.build/schema.json",
69
+ "tasks": {
70
+ "build": {
71
+ "dependsOn": ["^build"], // build deps first (topo order)
72
+ "inputs": ["src/**", "tsconfig.json", "package.json"],
73
+ "outputs": ["dist/**"] // MUST list outputs or cache restores nothing
74
+ },
75
+ "test": { "dependsOn": ["build"], "outputs": ["coverage/**"] },
76
+ "lint": { "dependsOn": [] },
77
+ "dev": { "cache": false, "persistent": true } // never cache long-running dev servers
78
+ }
79
+ }
80
+ ```
81
+ `^build` = "build all internal dependencies first"; `build` (no caret) = "this package's own build". Add `"globalDependencies": ["tsconfig.base.json", ".env"]` so a base-config change busts every cache. Anything generated must appear in `outputs` or the cache restore is empty and reruns do real work.
82
+
83
+ 6. **Turn on remote cache so CI and teammates share results.** Local cache only helps the same machine. `npx turbo login && npx turbo link` (Vercel Remote Cache) or self-host with a `TURBO_TOKEN` + `TURBO_API` env in CI. Now a build artifact produced on one PR/runner is reused everywhere — the single biggest CI-time win.
84
+
85
+ 7. **Run affected/changed-only in CI.** Replace "build everything" with a graph-filtered command so untouched packages are skipped (cache hit) or never scheduled:
86
+ - **Turbo:** `turbo run build test lint --filter='...[origin/main]'` — runs only packages changed since `main` plus everything that depends on them.
87
+ - **Nx:** `nx affected -t build test lint --base=origin/main`.
88
+ - Require `fetch-depth: 0` (full history) in CI checkout or the merge-base is wrong and the filter degrades to "run everything." This is the most common reason affected runs silently rebuild all.
89
+
90
+ 8. **Share one base config; extend per package — don't copy.** A single source of truth in a `@acme/config` package, extended by every other package:
91
+ ```jsonc
92
+ // tsconfig.base.json (root) → strict, composite for project refs
93
+ { "compilerOptions": { "strict": true, "composite": true, "declaration": true } }
94
+ // packages/ui/tsconfig.json
95
+ { "extends": "../../tsconfig.base.json", "include": ["src"], "compilerOptions": { "outDir": "dist" } }
96
+ ```
97
+ Same pattern for ESLint flat config and Prettier: define once in `@acme/config`, `import`/`extends` it everywhere. Per-package files hold only the genuine deltas (paths, env). (The lint/format/hook *content* itself → setup-lint-format-precommit.)
98
+
99
+ 9. **Add Changesets for versioning/release intent.** `pnpm add -Dw @changesets/cli && pnpm changeset init`. Workflow: contributor runs `pnpm changeset` (records which packages changed + semver bump + a user-facing note); CI runs `changeset version` to bump versions, rewrite `workspace:*` → real versions, and update CHANGELOGs, opening a "Version Packages" PR. Set `"linked"`/`"fixed"` groups in `.changeset/config.json` only if packages must move in lockstep. The actual `npm publish` after merge is publish-package-registry's job.
100
+
101
+ ## Common Errors
102
+
103
+ - **Missing `outputs` in `turbo.json`.** Cache key matches → "cache hit" → but `dist/` is never restored, so downstream tasks see no artifacts and fail or rebuild. Every task that emits files must declare `outputs`.
104
+ - **Shallow CI checkout (`fetch-depth: 1`).** `--filter='...[origin/main]'` / `nx affected --base` can't compute a merge-base and falls back to running everything. Use `fetch-depth: 0`.
105
+ - **Version range instead of `workspace:*` for an internal dep.** Pulls the published registry copy instead of local source; edits don't propagate and you debug a stale version. Always `workspace:*` (or `workspace:^`) for internal deps.
106
+ - **No `dependsOn: ["^build"]`.** Packages build in arbitrary order and a consumer compiles against a missing/old `dist/` of its dependency — flaky "module not found" that vanishes on rerun (warm cache). Declare the topo dependency.
107
+ - **Caching `dev`/`watch`/`start`.** Persistent tasks have no terminal output to cache; mark them `"cache": false, "persistent": true` or Turbo waits forever / serves stale.
108
+ - **Circular package dependency.** `@acme/a` ↔ `@acme/b` makes topological ordering impossible and silently corrupts incremental builds. Break it by extracting the shared piece into a third leaf package. Gate with `madge --circular` in CI.
109
+ - **Reaching into a sibling via relative path** (`import x from "../../other-pkg/src/util"`). Bypasses the declared graph, so affected-detection and caching miss the edge. Import via the package name and add the `dependency`; ban relative cross-package imports with lint.
110
+ - **Hoisting/phantom deps.** A package uses something it never declared but that happens to be hoisted to the root `node_modules`. Works locally, breaks when published or with stricter installs. pnpm's strict layout surfaces these — fix by adding the real `dependency`; don't reach for `shamefully-hoist`.
111
+ - **Base config changes that don't bust the cache.** Edit `tsconfig.base.json`, rerun, get stale cached builds. List shared base files in Turbo `globalDependencies` (or each task's `inputs`).
112
+ - **A `changeset` not added with a PR.** The release PR then ships no version bump / empty changelog for that change. Add a CI check that PRs touching `packages/**` include a `.changeset/*.md` (or are explicitly marked no-release).
113
+
114
+ ## Verify
115
+
116
+ 1. **Cache hit on rerun (the core proof):** `turbo run build` once (cold), then **again with no changes** → second run reports `FULL TURBO` / every task `cache hit, replaying logs` and finishes in ~seconds. If it rebuilds, `outputs`/`inputs` are wrong.
117
+ 2. **Affected graph is correct:** edit one file in a single leaf package, then `turbo run build --filter='...[origin/main]' --dry=json` (or `nx affected:graph`) → the task list includes that package **and only its dependents**, not the whole repo.
118
+ 3. **Targeted invalidation:** change a file in `@acme/ui` → on next run `@acme/ui` and its consumers rebuild while unrelated packages stay cache-hit. Change `tsconfig.base.json` → **everything** rebuilds (global dep busted).
119
+ 4. **Acyclic graph:** `pnpm dlx madge --circular --extensions ts,tsx packages apps` exits 0; `turbo run build` reports no cyclic-dependency error.
120
+ 5. **Internal linking real:** `pnpm why @acme/ui` (from a consumer) resolves to the local workspace path, not a registry version; grep confirms `workspace:` on every internal dep.
121
+ 6. **Boundaries enforced:** a relative cross-package import (`../other-pkg/src/...`) fails lint; an undeclared import fails install/typecheck under pnpm's strict layout.
122
+ 7. **Remote cache shared:** run `turbo run build` on a clean checkout / second machine (or fresh CI runner) with the remote cache configured → cache hits sourced remotely, no local rebuild.
123
+ 8. **Release intent works:** `pnpm changeset` then `pnpm changeset version` bumps only the listed packages, rewrites their `workspace:*` to concrete versions, and updates each CHANGELOG.
124
+
125
+ Done = a no-op rerun is `FULL TURBO` (cache hit), the affected filter rebuilds exactly the changed packages plus their dependents (not the repo), the package graph is acyclic with all internal deps on `workspace:` and enforced boundaries, base configs are extended (not copied), and the remote cache is shared across machines/CI.