clud-bug 0.2.0 → 0.4.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 +88 -9
- package/bin/clud-bug.js +237 -38
- package/lib/audit.js +111 -0
- package/lib/edit-workflow.js +47 -0
- package/lib/skills.js +8 -1
- package/lib/update.js +98 -0
- package/package.json +2 -4
- package/templates/audit.yml.tmpl +134 -0
- package/templates/self-update.yml.tmpl +92 -0
- package/templates/workflow-py.yml.tmpl +89 -4
- package/templates/workflow-ts.yml.tmpl +89 -4
- package/templates/workflow.yml.tmpl +133 -4
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
// Validates that the working tree's pending changes are scoped to
|
|
4
|
+
// .github/workflows/clud-bug-*.yml. If yes, creates a branch, commits,
|
|
5
|
+
// pushes, and prints the gh command for opening a PR. Mixed changes are
|
|
6
|
+
// refused — the whole point is to keep workflow edits in an isolated PR
|
|
7
|
+
// so claude-code-action's workflow-self-mod guard doesn't bundle them
|
|
8
|
+
// with other reviewable work.
|
|
9
|
+
|
|
10
|
+
export function getPendingWorkflowEdits(cwd = process.cwd()) {
|
|
11
|
+
// --untracked-files=all so new files in new directories are reported as
|
|
12
|
+
// individual paths instead of being collapsed to the parent dir (default
|
|
13
|
+
// 'normal' mode would emit '.github/' for a brand-new clud-bug-review.yml).
|
|
14
|
+
const r = spawnSync('git', ['status', '--porcelain', '--untracked-files=all'], { cwd, encoding: 'utf8' });
|
|
15
|
+
if (r.status !== 0) {
|
|
16
|
+
throw new Error(`git status failed: ${r.stderr.trim()}`);
|
|
17
|
+
}
|
|
18
|
+
const lines = r.stdout.split('\n').filter(Boolean);
|
|
19
|
+
if (lines.length === 0) return { allWorkflow: true, files: [], nonWorkflow: [] };
|
|
20
|
+
|
|
21
|
+
const files = [];
|
|
22
|
+
const nonWorkflow = [];
|
|
23
|
+
for (const line of lines) {
|
|
24
|
+
// porcelain format: XY <space> <path> ; rename: XY old -> new
|
|
25
|
+
const path = line.slice(3).split(' -> ').pop().trim();
|
|
26
|
+
files.push(path);
|
|
27
|
+
if (!isWorkflowFile(path)) nonWorkflow.push(path);
|
|
28
|
+
}
|
|
29
|
+
return { allWorkflow: nonWorkflow.length === 0, files, nonWorkflow };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isWorkflowFile(path) {
|
|
33
|
+
return /^\.github\/workflows\/clud-bug-.*\.ya?ml$/.test(path);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function makeBranchName(date = new Date()) {
|
|
37
|
+
const stamp = date.toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
38
|
+
return `clud-bug/edit-workflow-${stamp}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function git(cwd, args, opts = {}) {
|
|
42
|
+
const r = spawnSync('git', args, { cwd, encoding: 'utf8' });
|
|
43
|
+
if (r.status !== 0 && !opts.allowFail) {
|
|
44
|
+
throw new Error(`git ${args.join(' ')} failed (${r.status}): ${r.stderr.trim()}`);
|
|
45
|
+
}
|
|
46
|
+
return { ok: r.status === 0, stdout: r.stdout.trim(), stderr: r.stderr.trim() };
|
|
47
|
+
}
|
package/lib/skills.js
CHANGED
|
@@ -133,6 +133,7 @@ export async function readManifest(targetDir) {
|
|
|
133
133
|
const text = await readFile(join(targetDir, MANIFEST_FILE), 'utf8');
|
|
134
134
|
const data = JSON.parse(text);
|
|
135
135
|
return {
|
|
136
|
+
...data,
|
|
136
137
|
version: data.version || MANIFEST_VERSION,
|
|
137
138
|
installed: Array.isArray(data.installed) ? data.installed : [],
|
|
138
139
|
};
|
|
@@ -143,7 +144,10 @@ export async function readManifest(targetDir) {
|
|
|
143
144
|
|
|
144
145
|
export async function writeManifest(targetDir, manifest) {
|
|
145
146
|
await mkdir(targetDir, { recursive: true });
|
|
147
|
+
// Preserve any additional fields callers want to stamp (e.g. lastUpdate,
|
|
148
|
+
// lastUpdateVersion, pinVersion). Only `version` and `installed` are normalized.
|
|
146
149
|
const out = {
|
|
150
|
+
...manifest,
|
|
147
151
|
version: manifest.version || MANIFEST_VERSION,
|
|
148
152
|
installed: manifest.installed || [],
|
|
149
153
|
};
|
|
@@ -158,7 +162,10 @@ export function mergeManifest(existing, newEntries) {
|
|
|
158
162
|
for (const entry of newEntries) {
|
|
159
163
|
byKey.set(entryKey(entry), entry);
|
|
160
164
|
}
|
|
161
|
-
|
|
165
|
+
// Spread `existing` so caller-set fields (pinVersion, lastUpdate,
|
|
166
|
+
// lastUpdateVersion, etc.) survive merges performed by writeSkills /
|
|
167
|
+
// refresh / add. Only `installed` is rebuilt; everything else carries.
|
|
168
|
+
return { ...existing, version: MANIFEST_VERSION, installed: [...byKey.values()] };
|
|
162
169
|
}
|
|
163
170
|
|
|
164
171
|
function entryKey(entry) {
|
package/lib/update.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir, stat } from 'node:fs/promises';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { renderFile, pickTemplate } from './render.js';
|
|
4
|
+
import { detect, buildDescriptionLine } from './detect.js';
|
|
5
|
+
import { loadBaseline, readManifest, writeManifest } from './skills.js';
|
|
6
|
+
|
|
7
|
+
// Re-render the user's workflow + refresh baseline skills using the
|
|
8
|
+
// templates / baseline shipped with the currently-installed clud-bug.
|
|
9
|
+
//
|
|
10
|
+
// Honors three protections:
|
|
11
|
+
// - Custom skills (anything in .claude/skills/ not in the manifest) are
|
|
12
|
+
// never modified.
|
|
13
|
+
// - Remote skills (from skills.sh, kind: 'remote' in manifest) are left
|
|
14
|
+
// alone unless { refreshRemote: true }.
|
|
15
|
+
// - The audit workflow is also re-rendered if it's installed.
|
|
16
|
+
//
|
|
17
|
+
// Returns a diff summary with file paths and a short reason per file.
|
|
18
|
+
export async function runUpdate({
|
|
19
|
+
cwd,
|
|
20
|
+
templatesDir,
|
|
21
|
+
baselineDir,
|
|
22
|
+
ourVersion,
|
|
23
|
+
refreshRemote = false,
|
|
24
|
+
} = {}) {
|
|
25
|
+
if (!cwd || !templatesDir || !baselineDir || !ourVersion) {
|
|
26
|
+
throw new Error('runUpdate requires cwd, templatesDir, baselineDir, ourVersion');
|
|
27
|
+
}
|
|
28
|
+
const skillsDir = join(cwd, '.claude', 'skills');
|
|
29
|
+
const manifest = await readManifest(skillsDir);
|
|
30
|
+
if (manifest.installed.length === 0 && !(await pathExists(join(cwd, '.github/workflows/clud-bug-review.yml')))) {
|
|
31
|
+
return { changed: [], unchanged: [], missing: 'init' };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const changed = [];
|
|
35
|
+
const unchanged = [];
|
|
36
|
+
|
|
37
|
+
// 1. Re-render review workflow with the latest template.
|
|
38
|
+
const signals = await detect(cwd);
|
|
39
|
+
const tmplName = pickTemplate(signals.languages);
|
|
40
|
+
const newReview = await renderFile(join(templatesDir, tmplName), {
|
|
41
|
+
PROJECT_DESCRIPTION: buildDescriptionLine(signals),
|
|
42
|
+
LANGUAGE_HINTS: '',
|
|
43
|
+
});
|
|
44
|
+
await maybeWrite(join(cwd, '.github/workflows/clud-bug-review.yml'), newReview, changed, unchanged, 'review workflow');
|
|
45
|
+
|
|
46
|
+
// 2. Re-render audit workflow if it's installed (init from v0.3+ ships it).
|
|
47
|
+
const auditPath = join(cwd, '.github/workflows/clud-bug-audit.yml');
|
|
48
|
+
if (await pathExists(auditPath)) {
|
|
49
|
+
const newAudit = await readFile(join(templatesDir, 'audit.yml.tmpl'), 'utf8');
|
|
50
|
+
await maybeWrite(auditPath, newAudit, changed, unchanged, 'audit workflow');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 3. Refresh baseline skills (always controlled by clud-bug).
|
|
54
|
+
const baseline = await loadBaseline(baselineDir);
|
|
55
|
+
for (const skill of baseline) {
|
|
56
|
+
const skillPath = join(skillsDir, sanitize(skill.name), 'SKILL.md');
|
|
57
|
+
await maybeWrite(skillPath, skill.content, changed, unchanged, `baseline ${skill.name}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 4. Optionally refresh remote skills (off by default).
|
|
61
|
+
// Custom skills are never touched.
|
|
62
|
+
// (Remote refresh is intentionally minimal here — `clud-bug refresh`
|
|
63
|
+
// already covers add/remove diffs against skills.sh.)
|
|
64
|
+
if (refreshRemote) {
|
|
65
|
+
// Placeholder for parity with the flag; full logic remains in
|
|
66
|
+
// `clud-bug refresh`. We just emit an advisory.
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 5. Stamp the manifest with the version that ran the update.
|
|
70
|
+
manifest.lastUpdate = new Date().toISOString();
|
|
71
|
+
manifest.lastUpdateVersion = ourVersion;
|
|
72
|
+
await writeManifest(skillsDir, manifest);
|
|
73
|
+
|
|
74
|
+
return { changed, unchanged, ourVersion };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function maybeWrite(path, contents, changed, unchanged, label) {
|
|
78
|
+
const prior = await readSafe(path);
|
|
79
|
+
if (prior === contents) {
|
|
80
|
+
unchanged.push({ path, label });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
await mkdir(dirname(path), { recursive: true });
|
|
84
|
+
await writeFile(path, contents);
|
|
85
|
+
changed.push({ path, label });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function readSafe(path) {
|
|
89
|
+
try { return await readFile(path, 'utf8'); } catch { return null; }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function pathExists(path) {
|
|
93
|
+
try { await stat(path); return true; } catch { return false; }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function sanitize(name) {
|
|
97
|
+
return name.toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
98
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clud-bug",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|
|
@@ -20,9 +20,7 @@
|
|
|
20
20
|
"ai",
|
|
21
21
|
"skills"
|
|
22
22
|
],
|
|
23
|
-
"bin":
|
|
24
|
-
"clud-bug": "./bin/clud-bug.js"
|
|
25
|
-
},
|
|
23
|
+
"bin": "bin/clud-bug.js",
|
|
26
24
|
"files": [
|
|
27
25
|
"bin",
|
|
28
26
|
"lib",
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
name: Clud Bug 🐛 Audit
|
|
2
|
+
|
|
3
|
+
# A scheduled / on-demand walk through the whole habitat (or a recent slice).
|
|
4
|
+
# Output lands as a PR titled "🐛 Clud Bug audit — YYYY-MM-DD" containing a
|
|
5
|
+
# single audits/<date>.md file with findings.
|
|
6
|
+
#
|
|
7
|
+
# Triggers:
|
|
8
|
+
# workflow_dispatch — manual button in the Actions tab. Default.
|
|
9
|
+
# schedule (commented) — uncomment for weekly Mondays 09:00 UTC.
|
|
10
|
+
|
|
11
|
+
on:
|
|
12
|
+
workflow_dispatch:
|
|
13
|
+
inputs:
|
|
14
|
+
changed_in:
|
|
15
|
+
description: "Audit files changed in (e.g. 7d, 2w, 1mo). Blank = full repo."
|
|
16
|
+
required: false
|
|
17
|
+
default: ""
|
|
18
|
+
scope:
|
|
19
|
+
description: "Glob to limit scope (e.g. src/**/*.ts). Blank = no scope filter."
|
|
20
|
+
required: false
|
|
21
|
+
default: ""
|
|
22
|
+
# Uncomment for a weekly Monday 09:00 UTC audit:
|
|
23
|
+
# schedule:
|
|
24
|
+
# - cron: '0 9 * * 1'
|
|
25
|
+
|
|
26
|
+
permissions:
|
|
27
|
+
contents: write
|
|
28
|
+
pull-requests: write
|
|
29
|
+
id-token: write
|
|
30
|
+
|
|
31
|
+
env:
|
|
32
|
+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
|
|
33
|
+
|
|
34
|
+
jobs:
|
|
35
|
+
audit:
|
|
36
|
+
runs-on: ubuntu-latest
|
|
37
|
+
steps:
|
|
38
|
+
- uses: actions/checkout@v5
|
|
39
|
+
with:
|
|
40
|
+
fetch-depth: 0 # full history needed for git --since
|
|
41
|
+
|
|
42
|
+
- uses: actions/setup-node@v5
|
|
43
|
+
with:
|
|
44
|
+
node-version: '20'
|
|
45
|
+
|
|
46
|
+
# Audit runs on workflow_dispatch / schedule — no PR context, so the
|
|
47
|
+
# fork-PR carve-out doesn't apply. Just fail loud if the secret is missing.
|
|
48
|
+
- name: Guard — require ANTHROPIC_API_KEY
|
|
49
|
+
env:
|
|
50
|
+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
51
|
+
run: |
|
|
52
|
+
if [ -z "$ANTHROPIC_API_KEY" ]; then
|
|
53
|
+
echo "::error title=Clud Bug 🐛::ANTHROPIC_API_KEY secret is not set."
|
|
54
|
+
echo "::error::Set it: Settings → Secrets and variables → Actions → New repository secret."
|
|
55
|
+
exit 1
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
- name: Compute audit scope and create branch
|
|
59
|
+
id: prep
|
|
60
|
+
env:
|
|
61
|
+
CHANGED_IN: ${{ inputs.changed_in }}
|
|
62
|
+
SCOPE: ${{ inputs.scope }}
|
|
63
|
+
run: |
|
|
64
|
+
DATE=$(date -u +%Y-%m-%d)
|
|
65
|
+
# Add a run-number suffix so same-day re-runs don't collide on the
|
|
66
|
+
# remote branch (push would otherwise be rejected as non-fast-forward).
|
|
67
|
+
BRANCH="clud-bug/audit-${DATE}-${{ github.run_number }}"
|
|
68
|
+
echo "branch=$BRANCH" >> "$GITHUB_OUTPUT"
|
|
69
|
+
echo "date=$DATE" >> "$GITHUB_OUTPUT"
|
|
70
|
+
|
|
71
|
+
# Use github-actions[bot]'s real identity (clud-bug isn't a registered
|
|
72
|
+
# GitHub App, so its name wouldn't resolve to a recognizable bot).
|
|
73
|
+
git config user.name "github-actions[bot]"
|
|
74
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
75
|
+
git checkout -b "$BRANCH"
|
|
76
|
+
|
|
77
|
+
ARGS=()
|
|
78
|
+
[ -n "$CHANGED_IN" ] && ARGS+=("--changed-in" "$CHANGED_IN")
|
|
79
|
+
[ -n "$SCOPE" ] && ARGS+=("--scope" "$SCOPE")
|
|
80
|
+
|
|
81
|
+
npx -y clud-bug@latest audit "${ARGS[@]}"
|
|
82
|
+
|
|
83
|
+
if [ ! -f "audits/$DATE.md" ]; then
|
|
84
|
+
echo "::warning::clud-bug audit produced no stub file — nothing in scope."
|
|
85
|
+
echo "stub_written=false" >> "$GITHUB_OUTPUT"
|
|
86
|
+
exit 0
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
git add audits/
|
|
90
|
+
git commit -m "🐛 Clud Bug audit — $DATE (stub)"
|
|
91
|
+
git push -u origin "$BRANCH"
|
|
92
|
+
echo "stub_written=true" >> "$GITHUB_OUTPUT"
|
|
93
|
+
|
|
94
|
+
- uses: anthropics/claude-code-action@v1
|
|
95
|
+
if: steps.prep.outputs.stub_written == 'true'
|
|
96
|
+
env:
|
|
97
|
+
AUDIT_DATE: ${{ steps.prep.outputs.date }}
|
|
98
|
+
AUDIT_BRANCH: ${{ steps.prep.outputs.branch }}
|
|
99
|
+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
|
|
100
|
+
with:
|
|
101
|
+
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
102
|
+
track_progress: true
|
|
103
|
+
claude_args: |
|
|
104
|
+
--allowedTools "Bash(gh pr create:*),Bash(gh pr edit:*),Bash(gh pr view:*),Bash(cat:*),Bash(head:*),Bash(tail:*),Bash(rg:*),Bash(grep:*),Bash(sed:*),Bash(git:*),Bash(ls:*),Bash(wc:*)"
|
|
105
|
+
prompt: |
|
|
106
|
+
You are Clud Bug 🐛 conducting a habitat audit. The branch
|
|
107
|
+
${{ steps.prep.outputs.branch }} contains a stub file at
|
|
108
|
+
audits/${{ steps.prep.outputs.date }}.md listing the file manifest
|
|
109
|
+
for this audit.
|
|
110
|
+
|
|
111
|
+
Procedure:
|
|
112
|
+
1. Read audits/${{ steps.prep.outputs.date }}.md to see the manifest.
|
|
113
|
+
2. Walk the manifest. For each file, look for critical issues only:
|
|
114
|
+
bugs / logic errors, security vulnerabilities (injection, secrets,
|
|
115
|
+
unsafe shell, etc.), performance defects, and broken or missing
|
|
116
|
+
test coverage on logic that needs it. Skip style nits.
|
|
117
|
+
3. Append a "## Findings" section to the audit file with one
|
|
118
|
+
entry per issue: file:line, the offending snippet, and the fix.
|
|
119
|
+
Group by severity. Cite skills from .claude/skills/ where relevant.
|
|
120
|
+
4. End the file with `Skills referenced: [...]`.
|
|
121
|
+
5. Commit your changes to the audit file:
|
|
122
|
+
git add audits/${{ steps.prep.outputs.date }}.md
|
|
123
|
+
git commit -m "🐛 Clud Bug audit — $AUDIT_DATE (findings)"
|
|
124
|
+
git push
|
|
125
|
+
6. Open a PR from this branch to the default branch:
|
|
126
|
+
gh pr create \
|
|
127
|
+
--title "🐛 Clud Bug audit — $AUDIT_DATE" \
|
|
128
|
+
--body "Findings from a habitat walk on $AUDIT_DATE. Review and act on each item; merge if you want the report committed, close otherwise."
|
|
129
|
+
|
|
130
|
+
Tone: concise field-naturalist voice — you are documenting specimens.
|
|
131
|
+
Don't perform the bit; let the precision speak.
|
|
132
|
+
|
|
133
|
+
If you find no critical issues, still open the PR with the audit
|
|
134
|
+
file noting "No critical issues observed." Maintainers will close it.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
name: Clud Bug 🐛 Self-Update
|
|
2
|
+
|
|
3
|
+
# Weekly check for a newer published clud-bug. If one exists, runs
|
|
4
|
+
# `clud-bug update` (which re-renders the workflow templates and refreshes
|
|
5
|
+
# the bundled baseline specimens, leaving custom and skills.sh-installed
|
|
6
|
+
# specimens alone), then opens a PR titled
|
|
7
|
+
# "🐛 Clud Bug self-update: vX.Y.Z → vA.B.C".
|
|
8
|
+
#
|
|
9
|
+
# To pin to a specific clud-bug version and stop self-updates, add a
|
|
10
|
+
# "pinVersion" field to .claude/skills/.clud-bug.json:
|
|
11
|
+
# { "pinVersion": "0.3.0", ... }
|
|
12
|
+
|
|
13
|
+
on:
|
|
14
|
+
workflow_dispatch:
|
|
15
|
+
schedule:
|
|
16
|
+
- cron: '0 12 * * 1' # Mondays 12:00 UTC
|
|
17
|
+
|
|
18
|
+
permissions:
|
|
19
|
+
contents: write
|
|
20
|
+
pull-requests: write
|
|
21
|
+
|
|
22
|
+
env:
|
|
23
|
+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
|
|
24
|
+
|
|
25
|
+
jobs:
|
|
26
|
+
check:
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@v5
|
|
30
|
+
|
|
31
|
+
- uses: actions/setup-node@v5
|
|
32
|
+
with:
|
|
33
|
+
node-version: '20'
|
|
34
|
+
|
|
35
|
+
- name: Compare installed vs npm latest
|
|
36
|
+
id: compare
|
|
37
|
+
run: |
|
|
38
|
+
MANIFEST=".claude/skills/.clud-bug.json"
|
|
39
|
+
INSTALLED="(none)"
|
|
40
|
+
PIN=""
|
|
41
|
+
if [ -f "$MANIFEST" ]; then
|
|
42
|
+
INSTALLED=$(node -e "console.log(JSON.parse(require('fs').readFileSync('$MANIFEST','utf8')).lastUpdateVersion || '(unknown)')")
|
|
43
|
+
PIN=$(node -e "console.log(JSON.parse(require('fs').readFileSync('$MANIFEST','utf8')).pinVersion || '')")
|
|
44
|
+
fi
|
|
45
|
+
LATEST=$(npm view clud-bug version)
|
|
46
|
+
echo "installed=$INSTALLED" >> "$GITHUB_OUTPUT"
|
|
47
|
+
echo "latest=$LATEST" >> "$GITHUB_OUTPUT"
|
|
48
|
+
echo "pin=$PIN" >> "$GITHUB_OUTPUT"
|
|
49
|
+
echo "Installed: $INSTALLED Latest: $LATEST Pin: ${PIN:-(none)}"
|
|
50
|
+
|
|
51
|
+
if [ -n "$PIN" ]; then
|
|
52
|
+
echo "Pinned to $PIN — skipping update check."
|
|
53
|
+
echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
54
|
+
exit 0
|
|
55
|
+
fi
|
|
56
|
+
if [ "$INSTALLED" = "$LATEST" ]; then
|
|
57
|
+
echo "Already on latest. Nothing to do."
|
|
58
|
+
echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
59
|
+
exit 0
|
|
60
|
+
fi
|
|
61
|
+
echo "skip=false" >> "$GITHUB_OUTPUT"
|
|
62
|
+
|
|
63
|
+
- name: Run clud-bug update + open PR
|
|
64
|
+
if: steps.compare.outputs.skip == 'false'
|
|
65
|
+
env:
|
|
66
|
+
INSTALLED: ${{ steps.compare.outputs.installed }}
|
|
67
|
+
LATEST: ${{ steps.compare.outputs.latest }}
|
|
68
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
69
|
+
run: |
|
|
70
|
+
BRANCH="clud-bug/self-update-${LATEST}"
|
|
71
|
+
# Use github-actions[bot]'s real identity so commits resolve to a
|
|
72
|
+
# recognizable bot in the GitHub UI. (A "clud-bug[bot]" identity
|
|
73
|
+
# would only resolve if we registered an actual GitHub App for
|
|
74
|
+
# clud-bug — out of scope today.)
|
|
75
|
+
git config user.name "github-actions[bot]"
|
|
76
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
77
|
+
git checkout -b "$BRANCH"
|
|
78
|
+
|
|
79
|
+
npx -y "clud-bug@${LATEST}" update
|
|
80
|
+
|
|
81
|
+
# Note: runUpdate always stamps lastUpdate / lastUpdateVersion in
|
|
82
|
+
# the manifest, so there will always be at least one diff in this
|
|
83
|
+
# branch. We rely on `git diff` simply to detect *real* file changes
|
|
84
|
+
# outside the manifest, and let the PR open either way.
|
|
85
|
+
git add -A
|
|
86
|
+
git commit -m "🐛 Clud Bug self-update: ${INSTALLED} → ${LATEST}"
|
|
87
|
+
git push -u origin "$BRANCH"
|
|
88
|
+
gh pr create \
|
|
89
|
+
--title "🐛 Clud Bug self-update: ${INSTALLED} → ${LATEST}" \
|
|
90
|
+
--body "Automated update from clud-bug ${INSTALLED} → ${LATEST}. Custom and skills.sh-installed specimens were left alone; only baseline specimens and the workflow templates were refreshed.
|
|
91
|
+
|
|
92
|
+
Review the diff. To stay on this version permanently, add \`\"pinVersion\": \"${INSTALLED}\"\` to \`.claude/skills/.clud-bug.json\` before merging."
|
|
@@ -13,16 +13,50 @@ jobs:
|
|
|
13
13
|
id-token: write
|
|
14
14
|
|
|
15
15
|
steps:
|
|
16
|
-
- uses: actions/checkout@
|
|
16
|
+
- uses: actions/checkout@v5
|
|
17
|
+
with:
|
|
18
|
+
fetch-depth: 0 # strict-mode gate reads base ref's manifest
|
|
19
|
+
|
|
20
|
+
# Three-way guard — see workflow.yml.tmpl for full design notes.
|
|
21
|
+
- name: Guard — require ANTHROPIC_API_KEY
|
|
22
|
+
id: guard
|
|
23
|
+
env:
|
|
24
|
+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
25
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
26
|
+
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
27
|
+
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
|
|
28
|
+
HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
|
|
29
|
+
BASE_REPO: ${{ github.repository }}
|
|
30
|
+
run: |
|
|
31
|
+
if [ -n "$ANTHROPIC_API_KEY" ]; then echo "skip=false" >> "$GITHUB_OUTPUT"; exit 0; fi
|
|
32
|
+
IS_FORK=false; [ "$HEAD_REPO" != "$BASE_REPO" ] && IS_FORK=true
|
|
33
|
+
IS_BOT=false; case "$PR_AUTHOR" in *\[bot\]) IS_BOT=true ;; esac
|
|
34
|
+
if $IS_FORK || $IS_BOT; then
|
|
35
|
+
REASON=$($IS_BOT && echo "bot author ($PR_AUTHOR)" || echo "fork ($HEAD_REPO)")
|
|
36
|
+
EXISTING=$(gh api "repos/${BASE_REPO}/issues/${PR_NUMBER}/comments?per_page=100" \
|
|
37
|
+
--jq '[.[] | select(.user.login == "claude[bot]" and (.body | startswith("## 🐛 Clud Bug skipped")))] | length')
|
|
38
|
+
if [ "${EXISTING:-0}" = "0" ]; then
|
|
39
|
+
BODY=$(printf '## 🐛 Clud Bug skipped\n\nThis PR is from a %s. GitHub deliberately does not pass repository secrets to such workflows, so Clud Bug could not authenticate against Anthropic. Review the diff manually.' "$REASON")
|
|
40
|
+
gh pr comment "$PR_NUMBER" --body "$BODY" || true
|
|
41
|
+
fi
|
|
42
|
+
echo "::warning title=Clud Bug 🐛::Skipped — $REASON cannot access repository secrets."
|
|
43
|
+
echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
44
|
+
exit 0
|
|
45
|
+
fi
|
|
46
|
+
echo "::error title=Clud Bug 🐛::ANTHROPIC_API_KEY secret is not set on this repository."
|
|
47
|
+
echo "::error::Set it: Settings → Secrets and variables → Actions → New repository secret."
|
|
48
|
+
exit 1
|
|
17
49
|
|
|
18
50
|
- uses: anthropics/claude-code-action@v1
|
|
51
|
+
if: steps.guard.outputs.skip != 'true'
|
|
19
52
|
env:
|
|
20
53
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
54
|
+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
|
|
21
55
|
with:
|
|
22
56
|
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
23
57
|
track_progress: true
|
|
24
58
|
claude_args: |
|
|
25
|
-
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh
|
|
59
|
+
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh api graphql:*),Bash(gh api repos/:*),Bash(cat .claude/skills/.clud-bug.json)"
|
|
26
60
|
prompt: |
|
|
27
61
|
{{PROJECT_DESCRIPTION}}
|
|
28
62
|
|
|
@@ -39,8 +73,38 @@ jobs:
|
|
|
39
73
|
Skip style suggestions, minor naming issues, or anything that
|
|
40
74
|
doesn't affect correctness, security, or performance.
|
|
41
75
|
|
|
42
|
-
|
|
43
|
-
|
|
76
|
+
Skills are not background context — they are review rules with
|
|
77
|
+
authority. Before flagging any finding, scan the loaded skills in
|
|
78
|
+
.claude/skills/ for relevant guidance. If a skill applies, your
|
|
79
|
+
review MUST reference it by name in the finding (e.g. "[evidence-
|
|
80
|
+
based-review]: this claim isn't anchored to a line"). Generic
|
|
81
|
+
advice that contradicts a project skill is wrong by definition.
|
|
82
|
+
|
|
83
|
+
At the end of every review, append a single-line footer:
|
|
84
|
+
Skills referenced: [skill-name-1, skill-name-2, ...]
|
|
85
|
+
If you genuinely cited none, list "[none]" and explain why no
|
|
86
|
+
installed skill applied to this diff.
|
|
87
|
+
|
|
88
|
+
Strict-mode header (opt-in): if .claude/skills/.clud-bug.json
|
|
89
|
+
contains { "strictMode": true }, the comment header you post
|
|
90
|
+
MUST signal whether you flagged a critical issue:
|
|
91
|
+
IF you flagged any critical issue (bug, security,
|
|
92
|
+
performance, missing test coverage):
|
|
93
|
+
## 🐛 Clud Bug review — critical findings
|
|
94
|
+
OTHERWISE:
|
|
95
|
+
## 🐛 Clud Bug review — clean
|
|
96
|
+
A post-step in this workflow greps your posted comment for
|
|
97
|
+
that header and fails the check on "critical findings." The
|
|
98
|
+
gate is deterministic on top of your judgment.
|
|
99
|
+
|
|
100
|
+
If strictMode is NOT set (or absent), keep the existing
|
|
101
|
+
"## 🐛 Clud Bug review" header — strict mode is opt-in and
|
|
102
|
+
other repos use the plain header.
|
|
103
|
+
|
|
104
|
+
Tone: address the author conversationally. A concise field-naturalist
|
|
105
|
+
voice is welcome (you are Clud Bug, examining specimens of code) but
|
|
106
|
+
never at the cost of clarity, evidence, or the critical-issues-only
|
|
107
|
+
discipline. Don't perform the bit; let the precision speak.
|
|
44
108
|
|
|
45
109
|
When you finish, post your review as a single PR comment.
|
|
46
110
|
The comment body MUST start with this exact line so the
|
|
@@ -69,3 +133,24 @@ jobs:
|
|
|
69
133
|
For line-specific issues, use the github_inline_comment MCP tool
|
|
70
134
|
with confirmed: true.
|
|
71
135
|
If there are no critical issues, post a one-line comment saying so.
|
|
136
|
+
|
|
137
|
+
# Strict-mode gate — see workflow.yml.tmpl for full design notes.
|
|
138
|
+
- name: Strict mode — fail check on critical findings
|
|
139
|
+
if: success()
|
|
140
|
+
env:
|
|
141
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
142
|
+
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
143
|
+
run: |
|
|
144
|
+
BASE_MANIFEST=$(git show "origin/${{ github.base_ref }}:.claude/skills/.clud-bug.json" 2>&1) || {
|
|
145
|
+
echo "::warning::Base manifest not found on ${{ github.base_ref }} — strict mode disabled for this run."
|
|
146
|
+
exit 0
|
|
147
|
+
}
|
|
148
|
+
STRICT=$(echo "$BASE_MANIFEST" | node -e "let s='';process.stdin.on('data',c=>s+=c);process.stdin.on('end',()=>{try{console.log(JSON.parse(s).strictMode===true)}catch(e){console.log('false')}})")
|
|
149
|
+
[ "$STRICT" = "true" ] || exit 0
|
|
150
|
+
LATEST=$(gh api "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments?sort=created&direction=desc&per_page=100" \
|
|
151
|
+
--jq '[.[] | select(.user.login == "claude[bot]" and (.body | startswith("## 🐛 Clud Bug review")))][0].body // ""')
|
|
152
|
+
if echo "$LATEST" | head -n1 | grep -q "Clud Bug review — critical findings"; then
|
|
153
|
+
echo "::error title=Clud Bug 🐛::Critical issues found and strictMode is enabled — failing this check."
|
|
154
|
+
echo "::error::See the latest Clud Bug review comment for details. Push a fix and the gate will clear on the next run."
|
|
155
|
+
exit 1
|
|
156
|
+
fi
|
|
@@ -13,16 +13,50 @@ jobs:
|
|
|
13
13
|
id-token: write
|
|
14
14
|
|
|
15
15
|
steps:
|
|
16
|
-
- uses: actions/checkout@
|
|
16
|
+
- uses: actions/checkout@v5
|
|
17
|
+
with:
|
|
18
|
+
fetch-depth: 0 # strict-mode gate reads base ref's manifest
|
|
19
|
+
|
|
20
|
+
# Three-way guard — see workflow.yml.tmpl for full design notes.
|
|
21
|
+
- name: Guard — require ANTHROPIC_API_KEY
|
|
22
|
+
id: guard
|
|
23
|
+
env:
|
|
24
|
+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
25
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
26
|
+
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
27
|
+
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
|
|
28
|
+
HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
|
|
29
|
+
BASE_REPO: ${{ github.repository }}
|
|
30
|
+
run: |
|
|
31
|
+
if [ -n "$ANTHROPIC_API_KEY" ]; then echo "skip=false" >> "$GITHUB_OUTPUT"; exit 0; fi
|
|
32
|
+
IS_FORK=false; [ "$HEAD_REPO" != "$BASE_REPO" ] && IS_FORK=true
|
|
33
|
+
IS_BOT=false; case "$PR_AUTHOR" in *\[bot\]) IS_BOT=true ;; esac
|
|
34
|
+
if $IS_FORK || $IS_BOT; then
|
|
35
|
+
REASON=$($IS_BOT && echo "bot author ($PR_AUTHOR)" || echo "fork ($HEAD_REPO)")
|
|
36
|
+
EXISTING=$(gh api "repos/${BASE_REPO}/issues/${PR_NUMBER}/comments?per_page=100" \
|
|
37
|
+
--jq '[.[] | select(.user.login == "claude[bot]" and (.body | startswith("## 🐛 Clud Bug skipped")))] | length')
|
|
38
|
+
if [ "${EXISTING:-0}" = "0" ]; then
|
|
39
|
+
BODY=$(printf '## 🐛 Clud Bug skipped\n\nThis PR is from a %s. GitHub deliberately does not pass repository secrets to such workflows, so Clud Bug could not authenticate against Anthropic. Review the diff manually.' "$REASON")
|
|
40
|
+
gh pr comment "$PR_NUMBER" --body "$BODY" || true
|
|
41
|
+
fi
|
|
42
|
+
echo "::warning title=Clud Bug 🐛::Skipped — $REASON cannot access repository secrets."
|
|
43
|
+
echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
44
|
+
exit 0
|
|
45
|
+
fi
|
|
46
|
+
echo "::error title=Clud Bug 🐛::ANTHROPIC_API_KEY secret is not set on this repository."
|
|
47
|
+
echo "::error::Set it: Settings → Secrets and variables → Actions → New repository secret."
|
|
48
|
+
exit 1
|
|
17
49
|
|
|
18
50
|
- uses: anthropics/claude-code-action@v1
|
|
51
|
+
if: steps.guard.outputs.skip != 'true'
|
|
19
52
|
env:
|
|
20
53
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
54
|
+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
|
|
21
55
|
with:
|
|
22
56
|
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
23
57
|
track_progress: true
|
|
24
58
|
claude_args: |
|
|
25
|
-
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh
|
|
59
|
+
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh api graphql:*),Bash(gh api repos/:*),Bash(cat .claude/skills/.clud-bug.json)"
|
|
26
60
|
prompt: |
|
|
27
61
|
{{PROJECT_DESCRIPTION}}
|
|
28
62
|
|
|
@@ -40,8 +74,38 @@ jobs:
|
|
|
40
74
|
Skip style suggestions, minor naming issues, or anything that
|
|
41
75
|
doesn't affect correctness, security, or performance.
|
|
42
76
|
|
|
43
|
-
|
|
44
|
-
|
|
77
|
+
Skills are not background context — they are review rules with
|
|
78
|
+
authority. Before flagging any finding, scan the loaded skills in
|
|
79
|
+
.claude/skills/ for relevant guidance. If a skill applies, your
|
|
80
|
+
review MUST reference it by name in the finding (e.g. "[evidence-
|
|
81
|
+
based-review]: this claim isn't anchored to a line"). Generic
|
|
82
|
+
advice that contradicts a project skill is wrong by definition.
|
|
83
|
+
|
|
84
|
+
At the end of every review, append a single-line footer:
|
|
85
|
+
Skills referenced: [skill-name-1, skill-name-2, ...]
|
|
86
|
+
If you genuinely cited none, list "[none]" and explain why no
|
|
87
|
+
installed skill applied to this diff.
|
|
88
|
+
|
|
89
|
+
Strict-mode header (opt-in): if .claude/skills/.clud-bug.json
|
|
90
|
+
contains { "strictMode": true }, the comment header you post
|
|
91
|
+
MUST signal whether you flagged a critical issue:
|
|
92
|
+
IF you flagged any critical issue (bug, security,
|
|
93
|
+
performance, missing test coverage):
|
|
94
|
+
## 🐛 Clud Bug review — critical findings
|
|
95
|
+
OTHERWISE:
|
|
96
|
+
## 🐛 Clud Bug review — clean
|
|
97
|
+
A post-step in this workflow greps your posted comment for
|
|
98
|
+
that header and fails the check on "critical findings." The
|
|
99
|
+
gate is deterministic on top of your judgment.
|
|
100
|
+
|
|
101
|
+
If strictMode is NOT set (or absent), keep the existing
|
|
102
|
+
"## 🐛 Clud Bug review" header — strict mode is opt-in and
|
|
103
|
+
other repos use the plain header.
|
|
104
|
+
|
|
105
|
+
Tone: address the author conversationally. A concise field-naturalist
|
|
106
|
+
voice is welcome (you are Clud Bug, examining specimens of code) but
|
|
107
|
+
never at the cost of clarity, evidence, or the critical-issues-only
|
|
108
|
+
discipline. Don't perform the bit; let the precision speak.
|
|
45
109
|
|
|
46
110
|
When you finish, post your review as a single PR comment.
|
|
47
111
|
The comment body MUST start with this exact line so the
|
|
@@ -70,3 +134,24 @@ jobs:
|
|
|
70
134
|
For line-specific issues, use the github_inline_comment MCP tool
|
|
71
135
|
with confirmed: true.
|
|
72
136
|
If there are no critical issues, post a one-line comment saying so.
|
|
137
|
+
|
|
138
|
+
# Strict-mode gate — see workflow.yml.tmpl for full design notes.
|
|
139
|
+
- name: Strict mode — fail check on critical findings
|
|
140
|
+
if: success()
|
|
141
|
+
env:
|
|
142
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
143
|
+
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
144
|
+
run: |
|
|
145
|
+
BASE_MANIFEST=$(git show "origin/${{ github.base_ref }}:.claude/skills/.clud-bug.json" 2>&1) || {
|
|
146
|
+
echo "::warning::Base manifest not found on ${{ github.base_ref }} — strict mode disabled for this run."
|
|
147
|
+
exit 0
|
|
148
|
+
}
|
|
149
|
+
STRICT=$(echo "$BASE_MANIFEST" | node -e "let s='';process.stdin.on('data',c=>s+=c);process.stdin.on('end',()=>{try{console.log(JSON.parse(s).strictMode===true)}catch(e){console.log('false')}})")
|
|
150
|
+
[ "$STRICT" = "true" ] || exit 0
|
|
151
|
+
LATEST=$(gh api "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments?sort=created&direction=desc&per_page=100" \
|
|
152
|
+
--jq '[.[] | select(.user.login == "claude[bot]" and (.body | startswith("## 🐛 Clud Bug review")))][0].body // ""')
|
|
153
|
+
if echo "$LATEST" | head -n1 | grep -q "Clud Bug review — critical findings"; then
|
|
154
|
+
echo "::error title=Clud Bug 🐛::Critical issues found and strictMode is enabled — failing this check."
|
|
155
|
+
echo "::error::See the latest Clud Bug review comment for details. Push a fix and the gate will clear on the next run."
|
|
156
|
+
exit 1
|
|
157
|
+
fi
|