clud-bug 0.5.6 → 0.5.7

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/README.md CHANGED
@@ -164,20 +164,64 @@ If you want clud-bug to review fork PRs too, you have two options:
164
164
  1. **Maintainer re-pushes the branch** to your repo as a non-fork branch, and the review runs.
165
165
  2. **Switch the trigger to `pull_request_target`** (advanced) — this gives the workflow access to secrets but runs against the *base* ref, not the PR's code. To safely review the PR's actual code, follow [`anthropics/claude-code-action` security.md](https://github.com/anthropics/claude-code-action/blob/main/docs/security.md): check out the PR head into a **subdirectory** (not the workspace root) and pass it via `--add-dir`. Skipping this is a code-execution risk.
166
166
 
167
- clud-bug's generated workflow uses `pull_request` by default. If you understand the trade-offs, edit the trigger yourself.
167
+ Concretely, the safe shape:
168
+
169
+ ```yaml
170
+ on:
171
+ pull_request_target:
172
+ types: [opened, synchronize]
173
+
174
+ jobs:
175
+ clud-bug-review:
176
+ steps:
177
+ - uses: actions/checkout@v6 # base ref — trusted
178
+ - uses: actions/checkout@v6 # PR head — UNTRUSTED, into a subdir
179
+ with:
180
+ ref: ${{ github.event.pull_request.head.sha }}
181
+ path: pr-head
182
+ - uses: anthropics/claude-code-action@v1
183
+ with:
184
+ claude_args: --add-dir pr-head
185
+ # ... rest of args
186
+ ```
187
+
188
+ The key invariant: the base checkout (with secrets in scope) lives at the workspace root; the PR head (untrusted user code) only ever lives in a subdirectory the action explicitly opts into via `--add-dir`. Any deviation — checking out the PR head at the root, running `npm install` from the subdir, etc. — re-opens the code-execution risk.
189
+
190
+ clud-bug's generated workflow uses `pull_request` (not `pull_request_target`) by default. If you understand the trade-offs and want to handle fork PRs, edit the trigger yourself using the shape above.
168
191
 
169
192
  ## When you edit the workflow
170
193
 
171
- clud-bug uses [`anthropics/claude-code-action`](https://github.com/anthropics/claude-code-action), which **refuses to run when the PR being reviewed modifies the action's own workflow file**. That's a security guard against PRs that try to neuter the reviewer or exfiltrate secrets via prompt injection. If you edit `.github/workflows/clud-bug-review.yml` (or any clud-bug workflow), expect this check to fail with a 401 `App token exchange failed: Workflow validation failed`. That's the documented behavior, not a bug. Merge the workflow change in isolation, and subsequent PRs work normally.
194
+ > **TL;DR:** if you see `App token exchange failed: Workflow validation failed (401)` on a PR that edits a clud-bug workflow file, that's **expected and protective** — not a bug in your PR. Read on.
195
+
196
+ clud-bug uses [`anthropics/claude-code-action`](https://github.com/anthropics/claude-code-action), which **refuses to run when the PR being reviewed modifies the action's own workflow file**. That's a security guard: without it, a PR could neuter the reviewer or exfiltrate secrets via prompt injection in the workflow file itself.
197
+
198
+ ### What you'll see
199
+
200
+ When you push a PR that touches `.github/workflows/clud-bug-review.yml` (or any other clud-bug workflow):
201
+
202
+ - The `clud-bug-review` check fails with `App token exchange failed: 401 Unauthorized — Workflow validation failed. The workflow file must exist and have identical content to the version on the repository's default branch.`
203
+ - You'll get a GitHub email titled something like **"[thrillmot/your-repo] Run failed: Clud Bug 🐛 Crawls Your Code — `<branch-name>`"** — same wording for every workflow failure, so it doesn't visually distinguish "this is the expected self-mod guard" from "real failure."
204
+
205
+ ### How to merge
206
+
207
+ If the PR contains **only** workflow edits, this is the expected path:
208
+
209
+ 1. A maintainer reviews the diff directly (the bot can't).
210
+ 2. Merge via admin override (`gh pr merge --admin` or the "Merge without waiting for requirements" button) — the failing `clud-bug-review` check is the bot refusing to review *itself*, not a real defect.
211
+ 3. Subsequent PRs on the new workflow work normally — the validation gate compares against `main`, so once your edit is on `main`, the gate passes.
212
+
213
+ If the PR contains workflow edits **mixed with other code changes**, split them. The bot can't review either half while the workflow edit is in the diff, so any real findings get masked.
214
+
215
+ ### The helper command
172
216
 
173
- To make this easier, `clud-bug edit-workflow` packages the workflow change into a clean PR for you:
217
+ `clud-bug edit-workflow` packages the workflow change into a clean PR for you, refusing to run if your working tree has any non-workflow changes:
174
218
 
175
219
  ```bash
176
220
  # Edit .github/workflows/clud-bug-*.yml as you like, then:
177
221
  clud-bug edit-workflow
178
222
  ```
179
223
 
180
- The command refuses to run if your working tree has any non-workflow changes — keeping the PR scoped to just the workflow edit.
224
+ This keeps the merge ceremony scoped to just the workflow edit.
181
225
 
182
226
  ## Verifying it works
183
227
 
package/bin/clud-bug.js CHANGED
@@ -608,16 +608,28 @@ async function runUpdateCmd(_args) {
608
608
  return;
609
609
  }
610
610
 
611
- if (result.changed.length === 0) {
611
+ const skipped = result.skipped ?? [];
612
+
613
+ if (result.changed.length === 0 && skipped.length === 0) {
612
614
  log(' Already current. Nothing to update.');
613
615
  return;
614
616
  }
615
617
 
616
- log(` ✓ Updated ${result.changed.length} file${result.changed.length === 1 ? '' : 's'}:`);
617
- for (const c of result.changed) log(` ${rel(cwd, c.path)} (${c.label})`);
618
+ if (result.changed.length > 0) {
619
+ log(` Updated ${result.changed.length} file${result.changed.length === 1 ? '' : 's'}:`);
620
+ for (const c of result.changed) {
621
+ const versionNote = c.from && c.to && c.from !== c.to ? ` (${c.label}, ${c.from} → ${c.to})` : ` (${c.label})`;
622
+ log(` • ${rel(cwd, c.path)}${versionNote}`);
623
+ }
624
+ }
618
625
  if (result.unchanged.length > 0) {
619
626
  log(` ${result.unchanged.length} file${result.unchanged.length === 1 ? ' was' : 's were'} already current.`);
620
627
  }
628
+ if (skipped.length > 0) {
629
+ log('');
630
+ log(` ! Skipped ${skipped.length} markerless file${skipped.length === 1 ? '' : 's'} (treated as user-customized):`);
631
+ for (const s of skipped) log(` • ${rel(cwd, s.path)} — ${s.reason}`);
632
+ }
621
633
  log('');
622
634
  log('Commit + push to apply the refreshed kit on the next PR.');
623
635
  }
package/lib/update.js CHANGED
@@ -8,14 +8,18 @@ import { applyToRepo as applyAgentDocs } from './agents-md.js';
8
8
  // Re-render the user's workflow + refresh baseline skills using the
9
9
  // templates / baseline shipped with the currently-installed clud-bug.
10
10
  //
11
- // Honors three protections:
11
+ // Honors four protections:
12
12
  // - Custom skills (anything in .claude/skills/ not in the manifest) are
13
13
  // never modified.
14
14
  // - Remote skills (from skills.sh, kind: 'remote' in manifest) are left
15
15
  // alone unless { refreshRemote: true }.
16
- // - The audit workflow is also re-rendered if it's installed.
16
+ // - The audit + self-update workflows are also refreshed if installed.
17
+ // - Markerless workflow files (no `# clud-bug-template-version:` header)
18
+ // are treated as user-customized and left alone — the user gets a
19
+ // printed warning + the documented "delete + clud-bug init" recovery
20
+ // path. Mirrors logmind v0.2.1's refresh-mode pattern.
17
21
  //
18
- // Returns a diff summary with file paths and a short reason per file.
22
+ // Returns { changed, unchanged, skipped, ourVersion }.
19
23
  export async function runUpdate({
20
24
  cwd,
21
25
  templatesDir,
@@ -35,6 +39,7 @@ export async function runUpdate({
35
39
 
36
40
  const changed = [];
37
41
  const unchanged = [];
42
+ const skipped = [];
38
43
 
39
44
  // 1. Re-render review workflow with the latest template.
40
45
  const signals = await detect(cwd);
@@ -43,13 +48,20 @@ export async function runUpdate({
43
48
  PROJECT_DESCRIPTION: buildDescriptionLine(signals),
44
49
  LANGUAGE_HINTS: '',
45
50
  });
46
- await maybeWrite(join(cwd, '.github/workflows/clud-bug-review.yml'), newReview, changed, unchanged, 'review workflow');
51
+ await maybeRefreshVersioned(join(cwd, '.github/workflows/clud-bug-review.yml'), newReview, changed, unchanged, skipped, 'review workflow');
47
52
 
48
53
  // 2. Re-render audit workflow if it's installed (init from v0.3+ ships it).
49
54
  const auditPath = join(cwd, '.github/workflows/clud-bug-audit.yml');
50
55
  if (await pathExists(auditPath)) {
51
56
  const newAudit = await readFile(join(templatesDir, 'audit.yml.tmpl'), 'utf8');
52
- await maybeWrite(auditPath, newAudit, changed, unchanged, 'audit workflow');
57
+ await maybeRefreshVersioned(auditPath, newAudit, changed, unchanged, skipped, 'audit workflow');
58
+ }
59
+
60
+ // 2b. Re-render self-update workflow if installed (init from v0.4+ ships it).
61
+ const selfUpdatePath = join(cwd, '.github/workflows/clud-bug-self-update.yml');
62
+ if (await pathExists(selfUpdatePath)) {
63
+ const newSelfUpdate = await readFile(join(templatesDir, 'self-update.yml.tmpl'), 'utf8');
64
+ await maybeRefreshVersioned(selfUpdatePath, newSelfUpdate, changed, unchanged, skipped, 'self-update workflow');
53
65
  }
54
66
 
55
67
  // 3. Refresh baseline skills (always controlled by clud-bug).
@@ -88,7 +100,7 @@ export async function runUpdate({
88
100
  manifest.lastUpdateVersion = ourVersion;
89
101
  await writeManifest(skillsDir, manifest);
90
102
 
91
- return { changed, unchanged, ourVersion };
103
+ return { changed, unchanged, skipped, ourVersion };
92
104
  }
93
105
 
94
106
  async function maybeWrite(path, contents, changed, unchanged, label) {
@@ -102,6 +114,61 @@ async function maybeWrite(path, contents, changed, unchanged, label) {
102
114
  changed.push({ path, label });
103
115
  }
104
116
 
117
+ // Refresh a versioned template (one that carries `# clud-bug-template-version:`
118
+ // on line 1). If the installed file lacks that marker, treat it as
119
+ // user-customized and leave it alone — recovery path is delete + `clud-bug init`.
120
+ // Mirrors logmind v0.2.1's refresh-mode contract.
121
+ async function maybeRefreshVersioned(path, contents, changed, unchanged, skipped, label) {
122
+ const tmplVersion = extractTemplateVersion(contents);
123
+ if (!tmplVersion) {
124
+ // Defensive: every versioned template is supposed to carry a marker.
125
+ // Falling back to byte-compare write here would silently mass-overwrite
126
+ // every installed file (including marker-bearing ones) the moment a
127
+ // future template regressed — the inverse of the protection contract
128
+ // this function exists to enforce. Throw so the regression surfaces
129
+ // in CI instead.
130
+ throw new Error(`Template for ${label} has no # clud-bug-template-version marker — refusing to refresh (templates must declare a marker so refresh-mode can reason about ownership).`);
131
+ }
132
+ const prior = await readSafe(path);
133
+ if (prior === null) {
134
+ // First time writing here; nothing to preserve.
135
+ await mkdir(dirname(path), { recursive: true });
136
+ await writeFile(path, contents);
137
+ changed.push({ path, label });
138
+ return;
139
+ }
140
+ const priorVersion = extractTemplateVersion(prior);
141
+ if (priorVersion === null) {
142
+ // Markerless installed file = customized. Preserve and warn.
143
+ skipped.push({
144
+ path,
145
+ label,
146
+ reason: 'markerless (user-customized); delete the file + run `clud-bug init` to refresh',
147
+ });
148
+ return;
149
+ }
150
+ if (prior === contents) {
151
+ unchanged.push({ path, label });
152
+ return;
153
+ }
154
+ // Marker present (current or stale) AND content drifted: refresh.
155
+ await mkdir(dirname(path), { recursive: true });
156
+ await writeFile(path, contents);
157
+ changed.push({ path, label, from: priorVersion, to: tmplVersion });
158
+ }
159
+
160
+ // Extract the template-version marker. Templates put it on line 1, but
161
+ // scan the first 5 lines so a leading blank or stray header doesn't hide it.
162
+ // Anchoring near the top means a stray `# clud-bug-template-version:` lower
163
+ // in the file (in a comment inside a heredoc, say) can't be mistaken for the
164
+ // authoritative marker. Returns null if not present.
165
+ function extractTemplateVersion(text) {
166
+ if (!text) return null;
167
+ const head = text.split('\n', 5).join('\n');
168
+ const m = head.match(/^# clud-bug-template-version:\s*(\S+)/m);
169
+ return m ? m[1] : null;
170
+ }
171
+
105
172
  async function readSafe(path) {
106
173
  try { return await readFile(path, 'utf8'); } catch { return null; }
107
174
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clud-bug",
3
- "version": "0.5.6",
3
+ "version": "0.5.7",
4
4
  "description": "Claude PR review with project-aware skills. CLI installs a working GitHub Actions workflow and curates skills from skills.sh.",
5
5
  "homepage": "https://cludbug.dev",
6
6
  "bugs": "https://github.com/thrillmot/clud-bug/issues",
@@ -1,3 +1,4 @@
1
+ # clud-bug-template-version: v1
1
2
  name: Clud Bug 🐛 Audit
2
3
 
3
4
  # A scheduled / on-demand walk through the whole habitat (or a recent slice).
@@ -1,3 +1,4 @@
1
+ # clud-bug-template-version: v1
1
2
  name: Clud Bug 🐛 Self-Update
2
3
 
3
4
  # Weekly check for a newer published clud-bug. If one exists, runs
@@ -71,6 +71,55 @@ When you write a custom skill, follow the SKILL.md frontmatter format
71
71
  Generic advice gets ignored; rules with examples and quoted-line evidence
72
72
  move the bot's behavior.
73
73
 
74
+ ### Example: a good custom skill
75
+
76
+ Don't write generic prose like "be careful with database code." That's
77
+ not actionable. Instead, anchor to specific files + behaviors:
78
+
79
+ ````markdown
80
+ ---
81
+ name: db-query-review
82
+ description: How to review changes under lib/db/. Always flag missing parameterization, N+1 query patterns, and missing transaction boundaries on multi-statement writes.
83
+ ---
84
+
85
+ # Reviewing lib/db/ changes
86
+
87
+ When the diff touches `lib/db/queries.ts` or any file under `lib/db/`,
88
+ apply these rules:
89
+
90
+ ## Always flag
91
+
92
+ 1. **SQL string interpolation** — anything that builds a query via `+`,
93
+ template literals, or `string.format` rather than parameterized
94
+ queries. Example to flag:
95
+
96
+ ```ts
97
+ // BAD — flag this
98
+ db.query(`SELECT * FROM users WHERE id = ${userId}`)
99
+ // GOOD — uses parameterization
100
+ db.query('SELECT * FROM users WHERE id = ?', [userId])
101
+ ```
102
+
103
+ 2. **N+1 patterns** — flag any `await` inside a `for`/`map` loop that
104
+ calls `db.query` or `db.queryOne`. The fix is usually a single
105
+ `WHERE id IN (...)` query or a join.
106
+
107
+ 3. **Multi-statement writes without `db.transaction`** — if a diff
108
+ adds two or more `db.query` calls that all write, demand a
109
+ transaction wrapper. Quote the lines.
110
+
111
+ ## Style of finding
112
+
113
+ Cite the specific line. "This is a SQL injection risk" alone isn't
114
+ enough — quote the unsafe interpolation directly and propose the
115
+ parameterized alternative.
116
+ ````
117
+
118
+ That's what "evidence-anchored" looks like: specific file paths, runnable
119
+ code examples for both bad and good, and explicit instructions on what
120
+ to quote in the finding. The bot loads this and uses it as concrete
121
+ review criteria, not vague guidance.
122
+
74
123
  ## When you edit `.github/workflows/clud-bug-*.yml`
75
124
 
76
125
  `anthropics/claude-code-action` **refuses to run on PRs that modify its
@@ -1,3 +1,4 @@
1
+ # clud-bug-template-version: v1
1
2
  name: Clud Bug 🐛 Crawls Your Code
2
3
 
3
4
  on:
@@ -1,3 +1,4 @@
1
+ # clud-bug-template-version: v1
1
2
  name: Clud Bug 🐛 Crawls Your Code
2
3
 
3
4
  on:
@@ -1,3 +1,4 @@
1
+ # clud-bug-template-version: v1
1
2
  name: Clud Bug 🐛 Crawls Your Code
2
3
 
3
4
  on: