claude-dev-env 1.82.0 → 1.84.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 (217) hide show
  1. package/CLAUDE.md +16 -13
  2. package/_shared/pr-loop/audit-contract.md +24 -12
  3. package/_shared/pr-loop/scripts/CLAUDE.md +1 -0
  4. package/_shared/pr-loop/scripts/README.md +1 -0
  5. package/_shared/pr-loop/scripts/_claude_permissions_common.py +16 -5
  6. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -3
  7. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
  8. package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviewer_availability_constants.py +12 -0
  9. package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +0 -2
  10. package/_shared/pr-loop/scripts/reviewer_availability.py +182 -0
  11. package/_shared/pr-loop/scripts/reviews_disabled.py +2 -0
  12. package/_shared/pr-loop/scripts/terminology_sweep.py +9 -33
  13. package/_shared/pr-loop/scripts/tests/CLAUDE.md +2 -0
  14. package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +212 -0
  15. package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +18 -0
  16. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +18 -0
  17. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +37 -0
  18. package/_shared/pr-loop/scripts/tests/test_reviewer_availability.py +159 -0
  19. package/_shared/pr-loop/scripts/tests/test_reviewer_availability_constants.py +36 -0
  20. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +14 -0
  21. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +14 -4
  22. package/agents/clean-coder.md +3 -0
  23. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
  24. package/bin/install.mjs +16 -8
  25. package/bin/install.test.mjs +17 -6
  26. package/commands/CLAUDE.md +0 -1
  27. package/docs/CODE_RULES.md +1 -1
  28. package/hooks/blocking/CLAUDE.md +2 -4
  29. package/hooks/blocking/code_rules_constants_config.py +164 -1
  30. package/hooks/blocking/code_rules_docstrings.py +428 -15
  31. package/hooks/blocking/code_rules_enforcer.py +33 -0
  32. package/hooks/blocking/code_rules_imports_logging.py +867 -1
  33. package/hooks/blocking/code_rules_magic_values.py +5 -0
  34. package/hooks/blocking/code_rules_naming_collection.py +152 -6
  35. package/hooks/blocking/code_rules_shared.py +34 -0
  36. package/hooks/blocking/code_rules_string_magic.py +68 -0
  37. package/hooks/blocking/duplicate_rmtree_helper_blocker.py +4 -4
  38. package/hooks/blocking/pre_tool_use_dispatcher.py +3 -3
  39. package/hooks/blocking/reviewer_spawn_gate.py +182 -0
  40. package/hooks/blocking/stale_comment_reference_blocker.py +267 -0
  41. package/hooks/blocking/state_description_blocker.py +96 -5
  42. package/hooks/blocking/test_code_rules_config_duplicate_path_anchor.py +132 -0
  43. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +2 -0
  44. package/hooks/blocking/test_code_rules_enforcer_docstring_delegation_summary.py +385 -0
  45. package/hooks/blocking/test_code_rules_enforcer_docstring_prose_wall_illustration.py +197 -0
  46. package/hooks/blocking/test_code_rules_enforcer_docstring_runon_sentence.py +27 -0
  47. package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +67 -0
  48. package/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py +40 -0
  49. package/hooks/blocking/test_code_rules_enforcer_naive_datetime.py +213 -0
  50. package/hooks/blocking/test_code_rules_enforcer_referenced_underscore_loop.py +169 -0
  51. package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +33 -0
  52. package/hooks/blocking/test_code_rules_enforcer_split_shared.py +23 -0
  53. package/hooks/blocking/test_code_rules_js_bare_flag_return_directive.py +266 -0
  54. package/hooks/blocking/test_code_rules_js_sibling_return_object_key_drift.py +490 -0
  55. package/hooks/blocking/test_code_rules_logging_adjacent_literals.py +171 -0
  56. package/hooks/blocking/test_code_rules_magic_values.py +54 -0
  57. package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +0 -6
  58. package/hooks/blocking/test_pr_converge_bugteam_enforcer_state_tolerance.py +184 -0
  59. package/hooks/blocking/test_pre_tool_use_dispatcher.py +9 -3
  60. package/hooks/blocking/test_reviewer_spawn_gate.py +230 -0
  61. package/hooks/blocking/test_shared_stdin_adoption.py +5 -30
  62. package/hooks/blocking/test_stale_comment_reference_blocker.py +236 -0
  63. package/hooks/blocking/test_state_description_blocker.py +135 -0
  64. package/hooks/hooks.json +5 -0
  65. package/hooks/hooks_constants/CLAUDE.md +3 -4
  66. package/hooks/hooks_constants/blocking_check_limits.py +46 -0
  67. package/hooks/hooks_constants/code_rules_enforcer_constants.py +41 -0
  68. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
  69. package/hooks/hooks_constants/duplicate_rmtree_helper_blocker_constants.py +0 -1
  70. package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -5
  71. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
  72. package/hooks/hooks_constants/reviewer_spawn_gate_constants.py +41 -0
  73. package/hooks/hooks_constants/stale_comment_reference_blocker_constants.py +76 -0
  74. package/hooks/hooks_constants/state_description_blocker_constants.py +8 -0
  75. package/hooks/validation/post_tool_use_dispatcher.py +1 -1
  76. package/hooks/validation/test_post_tool_use_dispatcher.py +51 -35
  77. package/hooks/workflow/CLAUDE.md +2 -8
  78. package/package.json +1 -1
  79. package/rules/CLAUDE.md +5 -2
  80. package/rules/claude-md-orphan-file.md +5 -0
  81. package/rules/docstring-prose-matches-implementation.md +10 -1
  82. package/rules/env-var-table-code-drift.md +5 -0
  83. package/rules/es-exe-file-search.md +17 -0
  84. package/rules/no-historical-clutter.md +12 -1
  85. package/rules/orphan-css-class.md +5 -0
  86. package/rules/package-inventory-stale-entry.md +10 -0
  87. package/rules/paired-test-coverage.md +5 -0
  88. package/rules/plain-illustrative-docstrings.md +40 -1
  89. package/rules/verify-before-asking.md +7 -0
  90. package/rules/verify-runtime-state.md +40 -0
  91. package/rules/windows-filesystem-safe.md +8 -0
  92. package/rules/workers-done-before-complete.md +33 -0
  93. package/rules/workflow-substitution-slots.md +5 -0
  94. package/skills/CLAUDE.md +7 -2
  95. package/skills/autoconverge/SKILL.md +48 -61
  96. package/skills/autoconverge/reference/closing-report.md +6 -6
  97. package/skills/autoconverge/reference/convergence.md +17 -15
  98. package/skills/autoconverge/reference/gotchas.md +6 -3
  99. package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +2 -9
  100. package/skills/autoconverge/workflow/converge.contract.test.mjs +153 -10
  101. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +55 -19
  102. package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +16 -9
  103. package/skills/autoconverge/workflow/converge.mjs +167 -106
  104. package/skills/autoconverge/workflow/render_report.py +7 -11
  105. package/skills/bugteam/CLAUDE.md +1 -1
  106. package/skills/bugteam/PROMPTS.md +7 -6
  107. package/skills/bugteam/SKILL.md +25 -72
  108. package/skills/bugteam/reference/CLAUDE.md +1 -3
  109. package/skills/bugteam/reference/README.md +1 -1
  110. package/skills/bugteam/reference/audit-and-teammates.md +1 -1
  111. package/skills/bugteam/reference/obstacles/fix-publish-summary.md +1 -1
  112. package/skills/bugteam/reference/team-setup.md +8 -7
  113. package/skills/bugteam/scripts/CLAUDE.md +0 -6
  114. package/skills/bugteam/scripts/README.md +0 -4
  115. package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +0 -1
  116. package/skills/code/SKILL.md +2 -0
  117. package/skills/copilot-review/CLAUDE.md +1 -1
  118. package/skills/copilot-review/SKILL.md +25 -23
  119. package/skills/everything-search/SKILL.md +5 -0
  120. package/skills/findbugs/CLAUDE.md +2 -2
  121. package/skills/findbugs/SKILL.md +22 -83
  122. package/skills/fixbugs/SKILL.md +2 -4
  123. package/skills/log-audit/CLAUDE.md +20 -0
  124. package/skills/log-audit/SKILL.md +68 -0
  125. package/skills/log-audit/reference/CLAUDE.md +9 -0
  126. package/skills/log-audit/reference/charter.md +52 -0
  127. package/skills/log-audit/scripts/CLAUDE.md +27 -0
  128. package/skills/log-audit/scripts/cluster_recurrences.py +261 -0
  129. package/skills/log-audit/scripts/collect_log_window.py +199 -0
  130. package/skills/log-audit/scripts/log_audit_constants/CLAUDE.md +12 -0
  131. package/skills/log-audit/scripts/log_audit_constants/cluster_recurrences_constants.py +23 -0
  132. package/skills/log-audit/scripts/log_audit_constants/collect_log_window_constants.py +24 -0
  133. package/skills/log-audit/scripts/log_audit_constants/mine_copilot_findings_constants.py +49 -0
  134. package/skills/log-audit/scripts/mine_copilot_findings.py +302 -0
  135. package/skills/log-audit/scripts/test_cluster_recurrences.py +160 -0
  136. package/skills/log-audit/scripts/test_collect_log_window.py +111 -0
  137. package/skills/log-audit/scripts/test_mine_copilot_findings.py +126 -0
  138. package/skills/monitor-open-prs/SKILL.md +2 -2
  139. package/skills/post-audit-findings/SKILL.md +84 -0
  140. package/skills/pr-converge/CLAUDE.md +2 -0
  141. package/skills/pr-converge/SKILL.md +72 -59
  142. package/skills/pr-converge/reference/CLAUDE.md +1 -1
  143. package/skills/pr-converge/reference/convergence-gates.md +16 -19
  144. package/skills/pr-converge/reference/examples.md +5 -5
  145. package/skills/pr-converge/reference/fix-protocol.md +16 -43
  146. package/skills/pr-converge/reference/obstacles/fix-publish-summary.md +1 -1
  147. package/skills/pr-converge/reference/per-tick.md +24 -45
  148. package/skills/pr-converge/reference/state-schema.md +15 -0
  149. package/skills/pr-converge/scripts/README.md +3 -5
  150. package/skills/pr-fix-protocol/SKILL.md +70 -0
  151. package/skills/pr-loop-lifecycle/SKILL.md +73 -0
  152. package/skills/{bugteam → pr-loop-lifecycle}/reference/teardown-publish-permissions.md +22 -24
  153. package/skills/pr-scope-resolve/SKILL.md +48 -0
  154. package/skills/qbug/CLAUDE.md +4 -4
  155. package/skills/qbug/SKILL.md +46 -144
  156. package/skills/qbug/test_qbug_skill_audit_schema.py +2 -2
  157. package/skills/qbug/test_qbug_skill_post_fix_audit.py +1 -1
  158. package/skills/reviewer-gates/SKILL.md +96 -0
  159. package/skills/session-log/CLAUDE.md +7 -7
  160. package/skills/session-log/SKILL.md +27 -44
  161. package/skills/test_markdown_link_integrity.py +103 -0
  162. package/commands/doc-gist.md +0 -16
  163. package/hooks/blocking/_md_to_html_blocker_test_support.py +0 -65
  164. package/hooks/blocking/conftest.py +0 -30
  165. package/hooks/blocking/md_path_exemptions.py +0 -224
  166. package/hooks/blocking/md_to_html_blocker.py +0 -155
  167. package/hooks/blocking/test_md_to_html_blocker_exemptions.py +0 -434
  168. package/hooks/blocking/test_md_to_html_blocker_extensions.py +0 -157
  169. package/hooks/blocking/test_md_to_html_blocker_path_resolution.py +0 -336
  170. package/hooks/hooks_constants/doc_gist_auto_publish_constants.py +0 -18
  171. package/hooks/hooks_constants/html_companion_constants.py +0 -20
  172. package/hooks/hooks_constants/md_to_html_blocker_constants.py +0 -76
  173. package/hooks/hooks_constants/test_md_to_html_blocker_constants.py +0 -125
  174. package/hooks/workflow/doc_gist_auto_publish.py +0 -144
  175. package/hooks/workflow/md_to_html_companion.py +0 -358
  176. package/hooks/workflow/test_doc_gist_auto_publish.py +0 -117
  177. package/hooks/workflow/test_md_to_html_companion.py +0 -613
  178. package/skills/bugteam/reference/audit-contract.md +0 -163
  179. package/skills/bugteam/scripts/_bugteam_permissions_common.py +0 -455
  180. package/skills/bugteam/scripts/bugteam_scripts_constants/claude_permissions_common_constants.py +0 -69
  181. package/skills/bugteam/scripts/grant_project_claude_permissions.py +0 -280
  182. package/skills/bugteam/scripts/revoke_project_claude_permissions.py +0 -266
  183. package/skills/bugteam/scripts/test__bugteam_permissions_common.py +0 -160
  184. package/skills/bugteam/scripts/test_agent_config_carveout.py +0 -356
  185. package/skills/bugteam/scripts/test_bugteam_permissions_common.py +0 -140
  186. package/skills/doc-gist/CLAUDE.md +0 -25
  187. package/skills/doc-gist/SKILL.md +0 -97
  188. package/skills/doc-gist/references/CLAUDE.md +0 -9
  189. package/skills/doc-gist/references/examples/01-exploration-code-approaches.html +0 -453
  190. package/skills/doc-gist/references/examples/02-exploration-visual-designs.html +0 -515
  191. package/skills/doc-gist/references/examples/03-code-review-pr.html +0 -638
  192. package/skills/doc-gist/references/examples/04-code-understanding.html +0 -491
  193. package/skills/doc-gist/references/examples/05-design-system.html +0 -629
  194. package/skills/doc-gist/references/examples/06-component-variants.html +0 -605
  195. package/skills/doc-gist/references/examples/07-prototype-animation.html +0 -455
  196. package/skills/doc-gist/references/examples/08-prototype-interaction.html +0 -396
  197. package/skills/doc-gist/references/examples/09-slide-deck.html +0 -592
  198. package/skills/doc-gist/references/examples/10-svg-illustrations.html +0 -492
  199. package/skills/doc-gist/references/examples/11-status-report.html +0 -528
  200. package/skills/doc-gist/references/examples/12-incident-report.html +0 -596
  201. package/skills/doc-gist/references/examples/13-flowchart-diagram.html +0 -395
  202. package/skills/doc-gist/references/examples/14-research-feature-explainer.html +0 -381
  203. package/skills/doc-gist/references/examples/15-research-concept-explainer.html +0 -368
  204. package/skills/doc-gist/references/examples/16-implementation-plan.html +0 -702
  205. package/skills/doc-gist/references/examples/17-pr-writeup.html +0 -595
  206. package/skills/doc-gist/references/examples/18-editor-triage-board.html +0 -573
  207. package/skills/doc-gist/references/examples/19-editor-feature-flags.html +0 -663
  208. package/skills/doc-gist/references/examples/20-editor-prompt-tuner.html +0 -722
  209. package/skills/doc-gist/references/examples/21-decision-signoff.html +0 -546
  210. package/skills/doc-gist/references/examples/CLAUDE.md +0 -25
  211. package/skills/doc-gist/references/examples/README.md +0 -5
  212. package/skills/doc-gist/scripts/CLAUDE.md +0 -27
  213. package/skills/doc-gist/scripts/doc_gist_scripts_constants/CLAUDE.md +0 -10
  214. package/skills/doc-gist/scripts/doc_gist_scripts_constants/gist_upload_constants.py +0 -16
  215. package/skills/doc-gist/scripts/gist_upload.py +0 -177
  216. package/skills/doc-gist/scripts/test_gist_upload.py +0 -51
  217. /package/skills/{doc-gist/scripts/doc_gist_scripts_constants → log-audit/scripts/log_audit_constants}/__init__.py +0 -0
@@ -1,663 +0,0 @@
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>Birchline — flags.production.json</title>
7
- <style>
8
- :root {
9
- --ivory: #FAF9F5;
10
- --slate: #141413;
11
- --clay: #D97757;
12
- --clay-d: #B85C3E;
13
- --oat: #E3DACC;
14
- --olive: #788C5D;
15
- --gray-50: #F0EEE6;
16
- --gray-200: #D1CFC5;
17
- --gray-500: #87867F;
18
- --gray-800: #3D3D3A;
19
- --white: #ffffff;
20
- --serif: ui-serif, Georgia, "Times New Roman", serif;
21
- --sans: system-ui, -apple-system, "Segoe UI", sans-serif;
22
- --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
23
- }
24
- * { box-sizing: border-box; }
25
- html, body {
26
- margin: 0;
27
- background: var(--ivory);
28
- color: var(--slate);
29
- font-family: var(--sans);
30
- font-size: 14px;
31
- line-height: 1.5;
32
- }
33
- .wrap {
34
- max-width: 1080px;
35
- margin: 0 auto;
36
- padding: 48px 32px 64px;
37
- }
38
-
39
- /* ---------- header ---------- */
40
- header { margin-bottom: 28px; }
41
- .eyebrow {
42
- font-family: var(--mono);
43
- font-size: 11px;
44
- letter-spacing: 0.08em;
45
- text-transform: uppercase;
46
- color: var(--gray-500);
47
- }
48
- h1 {
49
- font-family: var(--serif);
50
- font-weight: 500;
51
- font-size: 30px;
52
- letter-spacing: -0.01em;
53
- margin: 6px 0 4px;
54
- }
55
- .sub {
56
- color: var(--gray-500);
57
- max-width: 600px;
58
- }
59
-
60
- /* ---------- layout ---------- */
61
- .layout {
62
- display: grid;
63
- grid-template-columns: 1fr 320px;
64
- gap: 24px;
65
- align-items: start;
66
- }
67
- @media (max-width: 880px) {
68
- .layout { grid-template-columns: 1fr; }
69
- .sidebar { position: static !important; }
70
- }
71
-
72
- /* ---------- warning banner ---------- */
73
- .banner {
74
- grid-column: 1 / -1;
75
- display: none;
76
- align-items: baseline;
77
- gap: 8px;
78
- padding: 10px 14px;
79
- border: 1.5px solid #E8C2AE;
80
- background: #F8E9E0;
81
- color: var(--clay-d);
82
- border-radius: 10px;
83
- font-size: 13px;
84
- }
85
- .banner.show { display: flex; }
86
- .banner .glyph { font-size: 13px; line-height: 1; transform: translateY(1px); }
87
-
88
- /* ---------- groups / panels ---------- */
89
- .form-col { display: flex; flex-direction: column; gap: 16px; }
90
- .group {
91
- background: var(--white);
92
- border: 1.5px solid var(--gray-200);
93
- border-radius: 12px;
94
- overflow: hidden;
95
- }
96
- .group-head {
97
- font-family: var(--mono);
98
- font-size: 11px;
99
- letter-spacing: 0.08em;
100
- text-transform: uppercase;
101
- color: var(--gray-500);
102
- padding: 12px 18px 11px;
103
- border-bottom: 1.5px solid var(--gray-50);
104
- display: flex;
105
- justify-content: space-between;
106
- gap: 12px;
107
- }
108
- .group-head .name { color: var(--gray-800); }
109
- .group-head .meta { letter-spacing: 0.04em; }
110
-
111
- /* ---------- flag rows ---------- */
112
- .flag {
113
- display: flex;
114
- align-items: flex-start;
115
- gap: 16px;
116
- padding: 14px 18px;
117
- border-bottom: 1px solid var(--gray-50);
118
- border-left: 3px solid transparent;
119
- transition: border-left-color 140ms ease, background 140ms ease;
120
- }
121
- .flag:last-child { border-bottom: none; }
122
- .flag.warn {
123
- border-left-color: var(--clay);
124
- background: #FBF3EE;
125
- }
126
-
127
- .flag-info { flex: 1; min-width: 0; }
128
- .flag-key {
129
- font-family: var(--mono);
130
- font-size: 13px;
131
- color: var(--gray-800);
132
- display: flex;
133
- align-items: center;
134
- gap: 7px;
135
- flex-wrap: wrap;
136
- }
137
- .dot {
138
- display: none;
139
- width: 6px;
140
- height: 6px;
141
- border-radius: 50%;
142
- background: var(--clay);
143
- flex: none;
144
- }
145
- .flag.changed .dot { display: inline-block; }
146
- .rollout {
147
- font-family: var(--mono);
148
- font-size: 10px;
149
- letter-spacing: 0.03em;
150
- color: var(--gray-500);
151
- background: var(--gray-50);
152
- border: 1px solid var(--gray-200);
153
- border-radius: 999px;
154
- padding: 1px 7px 2px;
155
- }
156
- .flag-desc {
157
- color: var(--gray-500);
158
- font-size: 12.5px;
159
- margin-top: 2px;
160
- }
161
- .req-chip {
162
- display: inline-flex;
163
- align-items: baseline;
164
- gap: 5px;
165
- margin-top: 7px;
166
- font-family: var(--mono);
167
- font-size: 11px;
168
- color: var(--gray-500);
169
- background: var(--gray-50);
170
- border: 1px solid var(--gray-200);
171
- border-radius: 999px;
172
- padding: 2px 9px 3px;
173
- }
174
- .req-chip .warn-glyph { display: none; }
175
- .flag.warn .req-chip {
176
- color: var(--clay-d);
177
- background: #F8E1D5;
178
- border-color: #E8C2AE;
179
- }
180
- .flag.warn .req-chip .warn-glyph { display: inline; }
181
-
182
- /* ---------- toggle switch ---------- */
183
- .toggle {
184
- position: relative;
185
- flex: none;
186
- width: 38px;
187
- height: 22px;
188
- margin-top: 1px;
189
- }
190
- .toggle input {
191
- position: absolute;
192
- inset: 0;
193
- width: 100%;
194
- height: 100%;
195
- margin: 0;
196
- opacity: 0;
197
- cursor: pointer;
198
- }
199
- .toggle .track {
200
- position: absolute;
201
- inset: 0;
202
- background: var(--gray-200);
203
- border-radius: 999px;
204
- transition: background 160ms ease;
205
- pointer-events: none;
206
- }
207
- .toggle .track::after {
208
- content: "";
209
- position: absolute;
210
- top: 3px;
211
- left: 3px;
212
- width: 16px;
213
- height: 16px;
214
- border-radius: 50%;
215
- background: var(--white);
216
- box-shadow: 0 1px 2px rgba(20,20,19,0.18);
217
- transition: transform 160ms ease;
218
- }
219
- .toggle input:checked + .track { background: var(--olive); }
220
- .toggle input:checked + .track::after { transform: translateX(16px); }
221
- .toggle input:focus-visible + .track {
222
- outline: 2px solid var(--slate);
223
- outline-offset: 2px;
224
- }
225
-
226
- /* ---------- sidebar ---------- */
227
- .sidebar {
228
- position: sticky;
229
- top: 24px;
230
- background: var(--white);
231
- border: 1.5px solid var(--gray-200);
232
- border-radius: 12px;
233
- padding: 18px;
234
- display: flex;
235
- flex-direction: column;
236
- gap: 14px;
237
- }
238
- .side-label {
239
- font-family: var(--mono);
240
- font-size: 11px;
241
- letter-spacing: 0.08em;
242
- text-transform: uppercase;
243
- color: var(--gray-500);
244
- }
245
- .side-count {
246
- font-family: var(--serif);
247
- font-weight: 500;
248
- font-size: 19px;
249
- letter-spacing: -0.01em;
250
- }
251
- .side-count .warn-n { color: var(--clay-d); }
252
- .side-count .sep { color: var(--gray-200); margin: 0 4px; }
253
- .diff {
254
- margin: 0;
255
- font-family: var(--mono);
256
- font-size: 12px;
257
- line-height: 1.7;
258
- background: var(--gray-50);
259
- border: 1px solid var(--gray-200);
260
- border-radius: 8px;
261
- padding: 12px 14px;
262
- overflow-x: auto;
263
- max-height: 320px;
264
- overflow-y: auto;
265
- white-space: pre;
266
- }
267
- .diff .minus { color: var(--gray-500); }
268
- .diff .plus { color: var(--olive); }
269
- .diff .empty { color: var(--gray-500); }
270
-
271
- .btn {
272
- display: block;
273
- width: 100%;
274
- border: none;
275
- border-radius: 999px;
276
- font-family: var(--mono);
277
- font-size: 12px;
278
- padding: 9px 16px;
279
- cursor: pointer;
280
- text-align: center;
281
- transition: background 140ms ease, color 140ms ease, border-color 140ms ease;
282
- }
283
- .btn.primary {
284
- background: var(--slate);
285
- color: var(--ivory);
286
- }
287
- .btn.primary:hover:not(:disabled) { background: var(--gray-800); }
288
- .btn.primary:disabled {
289
- background: var(--gray-200);
290
- color: var(--gray-500);
291
- cursor: not-allowed;
292
- }
293
- .btn.primary.copied { background: var(--olive); }
294
- .btn.ghost {
295
- background: transparent;
296
- color: var(--gray-800);
297
- border: 1.5px solid var(--gray-200);
298
- }
299
- .btn.ghost:hover { border-color: var(--gray-500); }
300
- .btn.ghost.copied { border-color: var(--olive); color: var(--olive); }
301
- .btn-stack { display: flex; flex-direction: column; gap: 8px; }
302
- </style>
303
- </head>
304
- <body>
305
- <div class="wrap">
306
-
307
- <header>
308
- <div class="eyebrow">Birchline / editor / feature-flags</div>
309
- <h1>flags.production.json</h1>
310
- <p class="sub">A form-based editor for the production feature-flag config.
311
- Toggle flags, fix dependency warnings as they surface, then copy out
312
- only the lines that changed.</p>
313
- </header>
314
-
315
- <div class="layout">
316
- <div class="banner" id="banner">
317
- <span class="glyph">&#9888;</span>
318
- <span id="bannerText">1 flag is enabled without its prerequisite</span>
319
- </div>
320
-
321
- <div class="form-col" id="formCol"><!-- groups injected --></div>
322
-
323
- <aside class="sidebar">
324
- <div>
325
- <div class="side-label">Pending changes</div>
326
- <div class="side-count" id="sideCount">0 changed</div>
327
- </div>
328
- <pre class="diff" id="diff"><span class="empty">// no changes yet</span></pre>
329
- <div class="btn-stack">
330
- <button class="btn primary" id="copyDiff" disabled>Copy diff</button>
331
- <button class="btn ghost" id="copyJson">Copy full JSON</button>
332
- <button class="btn ghost" id="reset">Reset</button>
333
- </div>
334
- </aside>
335
- </div>
336
-
337
- </div>
338
-
339
- <script>
340
- "use strict";
341
-
342
- // ---------------------------------------------------------------------------
343
- // data — the production flag set, as it exists right now
344
- // ---------------------------------------------------------------------------
345
-
346
- const GROUPS = [
347
- {
348
- id: "onboarding",
349
- label: "Onboarding",
350
- flags: [
351
- { key: "onboarding.checklist_v2",
352
- desc: "New three-step setup checklist replacing the modal tour.",
353
- on: true },
354
- { key: "onboarding.invite_nudge",
355
- desc: "Nudge owners to invite teammates after creating their first project.",
356
- on: true },
357
- { key: "onboarding.workspace_templates",
358
- desc: "Offer prebuilt workspace templates during signup.",
359
- on: false, requires: "onboarding.checklist_v2" },
360
- { key: "onboarding.skip_email_verify",
361
- desc: "Let SSO-domain users in before email verification completes.",
362
- on: false }
363
- ]
364
- },
365
- {
366
- id: "sync",
367
- label: "Sync engine",
368
- flags: [
369
- { key: "sync.delta_compression",
370
- desc: "Send field-level deltas instead of full document snapshots.",
371
- on: true },
372
- { key: "sync.offline_queue_v2",
373
- desc: "Persist offline edits to IndexedDB and replay on reconnect.",
374
- on: false, requires: "sync.delta_compression" },
375
- { key: "sync.presence_cursors",
376
- desc: "Show live collaborator cursors in board and doc views.",
377
- on: true },
378
- { key: "sync.conflict_banner",
379
- desc: "Surface a merge banner instead of silently last-write-wins.",
380
- on: false, requires: "sync.offline_queue_v2" },
381
- { key: "sync.binary_ws_frames",
382
- desc: "Switch the realtime channel to binary WebSocket frames.",
383
- on: false, rollout: 10 }
384
- ]
385
- },
386
- {
387
- id: "billing",
388
- label: "Billing",
389
- flags: [
390
- { key: "billing.usage_meter",
391
- desc: "Show per-seat usage meter on the workspace billing page.",
392
- on: false },
393
- { key: "billing.annual_discount_banner",
394
- desc: "Promote the annual-plan discount in the upgrade flow.",
395
- on: true },
396
- { key: "billing.proration_preview",
397
- desc: "Preview the prorated charge before confirming a plan change.",
398
- on: false, requires: "billing.usage_meter" },
399
- { key: "billing.dunning_emails_v3",
400
- desc: "Use the rewritten dunning sequence with a 14-day grace window.",
401
- on: true, rollout: 25 }
402
- ]
403
- },
404
- {
405
- id: "internal",
406
- label: "Internal",
407
- flags: [
408
- { key: "internal.shadow_traffic",
409
- desc: "Mirror 1% of API traffic to the staging cluster for diffing.",
410
- on: false },
411
- { key: "internal.query_tracing",
412
- desc: "Attach OpenTelemetry spans to every Postgres query.",
413
- on: true },
414
- { key: "internal.kill_switch_ui",
415
- desc: "Expose the emergency kill-switch panel in the admin console.",
416
- on: true, requires: "internal.query_tracing" }
417
- ]
418
- }
419
- ];
420
-
421
- // flat lookup, plus a frozen copy of initial state
422
- const FLAGS = {};
423
- const INITIAL = {};
424
- GROUPS.forEach(g => g.flags.forEach(f => {
425
- FLAGS[f.key] = f;
426
- INITIAL[f.key] = f.on;
427
- }));
428
-
429
- // ---------------------------------------------------------------------------
430
- // render the form once; state lives in the checkboxes
431
- // ---------------------------------------------------------------------------
432
-
433
- const formCol = document.getElementById("formCol");
434
- const banner = document.getElementById("banner");
435
- const bannerTxt = document.getElementById("bannerText");
436
- const sideCount = document.getElementById("sideCount");
437
- const diffEl = document.getElementById("diff");
438
- const copyDiffBtn = document.getElementById("copyDiff");
439
- const copyJsonBtn = document.getElementById("copyJson");
440
- const resetBtn = document.getElementById("reset");
441
-
442
- function esc(s) {
443
- return String(s).replace(/[&<>"]/g, c =>
444
- ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]));
445
- }
446
-
447
- function buildForm() {
448
- formCol.innerHTML = "";
449
- for (const g of GROUPS) {
450
- const panel = document.createElement("section");
451
- panel.className = "group";
452
- panel.dataset.group = g.id;
453
-
454
- const head = document.createElement("div");
455
- head.className = "group-head";
456
- head.innerHTML =
457
- '<span class="name">' + esc(g.label) + '</span>' +
458
- '<span class="meta" data-meta></span>';
459
- panel.appendChild(head);
460
-
461
- for (const f of g.flags) {
462
- const row = document.createElement("div");
463
- row.className = "flag";
464
- row.dataset.key = f.key;
465
-
466
- const rolloutBadge = (typeof f.rollout === "number")
467
- ? '<span class="rollout">' + f.rollout + '%</span>'
468
- : "";
469
-
470
- const reqChip = f.requires
471
- ? '<div class="req-chip"><span class="warn-glyph">&#9888;</span>' +
472
- '<span>requires ' + esc(f.requires) + '</span></div>'
473
- : "";
474
-
475
- row.innerHTML =
476
- '<label class="toggle">' +
477
- '<input type="checkbox" data-key="' + esc(f.key) + '"' +
478
- (f.on ? " checked" : "") +
479
- ' aria-label="' + esc(f.key) + '">' +
480
- '<span class="track"></span>' +
481
- '</label>' +
482
- '<div class="flag-info">' +
483
- '<div class="flag-key"><span class="dot"></span>' +
484
- '<span>' + esc(f.key) + '</span>' + rolloutBadge +
485
- '</div>' +
486
- '<div class="flag-desc">' + esc(f.desc) + '</div>' +
487
- reqChip +
488
- '</div>';
489
-
490
- panel.appendChild(row);
491
- }
492
- formCol.appendChild(panel);
493
- }
494
- }
495
-
496
- // ---------------------------------------------------------------------------
497
- // state helpers
498
- // ---------------------------------------------------------------------------
499
-
500
- function currentState() {
501
- const state = {};
502
- formCol.querySelectorAll("input[type=checkbox]").forEach(cb => {
503
- state[cb.dataset.key] = cb.checked;
504
- });
505
- return state;
506
- }
507
-
508
- function computeDiff(state) {
509
- const lines = [];
510
- for (const g of GROUPS) {
511
- for (const f of g.flags) {
512
- if (state[f.key] !== INITIAL[f.key]) {
513
- lines.push({ key: f.key, from: INITIAL[f.key], to: state[f.key] });
514
- }
515
- }
516
- }
517
- return lines;
518
- }
519
-
520
- function diffText(lines) {
521
- return lines.map(l =>
522
- '- "' + l.key + '": ' + l.from + '\n' +
523
- '+ "' + l.key + '": ' + l.to
524
- ).join("\n");
525
- }
526
-
527
- function fullJson(state) {
528
- // grouped, ordered, pretty-printed — what you'd actually paste into the file
529
- let out = "{\n";
530
- GROUPS.forEach((g, gi) => {
531
- g.flags.forEach((f, fi) => {
532
- const last = gi === GROUPS.length - 1 && fi === g.flags.length - 1;
533
- out += ' "' + f.key + '": ' + state[f.key] + (last ? "" : ",") + "\n";
534
- });
535
- });
536
- out += "}";
537
- return out;
538
- }
539
-
540
- // ---------------------------------------------------------------------------
541
- // the live update pass
542
- // ---------------------------------------------------------------------------
543
-
544
- function update() {
545
- const state = currentState();
546
- let warnCount = 0;
547
-
548
- // per-row decorations
549
- for (const g of GROUPS) {
550
- let onCount = 0;
551
- for (const f of g.flags) {
552
- const row = formCol.querySelector('.flag[data-key="' + CSS.escape(f.key) + '"]');
553
- const on = state[f.key];
554
- if (on) onCount++;
555
-
556
- const changed = on !== INITIAL[f.key];
557
- row.classList.toggle("changed", changed);
558
-
559
- let warn = false;
560
- if (f.requires && on && !state[f.requires]) warn = true;
561
- row.classList.toggle("warn", warn);
562
- if (warn) warnCount++;
563
- }
564
- const meta = formCol.querySelector(
565
- '.group[data-group="' + g.id + '"] [data-meta]');
566
- meta.textContent = g.flags.length + " flags, " + onCount + " on";
567
- }
568
-
569
- // diff + sidebar
570
- const lines = computeDiff(state);
571
- if (lines.length === 0) {
572
- diffEl.innerHTML = '<span class="empty">// no changes yet</span>';
573
- copyDiffBtn.disabled = true;
574
- } else {
575
- diffEl.innerHTML = lines.map(l =>
576
- '<span class="minus">- "' + esc(l.key) + '": ' + l.from + '</span>\n' +
577
- '<span class="plus">+ "' + esc(l.key) + '": ' + l.to + '</span>'
578
- ).join("\n");
579
- copyDiffBtn.disabled = false;
580
- }
581
-
582
- sideCount.innerHTML =
583
- lines.length + " changed" +
584
- '<span class="sep">&middot;</span>' +
585
- '<span class="' + (warnCount ? "warn-n" : "") + '">' +
586
- warnCount + " warning" + (warnCount === 1 ? "" : "s") +
587
- "</span>";
588
-
589
- // banner
590
- if (warnCount > 0) {
591
- bannerTxt.textContent =
592
- warnCount === 1
593
- ? "1 flag is enabled without its prerequisite"
594
- : warnCount + " flags are enabled without their prerequisites";
595
- banner.classList.add("show");
596
- } else {
597
- banner.classList.remove("show");
598
- }
599
- }
600
-
601
- // ---------------------------------------------------------------------------
602
- // clipboard + buttons
603
- // ---------------------------------------------------------------------------
604
-
605
- function flash(btn, label) {
606
- const orig = btn.textContent;
607
- btn.textContent = label;
608
- btn.classList.add("copied");
609
- btn.disabled = true;
610
- setTimeout(() => {
611
- btn.textContent = orig;
612
- btn.classList.remove("copied");
613
- btn.disabled = false;
614
- update(); // restore disabled state on copyDiff if needed
615
- }, 1200);
616
- }
617
-
618
- function writeClipboard(text) {
619
- if (navigator.clipboard && navigator.clipboard.writeText) {
620
- return navigator.clipboard.writeText(text);
621
- }
622
- // fallback for file:// contexts without clipboard permission
623
- const ta = document.createElement("textarea");
624
- ta.value = text;
625
- ta.style.position = "fixed";
626
- ta.style.left = "-9999px";
627
- document.body.appendChild(ta);
628
- ta.select();
629
- try { document.execCommand("copy"); } catch (e) { /* ignore */ }
630
- document.body.removeChild(ta);
631
- return Promise.resolve();
632
- }
633
-
634
- copyDiffBtn.addEventListener("click", () => {
635
- const lines = computeDiff(currentState());
636
- if (!lines.length) return;
637
- writeClipboard(diffText(lines)).then(() => flash(copyDiffBtn, "Copied ✓"));
638
- });
639
-
640
- copyJsonBtn.addEventListener("click", () => {
641
- writeClipboard(fullJson(currentState())).then(() => flash(copyJsonBtn, "Copied ✓"));
642
- });
643
-
644
- resetBtn.addEventListener("click", () => {
645
- formCol.querySelectorAll("input[type=checkbox]").forEach(cb => {
646
- cb.checked = INITIAL[cb.dataset.key];
647
- });
648
- update();
649
- });
650
-
651
- formCol.addEventListener("change", e => {
652
- if (e.target.matches("input[type=checkbox]")) update();
653
- });
654
-
655
- // ---------------------------------------------------------------------------
656
- // boot
657
- // ---------------------------------------------------------------------------
658
-
659
- buildForm();
660
- update();
661
- </script>
662
- </body>
663
- </html>