clud-bug 0.4.1 → 0.5.1
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 +3 -1
- package/bin/clud-bug.js +34 -2
- package/lib/agents-md.js +183 -0
- package/lib/skills.js +103 -2
- package/lib/update.js +19 -2
- package/package.json +1 -1
- package/templates/skills/baseline/clud-bug-collaboration.md +124 -0
package/README.md
CHANGED
|
@@ -28,12 +28,14 @@ The naturalist arrives at your repo, surveys the habitat, and assembles a field
|
|
|
28
28
|
|
|
29
29
|
1. **Surveys habitat.** Reads `package.json`, `pyproject.toml`, `go.mod`, `Cargo.toml`, etc., to learn what your stack is.
|
|
30
30
|
2. **Consults [skills.sh](https://skills.sh).** Pulls review skills relevant to your dependencies (e.g. a Next.js project gets Next.js review specimens).
|
|
31
|
-
3. **Pins
|
|
31
|
+
3. **Pins baseline specimens** that enforce review discipline regardless of stack:
|
|
32
32
|
- `critical-issues-only` — flag bugs, security, perf only. Skip nits.
|
|
33
33
|
- `evidence-based-review` — every claim must quote the line being criticized.
|
|
34
34
|
- `respect-existing-conventions` — don't suggest fights with the codebase's patterns.
|
|
35
|
+
- `clud-bug-collaboration` — guidance for any other Claude Code agents working in your repo: how to coexist with bot review threads, how to read the gate, why workflow self-mods break the action, etc.
|
|
35
36
|
4. **Writes** the chosen specimens to `.claude/skills/<name>/SKILL.md` (Claude Code auto-loads them in the GitHub Action).
|
|
36
37
|
5. **Drafts the field kit** at `.github/workflows/clud-bug-review.yml` with your project description filled in and the right permissions/tool allowlist for `gh pr comment` to actually post.
|
|
38
|
+
6. **Briefs other agents** by adding a `<!-- clud-bug-start -->` block to `AGENTS.md` (creating it if missing — it's the cross-tool canonical), and idempotently to `CLAUDE.md`, `GEMINI.md`, `.github/copilot-instructions.md`, `.cursorrules`, `.windsurfrules`, `.clinerules`, `.continuerules`, and `.cursor/rules/*.md` where they already exist. Re-runs replace the prior block in place. Files you didn't already have are left uncreated — no proliferating stubs.
|
|
37
39
|
|
|
38
40
|
## CLI options
|
|
39
41
|
|
package/bin/clud-bug.js
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import { computeAuditFileSet, renderAuditHeader } from '../lib/audit.js';
|
|
16
16
|
import { runUpdate } from '../lib/update.js';
|
|
17
17
|
import { getPendingWorkflowEdits, makeBranchName, git as gitCmd } from '../lib/edit-workflow.js';
|
|
18
|
+
import { applyToRepo as applyAgentDocs } from '../lib/agents-md.js';
|
|
18
19
|
|
|
19
20
|
const PKG_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
20
21
|
const TEMPLATES = join(PKG_ROOT, 'templates');
|
|
@@ -107,7 +108,13 @@ async function runInit(args) {
|
|
|
107
108
|
log(` search terms: ${signals.searchTerms.join(', ') || '(none)'}`);
|
|
108
109
|
|
|
109
110
|
const baseline = await loadBaseline(BASELINE_DIR);
|
|
110
|
-
|
|
111
|
+
const fromAgentSkills = baseline.filter((s) => s._source === 'agent-skills').length;
|
|
112
|
+
const sourceLabel = baseline.length === 0
|
|
113
|
+
? ''
|
|
114
|
+
: fromAgentSkills === baseline.length ? ' (from thrillmot/agent-skills)'
|
|
115
|
+
: fromAgentSkills === 0 ? ' (bundled fallback)'
|
|
116
|
+
: ` (${fromAgentSkills} from agent-skills, ${baseline.length - fromAgentSkills} bundled)`;
|
|
117
|
+
log(` baseline kit: ${baseline.length} specimens${sourceLabel}`);
|
|
111
118
|
|
|
112
119
|
let curated = [];
|
|
113
120
|
let searched = [];
|
|
@@ -198,9 +205,34 @@ async function runInit(args) {
|
|
|
198
205
|
}
|
|
199
206
|
await writeManifest(skillsDirPath, manifest);
|
|
200
207
|
|
|
208
|
+
// Tell other agents what's installed and how to coexist with the bot.
|
|
209
|
+
// Idempotent — re-runs replace the prior block in place. AGENTS.md is the
|
|
210
|
+
// canonical home (cross-tool); CLAUDE.md / GEMINI.md / Cursor / Windsurf
|
|
211
|
+
// / Cline / Continue rules files get the same block appended IF they
|
|
212
|
+
// already exist (we don't proliferate stubs the user didn't ask for).
|
|
213
|
+
log(' briefing other agents (AGENTS.md / CLAUDE.md)...');
|
|
214
|
+
// Pass `=== true` (not `!== false`) so the rendered block matches the
|
|
215
|
+
// workflow's gate predicate exactly. A v0.3 advisory upgrade where
|
|
216
|
+
// strictMode is undefined renders "off" — which is what the workflow
|
|
217
|
+
// actually does on that manifest.
|
|
218
|
+
const agentDocs = await applyAgentDocs(cwd, {
|
|
219
|
+
version: manifest.lastUpdateVersion,
|
|
220
|
+
strictMode: manifest.strictMode === true,
|
|
221
|
+
});
|
|
222
|
+
for (const p of agentDocs.created) log(` created ${p}`);
|
|
223
|
+
for (const p of agentDocs.touched) log(` updated ${p}`);
|
|
224
|
+
|
|
201
225
|
if (args.commit) {
|
|
202
226
|
log(' committing...');
|
|
203
|
-
|
|
227
|
+
const toAdd = [
|
|
228
|
+
'.claude',
|
|
229
|
+
'.github/workflows/clud-bug-review.yml',
|
|
230
|
+
'.github/workflows/clud-bug-audit.yml',
|
|
231
|
+
'.github/workflows/clud-bug-self-update.yml',
|
|
232
|
+
...agentDocs.created,
|
|
233
|
+
...agentDocs.touched,
|
|
234
|
+
];
|
|
235
|
+
spawnSync('git', ['add', ...toAdd], { cwd, stdio: 'inherit' });
|
|
204
236
|
spawnSync('git', ['commit', '-m', 'Add clud-bug 🐛 — a field guide to specimens crawling your code'], { cwd, stdio: 'inherit' });
|
|
205
237
|
}
|
|
206
238
|
|
package/lib/agents-md.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { readFile, writeFile, stat, readdir } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
// Manages a clud-bug-owned section inside AGENTS.md / CLAUDE.md and adjacent
|
|
5
|
+
// agent-instruction files. Mirrors the well-established `<!-- logmind-start -->`
|
|
6
|
+
// pattern: a marked block that other agents can read for collaboration rules,
|
|
7
|
+
// idempotently rewritten on each `clud-bug init` so the content stays current.
|
|
8
|
+
|
|
9
|
+
const START_MARKER = '<!-- clud-bug-start -->';
|
|
10
|
+
const END_MARKER = '<!-- clud-bug-end -->';
|
|
11
|
+
const BLOCK_VERSION = 'v1';
|
|
12
|
+
|
|
13
|
+
// Files we'll touch when present, plus files we'll create if missing.
|
|
14
|
+
// AGENTS.md is the cross-tool canonical (logmind made it canonical too).
|
|
15
|
+
// CLAUDE.md, GEMINI.md, .cursorrules, .windsurfrules etc. are tool-specific
|
|
16
|
+
// stubs/instructions; we append the same block where they exist but don't
|
|
17
|
+
// create them (logmind already creates the ones it knows about).
|
|
18
|
+
const ALWAYS_TOUCH = ['AGENTS.md']; // create if missing
|
|
19
|
+
const TOUCH_IF_PRESENT = [
|
|
20
|
+
'CLAUDE.md',
|
|
21
|
+
'GEMINI.md',
|
|
22
|
+
'.github/copilot-instructions.md',
|
|
23
|
+
'.cursorrules',
|
|
24
|
+
'.windsurfrules',
|
|
25
|
+
'.clinerules',
|
|
26
|
+
'.continuerules',
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
// Render the clud-bug block. Bundled here rather than in a template file so
|
|
30
|
+
// updates ship with the CLI itself.
|
|
31
|
+
//
|
|
32
|
+
// `strictMode` MUST match the workflow's gate predicate exactly so the block
|
|
33
|
+
// can't lie about repo state. The workflow at `templates/workflow*.yml.tmpl`
|
|
34
|
+
// reads the manifest with `JSON.parse(s).strictMode === true` — meaning the
|
|
35
|
+
// gate fires ONLY on an explicit `true`, and anything else (missing field,
|
|
36
|
+
// `false`, `null`) is advisory. Mirror that here: render "on" only when the
|
|
37
|
+
// caller explicitly passes `true`. Anything else is "off".
|
|
38
|
+
//
|
|
39
|
+
// Why this matters: a v0.3-era install (no `strictMode` field, `lastUpdate`
|
|
40
|
+
// set) is a documented advisory upgrade path — `clud-bug init` deliberately
|
|
41
|
+
// preserves that state. If the block rendered "on" for that case, other
|
|
42
|
+
// agents reading AGENTS.md would get a wrong model of the gate.
|
|
43
|
+
export function renderBlock({ version, strictMode } = {}) {
|
|
44
|
+
const versionLine = version ? `_Installed at clud-bug v${version}._` : '';
|
|
45
|
+
const strictNote = strictMode === true
|
|
46
|
+
? 'Strict mode is **on** in this repo (workflow check fails on critical findings).'
|
|
47
|
+
: 'Strict mode is **off** in this repo (advisory only).';
|
|
48
|
+
return `${START_MARKER}
|
|
49
|
+
<!-- clud-bug-block-version: ${BLOCK_VERSION} -->
|
|
50
|
+
## clud-bug — Claude PR review
|
|
51
|
+
|
|
52
|
+
This repository uses [clud-bug](https://cludbug.dev) to review pull requests
|
|
53
|
+
automatically. Three things matter when other agents (or future-you) work
|
|
54
|
+
in this repo:
|
|
55
|
+
|
|
56
|
+
### When you push fixes addressing prior Clud Bug review threads
|
|
57
|
+
|
|
58
|
+
The bot resolves its own prior review threads on the next pass when it can
|
|
59
|
+
verify the fix in the diff. You don't need to manually resolve threads it
|
|
60
|
+
opened — push the fix, wait ~2 minutes, and check the PR. If a thread it
|
|
61
|
+
left isn't auto-resolved after a fix, the bot judged the issue still open;
|
|
62
|
+
read its latest review comment for what it's still flagging.
|
|
63
|
+
|
|
64
|
+
### Strict mode
|
|
65
|
+
|
|
66
|
+
${strictNote} Toggle by editing \`.claude/skills/.clud-bug.json\`:
|
|
67
|
+
|
|
68
|
+
\`\`\`json
|
|
69
|
+
{ "strictMode": true | false, ... }
|
|
70
|
+
\`\`\`
|
|
71
|
+
|
|
72
|
+
The setting is read from the **base ref** of any open PR, so PRs cannot
|
|
73
|
+
disable strict mode on themselves. Changes take effect on PRs opened after
|
|
74
|
+
they merge to the base branch.
|
|
75
|
+
|
|
76
|
+
### Where the skills live
|
|
77
|
+
|
|
78
|
+
Project-aware review rules live in \`.claude/skills/<name>/SKILL.md\`. A
|
|
79
|
+
small baseline kit ships with every install — see
|
|
80
|
+
\`.claude/skills/.clud-bug.json\` for the current set. Add more via
|
|
81
|
+
\`clud-bug add <source/name>\` (from skills.sh) or by dropping your own
|
|
82
|
+
\`.md\` files there. They auto-load into the reviewer.
|
|
83
|
+
|
|
84
|
+
### Editing the workflow
|
|
85
|
+
|
|
86
|
+
Anthropic's \`claude-code-action\` refuses to run on PRs that modify its own
|
|
87
|
+
workflow file. Use \`clud-bug edit-workflow\` to bundle workflow tweaks into
|
|
88
|
+
their own isolated PR — see [README](https://github.com/thrillmot/clud-bug#when-you-edit-the-workflow).
|
|
89
|
+
|
|
90
|
+
${versionLine}
|
|
91
|
+
${END_MARKER}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Replace an existing clud-bug block in `content`, OR append if absent.
|
|
95
|
+
// Idempotent: running multiple times leaves a single block.
|
|
96
|
+
export function upsertBlock(content, block) {
|
|
97
|
+
const startRe = new RegExp(escapeRe(START_MARKER));
|
|
98
|
+
const endRe = new RegExp(escapeRe(END_MARKER));
|
|
99
|
+
if (startRe.test(content) && endRe.test(content)) {
|
|
100
|
+
// Replace from START_MARKER through END_MARKER (greedy multi-line).
|
|
101
|
+
const re = new RegExp(`${escapeRe(START_MARKER)}[\\s\\S]*?${escapeRe(END_MARKER)}`);
|
|
102
|
+
return content.replace(re, block);
|
|
103
|
+
}
|
|
104
|
+
// Append with a separating blank line, no trailing newline duplication.
|
|
105
|
+
const sep = content.endsWith('\n') ? '\n' : '\n\n';
|
|
106
|
+
return `${content}${sep}${block}\n`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function escapeRe(s) {
|
|
110
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Touches all relevant agent-instruction files in `cwd`.
|
|
114
|
+
// Creates AGENTS.md if it doesn't exist (it's the canonical home).
|
|
115
|
+
// Updates other files only if they already exist (don't proliferate stubs;
|
|
116
|
+
// logmind or the user owns those creation decisions).
|
|
117
|
+
//
|
|
118
|
+
// Returns { touched: string[], created: string[] } for the caller to log.
|
|
119
|
+
export async function applyToRepo(cwd, blockOpts = {}) {
|
|
120
|
+
const block = renderBlock(blockOpts);
|
|
121
|
+
const touched = [];
|
|
122
|
+
const created = [];
|
|
123
|
+
|
|
124
|
+
for (const path of ALWAYS_TOUCH) {
|
|
125
|
+
const full = join(cwd, path);
|
|
126
|
+
const existed = await fileExists(full);
|
|
127
|
+
const prior = existed ? await readFile(full, 'utf8') : seedFile(path);
|
|
128
|
+
const next = upsertBlock(prior, block);
|
|
129
|
+
if (next !== prior) {
|
|
130
|
+
await writeFile(full, next);
|
|
131
|
+
(existed ? touched : created).push(path);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (const path of TOUCH_IF_PRESENT) {
|
|
136
|
+
const full = join(cwd, path);
|
|
137
|
+
if (!(await fileExists(full))) continue;
|
|
138
|
+
const prior = await readFile(full, 'utf8');
|
|
139
|
+
const next = upsertBlock(prior, block);
|
|
140
|
+
if (next !== prior) {
|
|
141
|
+
await writeFile(full, next);
|
|
142
|
+
touched.push(path);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// .cursor/rules/*.md — append to every file that exists.
|
|
147
|
+
const cursorRulesDir = join(cwd, '.cursor', 'rules');
|
|
148
|
+
if (await fileExists(cursorRulesDir)) {
|
|
149
|
+
let entries = [];
|
|
150
|
+
try { entries = await readdir(cursorRulesDir); } catch {}
|
|
151
|
+
for (const name of entries) {
|
|
152
|
+
if (!name.endsWith('.md')) continue;
|
|
153
|
+
const full = join(cursorRulesDir, name);
|
|
154
|
+
const prior = await readFile(full, 'utf8');
|
|
155
|
+
const next = upsertBlock(prior, block);
|
|
156
|
+
if (next !== prior) {
|
|
157
|
+
await writeFile(full, next);
|
|
158
|
+
touched.push(`.cursor/rules/${name}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return { touched, created };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function seedFile(name) {
|
|
167
|
+
// When AGENTS.md doesn't exist (no logmind, no prior tooling), seed with a
|
|
168
|
+
// minimal canonical header so the clud-bug block has context.
|
|
169
|
+
if (name === 'AGENTS.md') {
|
|
170
|
+
return `# AGENTS.md
|
|
171
|
+
|
|
172
|
+
This file is the canonical instruction file for AI coding agents working in
|
|
173
|
+
this repository. Tools that understand AGENTS.md (Cursor, Codex, Windsurf,
|
|
174
|
+
Claude Code, Cline, Continue, Aider, ...) read it directly.
|
|
175
|
+
|
|
176
|
+
`;
|
|
177
|
+
}
|
|
178
|
+
return '';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function fileExists(path) {
|
|
182
|
+
try { await stat(path); return true; } catch { return false; }
|
|
183
|
+
}
|
package/lib/skills.js
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
import { mkdir, writeFile, readdir, readFile, rm, stat } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
3
5
|
|
|
4
6
|
const API_BASE = 'https://skills.sh/api/v1';
|
|
5
7
|
const MAX_SKILLS = 8;
|
|
6
8
|
const MANIFEST_FILE = '.clud-bug.json';
|
|
7
9
|
const MANIFEST_VERSION = 1;
|
|
8
10
|
|
|
11
|
+
// Canonical home for clud-bug's baseline skills.
|
|
12
|
+
// PINNED TO A COMMIT SHA, NOT `main`. This re-couples the trust boundary
|
|
13
|
+
// to clud-bug releases: a compromised commit on agent-skills@main cannot
|
|
14
|
+
// silently land in users' Claude review skills mid-cycle. To roll new
|
|
15
|
+
// skill content, bump BASELINE_SKILLS_REF below in the same clud-bug PR
|
|
16
|
+
// that ships the corresponding bundled fallback update.
|
|
17
|
+
// See thrillmot/agent-skills — skills.sh `skills/<name>/SKILL.md` layout.
|
|
18
|
+
const BASELINE_SKILLS_REF = '977e439ec861860351239ed89dd56edcd48cbf6b';
|
|
19
|
+
const AGENT_SKILLS_BASE = process.env.CLUD_BUG_AGENT_SKILLS_BASE
|
|
20
|
+
?? `https://raw.githubusercontent.com/thrillmot/agent-skills/${BASELINE_SKILLS_REF}/skills`;
|
|
21
|
+
const SKILL_FETCH_TIMEOUT_MS = 5000;
|
|
22
|
+
const SKILL_CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24h
|
|
23
|
+
|
|
9
24
|
export class SkillsClient {
|
|
10
25
|
constructor({ fetch = globalThis.fetch, base, userAgent = 'clud-bug' } = {}) {
|
|
11
26
|
this.fetch = fetch;
|
|
@@ -78,7 +93,34 @@ export function rankAndCap(curated, searched, baseline, cap = MAX_SKILLS) {
|
|
|
78
93
|
return out;
|
|
79
94
|
}
|
|
80
95
|
|
|
81
|
-
|
|
96
|
+
// Loads the baseline skills, preferring the pinned thrillmot/agent-skills
|
|
97
|
+
// commit and falling back to the bundled npm-package copy on any fetch failure.
|
|
98
|
+
// Returns the same shape as before, plus a `_source` of either 'agent-skills'
|
|
99
|
+
// or 'bundled' so the CLI can report which path was used.
|
|
100
|
+
//
|
|
101
|
+
// Options:
|
|
102
|
+
// - fetch — injectable for tests (defaults to globalThis.fetch)
|
|
103
|
+
// - cacheDir — where to cache fetched SKILL.md files (defaults to
|
|
104
|
+
// ~/.cache/clud-bug/skills/, skipped if null)
|
|
105
|
+
export async function loadBaseline(baselineDir, opts = {}) {
|
|
106
|
+
const fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
107
|
+
const cacheDir = opts.cacheDir === null ? null
|
|
108
|
+
: (opts.cacheDir ?? join(homedir(), '.cache', 'clud-bug', 'skills'));
|
|
109
|
+
|
|
110
|
+
// First, enumerate the bundled baseline skills (source of truth for which
|
|
111
|
+
// names exist). Then fetch each in parallel — sequential awaits would
|
|
112
|
+
// stack timeouts (3 baselines × 5s = 15s before fallback when offline).
|
|
113
|
+
const bundled = await readBundled(baselineDir);
|
|
114
|
+
const remotes = await Promise.all(
|
|
115
|
+
bundled.map((s) => tryFetchSkill(s.name, fetchImpl, cacheDir)),
|
|
116
|
+
);
|
|
117
|
+
return bundled.map((skill, i) => remotes[i]
|
|
118
|
+
? { ...skill, content: remotes[i], _source: 'agent-skills' }
|
|
119
|
+
: { ...skill, _source: 'bundled' });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Reads the bundled baseline from the npm-package directory.
|
|
123
|
+
async function readBundled(baselineDir) {
|
|
82
124
|
const skills = [];
|
|
83
125
|
let entries;
|
|
84
126
|
try {
|
|
@@ -92,7 +134,7 @@ export async function loadBaseline(baselineDir) {
|
|
|
92
134
|
skills.push({
|
|
93
135
|
source: 'clud-bug-baseline',
|
|
94
136
|
name: entry.name.replace(/\.md$/, ''),
|
|
95
|
-
description: '(
|
|
137
|
+
description: '(baseline)',
|
|
96
138
|
installs: 0,
|
|
97
139
|
kind: 'baseline',
|
|
98
140
|
content,
|
|
@@ -101,6 +143,65 @@ export async function loadBaseline(baselineDir) {
|
|
|
101
143
|
return skills;
|
|
102
144
|
}
|
|
103
145
|
|
|
146
|
+
// Try to read from cache, then fall back to network. Returns the SKILL.md
|
|
147
|
+
// content string on success, null on any failure (caller falls back to bundled).
|
|
148
|
+
async function tryFetchSkill(name, fetchImpl, cacheDir) {
|
|
149
|
+
// Cache lookup first.
|
|
150
|
+
if (cacheDir) {
|
|
151
|
+
const cached = await readFromCache(cacheDir, name);
|
|
152
|
+
if (cached !== null) return cached;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Network fetch with timeout covering BOTH the connection AND the body
|
|
156
|
+
// read (clearTimeout in finally guarantees the timer doesn't keep the
|
|
157
|
+
// event loop alive for up to 5s past a failed CLI run).
|
|
158
|
+
const url = `${AGENT_SKILLS_BASE}/${encodeURIComponent(name)}/SKILL.md`;
|
|
159
|
+
const ctrl = new AbortController();
|
|
160
|
+
const timer = setTimeout(() => ctrl.abort(), SKILL_FETCH_TIMEOUT_MS);
|
|
161
|
+
try {
|
|
162
|
+
const res = await fetchImpl(url, { signal: ctrl.signal });
|
|
163
|
+
if (!res.ok) return null;
|
|
164
|
+
const content = await res.text();
|
|
165
|
+
if (!content || !content.trim()) return null;
|
|
166
|
+
if (cacheDir) await writeToCache(cacheDir, name, content);
|
|
167
|
+
return content;
|
|
168
|
+
} catch {
|
|
169
|
+
return null;
|
|
170
|
+
} finally {
|
|
171
|
+
clearTimeout(timer);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function readFromCache(cacheDir, name) {
|
|
176
|
+
const path = cachePath(cacheDir, name);
|
|
177
|
+
try {
|
|
178
|
+
const st = await stat(path);
|
|
179
|
+
if (Date.now() - st.mtimeMs > SKILL_CACHE_TTL_MS) return null;
|
|
180
|
+
return await readFile(path, 'utf8');
|
|
181
|
+
} catch {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function writeToCache(cacheDir, name, content) {
|
|
187
|
+
try {
|
|
188
|
+
await mkdir(cacheDir, { recursive: true });
|
|
189
|
+
await writeFile(cachePath(cacheDir, name), content);
|
|
190
|
+
} catch {
|
|
191
|
+
// Cache write failures are non-fatal — we already have the content.
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function cachePath(cacheDir, name) {
|
|
196
|
+
// Include AGENT_SKILLS_BASE in the hash so different upstream URLs (e.g.
|
|
197
|
+
// a fork via CLUD_BUG_AGENT_SKILLS_BASE, or a different pinned SHA after
|
|
198
|
+
// a clud-bug release) get different cache entries. Otherwise switching
|
|
199
|
+
// bases would silently return the previously-cached content from a
|
|
200
|
+
// different upstream — cross-base cache poisoning.
|
|
201
|
+
const hash = createHash('sha256').update(`${AGENT_SKILLS_BASE}\n${name}`).digest('hex').slice(0, 16);
|
|
202
|
+
return join(cacheDir, `${hash}.md`);
|
|
203
|
+
}
|
|
204
|
+
|
|
104
205
|
export async function writeSkills(targetDir, skills, client) {
|
|
105
206
|
await mkdir(targetDir, { recursive: true });
|
|
106
207
|
const written = [];
|
package/lib/update.js
CHANGED
|
@@ -3,6 +3,7 @@ import { join, dirname } from 'node:path';
|
|
|
3
3
|
import { renderFile, pickTemplate } from './render.js';
|
|
4
4
|
import { detect, buildDescriptionLine } from './detect.js';
|
|
5
5
|
import { loadBaseline, readManifest, writeManifest } from './skills.js';
|
|
6
|
+
import { applyToRepo as applyAgentDocs } from './agents-md.js';
|
|
6
7
|
|
|
7
8
|
// Re-render the user's workflow + refresh baseline skills using the
|
|
8
9
|
// templates / baseline shipped with the currently-installed clud-bug.
|
|
@@ -21,6 +22,7 @@ export async function runUpdate({
|
|
|
21
22
|
baselineDir,
|
|
22
23
|
ourVersion,
|
|
23
24
|
refreshRemote = false,
|
|
25
|
+
loadBaselineOpts, // forwarded to loadBaseline (e.g. for tests: { fetch, cacheDir: null })
|
|
24
26
|
} = {}) {
|
|
25
27
|
if (!cwd || !templatesDir || !baselineDir || !ourVersion) {
|
|
26
28
|
throw new Error('runUpdate requires cwd, templatesDir, baselineDir, ourVersion');
|
|
@@ -51,7 +53,7 @@ export async function runUpdate({
|
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
// 3. Refresh baseline skills (always controlled by clud-bug).
|
|
54
|
-
const baseline = await loadBaseline(baselineDir);
|
|
56
|
+
const baseline = await loadBaseline(baselineDir, loadBaselineOpts);
|
|
55
57
|
for (const skill of baseline) {
|
|
56
58
|
const skillPath = join(skillsDir, sanitize(skill.name), 'SKILL.md');
|
|
57
59
|
await maybeWrite(skillPath, skill.content, changed, unchanged, `baseline ${skill.name}`);
|
|
@@ -66,7 +68,22 @@ export async function runUpdate({
|
|
|
66
68
|
// `clud-bug refresh`. We just emit an advisory.
|
|
67
69
|
}
|
|
68
70
|
|
|
69
|
-
// 5.
|
|
71
|
+
// 5. Refresh the AGENTS.md / CLAUDE.md clud-bug block. The block embeds
|
|
72
|
+
// the version + strict-mode state, so an update with a new version
|
|
73
|
+
// rewrites it. Files that don't already exist (other than AGENTS.md)
|
|
74
|
+
// are left alone, so this never silently creates instruction stubs.
|
|
75
|
+
// `=== true` mirrors the workflow's gate predicate at
|
|
76
|
+
// templates/workflow*.yml.tmpl. A v0.3 advisory manifest (strictMode
|
|
77
|
+
// undefined, lastUpdate set) renders "off" — matching the gate, not the
|
|
78
|
+
// default-on behavior of fresh v0.4+ installs.
|
|
79
|
+
const agentDocs = await applyAgentDocs(cwd, {
|
|
80
|
+
version: ourVersion,
|
|
81
|
+
strictMode: manifest.strictMode === true,
|
|
82
|
+
});
|
|
83
|
+
for (const p of agentDocs.created) changed.push({ path: join(cwd, p), label: `agent docs: created ${p}` });
|
|
84
|
+
for (const p of agentDocs.touched) changed.push({ path: join(cwd, p), label: `agent docs: ${p}` });
|
|
85
|
+
|
|
86
|
+
// 6. Stamp the manifest with the version that ran the update.
|
|
70
87
|
manifest.lastUpdate = new Date().toISOString();
|
|
71
88
|
manifest.lastUpdateVersion = ourVersion;
|
|
72
89
|
await writeManifest(skillsDir, manifest);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clud-bug",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
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",
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: clud-bug-collaboration
|
|
3
|
+
description: How Claude Code agents working in a clud-bug-installed repo should interact with the bot's review threads, strict-mode gate, and skill set. Use this skill whenever you're about to push a commit, address a clud-bug PR review comment, edit anything under .claude/skills/, modify .github/workflows/clud-bug-*.yml, or wonder why a PR check is red. Also use when planning work in a repo that has a `clud-bug-review` workflow installed — even if the user didn't mention clud-bug by name.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Working in a clud-bug-installed repo
|
|
7
|
+
|
|
8
|
+
Clud Bug reviews every PR via `anthropics/claude-code-action`. As another
|
|
9
|
+
Claude Code agent working alongside it, here's what to know — these aren't
|
|
10
|
+
arbitrary rules, they're the consequences of how the gate is wired.
|
|
11
|
+
|
|
12
|
+
## When you push fixes to a clud-bug-reviewed PR
|
|
13
|
+
|
|
14
|
+
The bot reviews on `pull_request: synchronize` (every push). When you push
|
|
15
|
+
a commit that addresses an issue Clud Bug flagged in a prior review, the bot
|
|
16
|
+
will re-review the PR within ~2 minutes and **resolve its own prior
|
|
17
|
+
unresolved inline review threads** where the flagged issue is verifiably
|
|
18
|
+
fixed in the current diff. You don't have to resolve threads manually.
|
|
19
|
+
|
|
20
|
+
If a thread it left isn't auto-resolved after your fix:
|
|
21
|
+
- The bot judged the issue still open (read its latest comment for why).
|
|
22
|
+
- Or the resolution call hit a transient API issue (re-push to retry).
|
|
23
|
+
|
|
24
|
+
Don't manually resolve clud-bug threads on its behalf. The check
|
|
25
|
+
(`required_conversation_resolution` branch protection) will block merge if
|
|
26
|
+
unresolved threads remain — and the right fix is usually "fix the actual
|
|
27
|
+
issue and re-push," not "mark the conversation resolved."
|
|
28
|
+
|
|
29
|
+
## When you read the PR's `clud-bug-review` check status
|
|
30
|
+
|
|
31
|
+
- **Green** = Clud Bug ran and either found no critical issues OR strict
|
|
32
|
+
mode is off (advisory).
|
|
33
|
+
- **Red** = either the action errored OR strict mode is on AND Clud Bug
|
|
34
|
+
flagged a critical issue. Read the latest `## 🐛 Clud Bug review` comment
|
|
35
|
+
on the PR — the body indicates "critical findings" or "clean."
|
|
36
|
+
- **Skipped/green-with-comment** = bot- or fork-authored PR (Dependabot,
|
|
37
|
+
Renovate, fork contributor). GitHub deliberately doesn't pass repo
|
|
38
|
+
secrets to those workflows. The bot posts a one-line "Clud Bug skipped"
|
|
39
|
+
comment and exits 0. Review the diff manually.
|
|
40
|
+
|
|
41
|
+
## Strict mode
|
|
42
|
+
|
|
43
|
+
Read `.claude/skills/.clud-bug.json` to check this repo's setting.
|
|
44
|
+
`strictMode: true` means the workflow check fails on critical findings.
|
|
45
|
+
The setting is read from the **base ref**, so a PR cannot disable strict
|
|
46
|
+
mode on itself by editing the manifest. Changes take effect for PRs opened
|
|
47
|
+
after the change merges to the base branch.
|
|
48
|
+
|
|
49
|
+
To disable strict mode for a repo, edit the manifest on the base branch:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{ "strictMode": false, ... }
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## When you modify a clud-bug skill
|
|
56
|
+
|
|
57
|
+
Skills live in `.claude/skills/<slug>/SKILL.md`. Three groups:
|
|
58
|
+
|
|
59
|
+
- **Baseline** (`critical-issues-only`, `evidence-based-review`,
|
|
60
|
+
`respect-existing-conventions`) — managed by clud-bug; if you edit them
|
|
61
|
+
in-place they'll be overwritten on the next `clud-bug update`. To
|
|
62
|
+
customize behavior for this repo, write a NEW skill rather than mutating
|
|
63
|
+
a baseline.
|
|
64
|
+
- **From skills.sh** — installed via `clud-bug add <source/name>`. Tracked
|
|
65
|
+
in `.claude/skills/.clud-bug.json`. Use `clud-bug refresh` to sync.
|
|
66
|
+
- **Custom** (anything not in the manifest) — yours, never touched by any
|
|
67
|
+
clud-bug command. Drop a new `.md` here and it auto-loads on the next PR.
|
|
68
|
+
|
|
69
|
+
When you write a custom skill, follow the SKILL.md frontmatter format
|
|
70
|
+
(`name`, `description`) and write specific, evidence-anchored guidance.
|
|
71
|
+
Generic advice gets ignored; rules with examples and quoted-line evidence
|
|
72
|
+
move the bot's behavior.
|
|
73
|
+
|
|
74
|
+
## When you edit `.github/workflows/clud-bug-*.yml`
|
|
75
|
+
|
|
76
|
+
`anthropics/claude-code-action` **refuses to run on PRs that modify its
|
|
77
|
+
own workflow file** (App token exchange fails with 401, "Workflow
|
|
78
|
+
validation failed"). This is a security guard against PRs that try to
|
|
79
|
+
neuter the reviewer or exfiltrate secrets.
|
|
80
|
+
|
|
81
|
+
Consequence: if you bundle a workflow tweak with other work, the
|
|
82
|
+
`clud-bug-review` check on that PR will fail and not actually review your
|
|
83
|
+
other changes either.
|
|
84
|
+
|
|
85
|
+
The fix: split workflow edits into their own PR. The CLI helps:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
# After editing .github/workflows/clud-bug-*.yml locally:
|
|
89
|
+
clud-bug edit-workflow
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
It refuses to run if your working tree has non-workflow changes, so the
|
|
93
|
+
isolation guarantee is enforced. Branches from `origin/main`, not HEAD —
|
|
94
|
+
unrelated commits on a feature branch can't leak in.
|
|
95
|
+
|
|
96
|
+
## When the secret is missing
|
|
97
|
+
|
|
98
|
+
`ANTHROPIC_API_KEY` must be set in the repo's Actions secrets. Without it,
|
|
99
|
+
the workflow's guard step fails loudly with an `::error::` annotation
|
|
100
|
+
explaining how to set it. (For bot/fork PRs where the secret legitimately
|
|
101
|
+
isn't passed, the guard posts a one-line advisory comment and exits 0
|
|
102
|
+
instead of failing red.)
|
|
103
|
+
|
|
104
|
+
## Updating clud-bug itself
|
|
105
|
+
|
|
106
|
+
`clud-bug-self-update.yml` runs weekly (Mondays 12:00 UTC) and opens a PR
|
|
107
|
+
when a newer clud-bug version is published to npm. Pin to a specific
|
|
108
|
+
version by adding `pinVersion: "x.y.z"` to `.claude/skills/.clud-bug.json`.
|
|
109
|
+
|
|
110
|
+
To trigger an update on demand:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
clud-bug update
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Re-renders workflow templates and refreshes baseline skills from the
|
|
117
|
+
currently-installed clud-bug version. Custom and skills.sh-installed
|
|
118
|
+
skills are left untouched.
|
|
119
|
+
|
|
120
|
+
## Where to find more
|
|
121
|
+
|
|
122
|
+
- Site: https://cludbug.dev
|
|
123
|
+
- Repo: https://github.com/thrillmot/clud-bug
|
|
124
|
+
- Skill catalog: https://github.com/thrillmot/agent-skills
|