claude-dev-env 1.38.1 → 1.40.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 (282) hide show
  1. package/CLAUDE.md +10 -36
  2. package/_shared/pr-loop/audit-reply-template.md +147 -0
  3. package/_shared/pr-loop/fix-protocol.md +25 -4
  4. package/_shared/pr-loop/gh-payloads.md +37 -50
  5. package/_shared/pr-loop/scripts/code_rules_gate.py +0 -60
  6. package/_shared/pr-loop/scripts/config/post_audit_thread_constants.py +199 -0
  7. package/_shared/pr-loop/scripts/config/reviews_disabled_constants.py +8 -0
  8. package/_shared/pr-loop/scripts/post_audit_thread.py +1242 -0
  9. package/_shared/pr-loop/scripts/preflight.py +129 -2
  10. package/_shared/pr-loop/scripts/reviews_disabled.py +59 -0
  11. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +0 -19
  12. package/_shared/pr-loop/scripts/tests/test_post_audit_thread.py +1116 -0
  13. package/_shared/pr-loop/scripts/tests/test_post_audit_thread_constants.py +127 -0
  14. package/_shared/pr-loop/scripts/tests/test_preflight.py +41 -0
  15. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +36 -0
  16. package/_shared/pr-loop/state-schema.md +1 -1
  17. package/agents/clean-coder.md +2 -2
  18. package/agents/pr-description-writer.md +150 -52
  19. package/bin/install.mjs +6 -7
  20. package/bin/install.test.mjs +8 -0
  21. package/commands/doc-gist.md +16 -0
  22. package/commands/plan.md +0 -2
  23. package/commands/review-plan.md +1 -1
  24. package/docs/CODE_RULES.md +122 -2
  25. package/docs/PR_DESCRIPTION_GUIDE.md +127 -64
  26. package/hooks/blocking/bot_mention_comment_blocker.py +75 -0
  27. package/hooks/blocking/code_rules_enforcer.py +1143 -129
  28. package/hooks/blocking/convergence_gate_blocker.py +130 -0
  29. package/hooks/blocking/destructive_command_blocker.py +74 -0
  30. package/hooks/blocking/gh_body_arg_blocker.py +30 -0
  31. package/hooks/blocking/md_to_html_blocker.py +119 -0
  32. package/hooks/blocking/pr_description_enforcer.py +57 -22
  33. package/hooks/blocking/test_bot_mention_comment_blocker.py +131 -0
  34. package/hooks/blocking/test_code_rules_enforcer.py +21 -0
  35. package/hooks/blocking/test_code_rules_enforcer_any_exempt_files.py +70 -0
  36. package/hooks/blocking/test_code_rules_enforcer_any_imports_and_cast.py +92 -0
  37. package/hooks/blocking/test_code_rules_enforcer_banned_import_alias.py +143 -0
  38. package/hooks/blocking/test_code_rules_enforcer_banned_prefixes.py +152 -0
  39. package/hooks/blocking/test_code_rules_enforcer_bare_except.py +120 -0
  40. package/hooks/blocking/test_code_rules_enforcer_boundary_types.py +175 -0
  41. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
  42. package/hooks/blocking/test_code_rules_enforcer_collection_prefix.py +50 -0
  43. package/hooks/blocking/test_code_rules_enforcer_docstring_format.py +255 -0
  44. package/hooks/blocking/test_code_rules_enforcer_inline_tuple_string_magic.py +130 -0
  45. package/hooks/blocking/test_code_rules_enforcer_stub_implementations.py +141 -0
  46. package/hooks/blocking/test_code_rules_enforcer_test_branching.py +143 -0
  47. package/hooks/blocking/test_code_rules_enforcer_thin_wrapper_files.py +169 -0
  48. package/hooks/blocking/test_code_rules_enforcer_todo_markers.py +99 -0
  49. package/hooks/blocking/test_code_rules_enforcer_typed_dict_pairs.py +141 -0
  50. package/hooks/blocking/test_convergence_gate_blocker.py +63 -0
  51. package/hooks/blocking/test_destructive_command_blocker.py +146 -0
  52. package/hooks/blocking/test_destructive_command_blocker_no_verify.py +102 -0
  53. package/hooks/blocking/test_gh_body_arg_blocker.py +45 -0
  54. package/hooks/blocking/test_md_to_html_blocker.py +317 -0
  55. package/hooks/blocking/test_pr_description_enforcer.py +69 -8
  56. package/hooks/config/any_type_config.py +7 -0
  57. package/hooks/config/banned_identifiers_constants.py +11 -0
  58. package/hooks/config/blocking_check_limits.py +38 -0
  59. package/hooks/config/bot_mention_comment_blocker_constants.py +20 -0
  60. package/hooks/config/code_rules_enforcer_constants.py +53 -0
  61. package/hooks/config/convergence_branch_constants.py +9 -0
  62. package/hooks/config/doc_gist_auto_publish_constants.py +18 -0
  63. package/hooks/config/html_companion_constants.py +20 -0
  64. package/hooks/config/inline_tuple_string_magic_constants.py +22 -0
  65. package/hooks/config/pr_description_enforcer_constants.py +14 -0
  66. package/hooks/config/test_banned_identifiers_constants.py +17 -0
  67. package/hooks/hooks.json +28 -20
  68. package/hooks/pyproject.toml +69 -0
  69. package/hooks/validators/mypy_integration.py +47 -1
  70. package/hooks/validators/run_all_validators.py +3 -3
  71. package/hooks/validators/test_mypy_integration.py +50 -1
  72. package/hooks/workflow/doc_gist_auto_publish.py +144 -0
  73. package/hooks/workflow/md_to_html_companion.py +365 -0
  74. package/hooks/workflow/test_doc_gist_auto_publish.py +117 -0
  75. package/hooks/workflow/test_md_to_html_companion.py +452 -0
  76. package/package.json +1 -1
  77. package/rules/gh-body-file.md +2 -0
  78. package/scripts/Install-SweepEmptyDirs.ps1 +111 -0
  79. package/scripts/check.ps1 +106 -0
  80. package/scripts/config/timing.py +11 -0
  81. package/scripts/sweep_empty_dirs.py +138 -0
  82. package/scripts/sync_to_cursor/rules.py +1 -1
  83. package/scripts/test_sweep_empty_dirs.py +183 -0
  84. package/skills/_shared/pr-loop/prompts/pr-consistency-audit.xml +323 -0
  85. package/skills/_shared/pr-loop/scripts/_cli_utils.py +22 -0
  86. package/skills/_shared/pr-loop/scripts/_path_resolver.py +165 -0
  87. package/skills/_shared/pr-loop/scripts/_xml_utils.py +20 -0
  88. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +182 -0
  89. package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +185 -0
  90. package/skills/_shared/pr-loop/scripts/config/__init__.py +0 -0
  91. package/skills/_shared/pr-loop/scripts/config/path_resolver_constants.py +78 -0
  92. package/skills/_shared/pr-loop/scripts/init_loop_state.py +135 -0
  93. package/skills/_shared/pr-loop/scripts/teardown_worktrees.py +175 -0
  94. package/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +182 -0
  95. package/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +206 -0
  96. package/skills/bugteam/CONSTRAINTS.md +21 -22
  97. package/skills/bugteam/EXAMPLES.md +3 -3
  98. package/skills/bugteam/PROMPTS.md +227 -67
  99. package/skills/bugteam/SKILL.md +132 -455
  100. package/skills/bugteam/reference/README.md +1 -1
  101. package/skills/bugteam/reference/audit-and-teammates.md +112 -39
  102. package/skills/bugteam/reference/audit-contract.md +4 -22
  103. package/skills/bugteam/reference/copilot-gap-analysis.md +8 -5
  104. package/skills/bugteam/reference/design-rationale.md +2 -2
  105. package/skills/bugteam/reference/github-pr-reviews.md +50 -57
  106. package/skills/bugteam/reference/obstacles/audit-assign-ids.md +13 -0
  107. package/skills/bugteam/reference/obstacles/audit-capture-excerpts.md +13 -0
  108. package/skills/bugteam/reference/obstacles/audit-walk-categories.md +13 -0
  109. package/skills/bugteam/reference/obstacles/audit-write-xml.md +13 -0
  110. package/skills/bugteam/reference/obstacles/fix-append-summary.md +13 -0
  111. package/skills/bugteam/reference/obstacles/fix-apply-fixes.md +13 -0
  112. package/skills/bugteam/reference/obstacles/fix-git-add-commit.md +13 -0
  113. package/skills/bugteam/reference/obstacles/fix-git-push.md +13 -0
  114. package/skills/bugteam/reference/obstacles/fix-post-reply.md +13 -0
  115. package/skills/bugteam/reference/obstacles/fix-publish-summary.md +13 -0
  116. package/skills/bugteam/reference/obstacles/fix-py-compile.md +13 -0
  117. package/skills/bugteam/reference/obstacles/fix-read-files.md +13 -0
  118. package/skills/bugteam/reference/obstacles/fix-resolve-thread.md +13 -0
  119. package/skills/bugteam/reference/obstacles/fix-test-suite.md +13 -0
  120. package/skills/bugteam/reference/obstacles/fix-violation-count.md +13 -0
  121. package/skills/bugteam/reference/obstacles/fix-write-xml.md +13 -0
  122. package/skills/bugteam/reference/team-setup.md +111 -9
  123. package/skills/bugteam/reference/teardown-publish-permissions.md +39 -8
  124. package/skills/bugteam/scripts/README.md +60 -0
  125. package/skills/bugteam/scripts/_claude_permissions_common.py +358 -0
  126. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +976 -0
  127. package/skills/bugteam/scripts/bugteam_fix_hookspath.py +375 -0
  128. package/skills/bugteam/scripts/bugteam_preflight.py +328 -0
  129. package/skills/bugteam/scripts/config/bugteam_code_rules_gate_constants.py +25 -0
  130. package/skills/bugteam/scripts/config/bugteam_fix_hookspath_constants.py +26 -0
  131. package/skills/bugteam/scripts/config/bugteam_preflight_constants.py +35 -0
  132. package/skills/bugteam/scripts/config/claude_permissions_common_constants.py +20 -0
  133. package/skills/bugteam/scripts/config/probe_code_rules_enforcer_check_constants.py +12 -0
  134. package/skills/bugteam/scripts/config/windows_safe_rmtree_constants.py +7 -0
  135. package/skills/bugteam/scripts/grant_project_claude_permissions.py +175 -0
  136. package/skills/bugteam/scripts/probe_code_rules_enforcer_check.py +107 -0
  137. package/skills/bugteam/scripts/revoke_project_claude_permissions.py +220 -0
  138. package/skills/bugteam/scripts/test__claude_permissions_common.py +112 -0
  139. package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +400 -0
  140. package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +384 -0
  141. package/skills/bugteam/scripts/test_bugteam_preflight.py +309 -0
  142. package/skills/bugteam/scripts/test_claude_permissions_common.py +195 -0
  143. package/skills/bugteam/scripts/test_grant_project_claude_permissions.py +55 -0
  144. package/skills/bugteam/scripts/test_probe_code_rules_enforcer_check.py +76 -0
  145. package/skills/bugteam/scripts/test_revoke_project_claude_permissions.py +55 -0
  146. package/skills/bugteam/scripts/test_windows_safe_rmtree.py +108 -0
  147. package/skills/bugteam/scripts/windows_safe_rmtree.py +100 -0
  148. package/skills/bugteam/test_skill_additions.py +1 -11
  149. package/skills/code/SKILL.md +176 -0
  150. package/skills/copilot-review/SKILL.md +16 -0
  151. package/skills/doc-gist/SKILL.md +99 -0
  152. package/skills/doc-gist/references/examples/01-exploration-code-approaches.html +453 -0
  153. package/skills/doc-gist/references/examples/02-exploration-visual-designs.html +515 -0
  154. package/skills/doc-gist/references/examples/03-code-review-pr.html +638 -0
  155. package/skills/doc-gist/references/examples/04-code-understanding.html +491 -0
  156. package/skills/doc-gist/references/examples/05-design-system.html +629 -0
  157. package/skills/doc-gist/references/examples/06-component-variants.html +605 -0
  158. package/skills/doc-gist/references/examples/07-prototype-animation.html +455 -0
  159. package/skills/doc-gist/references/examples/08-prototype-interaction.html +396 -0
  160. package/skills/doc-gist/references/examples/09-slide-deck.html +592 -0
  161. package/skills/doc-gist/references/examples/10-svg-illustrations.html +492 -0
  162. package/skills/doc-gist/references/examples/11-status-report.html +528 -0
  163. package/skills/doc-gist/references/examples/12-incident-report.html +596 -0
  164. package/skills/doc-gist/references/examples/13-flowchart-diagram.html +395 -0
  165. package/skills/doc-gist/references/examples/14-research-feature-explainer.html +381 -0
  166. package/skills/doc-gist/references/examples/15-research-concept-explainer.html +368 -0
  167. package/skills/doc-gist/references/examples/16-implementation-plan.html +702 -0
  168. package/skills/doc-gist/references/examples/17-pr-writeup.html +595 -0
  169. package/skills/doc-gist/references/examples/18-editor-triage-board.html +573 -0
  170. package/skills/doc-gist/references/examples/19-editor-feature-flags.html +663 -0
  171. package/skills/doc-gist/references/examples/20-editor-prompt-tuner.html +722 -0
  172. package/skills/doc-gist/references/examples/README.md +5 -0
  173. package/skills/doc-gist/scripts/config/__init__.py +0 -0
  174. package/skills/doc-gist/scripts/config/gist_upload_constants.py +16 -0
  175. package/skills/doc-gist/scripts/gist_upload.py +177 -0
  176. package/skills/doc-gist/scripts/test_gist_upload.py +51 -0
  177. package/skills/findbugs/SKILL.md +96 -2
  178. package/skills/monitor-open-prs/SKILL.md +14 -32
  179. package/skills/monitor-open-prs/test_skill_contract.py +0 -11
  180. package/skills/pr-consistency-audit/SKILL.md +112 -0
  181. package/skills/pr-consistency-audit/reference/detection-rules.md +96 -0
  182. package/skills/pr-consistency-audit/reference/illustrations.md +78 -0
  183. package/skills/pr-converge/SKILL.md +229 -23
  184. package/skills/pr-converge/config/__init__.py +0 -0
  185. package/skills/pr-converge/config/constants.py +63 -0
  186. package/skills/pr-converge/reference/convergence-gates.md +138 -44
  187. package/skills/pr-converge/reference/examples.md +43 -11
  188. package/skills/pr-converge/reference/fix-protocol.md +6 -5
  189. package/skills/pr-converge/reference/ground-rules.md +5 -3
  190. package/skills/pr-converge/reference/multi-pr-orchestration.md +44 -19
  191. package/skills/pr-converge/reference/obstacles/fix-post-replies.md +13 -0
  192. package/skills/pr-converge/reference/obstacles/fix-publish-summary.md +13 -0
  193. package/skills/pr-converge/reference/obstacles/fix-push.md +13 -0
  194. package/skills/pr-converge/reference/obstacles/fix-read-filelines.md +13 -0
  195. package/skills/pr-converge/reference/obstacles/fix-reset-state.md +13 -0
  196. package/skills/pr-converge/reference/obstacles/fix-resolve-threads.md +13 -0
  197. package/skills/pr-converge/reference/obstacles/fix-spawn-clean-coder.md +13 -0
  198. package/skills/pr-converge/reference/obstacles/fix-stage-commit.md +13 -0
  199. package/skills/pr-converge/reference/obstacles/fix-trigger-bugbot.md +13 -0
  200. package/skills/pr-converge/reference/obstacles/fix-write-test.md +13 -0
  201. package/skills/pr-converge/reference/per-tick.md +107 -31
  202. package/skills/pr-converge/reference/state-schema.md +22 -1
  203. package/skills/pr-converge/reference/stop-conditions.md +9 -7
  204. package/skills/pr-converge/scripts/README.md +34 -46
  205. package/skills/pr-converge/scripts/check_bugbot_ci.py +279 -0
  206. package/skills/pr-converge/scripts/check_convergence.py +497 -0
  207. package/skills/pr-converge/scripts/check_pending_reviews.py +154 -0
  208. package/skills/pr-converge/scripts/config/pr_converge_constants.py +118 -0
  209. package/skills/pr-converge/scripts/fetch_copilot_reviews.py +134 -0
  210. package/skills/pr-converge/scripts/post_fix_reply.py +168 -0
  211. package/skills/pr-converge/scripts/test_check_bugbot_ci.py +312 -0
  212. package/skills/pr-converge/workflows/schedule-wakeup-loop.md +5 -12
  213. package/skills/qbug/SKILL.md +157 -27
  214. package/skills/session-log/SKILL.md +216 -114
  215. package/skills/session-tidy/SKILL.md +1 -1
  216. package/skills/skill-builder/SKILL.md +138 -56
  217. package/skills/skill-builder/references/delegation-map.md +72 -113
  218. package/skills/skill-builder/references/progressive-disclosure.md +122 -0
  219. package/skills/skill-builder/references/self-audit-checklist.md +92 -0
  220. package/skills/skill-builder/references/skill-types.md +228 -0
  221. package/skills/skill-builder/references/thariq-x-post-skills.json +33 -0
  222. package/skills/skill-builder/templates/gap-analysis.md +15 -8
  223. package/skills/skill-builder/workflows/improve-skill.md +86 -57
  224. package/skills/skill-builder/workflows/new-skill.md +80 -168
  225. package/skills/skill-builder/workflows/polish-skill.md +78 -54
  226. package/skills/structure-prompt/SKILL.md +50 -0
  227. package/skills/structure-prompt/reference/adversarial-tuning.md +62 -0
  228. package/skills/structure-prompt/reference/block-classification.md +27 -0
  229. package/skills/structure-prompt/reference/canonical-case.md +48 -0
  230. package/skills/structure-prompt/reference/citation-depth.md +70 -0
  231. package/skills/structure-prompt/reference/cleanup.md +33 -0
  232. package/skills/structure-prompt/reference/constraints.md +33 -0
  233. package/skills/structure-prompt/reference/directives.md +37 -0
  234. package/skills/structure-prompt/reference/examples.md +72 -0
  235. package/skills/structure-prompt/reference/instantiation.md +51 -0
  236. package/skills/structure-prompt/reference/output-contract.md +72 -0
  237. package/skills/structure-prompt/reference/per-category.md +23 -0
  238. package/skills/structure-prompt/reference/persona.md +38 -0
  239. package/skills/structure-prompt/reference/research.md +33 -0
  240. package/skills/structure-prompt/reference/structure.md +28 -0
  241. package/agents/code-standards-agent.md +0 -93
  242. package/agents/groq-coder.md +0 -113
  243. package/agents/plan-executor.md +0 -226
  244. package/agents/project-docs-analyzer.md +0 -53
  245. package/agents/project-structure-organizer-agent.md +0 -72
  246. package/agents/skill-to-agent-converter.md +0 -370
  247. package/agents/skill-writer-agent.md +0 -470
  248. package/agents/user-docs-writer.md +0 -67
  249. package/agents/workflow-visual-documenter.md +0 -82
  250. package/commands/readability-review.md +0 -20
  251. package/hooks/mypy.ini +0 -2
  252. package/hooks/notification/attention_needed_notify.py +0 -71
  253. package/hooks/notification/claude_notification_handler.py +0 -67
  254. package/hooks/notification/notification_utils.py +0 -267
  255. package/hooks/notification/subagent_complete_notify.py +0 -381
  256. package/hooks/notification/test_attention_needed_notify.py +0 -47
  257. package/hooks/notification/test_claude_notification_handler.py +0 -54
  258. package/hooks/notification/test_notification_utils.py +0 -91
  259. package/hooks/notification/test_subagent_complete_notify.py +0 -79
  260. package/scripts/config/groq_bugteam_config.py +0 -230
  261. package/scripts/config/test_groq_bugteam_config.py +0 -83
  262. package/scripts/config/test_spec_implementer_prompt.py +0 -32
  263. package/scripts/groq_bugteam.README.md +0 -131
  264. package/scripts/groq_bugteam.py +0 -647
  265. package/scripts/groq_bugteam_dotenv.py +0 -40
  266. package/scripts/groq_bugteam_spec.py +0 -226
  267. package/scripts/test_groq_bugteam.py +0 -529
  268. package/scripts/test_groq_bugteam_apply_fix_from_spec.py +0 -426
  269. package/scripts/test_groq_bugteam_dotenv.py +0 -66
  270. package/scripts/test_groq_bugteam_spec.py +0 -338
  271. package/skills/bugteam/SKILL_EVALS.md +0 -309
  272. package/skills/dream/SKILL.md +0 -118
  273. package/skills/ingest/SKILL.md +0 -40
  274. package/skills/npm-creator/SKILL.md +0 -187
  275. package/skills/readability-review/SKILL.md +0 -127
  276. package/skills/resume-review/SKILL.md +0 -261
  277. package/skills/rule-audit/SKILL.md +0 -307
  278. package/skills/rule-creator/SKILL.md +0 -150
  279. package/skills/searching-obsidian-vault/SKILL.md +0 -131
  280. package/skills/skill-writer/REFERENCE.md +0 -284
  281. package/skills/skill-writer/SKILL.md +0 -222
  282. package/skills/tdd-team/SKILL.md +0 -128
@@ -0,0 +1,381 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>How rate limiting works in birchline/api</title>
7
+ <style>
8
+ :root {
9
+ --ivory: #FAF9F5;
10
+ --slate: #141413;
11
+ --clay: #D97757;
12
+ --oat: #E3DACC;
13
+ --olive: #788C5D;
14
+ --gray-150:#F0EEE6;
15
+ --gray-300:#D1CFC5;
16
+ --gray-500:#87867F;
17
+ --gray-700:#3D3D3A;
18
+ --serif: ui-serif, Georgia, "Times New Roman", serif;
19
+ --sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
20
+ --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
21
+ }
22
+ * { box-sizing: border-box; margin: 0; padding: 0; }
23
+ body {
24
+ background: var(--ivory);
25
+ color: var(--gray-700);
26
+ font-family: var(--sans);
27
+ font-size: 15px;
28
+ line-height: 1.65;
29
+ -webkit-font-smoothing: antialiased;
30
+ padding: 56px 24px 120px;
31
+ }
32
+ .page {
33
+ max-width: 1100px;
34
+ margin: 0 auto;
35
+ display: grid;
36
+ grid-template-columns: 200px minmax(0, 1fr);
37
+ gap: 48px;
38
+ }
39
+ @media (max-width: 920px) { .page { grid-template-columns: 1fr; } nav { display: none; } }
40
+
41
+ /* ── nav ──────────────────────────────── */
42
+ nav {
43
+ position: sticky;
44
+ top: 32px;
45
+ align-self: start;
46
+ font-size: 13px;
47
+ }
48
+ nav .label {
49
+ font-family: var(--mono);
50
+ font-size: 10px;
51
+ letter-spacing: 0.1em;
52
+ text-transform: uppercase;
53
+ color: var(--gray-500);
54
+ margin-bottom: 12px;
55
+ }
56
+ nav a {
57
+ display: block;
58
+ padding: 5px 0 5px 12px;
59
+ border-left: 2px solid var(--gray-300);
60
+ color: var(--gray-700);
61
+ text-decoration: none;
62
+ }
63
+ nav a:hover { color: var(--slate); border-color: var(--slate); }
64
+ nav a.l2 { padding-left: 24px; font-size: 12.5px; color: var(--gray-500); }
65
+ nav .files {
66
+ margin-top: 28px;
67
+ border-top: 1px solid var(--gray-300);
68
+ padding-top: 16px;
69
+ }
70
+ nav .files code {
71
+ display: block;
72
+ font-family: var(--mono);
73
+ font-size: 11px;
74
+ color: var(--gray-500);
75
+ padding: 3px 0;
76
+ }
77
+
78
+ /* ── header ───────────────────────────── */
79
+ header { margin-bottom: 12px; }
80
+ .eyebrow {
81
+ font-family: var(--mono);
82
+ font-size: 11px;
83
+ letter-spacing: 0.08em;
84
+ text-transform: uppercase;
85
+ color: var(--gray-500);
86
+ margin-bottom: 10px;
87
+ }
88
+ h1 {
89
+ font-family: var(--serif);
90
+ font-weight: 500;
91
+ font-size: 32px;
92
+ color: var(--slate);
93
+ letter-spacing: -0.01em;
94
+ margin-bottom: 14px;
95
+ }
96
+ .tldr {
97
+ border: 1.5px solid var(--gray-300);
98
+ border-left: 3px solid var(--clay);
99
+ border-radius: 10px;
100
+ background: #fff;
101
+ padding: 16px 18px;
102
+ margin-bottom: 8px;
103
+ }
104
+ .tldr b { color: var(--slate); }
105
+
106
+ h2 {
107
+ font-family: var(--serif);
108
+ font-weight: 500;
109
+ font-size: 22px;
110
+ color: var(--slate);
111
+ margin: 40px 0 14px;
112
+ scroll-margin-top: 24px;
113
+ }
114
+ p { margin-bottom: 12px; max-width: 680px; }
115
+ code { font-family: var(--mono); font-size: 13px; }
116
+
117
+ /* ── collapsible ─────────────────────── */
118
+ details {
119
+ border: 1.5px solid var(--gray-300);
120
+ border-radius: 10px;
121
+ background: #fff;
122
+ margin: 14px 0;
123
+ overflow: hidden;
124
+ }
125
+ summary {
126
+ list-style: none;
127
+ cursor: pointer;
128
+ padding: 14px 16px;
129
+ font-family: var(--serif);
130
+ font-size: 16px;
131
+ color: var(--slate);
132
+ display: flex;
133
+ align-items: baseline;
134
+ gap: 10px;
135
+ }
136
+ summary::-webkit-details-marker { display: none; }
137
+ summary::before {
138
+ content: "▸";
139
+ color: var(--clay);
140
+ font-family: var(--sans);
141
+ font-size: 12px;
142
+ transition: transform 120ms;
143
+ }
144
+ details[open] summary::before { transform: rotate(90deg); }
145
+ summary .where {
146
+ font-family: var(--mono);
147
+ font-size: 11px;
148
+ color: var(--gray-500);
149
+ margin-left: auto;
150
+ }
151
+ details .body { padding: 0 16px 16px; }
152
+ details .body p { font-size: 14px; }
153
+
154
+ /* ── tabs ─────────────────────────────── */
155
+ .tabs {
156
+ border: 1.5px solid var(--gray-300);
157
+ border-radius: 10px;
158
+ background: #fff;
159
+ margin: 16px 0 8px;
160
+ overflow: hidden;
161
+ }
162
+ .tabbar {
163
+ display: flex;
164
+ border-bottom: 1px solid var(--gray-300);
165
+ background: var(--gray-150);
166
+ }
167
+ .tabbar button {
168
+ appearance: none;
169
+ border: none;
170
+ background: none;
171
+ font-family: var(--mono);
172
+ font-size: 12px;
173
+ color: var(--gray-500);
174
+ padding: 10px 16px;
175
+ cursor: pointer;
176
+ border-right: 1px solid var(--gray-300);
177
+ }
178
+ .tabbar button.on {
179
+ background: #fff;
180
+ color: var(--slate);
181
+ border-bottom: 2px solid var(--clay);
182
+ margin-bottom: -1px;
183
+ }
184
+ .tabs pre {
185
+ display: none;
186
+ margin: 0;
187
+ padding: 16px 18px;
188
+ font-family: var(--mono);
189
+ font-size: 12.5px;
190
+ line-height: 1.6;
191
+ color: var(--slate);
192
+ overflow-x: auto;
193
+ }
194
+ .tabs pre.on { display: block; }
195
+ .hl { color: var(--clay); }
196
+ .cm { color: var(--gray-500); }
197
+
198
+ /* ── callout ──────────────────────────── */
199
+ .callout {
200
+ display: flex;
201
+ gap: 12px;
202
+ border: 1.5px solid var(--oat);
203
+ background: rgba(227,218,204,0.35);
204
+ border-radius: 10px;
205
+ padding: 14px 16px;
206
+ margin: 18px 0;
207
+ font-size: 14px;
208
+ }
209
+ .callout .ico { color: var(--clay); font-weight: 600; }
210
+
211
+ /* ── faq ──────────────────────────────── */
212
+ dl.faq { margin-top: 8px; }
213
+ dl.faq dt {
214
+ font-family: var(--serif);
215
+ font-size: 16px;
216
+ color: var(--slate);
217
+ margin-top: 18px;
218
+ }
219
+ dl.faq dd { font-size: 14px; margin: 4px 0 0; max-width: 640px; }
220
+ </style>
221
+ </head>
222
+ <body>
223
+ <div class="page">
224
+
225
+ <!-- ── side nav ── -->
226
+ <nav>
227
+ <div class="label">On this page</div>
228
+ <a href="#tldr">TL;DR</a>
229
+ <a href="#path">Request path</a>
230
+ <a href="#path" class="l2">1. Identify</a>
231
+ <a href="#path" class="l2">2. Bucket lookup</a>
232
+ <a href="#path" class="l2">3. Consume</a>
233
+ <a href="#path" class="l2">4. Reject</a>
234
+ <a href="#config">Configuring a route</a>
235
+ <a href="#gotchas">Gotchas</a>
236
+ <a href="#faq">FAQ</a>
237
+ <div class="files">
238
+ <div class="label">Files read</div>
239
+ <code>middleware/ratelimit.ts</code>
240
+ <code>lib/tokenBucket.ts</code>
241
+ <code>config/limits.yaml</code>
242
+ <code>routes/*.ts</code>
243
+ </div>
244
+ </nav>
245
+
246
+ <!-- ── main ── -->
247
+ <main>
248
+ <header>
249
+ <div class="eyebrow">Research &amp; Learning · feature summary</div>
250
+ <h1>How rate limiting works in <code>birchline/api</code></h1>
251
+ <div class="tldr" id="tldr">
252
+ <b>TL;DR</b> — Every request passes through <code>rateLimit()</code> middleware, which resolves the
253
+ caller to a <em>bucket key</em>, fetches a token-bucket from Redis, and either consumes one token or
254
+ returns <code>429</code>. Limits are declared per-route in <code>config/limits.yaml</code>; routes
255
+ without an entry inherit the <code>default</code> tier (100 req/min per API key).
256
+ </div>
257
+ </header>
258
+
259
+ <h2 id="path">The request path, step by step</h2>
260
+ <p>Expand each step to see what runs and where it lives. The whole path is ~40 lines and adds about
261
+ 0.4&nbsp;ms p50 to every request.</p>
262
+
263
+ <details open>
264
+ <summary>1 · Identify the caller <span class="where">middleware/ratelimit.ts:21</span></summary>
265
+ <div class="body">
266
+ <p>The middleware first reduces the request to a <code>bucketKey</code>: API key if an
267
+ <code>Authorization</code> header is present, otherwise the client IP (via the
268
+ <code>x-forwarded-for</code> chain, trusting only our own LB). Anonymous IP traffic gets a much
269
+ lower default tier.</p>
270
+ </div>
271
+ </details>
272
+
273
+ <details>
274
+ <summary>2 · Look up the bucket <span class="where">lib/tokenBucket.ts:9</span></summary>
275
+ <div class="body">
276
+ <p>The route name plus bucket key map to a Redis hash (<code>rl:{route}:{key}</code>) holding
277
+ <code>tokens</code> and <code>updatedAt</code>. If the key is missing it's created lazily at full
278
+ capacity — there's no warm-up.</p>
279
+ </div>
280
+ </details>
281
+
282
+ <details>
283
+ <summary>3 · Refill and consume <span class="where">lib/tokenBucket.ts:31</span></summary>
284
+ <div class="body">
285
+ <p>Refill is computed from elapsed time (<code>rate × Δt</code>, capped at <code>burst</code>), then
286
+ one token is subtracted. The whole read-modify-write runs as a single Lua script so concurrent
287
+ requests can't double-spend.</p>
288
+ </div>
289
+ </details>
290
+
291
+ <details>
292
+ <summary>4 · Reject when empty <span class="where">middleware/ratelimit.ts:48</span></summary>
293
+ <div class="body">
294
+ <p>If the script returns <code>tokens &lt; 0</code> the middleware short-circuits with
295
+ <code>429 Too Many Requests</code> and sets <code>Retry-After</code> to the seconds until one token
296
+ refills. Successful responses always carry <code>X-RateLimit-Remaining</code>.</p>
297
+ </div>
298
+ </details>
299
+
300
+ <h2 id="config">Configuring a limit on your route</h2>
301
+ <p>You don't touch the middleware. Add an entry to <code>config/limits.yaml</code> keyed by route name,
302
+ and (optionally) tag the route so the middleware can find it.</p>
303
+
304
+ <div class="tabs" data-tabs>
305
+ <div class="tabbar">
306
+ <button class="on" data-t="0">limits.yaml</button>
307
+ <button data-t="1">route.ts</button>
308
+ <button data-t="2">client response</button>
309
+ </div>
310
+ <pre class="on"><span class="cm"># config/limits.yaml</span>
311
+ default:
312
+ rate: 100/min
313
+ burst: 120
314
+
315
+ <span class="hl">search.query</span>:
316
+ rate: 20/min
317
+ burst: 40
318
+ key: api_key <span class="cm"># or: ip</span></pre>
319
+ <pre><span class="cm">// routes/search.ts</span>
320
+ router.post(
321
+ "/search",
322
+ <span class="hl">rateLimit("search.query")</span>,
323
+ handler,
324
+ );</pre>
325
+ <pre>HTTP/1.1 429 Too Many Requests
326
+ Retry-After: 17
327
+ X-RateLimit-Limit: 20
328
+ X-RateLimit-Remaining: 0
329
+
330
+ { "error": "rate_limited", "retry_after": 17 }</pre>
331
+ </div>
332
+
333
+ <div class="callout">
334
+ <span class="ico">★</span>
335
+ <div>If you only need the default tier, you don't need a YAML entry at all — just wrap the handler in
336
+ <code>rateLimit()</code> with no argument. The route name is inferred from the path.</div>
337
+ </div>
338
+
339
+ <h2 id="gotchas">Gotchas worth knowing</h2>
340
+ <ul style="padding-left:20px; max-width:680px;">
341
+ <li style="margin-bottom:8px;"><b>Limits are per-process in dev.</b> The Redis client falls back to an
342
+ in-memory map when <code>REDIS_URL</code> is unset, so local testing won't reflect real cluster
343
+ behaviour.</li>
344
+ <li style="margin-bottom:8px;"><b>Burst ≠ rate.</b> <code>burst</code> is the bucket capacity; a caller
345
+ idle for a minute can fire <code>burst</code> requests instantly even if <code>rate</code> is low.</li>
346
+ <li><b>Streaming responses count once.</b> The token is consumed at request start; a 30-second SSE
347
+ stream still costs one token.</li>
348
+ </ul>
349
+
350
+ <h2 id="faq">FAQ</h2>
351
+ <dl class="faq">
352
+ <dt>How do I exempt internal traffic?</dt>
353
+ <dd>Set <code>x-birchline-internal: 1</code> from the caller; the middleware checks it against the
354
+ mTLS peer name and skips the bucket entirely.</dd>
355
+
356
+ <dt>Where do I see who's getting limited?</dt>
357
+ <dd>Every <code>429</code> emits a <code>ratelimit.rejected</code> metric tagged with route and key
358
+ type. There's a Grafana panel under <em>API → Health</em>.</dd>
359
+
360
+ <dt>Can a single user have a higher limit?</dt>
361
+ <dd>Yes — add their API key under <code>overrides:</code> in the YAML. Overrides are reloaded without
362
+ a deploy.</dd>
363
+ </dl>
364
+ </main>
365
+
366
+ </div>
367
+
368
+ <script>
369
+ document.querySelectorAll("[data-tabs]").forEach(box => {
370
+ const btns = box.querySelectorAll("button");
371
+ const panes = box.querySelectorAll("pre");
372
+ btns.forEach(b => b.addEventListener("click", () => {
373
+ btns.forEach(x => x.classList.remove("on"));
374
+ panes.forEach(x => x.classList.remove("on"));
375
+ b.classList.add("on");
376
+ panes[+b.dataset.t].classList.add("on");
377
+ }));
378
+ });
379
+ </script>
380
+ </body>
381
+ </html>