claude-dev-env 1.83.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 (165) hide show
  1. package/_shared/pr-loop/audit-contract.md +24 -12
  2. package/_shared/pr-loop/scripts/_claude_permissions_common.py +16 -5
  3. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -3
  4. package/_shared/pr-loop/scripts/reviews_disabled.py +2 -0
  5. package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +212 -0
  6. package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +18 -0
  7. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +18 -0
  8. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +37 -0
  9. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +14 -0
  10. package/agents/clean-coder.md +3 -0
  11. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
  12. package/bin/install.mjs +16 -8
  13. package/bin/install.test.mjs +17 -6
  14. package/commands/CLAUDE.md +0 -1
  15. package/docs/CODE_RULES.md +1 -1
  16. package/hooks/blocking/CLAUDE.md +0 -4
  17. package/hooks/blocking/code_rules_constants_config.py +5 -0
  18. package/hooks/blocking/code_rules_docstrings.py +116 -6
  19. package/hooks/blocking/code_rules_enforcer.py +4 -0
  20. package/hooks/blocking/code_rules_magic_values.py +5 -0
  21. package/hooks/blocking/code_rules_naming_collection.py +11 -6
  22. package/hooks/blocking/code_rules_shared.py +34 -0
  23. package/hooks/blocking/duplicate_rmtree_helper_blocker.py +4 -4
  24. package/hooks/blocking/test_code_rules_enforcer_docstring_prose_wall_illustration.py +197 -0
  25. package/hooks/blocking/test_code_rules_enforcer_docstring_runon_sentence.py +27 -0
  26. package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +33 -0
  27. package/hooks/blocking/test_code_rules_enforcer_split_shared.py +23 -0
  28. package/hooks/blocking/test_code_rules_magic_values.py +54 -0
  29. package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +0 -6
  30. package/hooks/blocking/test_pr_converge_bugteam_enforcer_state_tolerance.py +184 -0
  31. package/hooks/blocking/test_shared_stdin_adoption.py +5 -30
  32. package/hooks/hooks_constants/CLAUDE.md +0 -3
  33. package/hooks/hooks_constants/blocking_check_limits.py +3 -0
  34. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
  35. package/hooks/hooks_constants/duplicate_rmtree_helper_blocker_constants.py +0 -1
  36. package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -5
  37. package/hooks/validation/post_tool_use_dispatcher.py +1 -1
  38. package/hooks/validation/test_post_tool_use_dispatcher.py +51 -35
  39. package/hooks/workflow/CLAUDE.md +2 -8
  40. package/package.json +1 -1
  41. package/rules/CLAUDE.md +1 -1
  42. package/rules/plain-illustrative-docstrings.md +35 -1
  43. package/skills/CLAUDE.md +6 -1
  44. package/skills/autoconverge/SKILL.md +38 -57
  45. package/skills/autoconverge/reference/closing-report.md +6 -6
  46. package/skills/autoconverge/reference/convergence.md +17 -15
  47. package/skills/autoconverge/reference/gotchas.md +6 -3
  48. package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +2 -9
  49. package/skills/autoconverge/workflow/converge.contract.test.mjs +87 -13
  50. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +1 -1
  51. package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +16 -9
  52. package/skills/autoconverge/workflow/converge.mjs +70 -73
  53. package/skills/autoconverge/workflow/render_report.py +7 -11
  54. package/skills/bugteam/CLAUDE.md +1 -1
  55. package/skills/bugteam/PROMPTS.md +7 -6
  56. package/skills/bugteam/SKILL.md +25 -72
  57. package/skills/bugteam/reference/CLAUDE.md +1 -3
  58. package/skills/bugteam/reference/README.md +1 -1
  59. package/skills/bugteam/reference/audit-and-teammates.md +1 -1
  60. package/skills/bugteam/reference/obstacles/fix-publish-summary.md +1 -1
  61. package/skills/bugteam/reference/team-setup.md +8 -7
  62. package/skills/bugteam/scripts/CLAUDE.md +0 -6
  63. package/skills/bugteam/scripts/README.md +0 -4
  64. package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +0 -1
  65. package/skills/code/SKILL.md +2 -0
  66. package/skills/copilot-review/CLAUDE.md +1 -1
  67. package/skills/copilot-review/SKILL.md +25 -23
  68. package/skills/findbugs/CLAUDE.md +2 -2
  69. package/skills/findbugs/SKILL.md +22 -83
  70. package/skills/fixbugs/SKILL.md +2 -4
  71. package/skills/log-audit/CLAUDE.md +20 -0
  72. package/skills/log-audit/SKILL.md +68 -0
  73. package/skills/log-audit/reference/CLAUDE.md +9 -0
  74. package/skills/log-audit/reference/charter.md +52 -0
  75. package/skills/log-audit/scripts/CLAUDE.md +27 -0
  76. package/skills/log-audit/scripts/cluster_recurrences.py +261 -0
  77. package/skills/log-audit/scripts/collect_log_window.py +199 -0
  78. package/skills/log-audit/scripts/log_audit_constants/CLAUDE.md +12 -0
  79. package/skills/log-audit/scripts/log_audit_constants/cluster_recurrences_constants.py +23 -0
  80. package/skills/log-audit/scripts/log_audit_constants/collect_log_window_constants.py +24 -0
  81. package/skills/log-audit/scripts/log_audit_constants/mine_copilot_findings_constants.py +49 -0
  82. package/skills/log-audit/scripts/mine_copilot_findings.py +302 -0
  83. package/skills/log-audit/scripts/test_cluster_recurrences.py +160 -0
  84. package/skills/log-audit/scripts/test_collect_log_window.py +111 -0
  85. package/skills/log-audit/scripts/test_mine_copilot_findings.py +126 -0
  86. package/skills/monitor-open-prs/SKILL.md +2 -2
  87. package/skills/post-audit-findings/SKILL.md +84 -0
  88. package/skills/pr-converge/CLAUDE.md +2 -0
  89. package/skills/pr-converge/SKILL.md +72 -59
  90. package/skills/pr-converge/reference/CLAUDE.md +1 -1
  91. package/skills/pr-converge/reference/convergence-gates.md +16 -19
  92. package/skills/pr-converge/reference/examples.md +5 -5
  93. package/skills/pr-converge/reference/fix-protocol.md +16 -43
  94. package/skills/pr-converge/reference/obstacles/fix-publish-summary.md +1 -1
  95. package/skills/pr-converge/reference/per-tick.md +24 -45
  96. package/skills/pr-converge/reference/state-schema.md +15 -0
  97. package/skills/pr-converge/scripts/README.md +3 -5
  98. package/skills/pr-fix-protocol/SKILL.md +70 -0
  99. package/skills/pr-loop-lifecycle/SKILL.md +73 -0
  100. package/skills/{bugteam → pr-loop-lifecycle}/reference/teardown-publish-permissions.md +22 -24
  101. package/skills/pr-scope-resolve/SKILL.md +48 -0
  102. package/skills/qbug/CLAUDE.md +4 -4
  103. package/skills/qbug/SKILL.md +46 -144
  104. package/skills/qbug/test_qbug_skill_audit_schema.py +2 -2
  105. package/skills/qbug/test_qbug_skill_post_fix_audit.py +1 -1
  106. package/skills/reviewer-gates/SKILL.md +96 -0
  107. package/skills/session-log/CLAUDE.md +7 -7
  108. package/skills/session-log/SKILL.md +27 -44
  109. package/skills/test_markdown_link_integrity.py +103 -0
  110. package/commands/doc-gist.md +0 -16
  111. package/hooks/blocking/_md_to_html_blocker_test_support.py +0 -65
  112. package/hooks/blocking/conftest.py +0 -30
  113. package/hooks/blocking/md_path_exemptions.py +0 -224
  114. package/hooks/blocking/md_to_html_blocker.py +0 -155
  115. package/hooks/blocking/test_md_to_html_blocker_exemptions.py +0 -434
  116. package/hooks/blocking/test_md_to_html_blocker_extensions.py +0 -157
  117. package/hooks/blocking/test_md_to_html_blocker_path_resolution.py +0 -336
  118. package/hooks/hooks_constants/doc_gist_auto_publish_constants.py +0 -18
  119. package/hooks/hooks_constants/html_companion_constants.py +0 -20
  120. package/hooks/hooks_constants/md_to_html_blocker_constants.py +0 -76
  121. package/hooks/hooks_constants/test_md_to_html_blocker_constants.py +0 -125
  122. package/hooks/workflow/doc_gist_auto_publish.py +0 -144
  123. package/hooks/workflow/md_to_html_companion.py +0 -358
  124. package/hooks/workflow/test_doc_gist_auto_publish.py +0 -117
  125. package/hooks/workflow/test_md_to_html_companion.py +0 -613
  126. package/skills/bugteam/reference/audit-contract.md +0 -163
  127. package/skills/bugteam/scripts/_bugteam_permissions_common.py +0 -455
  128. package/skills/bugteam/scripts/bugteam_scripts_constants/claude_permissions_common_constants.py +0 -69
  129. package/skills/bugteam/scripts/grant_project_claude_permissions.py +0 -280
  130. package/skills/bugteam/scripts/revoke_project_claude_permissions.py +0 -266
  131. package/skills/bugteam/scripts/test__bugteam_permissions_common.py +0 -160
  132. package/skills/bugteam/scripts/test_agent_config_carveout.py +0 -356
  133. package/skills/bugteam/scripts/test_bugteam_permissions_common.py +0 -140
  134. package/skills/doc-gist/CLAUDE.md +0 -25
  135. package/skills/doc-gist/SKILL.md +0 -97
  136. package/skills/doc-gist/references/CLAUDE.md +0 -9
  137. package/skills/doc-gist/references/examples/01-exploration-code-approaches.html +0 -453
  138. package/skills/doc-gist/references/examples/02-exploration-visual-designs.html +0 -515
  139. package/skills/doc-gist/references/examples/03-code-review-pr.html +0 -638
  140. package/skills/doc-gist/references/examples/04-code-understanding.html +0 -491
  141. package/skills/doc-gist/references/examples/05-design-system.html +0 -629
  142. package/skills/doc-gist/references/examples/06-component-variants.html +0 -605
  143. package/skills/doc-gist/references/examples/07-prototype-animation.html +0 -455
  144. package/skills/doc-gist/references/examples/08-prototype-interaction.html +0 -396
  145. package/skills/doc-gist/references/examples/09-slide-deck.html +0 -592
  146. package/skills/doc-gist/references/examples/10-svg-illustrations.html +0 -492
  147. package/skills/doc-gist/references/examples/11-status-report.html +0 -528
  148. package/skills/doc-gist/references/examples/12-incident-report.html +0 -596
  149. package/skills/doc-gist/references/examples/13-flowchart-diagram.html +0 -395
  150. package/skills/doc-gist/references/examples/14-research-feature-explainer.html +0 -381
  151. package/skills/doc-gist/references/examples/15-research-concept-explainer.html +0 -368
  152. package/skills/doc-gist/references/examples/16-implementation-plan.html +0 -702
  153. package/skills/doc-gist/references/examples/17-pr-writeup.html +0 -595
  154. package/skills/doc-gist/references/examples/18-editor-triage-board.html +0 -573
  155. package/skills/doc-gist/references/examples/19-editor-feature-flags.html +0 -663
  156. package/skills/doc-gist/references/examples/20-editor-prompt-tuner.html +0 -722
  157. package/skills/doc-gist/references/examples/21-decision-signoff.html +0 -546
  158. package/skills/doc-gist/references/examples/CLAUDE.md +0 -25
  159. package/skills/doc-gist/references/examples/README.md +0 -5
  160. package/skills/doc-gist/scripts/CLAUDE.md +0 -27
  161. package/skills/doc-gist/scripts/doc_gist_scripts_constants/CLAUDE.md +0 -10
  162. package/skills/doc-gist/scripts/doc_gist_scripts_constants/gist_upload_constants.py +0 -16
  163. package/skills/doc-gist/scripts/gist_upload.py +0 -177
  164. package/skills/doc-gist/scripts/test_gist_upload.py +0 -51
  165. /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>