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