claude-dev-env 1.38.0 → 1.39.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 (271) 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 +189 -0
  7. package/_shared/pr-loop/scripts/post_audit_thread.py +947 -0
  8. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +0 -19
  9. package/_shared/pr-loop/scripts/tests/test_post_audit_thread.py +923 -0
  10. package/_shared/pr-loop/scripts/tests/test_post_audit_thread_constants.py +127 -0
  11. package/_shared/pr-loop/state-schema.md +1 -1
  12. package/agents/clean-coder.md +2 -2
  13. package/bin/install.mjs +6 -7
  14. package/bin/install.test.mjs +8 -0
  15. package/commands/doc-gist.md +16 -0
  16. package/commands/plan.md +0 -2
  17. package/commands/review-plan.md +1 -1
  18. package/docs/CODE_RULES.md +122 -2
  19. package/hooks/blocking/bot_mention_comment_blocker.py +75 -0
  20. package/hooks/blocking/code_rules_enforcer.py +1236 -161
  21. package/hooks/blocking/convergence_gate_blocker.py +130 -0
  22. package/hooks/blocking/destructive_command_blocker.py +74 -0
  23. package/hooks/blocking/gh_body_arg_blocker.py +30 -0
  24. package/hooks/blocking/md_to_html_blocker.py +119 -0
  25. package/hooks/blocking/test_bot_mention_comment_blocker.py +131 -0
  26. package/hooks/blocking/test_code_rules_enforcer.py +21 -0
  27. package/hooks/blocking/test_code_rules_enforcer_any_exempt_files.py +70 -0
  28. package/hooks/blocking/test_code_rules_enforcer_any_imports_and_cast.py +92 -0
  29. package/hooks/blocking/test_code_rules_enforcer_banned_import_alias.py +143 -0
  30. package/hooks/blocking/test_code_rules_enforcer_banned_prefixes.py +152 -0
  31. package/hooks/blocking/test_code_rules_enforcer_bare_except.py +120 -0
  32. package/hooks/blocking/test_code_rules_enforcer_boundary_types.py +175 -0
  33. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
  34. package/hooks/blocking/test_code_rules_enforcer_collection_prefix.py +50 -0
  35. package/hooks/blocking/test_code_rules_enforcer_docstring_format.py +255 -0
  36. package/hooks/blocking/test_code_rules_enforcer_inline_tuple_string_magic.py +130 -0
  37. package/hooks/blocking/test_code_rules_enforcer_stub_implementations.py +141 -0
  38. package/hooks/blocking/test_code_rules_enforcer_test_branching.py +143 -0
  39. package/hooks/blocking/test_code_rules_enforcer_thin_wrapper_files.py +169 -0
  40. package/hooks/blocking/test_code_rules_enforcer_todo_markers.py +99 -0
  41. package/hooks/blocking/test_code_rules_enforcer_typed_dict_pairs.py +141 -0
  42. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +158 -0
  43. package/hooks/blocking/test_convergence_gate_blocker.py +63 -0
  44. package/hooks/blocking/test_destructive_command_blocker.py +146 -0
  45. package/hooks/blocking/test_destructive_command_blocker_no_verify.py +102 -0
  46. package/hooks/blocking/test_gh_body_arg_blocker.py +45 -0
  47. package/hooks/blocking/test_md_to_html_blocker.py +317 -0
  48. package/hooks/config/any_type_config.py +7 -0
  49. package/hooks/config/banned_identifiers_constants.py +11 -0
  50. package/hooks/config/blocking_check_limits.py +38 -0
  51. package/hooks/config/bot_mention_comment_blocker_constants.py +20 -0
  52. package/hooks/config/code_rules_enforcer_constants.py +53 -0
  53. package/hooks/config/convergence_branch_constants.py +9 -0
  54. package/hooks/config/doc_gist_auto_publish_constants.py +18 -0
  55. package/hooks/config/html_companion_constants.py +20 -0
  56. package/hooks/config/inline_tuple_string_magic_constants.py +22 -0
  57. package/hooks/config/test_banned_identifiers_constants.py +17 -0
  58. package/hooks/hooks.json +28 -20
  59. package/hooks/pyproject.toml +69 -0
  60. package/hooks/validators/mypy_integration.py +47 -1
  61. package/hooks/validators/run_all_validators.py +3 -3
  62. package/hooks/validators/test_mypy_integration.py +50 -1
  63. package/hooks/workflow/doc_gist_auto_publish.py +144 -0
  64. package/hooks/workflow/md_to_html_companion.py +365 -0
  65. package/hooks/workflow/test_doc_gist_auto_publish.py +117 -0
  66. package/hooks/workflow/test_md_to_html_companion.py +452 -0
  67. package/package.json +1 -1
  68. package/rules/gh-body-file.md +2 -0
  69. package/scripts/Install-SweepEmptyDirs.ps1 +111 -0
  70. package/scripts/check.ps1 +106 -0
  71. package/scripts/config/timing.py +11 -0
  72. package/scripts/sweep_empty_dirs.py +138 -0
  73. package/scripts/sync_to_cursor/rules.py +1 -1
  74. package/scripts/test_sweep_empty_dirs.py +183 -0
  75. package/skills/_shared/pr-loop/prompts/pr-consistency-audit.xml +323 -0
  76. package/skills/_shared/pr-loop/scripts/_cli_utils.py +22 -0
  77. package/skills/_shared/pr-loop/scripts/_path_resolver.py +165 -0
  78. package/skills/_shared/pr-loop/scripts/_xml_utils.py +20 -0
  79. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +182 -0
  80. package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +185 -0
  81. package/skills/_shared/pr-loop/scripts/config/__init__.py +0 -0
  82. package/skills/_shared/pr-loop/scripts/config/path_resolver_constants.py +78 -0
  83. package/skills/_shared/pr-loop/scripts/init_loop_state.py +135 -0
  84. package/skills/_shared/pr-loop/scripts/teardown_worktrees.py +175 -0
  85. package/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +182 -0
  86. package/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +206 -0
  87. package/skills/bugteam/CONSTRAINTS.md +21 -22
  88. package/skills/bugteam/EXAMPLES.md +3 -3
  89. package/skills/bugteam/PROMPTS.md +227 -67
  90. package/skills/bugteam/SKILL.md +114 -455
  91. package/skills/bugteam/reference/README.md +1 -1
  92. package/skills/bugteam/reference/audit-and-teammates.md +112 -39
  93. package/skills/bugteam/reference/audit-contract.md +4 -22
  94. package/skills/bugteam/reference/copilot-gap-analysis.md +8 -5
  95. package/skills/bugteam/reference/design-rationale.md +2 -2
  96. package/skills/bugteam/reference/github-pr-reviews.md +50 -57
  97. package/skills/bugteam/reference/obstacles/audit-assign-ids.md +13 -0
  98. package/skills/bugteam/reference/obstacles/audit-capture-excerpts.md +13 -0
  99. package/skills/bugteam/reference/obstacles/audit-walk-categories.md +13 -0
  100. package/skills/bugteam/reference/obstacles/audit-write-xml.md +13 -0
  101. package/skills/bugteam/reference/obstacles/fix-append-summary.md +13 -0
  102. package/skills/bugteam/reference/obstacles/fix-apply-fixes.md +13 -0
  103. package/skills/bugteam/reference/obstacles/fix-git-add-commit.md +13 -0
  104. package/skills/bugteam/reference/obstacles/fix-git-push.md +13 -0
  105. package/skills/bugteam/reference/obstacles/fix-post-reply.md +13 -0
  106. package/skills/bugteam/reference/obstacles/fix-publish-summary.md +13 -0
  107. package/skills/bugteam/reference/obstacles/fix-py-compile.md +13 -0
  108. package/skills/bugteam/reference/obstacles/fix-read-files.md +13 -0
  109. package/skills/bugteam/reference/obstacles/fix-resolve-thread.md +13 -0
  110. package/skills/bugteam/reference/obstacles/fix-test-suite.md +13 -0
  111. package/skills/bugteam/reference/obstacles/fix-violation-count.md +13 -0
  112. package/skills/bugteam/reference/obstacles/fix-write-xml.md +13 -0
  113. package/skills/bugteam/reference/team-setup.md +106 -9
  114. package/skills/bugteam/reference/teardown-publish-permissions.md +39 -8
  115. package/skills/bugteam/scripts/README.md +60 -0
  116. package/skills/bugteam/scripts/_claude_permissions_common.py +358 -0
  117. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +976 -0
  118. package/skills/bugteam/scripts/bugteam_fix_hookspath.py +375 -0
  119. package/skills/bugteam/scripts/bugteam_preflight.py +294 -0
  120. package/skills/bugteam/scripts/config/bugteam_code_rules_gate_constants.py +25 -0
  121. package/skills/bugteam/scripts/config/bugteam_fix_hookspath_constants.py +26 -0
  122. package/skills/bugteam/scripts/config/bugteam_preflight_constants.py +35 -0
  123. package/skills/bugteam/scripts/config/claude_permissions_common_constants.py +20 -0
  124. package/skills/bugteam/scripts/config/probe_code_rules_enforcer_check_constants.py +12 -0
  125. package/skills/bugteam/scripts/config/windows_safe_rmtree_constants.py +7 -0
  126. package/skills/bugteam/scripts/grant_project_claude_permissions.py +175 -0
  127. package/skills/bugteam/scripts/probe_code_rules_enforcer_check.py +107 -0
  128. package/skills/bugteam/scripts/revoke_project_claude_permissions.py +220 -0
  129. package/skills/bugteam/scripts/test__claude_permissions_common.py +112 -0
  130. package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +400 -0
  131. package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +384 -0
  132. package/skills/bugteam/scripts/test_bugteam_preflight.py +268 -0
  133. package/skills/bugteam/scripts/test_claude_permissions_common.py +195 -0
  134. package/skills/bugteam/scripts/test_grant_project_claude_permissions.py +55 -0
  135. package/skills/bugteam/scripts/test_probe_code_rules_enforcer_check.py +76 -0
  136. package/skills/bugteam/scripts/test_revoke_project_claude_permissions.py +55 -0
  137. package/skills/bugteam/scripts/test_windows_safe_rmtree.py +108 -0
  138. package/skills/bugteam/scripts/windows_safe_rmtree.py +100 -0
  139. package/skills/bugteam/test_skill_additions.py +1 -11
  140. package/skills/code/SKILL.md +176 -0
  141. package/skills/doc-gist/SKILL.md +99 -0
  142. package/skills/doc-gist/references/examples/01-exploration-code-approaches.html +453 -0
  143. package/skills/doc-gist/references/examples/02-exploration-visual-designs.html +515 -0
  144. package/skills/doc-gist/references/examples/03-code-review-pr.html +638 -0
  145. package/skills/doc-gist/references/examples/04-code-understanding.html +491 -0
  146. package/skills/doc-gist/references/examples/05-design-system.html +629 -0
  147. package/skills/doc-gist/references/examples/06-component-variants.html +605 -0
  148. package/skills/doc-gist/references/examples/07-prototype-animation.html +455 -0
  149. package/skills/doc-gist/references/examples/08-prototype-interaction.html +396 -0
  150. package/skills/doc-gist/references/examples/09-slide-deck.html +592 -0
  151. package/skills/doc-gist/references/examples/10-svg-illustrations.html +492 -0
  152. package/skills/doc-gist/references/examples/11-status-report.html +528 -0
  153. package/skills/doc-gist/references/examples/12-incident-report.html +596 -0
  154. package/skills/doc-gist/references/examples/13-flowchart-diagram.html +395 -0
  155. package/skills/doc-gist/references/examples/14-research-feature-explainer.html +381 -0
  156. package/skills/doc-gist/references/examples/15-research-concept-explainer.html +368 -0
  157. package/skills/doc-gist/references/examples/16-implementation-plan.html +702 -0
  158. package/skills/doc-gist/references/examples/17-pr-writeup.html +595 -0
  159. package/skills/doc-gist/references/examples/18-editor-triage-board.html +573 -0
  160. package/skills/doc-gist/references/examples/19-editor-feature-flags.html +663 -0
  161. package/skills/doc-gist/references/examples/20-editor-prompt-tuner.html +722 -0
  162. package/skills/doc-gist/references/examples/README.md +5 -0
  163. package/skills/doc-gist/scripts/config/__init__.py +0 -0
  164. package/skills/doc-gist/scripts/config/gist_upload_constants.py +16 -0
  165. package/skills/doc-gist/scripts/gist_upload.py +177 -0
  166. package/skills/doc-gist/scripts/test_gist_upload.py +51 -0
  167. package/skills/findbugs/SKILL.md +68 -2
  168. package/skills/monitor-open-prs/SKILL.md +13 -32
  169. package/skills/monitor-open-prs/test_skill_contract.py +0 -11
  170. package/skills/pr-consistency-audit/SKILL.md +112 -0
  171. package/skills/pr-consistency-audit/reference/detection-rules.md +96 -0
  172. package/skills/pr-consistency-audit/reference/illustrations.md +78 -0
  173. package/skills/pr-converge/SKILL.md +227 -23
  174. package/skills/pr-converge/config/__init__.py +0 -0
  175. package/skills/pr-converge/config/constants.py +62 -0
  176. package/skills/pr-converge/reference/convergence-gates.md +138 -44
  177. package/skills/pr-converge/reference/examples.md +43 -11
  178. package/skills/pr-converge/reference/fix-protocol.md +6 -5
  179. package/skills/pr-converge/reference/ground-rules.md +5 -3
  180. package/skills/pr-converge/reference/multi-pr-orchestration.md +44 -19
  181. package/skills/pr-converge/reference/obstacles/fix-post-replies.md +13 -0
  182. package/skills/pr-converge/reference/obstacles/fix-publish-summary.md +13 -0
  183. package/skills/pr-converge/reference/obstacles/fix-push.md +13 -0
  184. package/skills/pr-converge/reference/obstacles/fix-read-filelines.md +13 -0
  185. package/skills/pr-converge/reference/obstacles/fix-reset-state.md +13 -0
  186. package/skills/pr-converge/reference/obstacles/fix-resolve-threads.md +13 -0
  187. package/skills/pr-converge/reference/obstacles/fix-spawn-clean-coder.md +13 -0
  188. package/skills/pr-converge/reference/obstacles/fix-stage-commit.md +13 -0
  189. package/skills/pr-converge/reference/obstacles/fix-trigger-bugbot.md +13 -0
  190. package/skills/pr-converge/reference/obstacles/fix-write-test.md +13 -0
  191. package/skills/pr-converge/reference/per-tick.md +90 -31
  192. package/skills/pr-converge/reference/state-schema.md +22 -1
  193. package/skills/pr-converge/reference/stop-conditions.md +9 -7
  194. package/skills/pr-converge/scripts/README.md +34 -46
  195. package/skills/pr-converge/scripts/check_bugbot_ci.py +174 -0
  196. package/skills/pr-converge/scripts/check_convergence.py +497 -0
  197. package/skills/pr-converge/scripts/check_pending_reviews.py +154 -0
  198. package/skills/pr-converge/scripts/config/pr_converge_constants.py +118 -0
  199. package/skills/pr-converge/scripts/fetch_copilot_reviews.py +134 -0
  200. package/skills/pr-converge/scripts/post_fix_reply.py +168 -0
  201. package/skills/pr-converge/workflows/schedule-wakeup-loop.md +5 -12
  202. package/skills/qbug/SKILL.md +132 -27
  203. package/skills/session-log/SKILL.md +216 -114
  204. package/skills/session-tidy/SKILL.md +1 -1
  205. package/skills/skill-builder/SKILL.md +138 -56
  206. package/skills/skill-builder/references/delegation-map.md +72 -113
  207. package/skills/skill-builder/references/progressive-disclosure.md +122 -0
  208. package/skills/skill-builder/references/self-audit-checklist.md +92 -0
  209. package/skills/skill-builder/references/skill-types.md +228 -0
  210. package/skills/skill-builder/references/thariq-x-post-skills.json +33 -0
  211. package/skills/skill-builder/templates/gap-analysis.md +15 -8
  212. package/skills/skill-builder/workflows/improve-skill.md +86 -57
  213. package/skills/skill-builder/workflows/new-skill.md +80 -168
  214. package/skills/skill-builder/workflows/polish-skill.md +78 -54
  215. package/skills/structure-prompt/SKILL.md +50 -0
  216. package/skills/structure-prompt/reference/adversarial-tuning.md +62 -0
  217. package/skills/structure-prompt/reference/block-classification.md +27 -0
  218. package/skills/structure-prompt/reference/canonical-case.md +48 -0
  219. package/skills/structure-prompt/reference/citation-depth.md +70 -0
  220. package/skills/structure-prompt/reference/cleanup.md +33 -0
  221. package/skills/structure-prompt/reference/constraints.md +33 -0
  222. package/skills/structure-prompt/reference/directives.md +37 -0
  223. package/skills/structure-prompt/reference/examples.md +72 -0
  224. package/skills/structure-prompt/reference/instantiation.md +51 -0
  225. package/skills/structure-prompt/reference/output-contract.md +72 -0
  226. package/skills/structure-prompt/reference/per-category.md +23 -0
  227. package/skills/structure-prompt/reference/persona.md +38 -0
  228. package/skills/structure-prompt/reference/research.md +33 -0
  229. package/skills/structure-prompt/reference/structure.md +28 -0
  230. package/agents/code-standards-agent.md +0 -93
  231. package/agents/groq-coder.md +0 -113
  232. package/agents/plan-executor.md +0 -226
  233. package/agents/project-docs-analyzer.md +0 -53
  234. package/agents/project-structure-organizer-agent.md +0 -72
  235. package/agents/skill-to-agent-converter.md +0 -370
  236. package/agents/skill-writer-agent.md +0 -470
  237. package/agents/user-docs-writer.md +0 -67
  238. package/agents/workflow-visual-documenter.md +0 -82
  239. package/commands/readability-review.md +0 -20
  240. package/hooks/mypy.ini +0 -2
  241. package/hooks/notification/attention_needed_notify.py +0 -71
  242. package/hooks/notification/claude_notification_handler.py +0 -67
  243. package/hooks/notification/notification_utils.py +0 -267
  244. package/hooks/notification/subagent_complete_notify.py +0 -381
  245. package/hooks/notification/test_attention_needed_notify.py +0 -47
  246. package/hooks/notification/test_claude_notification_handler.py +0 -54
  247. package/hooks/notification/test_notification_utils.py +0 -91
  248. package/hooks/notification/test_subagent_complete_notify.py +0 -79
  249. package/scripts/config/groq_bugteam_config.py +0 -230
  250. package/scripts/config/test_groq_bugteam_config.py +0 -83
  251. package/scripts/config/test_spec_implementer_prompt.py +0 -32
  252. package/scripts/groq_bugteam.README.md +0 -131
  253. package/scripts/groq_bugteam.py +0 -647
  254. package/scripts/groq_bugteam_dotenv.py +0 -40
  255. package/scripts/groq_bugteam_spec.py +0 -226
  256. package/scripts/test_groq_bugteam.py +0 -529
  257. package/scripts/test_groq_bugteam_apply_fix_from_spec.py +0 -426
  258. package/scripts/test_groq_bugteam_dotenv.py +0 -66
  259. package/scripts/test_groq_bugteam_spec.py +0 -338
  260. package/skills/bugteam/SKILL_EVALS.md +0 -309
  261. package/skills/dream/SKILL.md +0 -118
  262. package/skills/ingest/SKILL.md +0 -40
  263. package/skills/npm-creator/SKILL.md +0 -187
  264. package/skills/readability-review/SKILL.md +0 -127
  265. package/skills/resume-review/SKILL.md +0 -261
  266. package/skills/rule-audit/SKILL.md +0 -307
  267. package/skills/rule-creator/SKILL.md +0 -150
  268. package/skills/searching-obsidian-vault/SKILL.md +0 -131
  269. package/skills/skill-writer/REFERENCE.md +0 -284
  270. package/skills/skill-writer/SKILL.md +0 -222
  271. 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>