clud-bug 0.6.20 → 0.6.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.
- package/lib/prompts.js +11 -1
- package/lib/skills.js +107 -0
- package/package.json +1 -1
- package/templates/workflow-py.yml.tmpl +1 -1
- package/templates/workflow-ts.yml.tmpl +1 -1
- package/templates/workflow.yml.tmpl +1 -1
package/lib/prompts.js
CHANGED
|
@@ -148,7 +148,17 @@ Each SKILL.md frontmatter (first \`---\`-delimited block) has a
|
|
|
148
148
|
API-contract, test-discipline). Each gets its own H3 section.
|
|
149
149
|
- Missing → treat as \`shared\`.
|
|
150
150
|
|
|
151
|
-
|
|
151
|
+
Skill applies_to (v0.6.21 / 0.0.K):
|
|
152
|
+
Frontmatter may also have \`applies_to:\` with \`paths:\` (glob list)
|
|
153
|
+
and/or \`extensions:\` (extension list). Scan each skill's frontmatter
|
|
154
|
+
first (cheap — just the \`---\` block). If applies_to is present and
|
|
155
|
+
the PR's changed files (from \`gh pr diff --name-only\`) match NONE
|
|
156
|
+
of the declared paths or extensions, SKIP that skill's body — don't
|
|
157
|
+
read it, don't reference it. Skills without applies_to load
|
|
158
|
+
unconditionally (back-compat). Net effect: a UI-scoped skill stays
|
|
159
|
+
unread on a backend-only PR.
|
|
160
|
+
|
|
161
|
+
Read each applicable body capped: \`head -c "$MAX_SKILL_BYTES" .claude/skills/<name>/SKILL.md\`
|
|
152
162
|
|
|
153
163
|
At review end, append a single-line footer:
|
|
154
164
|
Skills referenced: [skill-name-1, skill-name-2, ...]
|
package/lib/skills.js
CHANGED
|
@@ -383,6 +383,113 @@ export function readReviewMode(skillContent) {
|
|
|
383
383
|
return value === 'dedicated' ? 'dedicated' : 'shared';
|
|
384
384
|
}
|
|
385
385
|
|
|
386
|
+
// 0.0.K (v0.6.21): parse the optional `applies_to:` frontmatter block.
|
|
387
|
+
//
|
|
388
|
+
// Schema:
|
|
389
|
+
// applies_to:
|
|
390
|
+
// paths:
|
|
391
|
+
// - "src/ui/**"
|
|
392
|
+
// - "lib/components/**"
|
|
393
|
+
// extensions: [".tsx", ".jsx"]
|
|
394
|
+
//
|
|
395
|
+
// Returns `{paths: string[], extensions: string[]}` if the field is
|
|
396
|
+
// present (either sub-list optional, both default to empty array), or
|
|
397
|
+
// `null` if absent. Skills without applies_to are scope-universal —
|
|
398
|
+
// the caller should treat null as "load unconditionally."
|
|
399
|
+
//
|
|
400
|
+
// Hand-rolled YAML parser scoped to this exact shape. The frontmatter
|
|
401
|
+
// is otherwise opaque (review_mode is parsed elsewhere with a similar
|
|
402
|
+
// single-key regex), so pulling in a YAML dep would be overkill.
|
|
403
|
+
export function readAppliesTo(skillContent) {
|
|
404
|
+
if (typeof skillContent !== 'string') return null;
|
|
405
|
+
const fm = skillContent.match(/^---\n([\s\S]*?)\n---/);
|
|
406
|
+
if (!fm) return null;
|
|
407
|
+
const block = fm[1];
|
|
408
|
+
// Anchor on `applies_to:` at start of line (the body of a SKILL.md
|
|
409
|
+
// could mention the term in prose; only the frontmatter key fires).
|
|
410
|
+
const head = block.match(/^applies_to:\s*$/m);
|
|
411
|
+
if (!head) return null;
|
|
412
|
+
// Slice from after the `applies_to:` line; the block ends at the
|
|
413
|
+
// next top-level key (a line starting with a word character + `:`)
|
|
414
|
+
// OR end-of-block.
|
|
415
|
+
const startIdx = head.index + head[0].length;
|
|
416
|
+
const rest = block.slice(startIdx);
|
|
417
|
+
const stop = rest.search(/^\w[\w-]*:/m);
|
|
418
|
+
const scoped = stop === -1 ? rest : rest.slice(0, stop);
|
|
419
|
+
const paths = parseYamlList(scoped, 'paths');
|
|
420
|
+
const extensions = parseYamlList(scoped, 'extensions');
|
|
421
|
+
if (paths.length === 0 && extensions.length === 0) return null;
|
|
422
|
+
return { paths, extensions };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Parse a YAML list under `<key>:`, handling both the inline-array
|
|
426
|
+
// form (`extensions: [".tsx", ".jsx"]`) and the block form
|
|
427
|
+
// (`paths:` followed by ` - "src/ui/**"` lines).
|
|
428
|
+
function parseYamlList(block, key) {
|
|
429
|
+
const inline = block.match(new RegExp(`^\\s{2}${key}:\\s*\\[(.*?)\\]\\s*$`, 'm'));
|
|
430
|
+
if (inline) {
|
|
431
|
+
return inline[1]
|
|
432
|
+
.split(',')
|
|
433
|
+
.map((s) => s.trim().replace(/^["']|["']$/g, ''))
|
|
434
|
+
.filter(Boolean);
|
|
435
|
+
}
|
|
436
|
+
const headerRe = new RegExp(`^\\s{2}${key}:\\s*$`, 'm');
|
|
437
|
+
const head = block.match(headerRe);
|
|
438
|
+
if (!head) return [];
|
|
439
|
+
const after = block.slice(head.index + head[0].length);
|
|
440
|
+
const items = [];
|
|
441
|
+
for (const line of after.split('\n')) {
|
|
442
|
+
const item = line.match(/^\s{4,}-\s*(.+?)\s*$/);
|
|
443
|
+
if (item) {
|
|
444
|
+
items.push(item[1].replace(/^["']|["']$/g, ''));
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
// Anything that isn't a list item (or blank) ends the list.
|
|
448
|
+
if (line.trim() !== '' && !item) break;
|
|
449
|
+
}
|
|
450
|
+
return items;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// 0.0.K: does `prPaths` contain at least one file matching the skill's
|
|
454
|
+
// applies_to? Skills without applies_to ALWAYS apply (back-compat).
|
|
455
|
+
//
|
|
456
|
+
// `prPaths` is the list of changed files in the PR (e.g. from
|
|
457
|
+
// `gh pr diff --name-only`). Match semantics:
|
|
458
|
+
// - paths: any glob in `paths` matches any of `prPaths`
|
|
459
|
+
// - extensions: any extension in `extensions` matches any of `prPaths`
|
|
460
|
+
// - paths OR extensions (NOT AND) — a single hit is enough
|
|
461
|
+
//
|
|
462
|
+
// Skill `paths` use the minimal glob set logmind already uses
|
|
463
|
+
// (`*` matches non-slash, `**` matches across slashes, `?` single
|
|
464
|
+
// char). Anything fancier would need a real glob lib.
|
|
465
|
+
export function appliesToPr(skillContent, prPaths) {
|
|
466
|
+
const rule = readAppliesTo(skillContent);
|
|
467
|
+
if (rule === null) return true; // back-compat: no rule → applies
|
|
468
|
+
if (!Array.isArray(prPaths)) return true; // be permissive on bad input
|
|
469
|
+
for (const path of prPaths) {
|
|
470
|
+
if (typeof path !== 'string') continue;
|
|
471
|
+
for (const ext of rule.extensions) {
|
|
472
|
+
if (path.endsWith(ext)) return true;
|
|
473
|
+
}
|
|
474
|
+
for (const glob of rule.paths) {
|
|
475
|
+
if (globMatch(glob, path)) return true;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Minimal glob → regex: `**` → `.*`, `*` → `[^/]*`, `?` → `.`,
|
|
482
|
+
// everything else escaped. Anchored full-string match.
|
|
483
|
+
function globMatch(glob, path) {
|
|
484
|
+
const escaped = glob
|
|
485
|
+
.replace(/([.+^${}()|[\]\\])/g, '\\$1')
|
|
486
|
+
.replace(/\*\*/g, '__DOUBLESTAR__')
|
|
487
|
+
.replace(/\*/g, '[^/]*')
|
|
488
|
+
.replace(/__DOUBLESTAR__/g, '.*')
|
|
489
|
+
.replace(/\?/g, '.');
|
|
490
|
+
return new RegExp(`^${escaped}$`).test(path);
|
|
491
|
+
}
|
|
492
|
+
|
|
386
493
|
// Partition a set of loaded skills into {shared, dedicated} buckets per
|
|
387
494
|
// each skill's review_mode frontmatter. Expects skills with a `content`
|
|
388
495
|
// field (SKILL.md text). Skills without content default to `shared`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clud-bug",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.21",
|
|
4
4
|
"description": "Skill-driven Claude PR review. Ship a brand-voice skill, get brand reviews. Each finding cites the skill that motivated it. CLI installs the workflow + a baseline kit; add more from skills.sh.",
|
|
5
5
|
"homepage": "https://cludbug.dev",
|
|
6
6
|
"bugs": "https://github.com/thrillmade/clud-bug/issues",
|
|
@@ -156,6 +156,6 @@ jobs:
|
|
|
156
156
|
# Strict-mode gate — composite action; see workflow.yml.tmpl for design notes.
|
|
157
157
|
- name: Strict mode — fail check on critical findings
|
|
158
158
|
if: success()
|
|
159
|
-
uses: thrillmade/clud-bug/.github/actions/strict-mode-gate@v0.6.
|
|
159
|
+
uses: thrillmade/clud-bug/.github/actions/strict-mode-gate@v0.6.21
|
|
160
160
|
with:
|
|
161
161
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -156,6 +156,6 @@ jobs:
|
|
|
156
156
|
# Strict-mode gate — composite action; see workflow.yml.tmpl for design notes.
|
|
157
157
|
- name: Strict mode — fail check on critical findings
|
|
158
158
|
if: success()
|
|
159
|
-
uses: thrillmade/clud-bug/.github/actions/strict-mode-gate@v0.6.
|
|
159
|
+
uses: thrillmade/clud-bug/.github/actions/strict-mode-gate@v0.6.21
|
|
160
160
|
with:
|
|
161
161
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -247,6 +247,6 @@ jobs:
|
|
|
247
247
|
# Letting the action's own failure fail the check is louder and right.
|
|
248
248
|
- name: Strict mode — fail check on critical findings
|
|
249
249
|
if: success()
|
|
250
|
-
uses: thrillmade/clud-bug/.github/actions/strict-mode-gate@v0.6.
|
|
250
|
+
uses: thrillmade/clud-bug/.github/actions/strict-mode-gate@v0.6.21
|
|
251
251
|
with:
|
|
252
252
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|