@slowdini/slow-powers-opencode 0.4.4 → 0.5.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 (48) hide show
  1. package/README.md +2 -2
  2. package/package.json +14 -14
  3. package/skills/evaluating-skills/SKILL.md +6 -6
  4. package/skills/evaluating-skills/evals/baseline/BASELINE.md +2 -3
  5. package/skills/hardening-plans/evals/baseline/BASELINE.md +2 -3
  6. package/skills/{systematic-debugging → investigating-bugs}/SKILL.md +5 -7
  7. package/skills/{systematic-debugging → investigating-bugs}/condition-based-waiting-example.ts +3 -3
  8. package/skills/{systematic-debugging → investigating-bugs}/condition-based-waiting.md +1 -9
  9. package/skills/investigating-bugs/evals/baseline/BASELINE.md +23 -0
  10. package/skills/investigating-bugs/evals/baseline/benchmark.json +51 -0
  11. package/skills/investigating-bugs/evals/baseline/grading/feature-request-no-debugging__with_skill.json +17 -0
  12. package/skills/investigating-bugs/evals/baseline/grading/feature-request-no-debugging__without_skill.json +17 -0
  13. package/skills/investigating-bugs/evals/baseline/grading/null-id-crash-investigate-first__with_skill.json +46 -0
  14. package/skills/investigating-bugs/evals/baseline/grading/null-id-crash-investigate-first__without_skill.json +31 -0
  15. package/skills/investigating-bugs/evals/baseline/grading/seeded-stacked-guess-investigate-first__with_skill.json +46 -0
  16. package/skills/investigating-bugs/evals/baseline/grading/seeded-stacked-guess-investigate-first__without_skill.json +31 -0
  17. package/skills/investigating-bugs/evals/baseline/grading/seeded-three-fix-limit-stop__with_skill.json +39 -0
  18. package/skills/investigating-bugs/evals/baseline/grading/seeded-three-fix-limit-stop__without_skill.json +24 -0
  19. package/skills/investigating-bugs/evals/evals.json +89 -0
  20. package/skills/test-driven-development/SKILL.md +2 -0
  21. package/skills/verifying-development-work/SKILL.md +37 -20
  22. package/skills/verifying-development-work/code-review.md +49 -10
  23. package/skills/verifying-development-work/evals/baseline/NOTES.md +4 -4
  24. package/skills/verifying-development-work/evals/evals.json +57 -5
  25. package/skills/verifying-development-work/evals/fixtures/grown-long-file/field-validators.test.ts +47 -0
  26. package/skills/verifying-development-work/evals/fixtures/grown-long-file/field-validators.ts +532 -0
  27. package/skills/verifying-development-work/long-files.md +141 -0
  28. package/skills/working-in-isolation/SKILL.md +16 -2
  29. package/skills/working-in-isolation/evals/evals.json +4 -4
  30. package/skills/writing-skills/SKILL.md +2 -2
  31. package/skills/systematic-debugging/CREATION-LOG.md +0 -119
  32. package/skills/systematic-debugging/defense-in-depth.md +0 -122
  33. package/skills/systematic-debugging/evals/baseline/BASELINE.md +0 -22
  34. package/skills/systematic-debugging/evals/baseline/benchmark.json +0 -51
  35. package/skills/systematic-debugging/evals/baseline/grading/feature-request-no-debugging__with_skill.json +0 -17
  36. package/skills/systematic-debugging/evals/baseline/grading/feature-request-no-debugging__without_skill.json +0 -17
  37. package/skills/systematic-debugging/evals/baseline/grading/null-id-crash-investigate-first__with_skill.json +0 -46
  38. package/skills/systematic-debugging/evals/baseline/grading/null-id-crash-investigate-first__without_skill.json +0 -31
  39. package/skills/systematic-debugging/evals/evals.json +0 -45
  40. package/skills/systematic-debugging/find-polluter.sh +0 -63
  41. package/skills/systematic-debugging/root-cause-tracing.md +0 -167
  42. package/skills/systematic-debugging/test-academic.md +0 -14
  43. package/skills/systematic-debugging/test-pressure-1.md +0 -58
  44. package/skills/systematic-debugging/test-pressure-2.md +0 -68
  45. package/skills/systematic-debugging/test-pressure-3.md +0 -69
  46. package/skills/verifying-development-work/comment-review.md +0 -85
  47. /package/skills/{systematic-debugging → investigating-bugs}/evals/fixtures/order-bug/orderHandler.ts +0 -0
  48. /package/skills/{systematic-debugging → investigating-bugs}/evals/fixtures/order-bug/repro.ts +0 -0
@@ -0,0 +1,532 @@
1
+ // Shared field-validation helpers used across the signup, billing, and
2
+ // scheduling forms. Each validator returns a ValidationResult so callers can
3
+ // surface a specific message instead of a bare boolean.
4
+
5
+ export interface ValidationResult {
6
+ valid: boolean;
7
+ message?: string;
8
+ }
9
+
10
+ const ok: ValidationResult = { valid: true };
11
+ const fail = (message: string): ValidationResult => ({ valid: false, message });
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // String validators
15
+ // ---------------------------------------------------------------------------
16
+
17
+ export function isNonEmpty(value: string): ValidationResult {
18
+ if (value.trim().length === 0) {
19
+ return fail("Value must not be empty.");
20
+ }
21
+ return ok;
22
+ }
23
+
24
+ export function hasMinLength(value: string, min: number): ValidationResult {
25
+ if (value.length < min) {
26
+ return fail(`Must be at least ${min} characters.`);
27
+ }
28
+ return ok;
29
+ }
30
+
31
+ export function hasMaxLength(value: string, max: number): ValidationResult {
32
+ if (value.length > max) {
33
+ return fail(`Must be at most ${max} characters.`);
34
+ }
35
+ return ok;
36
+ }
37
+
38
+ export function isEmail(value: string): ValidationResult {
39
+ const at = value.indexOf("@");
40
+ const dot = value.lastIndexOf(".");
41
+ if (at < 1 || dot < at + 2 || dot === value.length - 1) {
42
+ return fail("Must be a valid email address.");
43
+ }
44
+ if (/\s/.test(value)) {
45
+ return fail("Email must not contain whitespace.");
46
+ }
47
+ return ok;
48
+ }
49
+
50
+ export function isUrl(value: string): ValidationResult {
51
+ if (!/^https?:\/\//.test(value)) {
52
+ return fail("Must start with http:// or https://.");
53
+ }
54
+ if (/\s/.test(value)) {
55
+ return fail("URL must not contain whitespace.");
56
+ }
57
+ return ok;
58
+ }
59
+
60
+ export function isSlug(value: string): ValidationResult {
61
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value)) {
62
+ return fail("Must be lowercase words separated by single hyphens.");
63
+ }
64
+ return ok;
65
+ }
66
+
67
+ export function isAlphanumeric(value: string): ValidationResult {
68
+ if (!/^[a-z0-9]+$/i.test(value)) {
69
+ return fail("Must contain only letters and numbers.");
70
+ }
71
+ return ok;
72
+ }
73
+
74
+ export function matchesPattern(
75
+ value: string,
76
+ pattern: RegExp,
77
+ ): ValidationResult {
78
+ if (!pattern.test(value)) {
79
+ return fail("Value does not match the expected format.");
80
+ }
81
+ return ok;
82
+ }
83
+
84
+ export function isNoLeadingTrailingSpace(value: string): ValidationResult {
85
+ if (value !== value.trim()) {
86
+ return fail("Must not have leading or trailing whitespace.");
87
+ }
88
+ return ok;
89
+ }
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // Number validators
93
+ // ---------------------------------------------------------------------------
94
+
95
+ export function isFiniteNumber(value: number): ValidationResult {
96
+ if (!Number.isFinite(value)) {
97
+ return fail("Must be a finite number.");
98
+ }
99
+ return ok;
100
+ }
101
+
102
+ export function isInteger(value: number): ValidationResult {
103
+ if (!Number.isInteger(value)) {
104
+ return fail("Must be a whole number.");
105
+ }
106
+ return ok;
107
+ }
108
+
109
+ export function isPositive(value: number): ValidationResult {
110
+ if (!(value > 0)) {
111
+ return fail("Must be greater than zero.");
112
+ }
113
+ return ok;
114
+ }
115
+
116
+ export function isNonNegative(value: number): ValidationResult {
117
+ if (!(value >= 0)) {
118
+ return fail("Must not be negative.");
119
+ }
120
+ return ok;
121
+ }
122
+
123
+ export function isInRange(
124
+ value: number,
125
+ min: number,
126
+ max: number,
127
+ ): ValidationResult {
128
+ if (value < min || value > max) {
129
+ return fail(`Must be between ${min} and ${max}.`);
130
+ }
131
+ return ok;
132
+ }
133
+
134
+ export function isMultipleOf(value: number, factor: number): ValidationResult {
135
+ if (factor === 0 || value % factor !== 0) {
136
+ return fail(`Must be a multiple of ${factor}.`);
137
+ }
138
+ return ok;
139
+ }
140
+
141
+ export function isPercentage(value: number): ValidationResult {
142
+ if (value < 0 || value > 100) {
143
+ return fail("Must be between 0 and 100.");
144
+ }
145
+ return ok;
146
+ }
147
+
148
+ export function isPort(value: number): ValidationResult {
149
+ if (!Number.isInteger(value) || value < 1 || value > 65535) {
150
+ return fail("Must be a valid port between 1 and 65535.");
151
+ }
152
+ return ok;
153
+ }
154
+
155
+ // ---------------------------------------------------------------------------
156
+ // Date validators
157
+ // ---------------------------------------------------------------------------
158
+
159
+ export function isIsoDate(value: string): ValidationResult {
160
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
161
+ return fail("Must be an ISO date (YYYY-MM-DD).");
162
+ }
163
+ const parsed = Date.parse(value);
164
+ if (Number.isNaN(parsed)) {
165
+ return fail("Must be a real calendar date.");
166
+ }
167
+ return ok;
168
+ }
169
+
170
+ export function isFuture(value: string, now: number): ValidationResult {
171
+ const parsed = Date.parse(value);
172
+ if (Number.isNaN(parsed)) {
173
+ return fail("Must be a real date.");
174
+ }
175
+ if (parsed <= now) {
176
+ return fail("Must be in the future.");
177
+ }
178
+ return ok;
179
+ }
180
+
181
+ export function isPast(value: string, now: number): ValidationResult {
182
+ const parsed = Date.parse(value);
183
+ if (Number.isNaN(parsed)) {
184
+ return fail("Must be a real date.");
185
+ }
186
+ if (parsed >= now) {
187
+ return fail("Must be in the past.");
188
+ }
189
+ return ok;
190
+ }
191
+
192
+ export function isWithinDays(
193
+ value: string,
194
+ now: number,
195
+ days: number,
196
+ ): ValidationResult {
197
+ const parsed = Date.parse(value);
198
+ if (Number.isNaN(parsed)) {
199
+ return fail("Must be a real date.");
200
+ }
201
+ const span = Math.abs(parsed - now);
202
+ if (span > days * 24 * 60 * 60 * 1000) {
203
+ return fail(`Must be within ${days} days.`);
204
+ }
205
+ return ok;
206
+ }
207
+
208
+ export function isValidAge(years: number): ValidationResult {
209
+ if (!Number.isInteger(years) || years < 0 || years > 130) {
210
+ return fail("Must be a realistic age.");
211
+ }
212
+ return ok;
213
+ }
214
+
215
+ // ---------------------------------------------------------------------------
216
+ // Collection validators
217
+ // ---------------------------------------------------------------------------
218
+
219
+ export function isNonEmptyArray<T>(value: T[]): ValidationResult {
220
+ if (!Array.isArray(value) || value.length === 0) {
221
+ return fail("Must contain at least one item.");
222
+ }
223
+ return ok;
224
+ }
225
+
226
+ export function hasUniqueItems<T>(value: T[]): ValidationResult {
227
+ if (new Set(value).size !== value.length) {
228
+ return fail("Items must be unique.");
229
+ }
230
+ return ok;
231
+ }
232
+
233
+ export function hasSize<T>(value: T[], size: number): ValidationResult {
234
+ if (value.length !== size) {
235
+ return fail(`Must contain exactly ${size} items.`);
236
+ }
237
+ return ok;
238
+ }
239
+
240
+ export function hasSizeWithin<T>(
241
+ value: T[],
242
+ min: number,
243
+ max: number,
244
+ ): ValidationResult {
245
+ if (value.length < min || value.length > max) {
246
+ return fail(`Must contain between ${min} and ${max} items.`);
247
+ }
248
+ return ok;
249
+ }
250
+
251
+ export function includesAll<T>(value: T[], required: T[]): ValidationResult {
252
+ const present = new Set(value);
253
+ for (const item of required) {
254
+ if (!present.has(item)) {
255
+ return fail("Missing one or more required items.");
256
+ }
257
+ }
258
+ return ok;
259
+ }
260
+
261
+ export function isSubsetOf<T>(value: T[], allowed: T[]): ValidationResult {
262
+ const permitted = new Set(allowed);
263
+ for (const item of value) {
264
+ if (!permitted.has(item)) {
265
+ return fail("Contains a value that is not allowed.");
266
+ }
267
+ }
268
+ return ok;
269
+ }
270
+
271
+ // ---------------------------------------------------------------------------
272
+ // Identity and web-format validators
273
+ // ---------------------------------------------------------------------------
274
+
275
+ export function isUuid(value: string): ValidationResult {
276
+ const pattern =
277
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
278
+ if (!pattern.test(value)) {
279
+ return fail("Must be a valid UUID.");
280
+ }
281
+ return ok;
282
+ }
283
+
284
+ export function isHexColor(value: string): ValidationResult {
285
+ if (!/^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i.test(value)) {
286
+ return fail("Must be a hex color like #fff or #ffffff.");
287
+ }
288
+ return ok;
289
+ }
290
+
291
+ export function isIpv4(value: string): ValidationResult {
292
+ const parts = value.split(".");
293
+ if (parts.length !== 4) {
294
+ return fail("Must be a dotted IPv4 address.");
295
+ }
296
+ for (const part of parts) {
297
+ const n = Number(part);
298
+ if (!/^\d+$/.test(part) || n < 0 || n > 255) {
299
+ return fail("Each IPv4 octet must be between 0 and 255.");
300
+ }
301
+ }
302
+ return ok;
303
+ }
304
+
305
+ export function isMacAddress(value: string): ValidationResult {
306
+ if (!/^(?:[0-9a-f]{2}:){5}[0-9a-f]{2}$/i.test(value)) {
307
+ return fail("Must be a colon-separated MAC address.");
308
+ }
309
+ return ok;
310
+ }
311
+
312
+ export function isSemver(value: string): ValidationResult {
313
+ if (!/^\d+\.\d+\.\d+(?:-[0-9a-z.-]+)?$/i.test(value)) {
314
+ return fail("Must be a semver string like 1.2.3.");
315
+ }
316
+ return ok;
317
+ }
318
+
319
+ export function isJwtShape(value: string): ValidationResult {
320
+ const segments = value.split(".");
321
+ if (segments.length !== 3 || segments.some((s) => s.length === 0)) {
322
+ return fail("Must look like a three-part JWT.");
323
+ }
324
+ return ok;
325
+ }
326
+
327
+ // ---------------------------------------------------------------------------
328
+ // Financial validators (added for the billing form)
329
+ // ---------------------------------------------------------------------------
330
+
331
+ export function isLuhnValid(value: string): ValidationResult {
332
+ const digits = value.replace(/\s+/g, "");
333
+ if (!/^\d+$/.test(digits)) {
334
+ return fail("Must contain only digits.");
335
+ }
336
+ let sum = 0;
337
+ let double = false;
338
+ for (let i = digits.length - 1; i >= 0; i--) {
339
+ let d = Number(digits[i]);
340
+ if (double) {
341
+ d *= 2;
342
+ if (d > 9) {
343
+ d -= 9;
344
+ }
345
+ }
346
+ sum += d;
347
+ double = !double;
348
+ }
349
+ if (sum % 10 !== 0) {
350
+ return fail("Failed the Luhn checksum.");
351
+ }
352
+ return ok;
353
+ }
354
+
355
+ export function isCreditCard(value: string): ValidationResult {
356
+ const digits = value.replace(/[\s-]/g, "");
357
+ if (digits.length < 13 || digits.length > 19) {
358
+ return fail("Card number length is out of range.");
359
+ }
360
+ return isLuhnValid(digits);
361
+ }
362
+
363
+ export function isCurrencyAmount(value: string): ValidationResult {
364
+ if (!/^\d+(?:\.\d{1,2})?$/.test(value)) {
365
+ return fail("Must be an amount with up to two decimal places.");
366
+ }
367
+ return ok;
368
+ }
369
+
370
+ export function isPositiveMoney(cents: number): ValidationResult {
371
+ if (!Number.isInteger(cents) || cents <= 0) {
372
+ return fail("Must be a positive whole number of cents.");
373
+ }
374
+ return ok;
375
+ }
376
+
377
+ export function isRoutingNumber(value: string): ValidationResult {
378
+ if (!/^\d{9}$/.test(value)) {
379
+ return fail("Must be nine digits.");
380
+ }
381
+ const d = value.split("").map(Number);
382
+ const checksum =
383
+ 3 * (d[0] + d[3] + d[6]) +
384
+ 7 * (d[1] + d[4] + d[7]) +
385
+ 1 * (d[2] + d[5] + d[8]);
386
+ if (checksum % 10 !== 0) {
387
+ return fail("Failed the routing-number checksum.");
388
+ }
389
+ return ok;
390
+ }
391
+
392
+ export function isIban(value: string): ValidationResult {
393
+ const compact = value.replace(/\s+/g, "").toUpperCase();
394
+ if (!/^[A-Z]{2}\d{2}[A-Z0-9]{10,30}$/.test(compact)) {
395
+ return fail("Must be a valid IBAN format.");
396
+ }
397
+ const rearranged = compact.slice(4) + compact.slice(0, 4);
398
+ let remainder = 0;
399
+ for (const ch of rearranged) {
400
+ const code = /[A-Z]/.test(ch) ? (ch.charCodeAt(0) - 55).toString() : ch;
401
+ for (const digit of code) {
402
+ remainder = (remainder * 10 + Number(digit)) % 97;
403
+ }
404
+ }
405
+ if (remainder !== 1) {
406
+ return fail("Failed the IBAN checksum.");
407
+ }
408
+ return ok;
409
+ }
410
+
411
+ // ---------------------------------------------------------------------------
412
+ // Credential and security validators
413
+ // ---------------------------------------------------------------------------
414
+
415
+ export function hasUppercase(value: string): ValidationResult {
416
+ if (!/[A-Z]/.test(value)) {
417
+ return fail("Must contain an uppercase letter.");
418
+ }
419
+ return ok;
420
+ }
421
+
422
+ export function hasLowercase(value: string): ValidationResult {
423
+ if (!/[a-z]/.test(value)) {
424
+ return fail("Must contain a lowercase letter.");
425
+ }
426
+ return ok;
427
+ }
428
+
429
+ export function hasDigit(value: string): ValidationResult {
430
+ if (!/\d/.test(value)) {
431
+ return fail("Must contain a digit.");
432
+ }
433
+ return ok;
434
+ }
435
+
436
+ export function hasSymbol(value: string): ValidationResult {
437
+ if (!/[^A-Za-z0-9]/.test(value)) {
438
+ return fail("Must contain a symbol.");
439
+ }
440
+ return ok;
441
+ }
442
+
443
+ export function isStrongPassword(value: string): ValidationResult {
444
+ return all(
445
+ hasMinLength(value, 12),
446
+ hasUppercase(value),
447
+ hasLowercase(value),
448
+ hasDigit(value),
449
+ hasSymbol(value),
450
+ );
451
+ }
452
+
453
+ export function isNotCommonPassword(
454
+ value: string,
455
+ blocklist: string[],
456
+ ): ValidationResult {
457
+ if (blocklist.includes(value.toLowerCase())) {
458
+ return fail("Password is too common.");
459
+ }
460
+ return ok;
461
+ }
462
+
463
+ // ---------------------------------------------------------------------------
464
+ // Contact and address validators
465
+ // ---------------------------------------------------------------------------
466
+
467
+ export function isPhoneE164(value: string): ValidationResult {
468
+ if (!/^\+[1-9]\d{1,14}$/.test(value)) {
469
+ return fail("Must be an E.164 phone number like +14155550123.");
470
+ }
471
+ return ok;
472
+ }
473
+
474
+ export function isUsZip(value: string): ValidationResult {
475
+ if (!/^\d{5}(?:-\d{4})?$/.test(value)) {
476
+ return fail("Must be a US ZIP code.");
477
+ }
478
+ return ok;
479
+ }
480
+
481
+ export function isCountryCode(value: string): ValidationResult {
482
+ if (!/^[A-Z]{2}$/.test(value)) {
483
+ return fail("Must be a two-letter ISO country code.");
484
+ }
485
+ return ok;
486
+ }
487
+
488
+ export function isLatitude(value: number): ValidationResult {
489
+ if (value < -90 || value > 90) {
490
+ return fail("Latitude must be between -90 and 90.");
491
+ }
492
+ return ok;
493
+ }
494
+
495
+ export function isLongitude(value: number): ValidationResult {
496
+ if (value < -180 || value > 180) {
497
+ return fail("Longitude must be between -180 and 180.");
498
+ }
499
+ return ok;
500
+ }
501
+
502
+ // ---------------------------------------------------------------------------
503
+ // Combinators
504
+ // ---------------------------------------------------------------------------
505
+
506
+ export function all(...results: ValidationResult[]): ValidationResult {
507
+ for (const result of results) {
508
+ if (!result.valid) {
509
+ return result;
510
+ }
511
+ }
512
+ return ok;
513
+ }
514
+
515
+ export function any(...results: ValidationResult[]): ValidationResult {
516
+ for (const result of results) {
517
+ if (result.valid) {
518
+ return ok;
519
+ }
520
+ }
521
+ return fail("No candidate passed validation.");
522
+ }
523
+
524
+ export function not(
525
+ result: ValidationResult,
526
+ message: string,
527
+ ): ValidationResult {
528
+ if (result.valid) {
529
+ return fail(message);
530
+ }
531
+ return ok;
532
+ }
@@ -0,0 +1,141 @@
1
+ # Reviewing a File Your Change Made Long
2
+
3
+ This is a sub-process of **phase 1** of the finishing sequence in
4
+ [`./SKILL.md`](./SKILL.md), reached from [`./code-review.md`](./code-review.md). It
5
+ runs when your change grew a file past the line limit.
6
+
7
+ You've added code to a file that's now too long for humans and agents to parse
8
+ effectively. Being long isn't the disease — it's the symptom. The file has likely
9
+ grown unfocused, losing a single clear purpose.
10
+
11
+ Your job now is **not to refactor the whole file.** It's to avoid making the
12
+ situation worse while laying groundwork for future updates. This is how large code
13
+ problems get solved without halting feature work or forcing a big refactor.
14
+
15
+ ---
16
+
17
+ > **THE IRON LAW:** If a change *you* made grew a file to over 500 lines, that file
18
+ > MUST go through this review before you finish. Handing back a newly-grown long
19
+ > file without a change or a declared exception is the failure this guidance exists
20
+ > to prevent.
21
+
22
+ This applies to files your change grew. A file someone else left long that you only
23
+ read past is out of scope.
24
+
25
+ ---
26
+
27
+ ## Thresholds
28
+
29
+ - **Over 500 lines** — a mandate to **review**, not necessarily to change. A review
30
+ can legitimately conclude "no change needed" — but you must still *report* that
31
+ conclusion and why. The size is a trigger, not a verdict.
32
+ - **Over 1000 lines** — never acceptable for a normal code file; no one can hold it
33
+ in their head. At minimum, your change must not be what leaves it there: carve your
34
+ addition into a new module rather than pile onto the existing bulk. Pre-existing
35
+ bulk you didn't create and can't shed within your scope gets *surfaced to the user*
36
+ as needing a dedicated effort — not silently accepted. Legitimately-exempt files
37
+ (below) stay exempt at any size.
38
+
39
+ ---
40
+
41
+ ## The process
42
+
43
+ Do this for **each** file that tripped the rule, **one at a time** — don't dump every
44
+ proposal on the user at once.
45
+
46
+ ### 1. Check for exceptions
47
+
48
+ Some files are meant to be long. This is the exception, not the rule. You MUST still
49
+ declare that you reviewed the file and found it exempt, naming the category and the
50
+ reason — a silent skip is not an exception.
51
+
52
+ | Category | What it covers | Examples |
53
+ |----------|----------------|----------|
54
+ | **Generated** | Machine-written files, not hand-authored code. | Lockfiles (`bun.lock`, `package-lock.json`), committed build output, generated API clients, generated GraphQL/protobuf types. |
55
+ | **Technical requirement** | Content a tool or framework expects by name and location. There's no clever workaround. | Prisma `schema.prisma`, Rails `db/schema.rb`, a single Django migration, a bundler entry file. |
56
+ | **Config / data** | Files that *store* values rather than express logic. | A large JSON config, an i18n string table, a generated constants file. Include what naturally belongs. |
57
+ | **Barrel** | Files whose only job is to re-export a module's public surface. Splitting them is *more* confusing, not less. | An `index.ts` that re-exports a package. |
58
+
59
+ **Multi-part files** need their own check. Some files intentionally hold distinct
60
+ parts — a JS import block, a Vue single-file component's template/script/style, Rust
61
+ modules with inline `#[cfg(test)]` tests. For these:
62
+
63
+ - Is there a standard way to split it? (Rust can move tests to a `tests/` directory.)
64
+ If so, follow it — and don't propose anything else unless the split *also* leaves a
65
+ long file.
66
+ - Treating each part as its own file, is every part under 500 lines? If so, there's
67
+ no real violation — declare that as the exception and leave it.
68
+ - Otherwise, continue with this file below.
69
+
70
+ ### 2. Understand why the file grew
71
+
72
+ You added to this file for a reason — what was it? Now look at the whole: does it
73
+ have one job that's simply outgrown a single file? Several jobs tangled together that
74
+ are really separate features now? Or no clear focus, just an accumulation of code
75
+ that resembles what you added? A clear read of *how* it grew is what makes the next
76
+ step a good fix instead of arbitrary cutting.
77
+
78
+ ### 3. Determine the smallest, focused change
79
+
80
+ This is the key step. The test: this change ships in the **same pull request** as the
81
+ rest of your work. Would a reviewer see it as part of that change set — or stop and
82
+ ask, "what's this doing here?"
83
+
84
+ Carve out the scope you need *right now* and leave the rest of the file untouched. The
85
+ best change keeps the file from growing and sets up how this area should evolve, so it
86
+ doesn't cross the line again next time. You are **not** doing the big refactor the file
87
+ may eventually need — even if you can see it.
88
+
89
+ Sometimes the right change is none: the file is correct as-is and the smallest honest
90
+ move is to leave it. That's a valid outcome over 500 lines (it is not valid over 1000).
91
+
92
+ ### 4. Apply or propose
93
+
94
+ - **Obvious, minimal, in-scope carve-out** → make it now and report it, like any other
95
+ review fix. (Moving or extracting code changes the build, so it happens here in phase
96
+ 1, before behavior is frozen and re-verified.)
97
+ - **Non-trivial or ambiguous** — it touches code outside your change, restructures a
98
+ shared surface, or would mean reverting the change that triggered this review → state
99
+ your proposal and reasoning, and **wait** for the user. These are subtle calls; the
100
+ user has the final say and may prefer the larger file. Don't push back past a clear
101
+ "no."
102
+
103
+ ### 5. Report every triggered file
104
+
105
+ For each file: a change made, a change proposed, or an exception declared with its
106
+ category and reason. No file that tripped the rule passes silently.
107
+
108
+ ---
109
+
110
+ ## Keep it proportional
111
+
112
+ [`./code-review.md`](./code-review.md) warns against a review louder than the change it
113
+ covers. This is the one place a small change earns a structured look — because the cost
114
+ of an unmaintainable file accrues quietly until a human inherits it. The *response*,
115
+ though, stays minimal: the smallest carve-out that fits the PR. A sprawling whole-file
116
+ refactor is the trap, not the fix.
117
+
118
+ ---
119
+
120
+ ## Common Rationalizations
121
+
122
+ | Excuse | Reality |
123
+ |--------|---------|
124
+ | "I'll just refactor this file properly while I'm here." | Not your job now. Carve out your scope; leave the rest. The big refactor is a separate, deliberate effort. |
125
+ | "The file was already long before I touched it." | If your change grew it past the line, you review it now. The trigger is the size you're leaving behind. |
126
+ | "It's only a little over 500 — not worth the ceremony." | 500 triggers a review that can conclude "no change." But you must still report that conclusion, not skip silently. |
127
+ | "I'll flag it in the PR description and move on." | Silently shipping a newly-grown long file is the worst outcome here. Resolve it in the diff, not in prose a reviewer may skim. |
128
+ | "Splitting it would make the diff bigger and noisier." | A minimal, in-scope carve-out is the goal; a sprawling split is the trap you're avoiding, not the fix you owe. |
129
+ | "It's over 1000 but the file just has to be that big." | Only if it's a declared exception. Otherwise your change must not leave it there — carve your addition out, and surface the pre-existing bulk. |
130
+
131
+ ---
132
+
133
+ ## Red Flags — STOP
134
+
135
+ - About to hand back a file your change grew past 500 lines without a change or a
136
+ declared exception.
137
+ - Proposing a whole-file refactor instead of carving out your change's scope.
138
+ - A normal code file you touched is now over 1000 lines and you're treating that as
139
+ fine.
140
+ - Declaring a file exempt without naming the category and the reason.
141
+ - Dumping proposals for several long files on the user at once instead of one at a time.
@@ -24,8 +24,9 @@ git worktree list # >1 entry = worktrees already exist
24
24
  2. **Dirty tree (staged or unstaged changes) OR worktrees already exist**
25
25
  → a human or another agent is mid-work here. Use a **new worktree** so your
26
26
  changes can't collide with theirs.
27
- 3. **On `dev` / `main` / `master`** → sync with origin and **check out a new
28
- branch**. Keeps the base clean and makes the work easy to review.
27
+ 3. **On `dev` / `main` / `master`** → sync with origin and **create a new
28
+ branch** using the rule 3 command below. Keeps the base clean and makes the
29
+ work easy to review.
29
30
  4. **On any other branch** → **work in place.** The user already isolated this
30
31
  workspace; adding a worktree is needless ceremony.
31
32
 
@@ -47,6 +48,19 @@ git-ignored, add it to `.gitignore` and commit that first. If worktree creation
47
48
  fails (sandbox or permission limits), say so and fall back to checking out a
48
49
  branch in place (rule 3).
49
50
 
51
+ ## Creating a branch (rule 3)
52
+
53
+ After syncing the base branch with origin, create the new branch from the
54
+ current `HEAD` with no upstream tracking:
55
+
56
+ ```bash
57
+ git switch --no-track --create <branch-name>
58
+ ```
59
+
60
+ Do not create the branch from `origin/dev`, `origin/main`, or `origin/master`.
61
+ `--no-track` keeps the new branch without an upstream until the user pushes it
62
+ explicitly.
63
+
50
64
  ## After the workspace is set
51
65
 
52
66
  Install dependencies and run the existing test suite once, to confirm a clean