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.
@@ -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
- return { version: MANIFEST_VERSION, installed: [...byKey.values()] };
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.2.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@v4
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 pr review:*),Bash(gh api graphql:*),Bash(gh api repos/:*)"
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
- Any project-specific skills loaded from .claude/skills/ should
43
- shape your review defer to their guidance over generic advice.
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@v4
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 pr review:*),Bash(gh api graphql:*),Bash(gh api repos/:*)"
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
- Any project-specific skills loaded from .claude/skills/ should
44
- shape your review defer to their guidance over generic advice.
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