skilldrop-cli 0.16.30 → 0.16.32

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 (96) hide show
  1. package/bin/skilldrop.js +133 -18
  2. package/catalogue.json +4 -2
  3. package/contracts/skill.schema.json +3 -3
  4. package/model-routing.json +8 -0
  5. package/package.json +1 -1
  6. package/packs/converters/skills/mermaid-render/manifest.json +11 -1
  7. package/packs/dev-team/loops/release/LOOP.md +7 -5
  8. package/packs/dev-team/loops/release/loop.json +2 -2
  9. package/packs/dev-team/skills/codebase-tour/evals/files/3/.github/workflows/ci.yml +2 -2
  10. package/packs/dev-team/skills/codebase-tour/evals/files/3/go.mod +2 -2
  11. package/packs/dev-team/skills/dependency-upgrade-review/SKILL.md +52 -34
  12. package/packs/dev-team/skills/dependency-upgrade-review/evals/evals.json +33 -0
  13. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/.github/workflows/ci.yml +39 -0
  14. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/.github/workflows/integration.yml +20 -0
  15. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/PR.md +39 -0
  16. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/go.mod +17 -0
  17. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/internal/notify/client.go +35 -0
  18. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/internal/payments/client.go +66 -0
  19. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/internal/payments/client_integration_test.go +38 -0
  20. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/internal/payments/client_test.go +53 -0
  21. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/internal/payments/settle.go +30 -0
  22. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/pr.diff +42 -0
  23. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/vendor-notes/httpkit-CHANGELOG.md +43 -0
  24. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/.github/workflows/ci.yml +67 -0
  25. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/.lintwright.json +7 -0
  26. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/.nvmrc +1 -0
  27. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/PR.md +110 -0
  28. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/package.json +25 -0
  29. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/pr.diff +146 -0
  30. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/src/due-date.ts +16 -0
  31. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/src/totals.ts +11 -0
  32. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/tests/due-date.test.ts +17 -0
  33. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/tests/totals.test.ts +12 -0
  34. package/packs/dev-team/skills/dependency-upgrade-review/examples/ledgerline-tidyparse.md +23 -14
  35. package/packs/dev-team/skills/dependency-upgrade-review/manifest.json +17 -5
  36. package/packs/dev-team/skills/dependency-upgrade-review/reference.md +36 -1
  37. package/packs/dev-team/skills/dependency-upgrade-review/scripts/upgrade_diff.py +22 -4
  38. package/packs/dev-team/skills/refactor-plan/SKILL.md +168 -0
  39. package/packs/dev-team/skills/refactor-plan/evals/eval_queries.json +46 -0
  40. package/packs/dev-team/skills/refactor-plan/evals/evals.json +106 -0
  41. package/packs/dev-team/skills/refactor-plan/evals/files/1/debt-register-row.md +5 -0
  42. package/packs/dev-team/skills/refactor-plan/evals/files/1/in-flight-prs.md +7 -0
  43. package/packs/dev-team/skills/refactor-plan/evals/files/1/order_service.py +74 -0
  44. package/packs/dev-team/skills/refactor-plan/evals/files/1/test-coverage.txt +14 -0
  45. package/packs/dev-team/skills/refactor-plan/evals/files/3/imports.txt +22 -0
  46. package/packs/dev-team/skills/refactor-plan/evals/files/3/register.md +3 -0
  47. package/packs/dev-team/skills/refactor-plan/evals/files/4/docs/open-prs.md +6 -0
  48. package/packs/dev-team/skills/refactor-plan/evals/files/4/docs/tech-debt-register.md +7 -0
  49. package/packs/dev-team/skills/refactor-plan/evals/files/4/reports/monthly_revenue.py +44 -0
  50. package/packs/dev-team/skills/refactor-plan/evals/files/4/src/billing/invoice.py +233 -0
  51. package/packs/dev-team/skills/refactor-plan/evals/files/4/src/partners/statement.py +29 -0
  52. package/packs/dev-team/skills/refactor-plan/evals/files/4/tests/billing/test_payments.py +13 -0
  53. package/packs/dev-team/skills/refactor-plan/evals/files/5/ci-notes.md +5 -0
  54. package/packs/dev-team/skills/refactor-plan/evals/files/5/open-prs.txt +4 -0
  55. package/packs/dev-team/skills/refactor-plan/evals/files/5/scripts/nightly-digest.js +36 -0
  56. package/packs/dev-team/skills/refactor-plan/evals/files/5/src/auth/passwordReset.ts +16 -0
  57. package/packs/dev-team/skills/refactor-plan/evals/files/5/src/notify/index.ts +2 -0
  58. package/packs/dev-team/skills/refactor-plan/evals/files/5/src/notify/legacyMailer.ts +149 -0
  59. package/packs/dev-team/skills/refactor-plan/evals/files/5/src/orders/onOrderShipped.ts +15 -0
  60. package/packs/dev-team/skills/refactor-plan/evals/files/5/tests/notify/legacyMailer.test.ts +7 -0
  61. package/packs/dev-team/skills/refactor-plan/evals/files/6/billing-service/src/main/kotlin/billing/DunningJob.kt +29 -0
  62. package/packs/dev-team/skills/refactor-plan/evals/files/6/billing-service/src/main/kotlin/billing/InvoiceService.kt +157 -0
  63. package/packs/dev-team/skills/refactor-plan/evals/files/6/billing-service/src/main/kotlin/billing/pdf/InvoicePdfRenderer.kt +36 -0
  64. package/packs/dev-team/skills/refactor-plan/evals/files/6/billing-service/src/test/kotlin/billing/InvoiceServiceTest.kt +40 -0
  65. package/packs/dev-team/skills/refactor-plan/evals/files/6/docs/adr-007-persistence.md +26 -0
  66. package/packs/dev-team/skills/refactor-plan/evals/files/6/docs/open-prs.md +9 -0
  67. package/packs/dev-team/skills/refactor-plan/evals/files/7/notes/open-work.md +19 -0
  68. package/packs/dev-team/skills/refactor-plan/evals/files/7/web/src/components/CheckoutPanel.tsx +207 -0
  69. package/packs/dev-team/skills/refactor-plan/evals/files/7/web/src/components/MiniCart.tsx +33 -0
  70. package/packs/dev-team/skills/refactor-plan/evals/files/7/web/src/components/__tests__/CheckoutPanel.test.tsx +13 -0
  71. package/packs/dev-team/skills/refactor-plan/evals/files/7/web/src/pages/OrderReview.tsx +24 -0
  72. package/packs/dev-team/skills/refactor-plan/examples/search-ranker.md +104 -0
  73. package/packs/dev-team/skills/refactor-plan/manifest.json +36 -0
  74. package/packs/dev-team/skills/refactor-plan/reference.md +149 -0
  75. package/packs/dev-team/skills/refactor-plan/templates/refactor-plan.md +82 -0
  76. package/packs/dev-team/skills/rollout-plan/SKILL.md +157 -0
  77. package/packs/dev-team/skills/rollout-plan/evals/eval_queries.json +12 -0
  78. package/packs/dev-team/skills/rollout-plan/evals/evals.json +109 -0
  79. package/packs/dev-team/skills/rollout-plan/evals/files/2/metrics-snapshot.txt +11 -0
  80. package/packs/dev-team/skills/rollout-plan/evals/files/2/pr-description.md +28 -0
  81. package/packs/dev-team/skills/rollout-plan/evals/files/4/PR_DESCRIPTION.md +24 -0
  82. package/packs/dev-team/skills/rollout-plan/evals/files/4/metrics_snapshot.txt +7 -0
  83. package/packs/dev-team/skills/rollout-plan/evals/files/5/docs/tax-engine-v2.md +24 -0
  84. package/packs/dev-team/skills/rollout-plan/evals/files/6/flags/smart_reminders_v2.yaml +14 -0
  85. package/packs/dev-team/skills/rollout-plan/evals/files/6/notes/push-pipeline.md +10 -0
  86. package/packs/dev-team/skills/rollout-plan/evals/files/7/docs/ratelimiter-v2.md +21 -0
  87. package/packs/dev-team/skills/rollout-plan/evals/files/7/ops/traffic-summary.txt +9 -0
  88. package/packs/dev-team/skills/rollout-plan/examples/sync-engine-workspaces.md +102 -0
  89. package/packs/dev-team/skills/rollout-plan/manifest.json +35 -0
  90. package/packs/dev-team/skills/rollout-plan/reference.md +100 -0
  91. package/packs/dev-team/skills/rollout-plan/scripts/canary_math.py +261 -0
  92. package/packs/dev-team/skills/rollout-plan/templates/rollout-plan.md +50 -0
  93. package/packs/dev-team/skills/sonar-review/manifest.json +12 -0
  94. package/packs/infra-as-code/skills/terraform-module/manifest.json +11 -0
  95. package/packs/infra-as-code/skills/terraform-plan-review/manifest.json +8 -1
  96. package/packs/solution-architect/skills/figma-diagrams/manifest.json +12 -1
@@ -0,0 +1,261 @@
1
+ #!/usr/bin/env python3
2
+ """How long each exposure step of a canary or flag rollout must run before its
3
+ numbers can detect the regression you care about.
4
+
5
+ Uses the normal approximation to the two-proportion z-test. The flag-on cohort
6
+ (treatment) is compared with a concurrent flag-off cohort (control). By default
7
+ the control is everyone not exposed (100% - exposure); pass --control-pct to fix
8
+ it at a holdback size instead.
9
+
10
+ For each step it prints the units (requests or users) each arm needs and the
11
+ hours that takes at the given traffic, flags steps that cannot reach that in
12
+ --max-hours, says the smallest regression the step *can* detect in that window,
13
+ and suggests the smallest exposure that reaches sufficiency in the window.
14
+
15
+ Rates and regressions accept a fraction (0.008), a percent ("0.8%") or, for an
16
+ absolute regression, percentage points ("0.3pp").
17
+
18
+ Usage:
19
+ python3 canary_math.py --baseline 0.8% --detect-abs 0.3pp --traffic 9000
20
+ python3 canary_math.py --baseline 3.1% --detect-rel 10% --direction down \\
21
+ --traffic 9000 --steps 1,5,25,50 --unit users
22
+ python3 canary_math.py ... --json
23
+
24
+ Exit codes: 0 printed, 2 bad input.
25
+ """
26
+
27
+ import argparse
28
+ import json
29
+ import math
30
+ import sys
31
+ from statistics import NormalDist
32
+
33
+ Z = NormalDist()
34
+
35
+
36
+ def fail(msg):
37
+ sys.stderr.write("error: " + msg + "\n")
38
+ sys.exit(2)
39
+
40
+
41
+ def parse_rate(text, name, allow_pp=False):
42
+ s = str(text).strip().lower().replace(" ", "")
43
+ try:
44
+ if s.endswith("pp"):
45
+ if not allow_pp:
46
+ fail(name + ": 'pp' only applies to --detect-abs")
47
+ return float(s[:-2]) / 100.0
48
+ if s.endswith("%"):
49
+ return float(s[:-1]) / 100.0
50
+ v = float(s)
51
+ except ValueError:
52
+ fail(name + ": not a number: " + str(text))
53
+ return v
54
+
55
+
56
+ def fmt_pct(x, digits=2):
57
+ return ("{:." + str(digits) + "f}").format(x * 100).rstrip("0").rstrip(".") + "%"
58
+
59
+
60
+ def fmt_pp(x):
61
+ return "{:.3f}".format(x * 100).rstrip("0").rstrip(".") + " pp"
62
+
63
+
64
+ def fmt_hours(h):
65
+ if h is None:
66
+ return "n/a"
67
+ if h < 1:
68
+ return "{:.0f} min".format(max(1, math.ceil(h * 60)))
69
+ if h < 48:
70
+ return "{:.1f} h".format(h)
71
+ return "{:.1f} h ({:.1f} d)".format(h, h / 24)
72
+
73
+
74
+ def z_values(alpha, power, one_sided):
75
+ za = Z.inv_cdf(1 - alpha) if one_sided else Z.inv_cdf(1 - alpha / 2)
76
+ zb = Z.inv_cdf(power)
77
+ return za, zb
78
+
79
+
80
+ def equal_arm_n(p1, p2, za, zb):
81
+ """Units per arm for two equal arms."""
82
+ pbar = (p1 + p2) / 2
83
+ num = za * math.sqrt(2 * pbar * (1 - pbar)) + zb * math.sqrt(p1 * (1 - p1) + p2 * (1 - p2))
84
+ return math.ceil((num / (p2 - p1)) ** 2)
85
+
86
+
87
+ def hours_needed(p1, p2, e, c, traffic, za, zb):
88
+ """Hours until a treatment arm of share e and control of share c is powered.
89
+
90
+ n1 = traffic*e*t (treatment, rate p2), n2 = traffic*c*t (control, rate p1).
91
+ Both arms scale with t, so the power equation solves in closed form.
92
+ """
93
+ if e <= 0 or c <= 0:
94
+ return None
95
+ pbar = (e * p2 + c * p1) / (e + c)
96
+ a = za * math.sqrt(pbar * (1 - pbar) * (1 / e + 1 / c))
97
+ b = zb * math.sqrt(p2 * (1 - p2) / e + p1 * (1 - p1) / c)
98
+ return ((a + b) / abs(p2 - p1)) ** 2 / traffic
99
+
100
+
101
+ def detectable_in(p1, direction, e, c, traffic, za, zb, hours):
102
+ """Smallest absolute regression detectable in `hours` (bisection)."""
103
+ if e <= 0 or c <= 0:
104
+ return None
105
+ hi = (1 - p1) if direction == "up" else p1
106
+ hi *= 0.999
107
+ lo = 1e-9
108
+ if hours_needed(p1, p1 + hi if direction == "up" else p1 - hi, e, c, traffic, za, zb) > hours:
109
+ return None
110
+ for _ in range(80):
111
+ mid = (lo + hi) / 2
112
+ p2 = p1 + mid if direction == "up" else p1 - mid
113
+ if hours_needed(p1, p2, e, c, traffic, za, zb) <= hours:
114
+ hi = mid
115
+ else:
116
+ lo = mid
117
+ return hi
118
+
119
+
120
+ def smallest_sufficient_exposure(p1, p2, traffic, za, zb, max_hours, control_pct):
121
+ """Smallest exposure (0.1% steps, up to 50%) that is powered within max_hours."""
122
+ for tenth in range(1, 501):
123
+ e = tenth / 1000.0
124
+ c = control_pct if control_pct is not None else 1 - e
125
+ if e + c > 1 + 1e-9:
126
+ break
127
+ h = hours_needed(p1, p2, e, c, traffic, za, zb)
128
+ if h is not None and h <= max_hours:
129
+ return e, h
130
+ return None, None
131
+
132
+
133
+ def main():
134
+ ap = argparse.ArgumentParser(description="Hours each canary/flag exposure step must run to detect a regression.")
135
+ ap.add_argument("--baseline", required=True, help="baseline rate of the metric in control, e.g. 0.8%% or 0.008")
136
+ g = ap.add_mutually_exclusive_group(required=True)
137
+ g.add_argument("--detect-abs", help="smallest regression worth catching, absolute: 0.3pp or 0.003")
138
+ g.add_argument("--detect-rel", help="smallest regression worth catching, relative to baseline: 25%% or 0.25")
139
+ ap.add_argument("--traffic", required=True, type=float, help="units per hour across all users (requests or users)")
140
+ ap.add_argument("--unit", default="requests", help="what --traffic counts; use the unit the flag randomises on (default: requests)")
141
+ ap.add_argument("--steps", default="1,5,25,50,100", help="exposure percentages, comma-separated (default: 1,5,25,50,100)")
142
+ ap.add_argument("--control-pct", help="fixed control (holdback) size in percent; default: everyone not exposed")
143
+ ap.add_argument("--direction", choices=["up", "down"], default="up",
144
+ help="up = a rise is the regression (error rate, default); down = a fall is (conversion)")
145
+ ap.add_argument("--alpha", type=float, default=0.05, help="false-alarm rate (default 0.05)")
146
+ ap.add_argument("--power", type=float, default=0.8, help="chance of catching a real regression (default 0.8)")
147
+ ap.add_argument("--one-sided", action="store_true", help="one-sided test (only the regression direction); default two-sided")
148
+ ap.add_argument("--max-hours", type=float, default=72.0, help="longest sensible step (default 72)")
149
+ ap.add_argument("--json", action="store_true", help="print JSON instead of a markdown table")
150
+ args = ap.parse_args()
151
+
152
+ p1 = parse_rate(args.baseline, "--baseline")
153
+ if not 0 < p1 < 1:
154
+ fail("--baseline must be between 0 and 100%")
155
+ if args.detect_abs is not None:
156
+ d = parse_rate(args.detect_abs, "--detect-abs", allow_pp=True)
157
+ else:
158
+ d = parse_rate(args.detect_rel, "--detect-rel") * p1
159
+ if d <= 0:
160
+ fail("the regression to detect must be greater than zero")
161
+ p2 = p1 + d if args.direction == "up" else p1 - d
162
+ if not 0 < p2 < 1:
163
+ fail("baseline plus the regression falls outside 0-100%")
164
+ if args.traffic <= 0:
165
+ fail("--traffic must be greater than zero")
166
+ if not 0 < args.alpha < 1 or not 0 < args.power < 1:
167
+ fail("--alpha and --power must be between 0 and 1")
168
+ if args.max_hours <= 0:
169
+ fail("--max-hours must be greater than zero")
170
+ control = None
171
+ if args.control_pct is not None:
172
+ control = parse_rate(args.control_pct.rstrip("%") + "%", "--control-pct")
173
+ if not 0 < control < 1:
174
+ fail("--control-pct must be between 0 and 100")
175
+ try:
176
+ steps = [float(s.strip().rstrip("%")) / 100.0 for s in args.steps.split(",") if s.strip()]
177
+ except ValueError:
178
+ fail("--steps must be comma-separated percentages, e.g. 1,5,25,50,100")
179
+ if not steps or any(not 0 < s <= 1 for s in steps):
180
+ fail("--steps must be percentages between 0 and 100")
181
+
182
+ za, zb = z_values(args.alpha, args.power, args.one_sided)
183
+ n_equal = equal_arm_n(p1, p2, za, zb)
184
+
185
+ rows = []
186
+ for e in steps:
187
+ c = control if control is not None else 1 - e
188
+ row = {"exposure_pct": round(e * 100, 4), "control_pct": round(c * 100, 4)}
189
+ if e + c > 1 + 1e-9:
190
+ row.update(status="invalid", note="exposure plus control exceeds 100%")
191
+ elif c <= 1e-9:
192
+ row.update(status="no-control", hours=None,
193
+ note="no concurrent control at 100%: promote on the previous step's comparison, "
194
+ "or keep a holdback (--control-pct) to keep comparing")
195
+ else:
196
+ h = hours_needed(p1, p2, e, c, args.traffic, za, zb)
197
+ row["hours"] = round(h, 3)
198
+ row["treatment_units"] = math.ceil(h * args.traffic * e)
199
+ row["control_units"] = math.ceil(h * args.traffic * c)
200
+ if h <= args.max_hours:
201
+ row["status"] = "sufficient"
202
+ else:
203
+ row["status"] = "insufficient"
204
+ mde = detectable_in(p1, args.direction, e, c, args.traffic, za, zb, args.max_hours)
205
+ row["detectable_in_max_hours_abs"] = None if mde is None else round(mde, 6)
206
+ rows.append(row)
207
+
208
+ best_e, best_h = smallest_sufficient_exposure(p1, p2, args.traffic, za, zb, args.max_hours, control)
209
+
210
+ result = {
211
+ "baseline": p1, "regression_abs": round(d, 6), "regressed_rate": round(p2, 6),
212
+ "direction": args.direction, "alpha": args.alpha, "power": args.power,
213
+ "test": "one-sided" if args.one_sided else "two-sided",
214
+ "traffic_per_hour": args.traffic, "unit": args.unit, "max_hours": args.max_hours,
215
+ "equal_arm_units_per_arm": n_equal, "steps": rows,
216
+ "smallest_sufficient_exposure_pct": None if best_e is None else round(best_e * 100, 1),
217
+ "smallest_sufficient_exposure_hours": None if best_h is None else round(best_h, 2),
218
+ }
219
+ if args.json:
220
+ print(json.dumps(result, indent=2))
221
+ return
222
+
223
+ u = args.unit
224
+ print("# Canary sufficiency")
225
+ print()
226
+ print("- Metric: baseline {} -> regression to catch {} ({} rate {}), {} test, alpha {}, power {}".format(
227
+ fmt_pct(p1, 3), fmt_pp(d), "rising to" if args.direction == "up" else "falling to",
228
+ fmt_pct(p2, 3), result["test"], args.alpha, args.power))
229
+ print("- Traffic: {:,.0f} {} per hour; control = {}".format(
230
+ args.traffic, u, "fixed {} holdback".format(fmt_pct(control)) if control is not None else "everyone not exposed"))
231
+ print("- Equal arms would need {:,} {} per arm".format(n_equal, u))
232
+ print()
233
+ print("| Exposure | Control | Treatment {u} needed | Control {u} needed | Minimum time | Status |".format(u=u))
234
+ print("|---|---|---|---|---|---|")
235
+ for r in rows:
236
+ exp = fmt_pct(r["exposure_pct"] / 100)
237
+ ctl = fmt_pct(r["control_pct"] / 100)
238
+ if r["status"] in ("no-control", "invalid"):
239
+ print("| {} | {} | - | - | n/a | {} |".format(exp, ctl, r["note"]))
240
+ continue
241
+ status = "ok" if r["status"] == "sufficient" else "INSUFFICIENT: over {:g} h".format(args.max_hours)
242
+ if r["status"] == "insufficient":
243
+ m = r.get("detectable_in_max_hours_abs")
244
+ status += "; in {:g} h it can only catch {}".format(
245
+ args.max_hours, "nothing measurable" if m is None else ">= " + fmt_pp(m))
246
+ print("| {} | {} | {:,} | {:,} | {} | {} |".format(
247
+ exp, ctl, r["treatment_units"], r["control_units"], fmt_hours(r["hours"]), status))
248
+ print()
249
+ if best_e is None:
250
+ print("No exposure up to 50% reaches sufficiency within {:g} h. Catch a larger regression, "
251
+ "run longer, or pick a metric with a higher baseline rate.".format(args.max_hours))
252
+ else:
253
+ print("Smallest exposure that reaches sufficiency within {:g} h: {} (about {}).".format(
254
+ args.max_hours, fmt_pct(best_e, 1), fmt_hours(best_h)))
255
+ print()
256
+ print("Minimum time is statistics only. A step also runs through at least one daily peak "
257
+ "and any delayed effect (nightly jobs, next-day emails) before it is promoted.")
258
+
259
+
260
+ if __name__ == "__main__":
261
+ main()
@@ -0,0 +1,50 @@
1
+ # Rollout plan — {change name}
2
+
3
+ **{Two lines: the answer to the user's question, the input fact that settles it, and the shape of the plan.}**
4
+ ✅ *"No: at 2,000 users an hour a 1% step needs about 20 days to see a 0.3 pp error rise, so Tuesday-to-Friday proves nothing. Run internal for a day, 10% for 3 days, 50% for a day, and reach 100% the Wednesday after."*
5
+
6
+ **Binding constraint:** {the fact that paces the plan: a partner cap, a billing or batch cycle, a deadline, the segments the flag can target, a shadow run, or volume. "None: traffic is high, the default ladder applies."}
7
+ **Flag:** `{key}`, evaluated {server / client / edge}, targets by {user / account / tenant / %} · **Assumptions:** {one line, each tagged `[assumption]`}
8
+
9
+ ## Ladder
10
+
11
+ | Step | Dates | Who is in it, and why | Judged on | Promote when |
12
+ |---|---|---|---|---|
13
+ | 0 | {Mon 7 Sep – Tue 8 Sep} | {internal / test accounts: crashes at no customer cost} | crashes only | {no abort for 24 h} |
14
+ | 1 | {…} | {named cohort or `hash(salt + id) mod 100 < 10`; why these; who is held back} | {units or cycle this step sees, in one phrase} | {every threshold below holds at the step's end} |
15
+ | 2 | {…} | | | |
16
+ | {n} | {date} | everyone | {holdback or the last comparison} | — |
17
+
18
+ {One sentence of arithmetic, with its inputs: what the smallest useful step is and why.}
19
+ {Deadline line when there is one: ✅ *"100% on 14 Sep, 9 days before the deadline; one failed cycle misses it."*}
20
+
21
+ ## Thresholds
22
+
23
+ All compare the flag-on cohort with the flag-off cohort over the same window. Counts, not
24
+ percentages, where a step sees fewer than about 100 of the unit.
25
+
26
+ | Metric | Baseline | Promote if | Abort (any time, {automatic / manual}) |
27
+ |---|---|---|---|
28
+ | {decision metric} | {from input} | {≤ control + 0.3 pp / zero unexplained} | {> 3× control for 5 min → flag off} |
29
+ | {guardrail} | | | |
30
+
31
+ - **Hold:** {sample short or metric between threshold and abort → extend once by {time or cycle}, then roll back}.
32
+ - **Watcher:** {role} · **Sign-off before each promotion:** {role, if one is needed} · **Override:** {one role, reason written down}.
33
+
34
+ ## What turning the flag off does not undo
35
+
36
+ Flag off: {who}, takes effect in {seconds / cache TTL / next app launch}. Only rows this change touches:
37
+
38
+ | What | Undone? | Handling |
39
+ |---|---|---|
40
+ | {messages sent / money moved / records written in a new format / third-party records} | no | {correction or reissue path, reversal procedure, what keeps reading the new format} |
41
+
42
+ **Point of no return:** {the step and why / none}.
43
+
44
+ ## Flag removal
45
+
46
+ **Owner:** {name from the input, else role} · **Date:** {date} · **Gate:** {100% for 14 days or one full cycle, no rollback} · **Removes:** `{key}`, the old path, its config and dashboards · **Keeps:** {any reader the old records need}.
47
+
48
+ ## To confirm
49
+
50
+ - {at most 4 facts, each one that would change a step}
@@ -33,5 +33,17 @@
33
33
  "model": {
34
34
  "tier": "standard",
35
35
  "rationale": "Formats Sonar API findings into a report. The hard analysis is Sonar's; this is synthesis. Escalates on very large diffs."
36
+ },
37
+ "permissions": {
38
+ "network": [
39
+ "sonarcloud.io"
40
+ ],
41
+ "commands": [
42
+ "sonar-scanner",
43
+ "git",
44
+ "curl"
45
+ ],
46
+ "files": "none",
47
+ "notes": "No scripts. Its instructions run sonar-scanner and query the Sonar API with curl, using SONAR_TOKEN from the environment, against sonarcloud.io or the self-hosted SonarQube URL the user configures."
36
48
  }
37
49
  }
@@ -37,5 +37,16 @@
37
37
  "model": {
38
38
  "tier": "standard",
39
39
  "rationale": "Structured generation against fixed module conventions and a secure-defaults checklist; the judgment is bounded to choosing defaults and validation rules for a described need."
40
+ },
41
+ "permissions": {
42
+ "network": [
43
+ "registry.terraform.io",
44
+ "releases.hashicorp.com"
45
+ ],
46
+ "commands": [
47
+ "terraform"
48
+ ],
49
+ "files": "none",
50
+ "notes": "No scripts. Its instructions run `terraform fmt`, `terraform init -backend=false` and `terraform validate` on the module it writes. `init` downloads the providers from the Terraform registry; none of the commands contacts a cloud account."
40
51
  }
41
52
  }
@@ -40,7 +40,14 @@
40
40
  "iam",
41
41
  "drift"
42
42
  ],
43
- "permissions": {"network": [], "commands": [], "files": "named-paths", "notes": "Reads a plan JSON, which can hold secrets in clear text; the script never prints attribute values."},
43
+ "permissions": {
44
+ "network": [],
45
+ "commands": [
46
+ "terraform"
47
+ ],
48
+ "files": "named-paths",
49
+ "notes": "Reads a plan JSON, which can hold secrets in clear text; the script never prints attribute values. Its instructions run `terraform show -json` to produce that plan JSON."
50
+ },
44
51
  "model": {
45
52
  "tier": "heavy",
46
53
  "rationale": "Adversarial review of a destructive change: judging which replacements lose data, which access changes widen exposure, and what is out of scope needs careful reasoning; a missed destroy costs far more than the model."
@@ -28,7 +28,18 @@
28
28
  "design",
29
29
  "api"
30
30
  ],
31
- "permissions": {"network": ["api.figma.com"], "commands": [], "files": "named-paths", "notes": "Reads FIGMA_TOKEN from the environment to call the Figma REST API."},
31
+ "permissions": {
32
+ "network": [
33
+ "api.figma.com",
34
+ "pypi.org",
35
+ "files.pythonhosted.org"
36
+ ],
37
+ "commands": [
38
+ "pip"
39
+ ],
40
+ "files": "named-paths",
41
+ "notes": "Reads FIGMA_TOKEN from the environment to call the Figma REST API. Its instructions install the pinned requirements once with `python3 -m pip install -r requirements.txt`, which reaches PyPI."
42
+ },
32
43
  "model": {
33
44
  "tier": "standard",
34
45
  "rationale": "API calls + spec generation. Bounded transformation."