clud-bug 0.7.0-rc.2 → 0.7.0-rc.21

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 (147) hide show
  1. package/README.md +20 -0
  2. package/data/canonical-v1.json +37 -0
  3. package/dist/cli/configure-github.d.ts +58 -0
  4. package/dist/cli/configure-github.d.ts.map +1 -0
  5. package/dist/cli/configure-github.js +242 -0
  6. package/dist/cli/configure-github.js.map +1 -0
  7. package/dist/cli/hooks.d.ts +51 -0
  8. package/dist/cli/hooks.d.ts.map +1 -0
  9. package/dist/cli/hooks.js +138 -0
  10. package/dist/cli/hooks.js.map +1 -0
  11. package/dist/cli/index.d.ts +1 -0
  12. package/dist/cli/index.d.ts.map +1 -1
  13. package/dist/cli/index.js +1 -0
  14. package/dist/cli/index.js.map +1 -1
  15. package/dist/cli/main.d.ts.map +1 -1
  16. package/dist/cli/main.js +949 -35
  17. package/dist/cli/main.js.map +1 -1
  18. package/dist/cli/post-check-run.d.ts +16 -0
  19. package/dist/cli/post-check-run.d.ts.map +1 -0
  20. package/dist/cli/post-check-run.js +93 -0
  21. package/dist/cli/post-check-run.js.map +1 -0
  22. package/dist/cli/review-prompt.d.ts +43 -0
  23. package/dist/cli/review-prompt.d.ts.map +1 -0
  24. package/dist/cli/review-prompt.js +287 -0
  25. package/dist/cli/review-prompt.js.map +1 -0
  26. package/dist/cli/skills.d.ts +7 -0
  27. package/dist/cli/skills.d.ts.map +1 -1
  28. package/dist/cli/skills.js +42 -2
  29. package/dist/cli/skills.js.map +1 -1
  30. package/dist/cli/update-notifier.d.ts +17 -0
  31. package/dist/cli/update-notifier.d.ts.map +1 -0
  32. package/dist/cli/update-notifier.js +88 -0
  33. package/dist/cli/update-notifier.js.map +1 -0
  34. package/dist/cli/update.d.ts.map +1 -1
  35. package/dist/cli/update.js +60 -11
  36. package/dist/cli/update.js.map +1 -1
  37. package/dist/core/auto-resolve.d.ts +235 -0
  38. package/dist/core/auto-resolve.d.ts.map +1 -0
  39. package/dist/core/auto-resolve.js +293 -0
  40. package/dist/core/auto-resolve.js.map +1 -0
  41. package/dist/core/budget-plan.d.ts +125 -0
  42. package/dist/core/budget-plan.d.ts.map +1 -0
  43. package/dist/core/budget-plan.js +211 -0
  44. package/dist/core/budget-plan.js.map +1 -0
  45. package/dist/core/check-verdict.d.ts +31 -0
  46. package/dist/core/check-verdict.d.ts.map +1 -0
  47. package/dist/core/check-verdict.js +66 -0
  48. package/dist/core/check-verdict.js.map +1 -0
  49. package/dist/core/configure-github.d.ts +179 -0
  50. package/dist/core/configure-github.d.ts.map +1 -0
  51. package/dist/core/configure-github.js +289 -0
  52. package/dist/core/configure-github.js.map +1 -0
  53. package/dist/core/design.d.ts +37 -0
  54. package/dist/core/design.d.ts.map +1 -0
  55. package/dist/core/design.js +47 -0
  56. package/dist/core/design.js.map +1 -0
  57. package/dist/core/diff-findings.d.ts +96 -0
  58. package/dist/core/diff-findings.d.ts.map +1 -0
  59. package/dist/core/diff-findings.js +261 -0
  60. package/dist/core/diff-findings.js.map +1 -0
  61. package/dist/core/formal-review.d.ts +79 -0
  62. package/dist/core/formal-review.d.ts.map +1 -0
  63. package/dist/core/formal-review.js +71 -0
  64. package/dist/core/formal-review.js.map +1 -0
  65. package/dist/core/index.d.ts +15 -2
  66. package/dist/core/index.d.ts.map +1 -1
  67. package/dist/core/index.js +53 -1
  68. package/dist/core/index.js.map +1 -1
  69. package/dist/core/inline-threads.d.ts +160 -0
  70. package/dist/core/inline-threads.d.ts.map +1 -0
  71. package/dist/core/inline-threads.js +369 -0
  72. package/dist/core/inline-threads.js.map +1 -0
  73. package/dist/core/multi-pass-aggregate.d.ts +161 -0
  74. package/dist/core/multi-pass-aggregate.d.ts.map +1 -0
  75. package/dist/core/multi-pass-aggregate.js +457 -0
  76. package/dist/core/multi-pass-aggregate.js.map +1 -0
  77. package/dist/core/plan-review.d.ts +57 -0
  78. package/dist/core/plan-review.d.ts.map +1 -0
  79. package/dist/core/plan-review.js +77 -0
  80. package/dist/core/plan-review.js.map +1 -0
  81. package/dist/core/prompt-builder.d.ts +18 -0
  82. package/dist/core/prompt-builder.d.ts.map +1 -1
  83. package/dist/core/prompt-builder.js +24 -2
  84. package/dist/core/prompt-builder.js.map +1 -1
  85. package/dist/core/render.d.ts.map +1 -1
  86. package/dist/core/render.js +1 -19
  87. package/dist/core/render.js.map +1 -1
  88. package/dist/core/resolve-verifier.d.ts +53 -0
  89. package/dist/core/resolve-verifier.d.ts.map +1 -0
  90. package/dist/core/resolve-verifier.js +204 -0
  91. package/dist/core/resolve-verifier.js.map +1 -0
  92. package/dist/core/review-context.d.ts +39 -0
  93. package/dist/core/review-context.d.ts.map +1 -0
  94. package/dist/core/review-context.js +99 -0
  95. package/dist/core/review-context.js.map +1 -0
  96. package/dist/core/review-plan.d.ts +219 -0
  97. package/dist/core/review-plan.d.ts.map +1 -0
  98. package/dist/core/review-plan.js +274 -0
  99. package/dist/core/review-plan.js.map +1 -0
  100. package/dist/core/review-writeback.d.ts +74 -0
  101. package/dist/core/review-writeback.d.ts.map +1 -1
  102. package/dist/core/review-writeback.js +71 -4
  103. package/dist/core/review-writeback.js.map +1 -1
  104. package/dist/core/skills.d.ts +25 -0
  105. package/dist/core/skills.d.ts.map +1 -1
  106. package/dist/core/skills.js +100 -4
  107. package/dist/core/skills.js.map +1 -1
  108. package/dist/core/version.d.ts +2 -0
  109. package/dist/core/version.d.ts.map +1 -0
  110. package/dist/core/version.js +4 -0
  111. package/dist/core/version.js.map +1 -0
  112. package/package.json +11 -5
  113. package/src/cli/configure-github.ts +329 -0
  114. package/src/cli/hooks.ts +155 -0
  115. package/src/cli/index.ts +6 -0
  116. package/src/cli/main.ts +1031 -37
  117. package/src/cli/post-check-run.ts +115 -0
  118. package/src/cli/review-prompt.ts +355 -0
  119. package/src/cli/skills.ts +42 -2
  120. package/src/cli/update-notifier.ts +100 -0
  121. package/src/cli/update.ts +60 -11
  122. package/src/core/auto-resolve.ts +488 -0
  123. package/src/core/budget-plan.ts +324 -0
  124. package/src/core/check-verdict.ts +93 -0
  125. package/src/core/configure-github.ts +497 -0
  126. package/src/core/design.ts +73 -0
  127. package/src/core/diff-findings.ts +323 -0
  128. package/src/core/formal-review.ts +168 -0
  129. package/src/core/index.ts +188 -0
  130. package/src/core/inline-threads.ts +471 -0
  131. package/src/core/multi-pass-aggregate.ts +591 -0
  132. package/src/core/plan-review.ts +136 -0
  133. package/src/core/prompt-builder.ts +44 -2
  134. package/src/core/render.ts +10 -16
  135. package/src/core/resolve-verifier.ts +254 -0
  136. package/src/core/review-context.ts +106 -0
  137. package/src/core/review-plan.ts +485 -0
  138. package/src/core/review-writeback.ts +161 -4
  139. package/src/core/skills.ts +128 -5
  140. package/src/core/version.ts +3 -0
  141. package/templates/clud-bug-review.md.tmpl +115 -0
  142. package/templates/skills/design/design-system-consistency.md +41 -0
  143. package/templates/skills/design/frontend-a11y.md +41 -0
  144. package/templates/skills/design/visual-polish.md +41 -0
  145. package/templates/workflow-py.yml.tmpl +92 -2
  146. package/templates/workflow-ts.yml.tmpl +92 -2
  147. package/templates/workflow.yml.tmpl +173 -2
@@ -46,6 +46,34 @@ export const SEVERITY_EMOJI = {
46
46
  preexisting: '\u{1F7E3}', // U+1F7E3 PURPLE CIRCLE
47
47
  } as const;
48
48
 
49
+ /**
50
+ * One parsed finding suitable for the SPEC §1.8.1 Resolved / Still-open
51
+ * blocks. Mirrors `ParsedFinding` from `./diff-findings.ts`; declared
52
+ * here as a structural interface (not re-imported) so callers without
53
+ * `diff-findings` in scope can still construct the input.
54
+ */
55
+ export interface RenderedFindingRef {
56
+ file: string;
57
+ line: number;
58
+ severity: 'critical' | 'minor' | 'preexisting';
59
+ skillName: string;
60
+ summary: string;
61
+ }
62
+
63
+ /**
64
+ * SPEC §6.7.3 cache telemetry — per-invocation token counts from the
65
+ * Anthropic-family `usage` block. When present, the renderer emits a
66
+ * `<!-- cache: ... -->` HTML comment immediately below the
67
+ * `<!-- review-sha: ... -->` metadata line so downstream cost analysis
68
+ * tooling can audit cache behaviour from the committed doc-file.
69
+ */
70
+ export interface CacheStats {
71
+ /** `cache_read_input_tokens` from the Anthropic usage block. */
72
+ cachedInputTokens: number;
73
+ /** `cache_creation_input_tokens` from the Anthropic usage block. */
74
+ cacheCreationInputTokens: number;
75
+ }
76
+
49
77
  export interface RenderReviewFileInput {
50
78
  review: Review;
51
79
  prNumber: number;
@@ -53,6 +81,34 @@ export interface RenderReviewFileInput {
53
81
  headSha: string;
54
82
  /** GitHub PR URL — appended verbatim to the trailing rule. */
55
83
  prUrl: string;
84
+ /**
85
+ * Findings present in the prior `docs/reviews/PR-<n>.md` that are NOT
86
+ * present in this round's `review`. When non-empty, emits the SPEC
87
+ * §1.8.1 `**Resolved this round:**` block AFTER the severity buckets
88
+ * and BEFORE the trailing `---` separator. Omitted entirely when
89
+ * empty/undefined (SPEC §1.8.1: blocks MUST be omitted when empty).
90
+ *
91
+ * Produced by `diffFindings()` from `./diff-findings.ts`.
92
+ */
93
+ resolvedFindings?: RenderedFindingRef[];
94
+ /**
95
+ * Findings present in BOTH the prior `docs/reviews/PR-<n>.md` AND
96
+ * this round's `review` — the persistent ones. When non-empty, emits
97
+ * the SPEC §1.8.1 `**Still open:**` block AFTER `**Resolved this
98
+ * round:**` and before the trailing `---` separator. Omitted entirely
99
+ * when empty/undefined.
100
+ *
101
+ * Produced by `diffFindings()` from `./diff-findings.ts`.
102
+ */
103
+ stillOpenFindings?: RenderedFindingRef[];
104
+ /**
105
+ * SPEC §6.7.3 cache telemetry. When present, emits a
106
+ * `<!-- cache: <read> read · <created> created -->` HTML comment
107
+ * immediately below the `<!-- review-sha: ... -->` metadata line.
108
+ * Omitted entirely when undefined (this comment is OPTIONAL per the
109
+ * SPEC, but SHOULD be implemented for cost-analysis tooling).
110
+ */
111
+ cacheStats?: CacheStats;
56
112
  }
57
113
 
58
114
  /**
@@ -66,7 +122,15 @@ export interface RenderReviewFileInput {
66
122
  * the underlying finding data but differ in container.
67
123
  */
68
124
  export function renderReviewFile(input: RenderReviewFileInput): string {
69
- const { review, prNumber, headSha, prUrl } = input;
125
+ const {
126
+ review,
127
+ prNumber,
128
+ headSha,
129
+ prUrl,
130
+ resolvedFindings,
131
+ stillOpenFindings,
132
+ cacheStats,
133
+ } = input;
70
134
  // Wire-shape Review carries findings in 3 severity arrays. Flatten to
71
135
  // internal `Finding[]` so the renderer's bucketing, count-derivation,
72
136
  // and per-skill aggregation can work uniformly.
@@ -83,6 +147,15 @@ export function renderReviewFile(input: RenderReviewFileInput): string {
83
147
  lines.push(`<!-- protocol-version: ${PROTOCOL_VERSION} -->`);
84
148
  lines.push(`<!-- written-by: ${WRITTEN_BY} -->`);
85
149
  lines.push(`<!-- review-sha: ${headSha} -->`);
150
+ // SPEC §6.7.3: cache telemetry comment goes immediately below the
151
+ // review-sha so a single grep can pull review-sha + cache stats out
152
+ // of a doc-file without re-parsing the whole block. Omitted entirely
153
+ // when cacheStats is undefined.
154
+ if (cacheStats !== undefined) {
155
+ lines.push(
156
+ `<!-- cache: ${cacheStats.cachedInputTokens} read · ${cacheStats.cacheCreationInputTokens} created -->`,
157
+ );
158
+ }
86
159
  lines.push('');
87
160
 
88
161
  // Summary line — SPEC §1.8.1 wording.
@@ -124,9 +197,31 @@ export function renderReviewFile(input: RenderReviewFileInput): string {
124
197
  lines.push('');
125
198
  }
126
199
 
127
- // D.2.0 never emits "Resolved this round" / "Still open" both lists
128
- // are empty until D.2.5 (multi-pass tracking). SPEC §1.8.1 says these
129
- // blocks MUST be omitted when empty.
200
+ // SPEC §1.8.1 Resolved this round / Still open blocks. Omitted
201
+ // entirely when the corresponding input list is empty/undefined.
202
+ //
203
+ // Bullet shape (one finding per line):
204
+ // - `path/file.ts:42` — `critical-issues-only`: null-deref ... (was 🔴 Critical)
205
+ //
206
+ // The `(was <emoji> <Severity>)` suffix carries the SEVERITY at the
207
+ // time the finding was last seen so a downgrade across rounds is
208
+ // visible without diffing the two doc files. Note: backticks around
209
+ // file and skill render in markdown viewers as inline-code, which
210
+ // makes the file path scannable in the GitHub UI.
211
+ if (resolvedFindings !== undefined && resolvedFindings.length > 0) {
212
+ lines.push('**Resolved this round:**');
213
+ for (const f of resolvedFindings) {
214
+ lines.push(renderDiffFinding(f));
215
+ }
216
+ lines.push('');
217
+ }
218
+ if (stillOpenFindings !== undefined && stillOpenFindings.length > 0) {
219
+ lines.push('**Still open:**');
220
+ for (const f of stillOpenFindings) {
221
+ lines.push(renderDiffFinding(f));
222
+ }
223
+ lines.push('');
224
+ }
130
225
 
131
226
  lines.push('---');
132
227
  lines.push('');
@@ -148,6 +243,30 @@ function bucketBySeverity(findings: Finding[]): Record<
148
243
  };
149
244
  }
150
245
 
246
+ /**
247
+ * Renders one finding for the SPEC §1.8.1 Resolved / Still-open blocks.
248
+ * Output shape:
249
+ *
250
+ * - `path/file.ts:42` — `critical-issues-only`: null-deref ... (was 🔴 Critical)
251
+ *
252
+ * Line `0` is treated as "no anchor" (cross-cutting / missing) and the
253
+ * `:N` suffix is omitted. The severity label uses the SPEC §1.8.1
254
+ * emoji + label so a reader can see at a glance whether the finding
255
+ * was downgraded (different severity in current round) or merely
256
+ * resolved.
257
+ */
258
+ function renderDiffFinding(f: RenderedFindingRef): string {
259
+ const location = f.line > 0 ? `${f.file}:${f.line}` : f.file;
260
+ const label =
261
+ f.severity === 'critical'
262
+ ? 'Critical'
263
+ : f.severity === 'minor'
264
+ ? 'Minor'
265
+ : 'Preexisting';
266
+ const emoji = SEVERITY_EMOJI[f.severity];
267
+ return `- \`${location}\` — \`${f.skillName}\`: ${f.summary} (was ${emoji} ${label})`;
268
+ }
269
+
151
270
  function renderFinding(f: Finding, includeReasoning: boolean): string {
152
271
  // findingItemSchema.file is z.string().optional() — the model is
153
272
  // instructed to always provide it, but Zod doesn't enforce. Without
@@ -199,9 +318,30 @@ export interface PassAttribution {
199
318
  note?: string;
200
319
  }
201
320
 
321
+ /**
322
+ * SPEC §6.10.1 consensus marker, derived from a finding's per-pass attributions
323
+ * by the aggregator's `deriveConsensus`. Defined here — the canonical home of
324
+ * the unified-finding shape — so it rides on `MultiPassReview.findings`.
325
+ *
326
+ * - `1-of-N` — single pass produced this finding; no consensus reached.
327
+ * §6.10.2 MUST NOT fire auto-fix on these.
328
+ * - `2-of-2` — two passes independently produced an equivalent finding.
329
+ * Qualifies for auto-fix per §6.10.2.
330
+ * - `arbitrated` — passes disagreed; an arbiter resolved the dispute. Today's
331
+ * cross-check shape treats `arbitrated` as NOT-qualifying (conservative).
332
+ */
333
+ export type Consensus = '1-of-N' | '2-of-2' | 'arbitrated';
334
+
202
335
  export interface UnifiedFinding extends Finding {
203
336
  /** One PassAttribution per pass involved with this finding. Order: by passNumber. */
204
337
  attributions: PassAttribution[];
338
+ /**
339
+ * SPEC §6.10.1 consensus verdict. Stamped by the aggregator's `finalize()`
340
+ * from `deriveConsensus(attributions)`. Optional: single-pass / non-aggregated
341
+ * findings omit it; consumers treat absence as '1-of-N' (the auto-fix gate
342
+ * default — see clud-bug-app/lib/auto-fix-policy.ts).
343
+ */
344
+ consensus?: Consensus;
205
345
  }
206
346
 
207
347
  /** Effective resolution verdict the multi-pass orchestrator emits. */
@@ -381,6 +521,15 @@ function renderUnifiedFinding(
381
521
  }
382
522
  const headLabel = formatAttributionLabel(head);
383
523
  const lines: string[] = [];
524
+ // SPEC §6.8.1 (NORMATIVE for multi-pass): a per-finding pass-attribution HTML
525
+ // comment immediately preceding the bullet, naming the originating pass as a
526
+ // stable lowercase identifier so consumers can aggregate per-pass metrics.
527
+ lines.push(`<!-- pass: ${passIdentifier(head.roleName)} -->`);
528
+ // SPEC §6.10.1 (NORMATIVE for gated reviewers): the consensus marker alongside
529
+ // the pass attribution, when the finding carries a consensus verdict.
530
+ if (f.consensus) {
531
+ lines.push(`<!-- consensus: ${f.consensus} -->`);
532
+ }
384
533
  lines.push(`- ${headLabel} **${location}** — ${f.skill}: ${f.summary}`);
385
534
  if (includeReasoning && f.reasoning) {
386
535
  lines.push(` Reasoning: ${f.reasoning}`);
@@ -403,6 +552,14 @@ function renderUnifiedFinding(
403
552
  * `source: 'first'` — i.e. when a later pass surfaced this finding without
404
553
  * Pass 1 raising it.
405
554
  */
555
+ /** SPEC §6.8.1 pass identifier: a stable lowercase slug of the role name. */
556
+ function passIdentifier(roleName: string): string {
557
+ return roleName
558
+ .toLowerCase()
559
+ .replace(/[^a-z0-9]+/g, '-')
560
+ .replace(/^-+|-+$/g, '');
561
+ }
562
+
406
563
  function formatAttributionLabel(a: PassAttribution): string {
407
564
  const independent =
408
565
  a.source === 'independent' && a.passNumber > 1
@@ -285,6 +285,60 @@ export function appliesToPr(skillContent: unknown, prPaths: unknown): boolean {
285
285
  return false;
286
286
  }
287
287
 
288
+ // v0.7.0-rc.6 (SPEC §1.10.1 v0.5.1+) — does this skill apply to a PR
289
+ // opened by `prAuthor`? Reads `applies_to.author` from the raw SKILL.md
290
+ // frontmatter. Returns:
291
+ // - `true` if `applies_to.author` is absent (no author filter set —
292
+ // skill loads regardless of author; v0.5.0 backward-compat default).
293
+ // - `true` if `applies_to.author === prAuthor` (case-sensitive match
294
+ // against the PR author's GitHub login).
295
+ // - `false` if `applies_to.author` is set to a different login.
296
+ //
297
+ // Designed to mirror `appliesToPr`'s signature (raw skill content +
298
+ // runtime PR context) so consumers can call both filters in a single
299
+ // loader pass. Skills with NO `applies_to` block at all also return
300
+ // `true` (the existing v0.5.0 unconditional-load behavior).
301
+ //
302
+ // Strict-AND composition per SPEC §1.10.1: when a skill sets BOTH
303
+ // `applies_to.author` AND `paths`/`extensions`, callers MUST call BOTH
304
+ // filters and AND the results. This helper only checks the author leg;
305
+ // the caller composes.
306
+ export function appliesToAuthor(
307
+ skillContent: unknown,
308
+ prAuthor: unknown,
309
+ ): boolean {
310
+ if (typeof skillContent !== 'string') return true;
311
+ const fm = skillContent.match(/^---\n([\s\S]*?)\n---/);
312
+ if (!fm) return true;
313
+ const block = fm[1] as string;
314
+ // Look for `applies_to:` block; if absent, no author constraint.
315
+ const head = block.match(/^applies_to:\s*$/m);
316
+ if (!head) return true;
317
+ const startIdx = (head.index as number) + head[0].length;
318
+ const rest = block.slice(startIdx);
319
+ // The block ends at the next top-level key (line starting with a
320
+ // word char + `:`) OR end-of-block. Mirrors `readAppliesTo`.
321
+ const stop = rest.search(/^\w[\w-]*:/m);
322
+ const scoped = stop === -1 ? rest : rest.slice(0, stop);
323
+ // Match ` author: <value>` — single string scalar; strip optional
324
+ // quotes. `author: [a, b]` form is intentionally NOT supported
325
+ // (SPEC: "single GitHub login string, no list").
326
+ const authorMatch = scoped.match(/^\s{2}author:\s*(.+?)\s*$/m);
327
+ if (!authorMatch) return true;
328
+ const declared = (authorMatch[1] as string)
329
+ .trim()
330
+ .replace(/^["']|["']$/g, '');
331
+ if (!declared) return true; // empty value treated as unset
332
+ if (typeof prAuthor !== 'string' || !prAuthor) return false;
333
+ // GitHub logins are case-insensitive (GitHub rejects creating two
334
+ // accounts that differ only in case; `pull_request.user.login` is
335
+ // case-preserved but routing is not case-sensitive). Compare
336
+ // case-insensitively so a skill author writing `Thrillmot` still
337
+ // matches a webhook delivering `thrillmot` (or vice versa).
338
+ // Reviewer-flagged Important on PR #179.
339
+ return declared.toLowerCase() === prAuthor.toLowerCase();
340
+ }
341
+
288
342
  // Minimal glob → regex: `**` → `.*`, `*` → `[^/]*`, `?` → `.`,
289
343
  // everything else escaped. Anchored full-string match.
290
344
  function globMatch(glob: string, path: string): boolean {
@@ -601,14 +655,39 @@ export type SkillReviewMode = 'shared' | 'dedicated';
601
655
  * `PromptSkillFrontmatter` is a narrower subset (name + description +
602
656
  * applies_to) so we keep the full shape here.
603
657
  */
658
+ export type SkillKind = 'rule' | 'voice' | 'design';
659
+ export type VoiceScope = 'personal' | 'team' | 'org' | 'community';
660
+
604
661
  export interface SkillFrontmatter {
605
662
  name: string;
606
663
  description: string;
607
664
  source: SkillSource | string; // string fallback for forward-compat
608
665
  review_mode: SkillReviewMode;
666
+ /**
667
+ * SPEC §1.10.1 v0.5.0+ skill classification. OPTIONAL — absent =
668
+ * `kind: rule` (the v0.4.0 default). Surfaced on the parsed
669
+ * frontmatter (v0.7.0-rc.6+) so consumers can route on the field;
670
+ * v0.7.0-rc.5 and earlier silently dropped it.
671
+ */
672
+ kind?: SkillKind;
673
+ /**
674
+ * SPEC §1.10.1 v0.5.0+ voice scope. REQUIRED when `kind: voice`,
675
+ * absent otherwise. Surfaced on the parsed frontmatter (v0.7.0-rc.6+).
676
+ */
677
+ voice_scope?: VoiceScope;
609
678
  applies_to?: {
610
679
  paths?: string[];
611
680
  extensions?: string[];
681
+ /**
682
+ * SPEC §1.10.1 v0.5.1+ — single GitHub login string (no `@`
683
+ * prefix, no list). When present, the skill MUST only load when
684
+ * `pull_request.user.login === author`. Filters via strict AND
685
+ * with the sibling `paths` + `extensions` filters when all are
686
+ * present. v0.7.0-rc.5 and earlier silently dropped this field
687
+ * (treated skills as if it were absent — load unconditionally);
688
+ * v0.7.0-rc.6+ enforces the filter via `appliesToAuthor`.
689
+ */
690
+ author?: string;
612
691
  };
613
692
  }
614
693
 
@@ -691,8 +770,32 @@ export function parseFrontmatter(raw: string): SkillFrontmatter {
691
770
  const reviewMode: SkillReviewMode =
692
771
  out['review_mode'] === 'dedicated' ? 'dedicated' : 'shared';
693
772
 
773
+ // v0.7.0-rc.6 — surface SPEC §1.10.1 v0.5.0+ kind + voice_scope on
774
+ // the parsed frontmatter. v0.7.0-rc.5 silently dropped these fields
775
+ // (the Wave 4d reviewer-flagged silent-drop). Validation is lenient:
776
+ // unknown values are surfaced as undefined so downstream code sees a
777
+ // clean type rather than a malformed value. SPEC enforcement (e.g.,
778
+ // "voice_scope REQUIRED when kind: voice") is the caller's job.
779
+ const kindRaw = out['kind'];
780
+ const kind: SkillKind | undefined =
781
+ kindRaw === 'voice'
782
+ ? 'voice'
783
+ : kindRaw === 'design'
784
+ ? 'design'
785
+ : kindRaw === 'rule'
786
+ ? 'rule'
787
+ : undefined;
788
+ const voiceScopeRaw = out['voice_scope'];
789
+ const voiceScope: VoiceScope | undefined =
790
+ voiceScopeRaw === 'personal' ||
791
+ voiceScopeRaw === 'team' ||
792
+ voiceScopeRaw === 'org' ||
793
+ voiceScopeRaw === 'community'
794
+ ? voiceScopeRaw
795
+ : undefined;
796
+
694
797
  const appliesToRaw = out['applies_to'] as
695
- | { paths?: unknown; extensions?: unknown }
798
+ | { paths?: unknown; extensions?: unknown; author?: unknown }
696
799
  | undefined;
697
800
  let appliesTo: SkillFrontmatter['applies_to'] | undefined;
698
801
  if (appliesToRaw) {
@@ -702,10 +805,28 @@ export function parseFrontmatter(raw: string): SkillFrontmatter {
702
805
  const extensions = Array.isArray(appliesToRaw.extensions)
703
806
  ? appliesToRaw.extensions.map(String)
704
807
  : undefined;
705
- appliesTo = {
706
- ...(paths !== undefined ? { paths } : {}),
707
- ...(extensions !== undefined ? { extensions } : {}),
708
- };
808
+ // v0.7.0-rc.6 — SPEC §1.10.1 v0.5.1+ author filter. Single string
809
+ // only; reject lists + falsy values to keep the filter contract
810
+ // unambiguous (a future SPEC could broaden to lists if customer
811
+ // demand argues for it).
812
+ const authorRaw = appliesToRaw.author;
813
+ const author =
814
+ typeof authorRaw === 'string' && authorRaw.trim().length > 0
815
+ ? authorRaw.trim()
816
+ : undefined;
817
+ // Reviewer-flagged Important on PR #179: an empty `applies_to:`
818
+ // block (no sub-keys) was emitting `applies_to: {}` on the
819
+ // returned frontmatter, which leaked into the prompt builder as
820
+ // JSON noise + semantically diverged from readAppliesTo's null
821
+ // return for the same input. Only construct appliesTo when at
822
+ // least one sub-field is present.
823
+ if (paths !== undefined || extensions !== undefined || author !== undefined) {
824
+ appliesTo = {
825
+ ...(paths !== undefined ? { paths } : {}),
826
+ ...(extensions !== undefined ? { extensions } : {}),
827
+ ...(author !== undefined ? { author } : {}),
828
+ };
829
+ }
709
830
  }
710
831
 
711
832
  return {
@@ -713,6 +834,8 @@ export function parseFrontmatter(raw: string): SkillFrontmatter {
713
834
  description,
714
835
  source,
715
836
  review_mode: reviewMode,
837
+ ...(kind !== undefined ? { kind } : {}),
838
+ ...(voiceScope !== undefined ? { voice_scope: voiceScope } : {}),
716
839
  ...(appliesTo !== undefined ? { applies_to: appliesTo } : {}),
717
840
  };
718
841
  }
@@ -0,0 +1,3 @@
1
+ // Generated by scripts/gen-version.mjs at build time. Do not edit.
2
+ // Regenerated automatically on every `npm run build` via "prebuild" hook.
3
+ export const PKG_VERSION = '0.7.0-rc.21';
@@ -0,0 +1,115 @@
1
+ ---
2
+ description: Review the current branch's open PR locally, in this Claude Code session
3
+ argument-hint: "[pr-number]"
4
+ ---
5
+
6
+ <!-- clud-bug-local-version: v1 -->
7
+
8
+ You are running **clud-bug** as a local code review — inside this Claude Code
9
+ session, using this session's own model tokens (Max or API, whatever you have).
10
+ No hosted App, no extra auth: you already have `gh`, `git`, and file access.
11
+
12
+ ## 1. Find the PR
13
+
14
+ PR number: `$ARGUMENTS`
15
+
16
+ If `$ARGUMENTS` is empty, detect the current branch's open PR:
17
+
18
+ ```bash
19
+ gh pr list --head "$(git branch --show-current)" --state open --json number --jq '.[0].number'
20
+ ```
21
+
22
+ If there is no open PR, stop and tell the user to open one (or pass a PR number).
23
+
24
+ ## 2. Load the review skills
25
+
26
+ Read the manifest and every referenced skill body from the checkout:
27
+
28
+ - `.claude/skills/.clud-bug.json` — which skills are installed, plus `strictMode`.
29
+ - For each installed skill, `.claude/skills/<name>/SKILL.md` — the discipline to apply.
30
+
31
+ Load skills **before** the diff. At minimum the baseline set applies:
32
+ **critical-issues-only** (flag only correctness / security / performance — skip
33
+ nits), **evidence-based-review** (quote the exact line you criticize — no
34
+ hand-waving), **respect-existing-conventions** (don't fight the codebase's
35
+ established patterns).
36
+
37
+ ## 3. Fetch the diff
38
+
39
+ ```bash
40
+ gh pr diff <PR_NUMBER>
41
+ ```
42
+
43
+ ## 4. Review
44
+
45
+ Review the diff against every loaded skill. For each finding record: `file`,
46
+ `line`, `severity` (`critical` | `minor` | `preexisting`), the `skill` that
47
+ motivated it, and a one-line `summary`. Apply the skills strictly — especially
48
+ critical-issues-only (no nits) and evidence-based-review (quote the code).
49
+
50
+ ## 5. Render the review comment
51
+
52
+ Build the body in clud-bug's standard shape (omit any empty findings section):
53
+
54
+ ```
55
+ ## 🐛 Clud Bug review — <clean | critical findings>
56
+
57
+ **This round:** N critical · N minor · N resolved from prior · N still open
58
+
59
+ Found: N 🔴 / N 🟡 / N 🟣
60
+
61
+ ### Per-skill scan
62
+ - [<skill>]: <one line on what it scanned + found>
63
+
64
+ ### Critical findings
65
+ 🔴 [<skill>]: <summary> (<file>:<line>).
66
+
67
+ ### Minor findings
68
+ 🟡 [<skill>]: <summary> (<file>:<line>).
69
+
70
+ ### Pre-existing findings
71
+ 🟣 [<skill>]: <summary> (<file>:<line>).
72
+
73
+ Skills referenced: [<skill>, <skill>]
74
+
75
+ <!-- written-by: @<your-gh-login> (clud-bug local-mode) -->
76
+ <!-- review-sha: <HEAD_SHA> -->
77
+ ```
78
+
79
+ Get your gh login with `gh api user --jq .login` and the head sha with
80
+ `git rev-parse HEAD`. The `written-by` marker **MUST** end with
81
+ `(clud-bug local-mode)` so the bot's auto-resolve never mistakes this for a
82
+ `clud-bug[bot]` comment.
83
+
84
+ ## 6. Post (or update) the comment
85
+
86
+ Look for an existing local-mode comment and edit it in place (one rolling review
87
+ comment), otherwise post a fresh one. Use the **REST** issues-comments endpoint
88
+ for the lookup — its `.id` is the **integer** comment id the PATCH endpoint needs.
89
+ (Do NOT use `gh pr view --json comments`; that returns GraphQL node IDs, which
90
+ 404 on the REST PATCH and cause duplicate comments.) `gh` fills `{owner}/{repo}`.
91
+
92
+ ```bash
93
+ # existing local-mode comment id (integer), if any:
94
+ EXISTING_ID=$(gh api --paginate "repos/{owner}/{repo}/issues/<PR_NUMBER>/comments" \
95
+ --jq '[.[] | select(.body | test("written-by: @.* \\(clud-bug local-mode\\)"))] | first | .id // empty')
96
+ ```
97
+
98
+ - `$EXISTING_ID` empty → `gh pr comment <PR_NUMBER> --body-file <body.md>`
99
+ - `$EXISTING_ID` set → `gh api "repos/{owner}/{repo}/issues/comments/$EXISTING_ID" -X PATCH -F body=@<body.md>`
100
+
101
+ ## 7. Summarize
102
+
103
+ Print one line:
104
+ `clud-bug local-review: N critical · N minor · N preexisting — <PASS | FAIL (strictMode) | advisory>`
105
+
106
+ If `.clud-bug.json` has `strictMode: true` and there are critical findings, say
107
+ **FAIL** and tell the user to fix them before merging.
108
+
109
+ ---
110
+
111
+ _Optional: if the `clud-bug-mcp` MCP server is connected, you may use its
112
+ structured tools (`load_skills_from_pr_base`, `fetch_pr_diff`,
113
+ `partition_findings_by_severity`, `emit_review_comment`) in place of the raw
114
+ `gh`/`git` calls above — same flow, with the base-ref skill read and the exact
115
+ review rendering done for you._
@@ -0,0 +1,41 @@
1
+ ---
2
+ name: design-system-consistency
3
+ description: Visual review — flag rendered UI that drifts from the design system's tokens, scale, and color discipline. Judges the screenshot, not just the code.
4
+ kind: design
5
+ review_mode: dedicated
6
+ applies_to:
7
+ paths: ["site/**", "app/**", "**/components/**", "**/ui/**"]
8
+ extensions: [".tsx", ".jsx", ".css", ".scss", ".vue", ".svelte"]
9
+ ---
10
+
11
+ # Design-system consistency
12
+
13
+ You are reviewing the **rendered** change (screenshots, light and dark) against the
14
+ project's design system. Cite the specific element you see in the screenshot — name it
15
+ and where it is — not just the source line.
16
+
17
+ ## Flag
18
+
19
+ 1. **Off-token values.** A hardcoded color, spacing, radius, shadow, or font-size where a
20
+ design token / CSS variable already exists for that role. Quote the value and name the
21
+ token it should use.
22
+ 2. **Color discipline.** Fills should be cool/neutral; strokes and text are the warm/ink
23
+ layer; a saturated red (or the system's "danger" hue) is reserved for destructive or
24
+ critical states. Flag a red used for a non-critical accent, or a one-off accent hue that
25
+ isn't in the palette. Prefer perceptually-uniform color (OKLCH/LCH) over ad-hoc hex when
26
+ the system uses it.
27
+ 3. **Off-scale spacing.** Margins/padding/gaps that don't land on the spacing scale (e.g. a
28
+ `13px` gap in a 4-/8-pt system). Rhythm should be consistent between sibling elements.
29
+ 4. **Type ramp drift.** A font-size/weight/line-height combination outside the defined type
30
+ scale, or the same semantic level rendered at two different sizes across the view.
31
+ 5. **Re-implementation.** A button/card/input hand-rolled inline when a system component
32
+ exists — visible as subtly different padding, radius, or hover from its siblings.
33
+
34
+ ## How to judge
35
+
36
+ - Compare the new surface against an existing, known-good surface in the same product.
37
+ - "It renders" is not the bar — **token-correct** is the bar. Flag a value that looks fine
38
+ but bypasses the system; say which token restores consistency.
39
+ - One concrete drift per finding, with the rendered element quoted and the fix named.
40
+
41
+ If the rendered change is token-clean and on-scale in both themes, say so in one line.
@@ -0,0 +1,41 @@
1
+ ---
2
+ name: frontend-a11y
3
+ description: Visual review — accessibility on the rendered surface: contrast, focus visibility, tap targets, semantics, motion. Cite the element and the failing ratio or state.
4
+ kind: design
5
+ review_mode: dedicated
6
+ applies_to:
7
+ paths: ["site/**", "app/**", "**/components/**", "**/ui/**"]
8
+ extensions: [".tsx", ".jsx", ".css", ".scss", ".vue", ".svelte"]
9
+ ---
10
+
11
+ # Frontend accessibility
12
+
13
+ You are reviewing the **rendered** change (screenshots, light and dark) plus the markup for
14
+ accessibility. Cite the element and the concrete failure (the measured ratio, the missing
15
+ state, the tag).
16
+
17
+ ## Flag
18
+
19
+ 1. **Contrast.** Body text below WCAG AA 4.5:1, or large text / UI borders below 3:1 — in
20
+ **either** theme (check both screenshots; dark mode is where contrast quietly fails).
21
+ Name the foreground/background and the approximate ratio.
22
+ 2. **Focus visibility.** Any interactive element with no visible focus indicator, or a focus
23
+ ring removed (`outline: none`) without a replacement. Keyboard users must see focus.
24
+ 3. **Tap / click targets.** Controls smaller than ~24x24 CSS px (44px on touch surfaces),
25
+ or targets packed too tightly to hit reliably.
26
+ 4. **Semantics.** Icon-only buttons without an accessible name (`aria-label`/visually-hidden
27
+ text); headings that skip levels; a `<div>` with a click handler where a `<button>`/`<a>`
28
+ belongs; images conveying meaning without alt text.
29
+ 5. **Color-only signaling.** State or meaning carried by color alone (e.g. red/green status
30
+ with no icon or label).
31
+ 6. **Motion.** Auto-playing or large motion that doesn't honor `prefers-reduced-motion`.
32
+
33
+ ## How to judge
34
+
35
+ - Prefer findings you can see or measure in the screenshot/markup; estimate the contrast
36
+ ratio rather than hand-waving "low contrast."
37
+ - A genuine WCAG-AA failure on interactive/text content is high severity; a borderline ratio
38
+ or a nice-to-have is medium/low.
39
+ - One issue per finding: element, the failure, the fix.
40
+
41
+ If the surface is clean on these checks in both themes, say so in one line.
@@ -0,0 +1,41 @@
1
+ ---
2
+ name: visual-polish
3
+ description: Visual review — hold the rendered UI to an elite bar: alignment, optical centering, spacing rhythm, glyph/pattern quality, state coverage, theme parity. Flag "fine but not elite," not only broken.
4
+ kind: design
5
+ review_mode: dedicated
6
+ applies_to:
7
+ paths: ["site/**", "app/**", "**/components/**", "**/ui/**"]
8
+ extensions: [".tsx", ".jsx", ".css", ".scss", ".vue", ".svelte"]
9
+ ---
10
+
11
+ # Visual polish
12
+
13
+ You are judging the **rendered** change (screenshots, light and dark) against a
14
+ Figma-grade bar. Most of what you flag will be "acceptable but not elite" — that is the
15
+ point. Cite the element and where it sits in the screenshot.
16
+
17
+ ## Flag
18
+
19
+ 1. **Alignment & optical centering.** Elements off a shared grid/baseline; an icon
20
+ geometrically centered in a button but optically off (glyphs with side-bearing need a
21
+ nudge); label and control not on the same baseline; ragged edges between siblings.
22
+ 2. **Spacing rhythm.** Inconsistent gaps in a list/stack; cramped or floaty padding; a
23
+ section that breaks the vertical rhythm the rest of the page establishes.
24
+ 3. **Glyph & pattern quality.** Blurry/missized icons, mismatched stroke widths, a pattern
25
+ or texture that tiles visibly or clips, emoji where a real glyph is warranted.
26
+ 4. **State coverage.** Interactive elements missing a distinct hover / focus-visible /
27
+ active / disabled state, or states that are present but indistinguishable.
28
+ 5. **Theme parity.** Compare the light and dark screenshots: a color that loses contrast,
29
+ a shadow that vanishes, a border that disappears, or an asset baked for one theme only.
30
+ 6. **Clutter & duplication.** Redundant chrome, two affordances doing the same job,
31
+ competing focal points, or content that overflows / truncates awkwardly at the captured
32
+ viewport.
33
+
34
+ ## How to judge
35
+
36
+ - Ask "would this ship in a top-tier product?" If it's *fine but not elite*, flag it with
37
+ the concrete upgrade (the nudge, the token, the missing state).
38
+ - Severity: layout breakage or an unreadable state is high; polish gaps are medium/low.
39
+ - One issue per finding; name the element, say what's off, say the fix.
40
+
41
+ If the surface is genuinely polished in both themes, say so — don't manufacture nits.