clud-bug 0.6.24 → 0.6.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/agents-md.js CHANGED
@@ -45,11 +45,19 @@ const TOUCH_IF_PRESENT = [
45
45
  // set) is a documented advisory upgrade path — `clud-bug init` deliberately
46
46
  // preserves that state. If the block rendered "on" for that case, other
47
47
  // agents reading AGENTS.md would get a wrong model of the gate.
48
- export function renderBlock({ version, strictMode } = {}) {
48
+ //
49
+ // v0.6.25 (gotcha #2 fix): when the consuming repo IS the publisher of
50
+ // the clud-bug-collaboration skill (skill source lives at
51
+ // `skills/clud-bug-collaboration/SKILL.md` instead of the consumer-install
52
+ // path `.claude/skills/...`), render the LOCAL repo path. Otherwise the
53
+ // link is dead in the publisher repo (this used to require a manual fix
54
+ // every v0.6.* propagation cycle on agent-skills).
55
+ export function renderBlock({ version, strictMode, skillRelPath } = {}) {
49
56
  const versionLine = version ? `_Installed at clud-bug v${version}._` : '';
50
57
  const strictNote = strictMode === true
51
58
  ? '**on** in this repo (workflow check fails on critical findings)'
52
59
  : '**off** in this repo (advisory only)';
60
+ const skillPath = skillRelPath || '.claude/skills/clud-bug-collaboration/SKILL.md';
53
61
  return `${START_MARKER}
54
62
  <!-- clud-bug-block-version: ${BLOCK_VERSION} -->
55
63
  ## clud-bug — Claude PR review
@@ -57,7 +65,7 @@ export function renderBlock({ version, strictMode } = {}) {
57
65
  This repo uses [clud-bug](https://cludbug.dev) for automatic PR reviews.
58
66
  Full collaboration rules — fix-push flow, skill structure, comment format,
59
67
  strict-mode mechanics, workflow-edit constraint — live in the bundled
60
- [\`clud-bug-collaboration\` skill](.claude/skills/clud-bug-collaboration/SKILL.md).
68
+ [\`clud-bug-collaboration\` skill](${skillPath}).
61
69
  Read that skill before pushing fixes addressing prior review threads.
62
70
 
63
71
  Strict mode is ${strictNote}. Toggle via \`.claude/skills/.clud-bug.json\`
@@ -71,6 +79,19 @@ ${versionLine}
71
79
  ${END_MARKER}`;
72
80
  }
73
81
 
82
+ // v0.6.25 (gotcha #2): detect repos that PUBLISH the
83
+ // clud-bug-collaboration skill (agent-skills is the canonical case).
84
+ // When the skill source exists at `skills/clud-bug-collaboration/SKILL.md`
85
+ // in the working tree, the AGENTS.md link should point there, not at the
86
+ // consumer-install `.claude/skills/...` path that doesn't exist in the
87
+ // publisher repo.
88
+ export async function detectSkillRelPath(cwd) {
89
+ const publisherPath = 'skills/clud-bug-collaboration/SKILL.md';
90
+ const consumerPath = '.claude/skills/clud-bug-collaboration/SKILL.md';
91
+ if (await fileExists(join(cwd, publisherPath))) return publisherPath;
92
+ return consumerPath;
93
+ }
94
+
74
95
  // Replace an existing clud-bug block in `content`, OR append if absent.
75
96
  // Idempotent: running multiple times leaves a single block.
76
97
  export function upsertBlock(content, block) {
@@ -126,7 +147,12 @@ function escapeRe(s) {
126
147
  //
127
148
  // Returns { touched: string[], created: string[] } for the caller to log.
128
149
  export async function applyToRepo(cwd, blockOpts = {}) {
129
- const block = renderBlock(blockOpts);
150
+ // v0.6.25 / gotcha #2: detect publisher repo + render local skill path.
151
+ // Pre-v0.6.25 always rendered the consumer install path → broke
152
+ // agent-skills' check-links every propagation cycle. Detection runs
153
+ // before block render so the path is correct from the first write.
154
+ const skillRelPath = blockOpts.skillRelPath ?? await detectSkillRelPath(cwd);
155
+ const block = renderBlock({ ...blockOpts, skillRelPath });
130
156
  const touched = [];
131
157
  const created = [];
132
158
 
package/lib/detect.js CHANGED
@@ -214,7 +214,15 @@ export function buildDescriptionLine(signals) {
214
214
  const parts = [];
215
215
  if (signals.name) parts.push(`This project is "${signals.name}".`);
216
216
  if (signals.description) {
217
- const desc = signals.description.trim();
217
+ // v0.6.25 / issue #89: when signals.description comes from a
218
+ // README first paragraph or similar multi-paragraph source, the
219
+ // raw `\n` characters survive into the rendered YAML's
220
+ // APPEND_SYSTEM_PROMPT value. The renderer's indent-aware
221
+ // substitution preserves layout but a literal `\n` inside a YAML
222
+ // double-quoted string is interpreted as a newline, breaking
223
+ // the YAML. Collapse any whitespace run (including newlines + tabs)
224
+ // to a single space before further processing.
225
+ const desc = signals.description.replace(/\s+/g, ' ').trim();
218
226
  parts.push(/[.!?]$/.test(desc) ? desc : `${desc}.`);
219
227
  }
220
228
  if (signals.primaryLanguage) {
package/lib/prompts.js CHANGED
@@ -177,6 +177,30 @@ Keep total output under ~600 tokens. Per finding:
177
177
  Not a hard cap (SDK doesn't expose max_tokens); brevity compounds
178
178
  across the org on every review.
179
179
 
180
+ Turn budget self-rationing (v0.6.25 / §5.5 Layer 2):
181
+ You are run with a finite \`--max-turns\` cap. The exact value, plus
182
+ the pre-flight estimate for this PR, lands in the per-PR prompt
183
+ section below as \`max_turns=N, estimated=M, files=F, +A/-D lines, threads=T\`.
184
+ Treat that as your budget, not a ceiling to test.
185
+
186
+ Rules:
187
+ - Reserve the LAST 5 turns for structured-output emit. It is
188
+ non-skippable; if you run out before emitting, the workflow
189
+ falls back to a synthetic summary scraped from your inline
190
+ findings (v0.6.26+ Layer 6) — strictly worse than a real one.
191
+ - Rough rates: ~1 turn per 50 lines of code, ~1 turn per 150 lines
192
+ of docs, ~1 turn per 100 lines of tests or config. Each prior
193
+ unresolved thread is ~1.5 turns to walk + decide.
194
+ - Every line of the diff MUST be in your scan. Never silently skip
195
+ a file. If you must shorten coverage to fit budget, switch from
196
+ deep-dive analysis to one-sentence per-file verdicts — but every
197
+ file gets a verdict, even if it's "no issues found".
198
+ - If you're falling behind pace (files_reviewed/files_total
199
+ materially less than turns_used/max_turns), broaden+shallow over
200
+ deep+narrow. Audit visibility lives in \`per_skill_scan\` — list
201
+ every file's verdict so a maintainer can verify nothing was
202
+ skipped.
203
+
180
204
  Incremental-diff handshake (v0.6.10+) — emit the SHA marker:
181
205
  At the very end of the summary (after the Skills-referenced footer,
182
206
  on its own line), append:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clud-bug",
3
- "version": "0.6.24",
3
+ "version": "0.6.26",
4
4
  "description": "Skill-driven Claude PR review. Ship a brand-voice skill, get brand reviews. Each finding cites the skill that motivated it. CLI installs the workflow + a baseline kit; add more from skills.sh.",
5
5
  "homepage": "https://cludbug.dev",
6
6
  "bugs": "https://github.com/thrillmade/clud-bug/issues",
@@ -1,10 +1,15 @@
1
- # clud-bug-template-version: v10
1
+ # clud-bug-template-version: v12
2
2
  name: Clud Bug 🐛 Crawls Your Code
3
3
 
4
4
  on:
5
5
  pull_request:
6
6
  types: [opened, synchronize]
7
7
 
8
+ # v0.6.25: workflow-level concurrency — see workflow.yml.tmpl.
9
+ concurrency:
10
+ group: clud-bug-review-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
11
+ cancel-in-progress: true
12
+
8
13
  jobs:
9
14
  # Pre-flight (v0.6.14 / 0.0.W + v0.6.15 / 0.0.R) — see workflow.yml.tmpl.
10
15
  paths-check:
@@ -16,6 +21,12 @@ jobs:
16
21
  is_workflow_only: ${{ steps.classify.outputs.is_workflow_only }}
17
22
  model: ${{ steps.classify.outputs.model }}
18
23
  max_turns: ${{ steps.classify.outputs.max_turns }}
24
+ # v0.6.25 / §5.5 Layer 1.5 (calibration) — see workflow.yml.tmpl.
25
+ turns_estimated: ${{ steps.classify.outputs.turns_estimated }}
26
+ files_count: ${{ steps.classify.outputs.files_count }}
27
+ lines_added: ${{ steps.classify.outputs.lines_added }}
28
+ lines_deleted: ${{ steps.classify.outputs.lines_deleted }}
29
+ threads_count: ${{ steps.classify.outputs.threads_count }}
19
30
  steps:
20
31
  - name: Classify PR diff
21
32
  id: classify
@@ -34,20 +45,40 @@ jobs:
34
45
  echo "is_workflow_only=false"
35
46
  echo "model=$MODEL"
36
47
  echo "max_turns=15"
48
+ echo "turns_estimated=0"
49
+ echo "files_count=0"
50
+ echo "lines_added=0"
51
+ echo "lines_deleted=0"
52
+ echo "threads_count=0"
37
53
  } >> "$GITHUB_OUTPUT"
38
54
  exit 0
39
55
  fi
40
- IS_WORKFLOW_ONLY=true
56
+ # v0.6.26 / 0.0.W² — see workflow.yml.tmpl for design notes.
57
+ ALL_IN_ALLOWLIST=true
58
+ HAS_WORKFLOW_CHANGE=false
41
59
  while IFS= read -r f; do
42
60
  case "$f" in
43
- .github/workflows/clud-bug-*.yml) ;;
44
- .github/actions/strict-mode-gate/*) ;;
45
- *) IS_WORKFLOW_ONLY=false; break ;;
61
+ .github/workflows/clud-bug-*.yml) HAS_WORKFLOW_CHANGE=true ;;
62
+ .github/actions/strict-mode-gate/*) HAS_WORKFLOW_CHANGE=true ;;
63
+ AGENTS.md) ;;
64
+ .cursorrules|.clinerules|.windsurfrules|.continuerules) ;;
65
+ .github/copilot-instructions.md) ;;
66
+ .claude/skills/.clud-bug.json) ;;
67
+ .claude/skills/critical-issues-only/SKILL.md) ;;
68
+ .claude/skills/evidence-based-review/SKILL.md) ;;
69
+ .claude/skills/respect-existing-conventions/SKILL.md) ;;
70
+ docs/timeline.md|docs/file-structure.md|docs/decisions.md) ;;
71
+ docs/decisions-branches/*.md) ;;
72
+ *) ALL_IN_ALLOWLIST=false; break ;;
46
73
  esac
47
74
  done <<< "$CHANGED"
75
+ IS_WORKFLOW_ONLY=false
76
+ if [ "$ALL_IN_ALLOWLIST" = "true" ] && [ "$HAS_WORKFLOW_CHANGE" = "true" ]; then
77
+ IS_WORKFLOW_ONLY=true
78
+ fi
48
79
  echo "is_workflow_only=$IS_WORKFLOW_ONLY" >> "$GITHUB_OUTPUT"
49
80
  if [ "$IS_WORKFLOW_ONLY" = "true" ]; then
50
- echo "::notice title=Clud Bug 🐛::Skipping LLM review — workflow-only PR."
81
+ echo "::notice title=Clud Bug 🐛::Skipping LLM review — clud-bug update output."
51
82
  echo "model=$MODEL" >> "$GITHUB_OUTPUT"
52
83
  exit 0
53
84
  fi
@@ -81,22 +112,57 @@ jobs:
81
112
  fi
82
113
  echo "model=$MODEL" >> "$GITHUB_OUTPUT"
83
114
 
84
- # Adaptive max-turns (v0.6.23 / §5) — see workflow.yml.tmpl for design notes.
85
- MAX_TURNS=15
115
+ # Smart budget (v0.6.25 / §5.5 Layer 1) — see workflow.yml.tmpl for design notes + formula.
116
+ FILE_COUNT=$(echo "$CHANGED" | wc -l | tr -d ' ')
117
+ THREAD_COUNT=$(gh api graphql -f query='{repository(owner:"'"$(echo "$REPO" | cut -d/ -f1)"'",name:"'"$(echo "$REPO" | cut -d/ -f2)"'"){pullRequest(number:'"$PR_NUMBER"'){reviewThreads(first:50){nodes{isResolved comments(first:1){nodes{author{login}}}}}}}}' --jq '[.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false and (.comments.nodes[0].author.login == "claude" or .comments.nodes[0].author.login == "claude[bot]"))] | length' 2>/dev/null || echo 0)
118
+ THREAD_COUNT=${THREAD_COUNT:-0}
119
+
86
120
  if [ "$IS_TRIVIAL" = "true" ]; then
87
121
  MAX_TURNS=10
122
+ TURNS_ESTIMATED=10
123
+ LINES_ADDED=0
124
+ LINES_DELETED=0
125
+ echo "::notice title=Clud Bug 🐛::Trivial (Haiku) budget: max_turns=10."
88
126
  else
89
- FILE_COUNT=$(echo "$CHANGED" | wc -l | tr -d ' ')
90
- THREAD_COUNT=$(gh api graphql -f query='{repository(owner:"'"$(echo "$REPO" | cut -d/ -f1)"'",name:"'"$(echo "$REPO" | cut -d/ -f2)"'"){pullRequest(number:'"$PR_NUMBER"'){reviewThreads(first:50){nodes{isResolved comments(first:1){nodes{author{login}}}}}}}}' --jq '[.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false and (.comments.nodes[0].author.login == "claude" or .comments.nodes[0].author.login == "claude[bot]"))] | length' 2>/dev/null || echo 0)
91
- THREAD_COUNT=${THREAD_COUNT:-0}
92
- if [ "$FILE_COUNT" -ge 30 ] || [ "$THREAD_COUNT" -ge 6 ]; then
93
- MAX_TURNS=40
94
- elif [ "$FILE_COUNT" -ge 10 ] || [ "$THREAD_COUNT" -ge 3 ]; then
95
- MAX_TURNS=25
96
- fi
97
- echo "::notice title=Clud Bug 🐛::Turn budget: $MAX_TURNS ($FILE_COUNT files, $THREAD_COUNT prior threads)."
127
+ FILES_JSON=$(gh pr view "$PR_NUMBER" -R "$REPO" --json files --jq '.files' 2>/dev/null || echo '[]')
128
+ LINES_ADDED=$(echo "$FILES_JSON" | jq '[.[].additions] | add // 0' 2>/dev/null || echo 0)
129
+ LINES_DELETED=$(echo "$FILES_JSON" | jq '[.[].deletions] | add // 0' 2>/dev/null || echo 0)
130
+ LINES_ADDED=${LINES_ADDED:-0}
131
+ LINES_DELETED=${LINES_DELETED:-0}
132
+
133
+ # jq implements the §5.5 Layer 1 formula — see workflow.yml.tmpl.
134
+ TURNS_ESTIMATED=$(echo "$FILES_JSON" | jq --argjson threads "$THREAD_COUNT" '
135
+ def per_line(path):
136
+ if (path | test("\\.(test|spec)\\.|__tests__|^tests?/")) then 0.01
137
+ elif (path | test("\\.(md|txt|rst|adoc|mdx)$")) then 0.00666667
138
+ elif (path | test("\\.(yml|yaml|toml|json|cfg|ini|conf|tmpl)$")) then 0.01
139
+ elif (path | test("\\.(ts|tsx|py|js|jsx|mjs|cjs|go|rs|java|kt|rb|php|cs|c|cpp|h|hpp|swift|scala)$")) then 0.02
140
+ else 0.0125 end;
141
+ map(
142
+ select(.path != "docs/timeline.md"
143
+ and .path != "docs/file-structure.md"
144
+ and .path != "docs/decisions.md")
145
+ | (.additions) as $add | (.deletions) as $del
146
+ | (if $add < $del then $add else $del end) as $mod
147
+ | ($add - $mod) as $pa | ($del - $mod) as $pd
148
+ | per_line(.path) as $tw
149
+ | 0.3 + ($pa * $tw) + ($mod * 1.5 * $tw) + ($pd * 0.1 * $tw)
150
+ ) | (add // 0) + 10 + ($threads * 1.5) | (. + 0.9999999) | floor
151
+ ' 2>/dev/null || echo 15)
152
+ TURNS_ESTIMATED=${TURNS_ESTIMATED:-15}
153
+ MAX_TURNS=$(( (TURNS_ESTIMATED * 12 + 9) / 10 ))
154
+ [ "$MAX_TURNS" -lt 15 ] && MAX_TURNS=15
155
+ [ "$MAX_TURNS" -gt 60 ] && MAX_TURNS=60
156
+ echo "::notice title=Clud Bug 🐛::Smart budget: estimated $TURNS_ESTIMATED turns → max_turns=$MAX_TURNS ($FILE_COUNT files, +$LINES_ADDED/-$LINES_DELETED lines, $THREAD_COUNT prior threads)."
98
157
  fi
99
- echo "max_turns=$MAX_TURNS" >> "$GITHUB_OUTPUT"
158
+ {
159
+ echo "max_turns=$MAX_TURNS"
160
+ echo "turns_estimated=$TURNS_ESTIMATED"
161
+ echo "files_count=$FILE_COUNT"
162
+ echo "lines_added=$LINES_ADDED"
163
+ echo "lines_deleted=$LINES_DELETED"
164
+ echo "threads_count=$THREAD_COUNT"
165
+ } >> "$GITHUB_OUTPUT"
100
166
 
101
167
  clud-bug-review:
102
168
  needs: paths-check
@@ -180,43 +246,100 @@ jobs:
180
246
  Review this pull request following the discipline in your
181
247
  system prompt — every rule about skill routing, comment
182
248
  format, the strict-mode header, the two-surface review
183
- shape, and the FIX-PUSH FLOW applies.
249
+ shape, the FIX-PUSH FLOW, and (v0.6.25+) turn-budget
250
+ self-rationing applies.
251
+
252
+ ## Your turn budget for this PR (v0.6.25 / §5.5 Layer 2)
253
+
254
+ max_turns=${{ needs.paths-check.outputs.max_turns }},
255
+ estimated=${{ needs.paths-check.outputs.turns_estimated }},
256
+ files=${{ needs.paths-check.outputs.files_count }},
257
+ +${{ needs.paths-check.outputs.lines_added }}/-${{ needs.paths-check.outputs.lines_deleted }} lines,
258
+ prior_threads=${{ needs.paths-check.outputs.threads_count }}.
259
+
260
+ Plan accordingly per the "Turn budget self-rationing"
261
+ section in your system prompt. Reserve 5 turns for emit.
184
262
  id: clud-bug-review
185
263
 
186
264
  # v0.6.22 / 0.0.O: render structured output → post via gh pr comment.
265
+ # v0.6.25 / §5.5 Layer 1.5: append calibration marker.
187
266
  - name: Render + post structured review
188
267
  if: success() && steps.clud-bug-review.outputs.structured_output != ''
189
268
  env:
190
269
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
191
270
  PR_NUMBER: ${{ github.event.pull_request.number }}
192
271
  STRUCTURED: ${{ steps.clud-bug-review.outputs.structured_output }}
272
+ TURNS_ESTIMATED: ${{ needs.paths-check.outputs.turns_estimated }}
273
+ MAX_TURNS: ${{ needs.paths-check.outputs.max_turns }}
274
+ FILES_COUNT: ${{ needs.paths-check.outputs.files_count }}
275
+ LINES_ADDED: ${{ needs.paths-check.outputs.lines_added }}
276
+ LINES_DELETED: ${{ needs.paths-check.outputs.lines_deleted }}
277
+ THREADS_COUNT: ${{ needs.paths-check.outputs.threads_count }}
193
278
  run: |
194
279
  set -euo pipefail
195
280
  BODY=$(printf '%s\n' "$STRUCTURED" | npx --yes clud-bug@{{CLUD_BUG_VERSION}} render --stdin)
196
- gh pr comment "$PR_NUMBER" --body "$BODY"
281
+ CALIBRATION="<!-- clud-bug-calibration: turns_estimated=$TURNS_ESTIMATED, max_turns=$MAX_TURNS, files=$FILES_COUNT, lines_added=$LINES_ADDED, lines_deleted=$LINES_DELETED, threads=$THREADS_COUNT -->"
282
+ gh pr comment "$PR_NUMBER" --body "$BODY
197
283
 
198
- # Fallback when structured_output is empty (max-retries hit).
284
+ $CALIBRATION"
285
+
286
+ # v0.6.26 / §5.5 Layer 6 fallback render-from-inlines — see workflow.yml.tmpl for design notes.
199
287
  - name: Fallback summary (structured_output empty)
200
288
  if: success() && steps.clud-bug-review.outputs.structured_output == ''
201
289
  env:
202
290
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
203
291
  PR_NUMBER: ${{ github.event.pull_request.number }}
292
+ REPO: ${{ github.repository }}
293
+ HEAD_SHA: ${{ github.event.pull_request.head.sha }}
294
+ TURNS_ESTIMATED: ${{ needs.paths-check.outputs.turns_estimated }}
295
+ MAX_TURNS: ${{ needs.paths-check.outputs.max_turns }}
296
+ FILES_COUNT: ${{ needs.paths-check.outputs.files_count }}
297
+ LINES_ADDED: ${{ needs.paths-check.outputs.lines_added }}
298
+ LINES_DELETED: ${{ needs.paths-check.outputs.lines_deleted }}
299
+ THREADS_COUNT: ${{ needs.paths-check.outputs.threads_count }}
204
300
  run: |
205
301
  set -euo pipefail
206
- gh pr comment "$PR_NUMBER" --body "## 🐛 Clud Bug review
302
+ INLINES=$(gh api "repos/$REPO/pulls/$PR_NUMBER/comments?per_page=100" \
303
+ --jq "[.[] | select(.user.login == \"claude[bot]\" and .commit_id == \"$HEAD_SHA\")]")
304
+ INLINE_COUNT=$(echo "$INLINES" | jq 'length')
305
+ CRITICAL=$(echo "$INLINES" | jq '[.[] | select(.body | test("🔴"))] | length')
306
+ MINOR=$(echo "$INLINES" | jq '[.[] | select(.body | test("🟡"))] | length')
307
+ PREEXISTING=$(echo "$INLINES" | jq '[.[] | select(.body | test("🟣"))] | length')
308
+ CALIBRATION="<!-- clud-bug-calibration: turns_estimated=$TURNS_ESTIMATED, max_turns=$MAX_TURNS, files=$FILES_COUNT, lines_added=$LINES_ADDED, lines_deleted=$LINES_DELETED, threads=$THREADS_COUNT, structured_output=empty, inline_findings=$INLINE_COUNT -->"
309
+ if [ "$INLINE_COUNT" -gt 0 ]; then
310
+ STATUS=""
311
+ [ "$CRITICAL" -gt 0 ] && STATUS=" — critical findings"
312
+ gh pr comment "$PR_NUMBER" --body "## 🐛 Clud Bug review${STATUS}
313
+
314
+ **This round:** $CRITICAL critical · $MINOR minor · 0 resolved from prior · 0 still open
315
+
316
+ Found: $CRITICAL 🔴 / $MINOR 🟡 / $PREEXISTING 🟣
317
+
318
+ ⚠️ **Synthetic summary** (v0.6.26 §5.5 Layer 6 fallback). Inline findings above ARE the substantive review.
319
+
320
+ <!-- last-reviewed-sha: $HEAD_SHA -->
321
+
322
+ $CALIBRATION"
323
+ else
324
+ gh pr comment "$PR_NUMBER" --body "## 🐛 Clud Bug review
207
325
 
208
326
  **This round:** 0 critical · 0 minor · 0 resolved from prior · 0 still open
209
327
 
210
328
  Found: 0 🔴 / 0 🟡 / 0 🟣
211
329
 
212
- ⚠️ Structured output (\`--json-schema\`) returned empty likely max-retries hit on schema validation. Investigate the action logs.
330
+ ⚠️ Structured output (\`--json-schema\`) returned empty AND no inline findings posted. Investigate run logs.
331
+
332
+ Skills referenced: [none]
213
333
 
214
- Skills referenced: [none]"
334
+ <!-- last-reviewed-sha: $HEAD_SHA -->
335
+
336
+ $CALIBRATION"
337
+ fi
215
338
 
216
339
  # Strict-mode gate — composite action; see workflow.yml.tmpl for design notes.
217
340
  - name: Strict mode — fail check on critical findings
218
341
  if: success()
219
- uses: thrillmade/clud-bug/.github/actions/strict-mode-gate@v0.6.24
342
+ uses: thrillmade/clud-bug/.github/actions/strict-mode-gate@v0.6.26
220
343
  with:
221
344
  github-token: ${{ secrets.GITHUB_TOKEN }}
222
345
  # v0.6.22 / 0.0.O: summary now posted by github-actions[bot].
@@ -1,10 +1,15 @@
1
- # clud-bug-template-version: v10
1
+ # clud-bug-template-version: v12
2
2
  name: Clud Bug 🐛 Crawls Your Code
3
3
 
4
4
  on:
5
5
  pull_request:
6
6
  types: [opened, synchronize]
7
7
 
8
+ # v0.6.25: workflow-level concurrency — see workflow.yml.tmpl.
9
+ concurrency:
10
+ group: clud-bug-review-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
11
+ cancel-in-progress: true
12
+
8
13
  jobs:
9
14
  # Pre-flight (v0.6.14 / 0.0.W + v0.6.15 / 0.0.R) — see workflow.yml.tmpl.
10
15
  paths-check:
@@ -16,6 +21,12 @@ jobs:
16
21
  is_workflow_only: ${{ steps.classify.outputs.is_workflow_only }}
17
22
  model: ${{ steps.classify.outputs.model }}
18
23
  max_turns: ${{ steps.classify.outputs.max_turns }}
24
+ # v0.6.25 / §5.5 Layer 1.5 (calibration) — see workflow.yml.tmpl.
25
+ turns_estimated: ${{ steps.classify.outputs.turns_estimated }}
26
+ files_count: ${{ steps.classify.outputs.files_count }}
27
+ lines_added: ${{ steps.classify.outputs.lines_added }}
28
+ lines_deleted: ${{ steps.classify.outputs.lines_deleted }}
29
+ threads_count: ${{ steps.classify.outputs.threads_count }}
19
30
  steps:
20
31
  - name: Classify PR diff
21
32
  id: classify
@@ -34,20 +45,40 @@ jobs:
34
45
  echo "is_workflow_only=false"
35
46
  echo "model=$MODEL"
36
47
  echo "max_turns=15"
48
+ echo "turns_estimated=0"
49
+ echo "files_count=0"
50
+ echo "lines_added=0"
51
+ echo "lines_deleted=0"
52
+ echo "threads_count=0"
37
53
  } >> "$GITHUB_OUTPUT"
38
54
  exit 0
39
55
  fi
40
- IS_WORKFLOW_ONLY=true
56
+ # v0.6.26 / 0.0.W² — see workflow.yml.tmpl for design notes.
57
+ ALL_IN_ALLOWLIST=true
58
+ HAS_WORKFLOW_CHANGE=false
41
59
  while IFS= read -r f; do
42
60
  case "$f" in
43
- .github/workflows/clud-bug-*.yml) ;;
44
- .github/actions/strict-mode-gate/*) ;;
45
- *) IS_WORKFLOW_ONLY=false; break ;;
61
+ .github/workflows/clud-bug-*.yml) HAS_WORKFLOW_CHANGE=true ;;
62
+ .github/actions/strict-mode-gate/*) HAS_WORKFLOW_CHANGE=true ;;
63
+ AGENTS.md) ;;
64
+ .cursorrules|.clinerules|.windsurfrules|.continuerules) ;;
65
+ .github/copilot-instructions.md) ;;
66
+ .claude/skills/.clud-bug.json) ;;
67
+ .claude/skills/critical-issues-only/SKILL.md) ;;
68
+ .claude/skills/evidence-based-review/SKILL.md) ;;
69
+ .claude/skills/respect-existing-conventions/SKILL.md) ;;
70
+ docs/timeline.md|docs/file-structure.md|docs/decisions.md) ;;
71
+ docs/decisions-branches/*.md) ;;
72
+ *) ALL_IN_ALLOWLIST=false; break ;;
46
73
  esac
47
74
  done <<< "$CHANGED"
75
+ IS_WORKFLOW_ONLY=false
76
+ if [ "$ALL_IN_ALLOWLIST" = "true" ] && [ "$HAS_WORKFLOW_CHANGE" = "true" ]; then
77
+ IS_WORKFLOW_ONLY=true
78
+ fi
48
79
  echo "is_workflow_only=$IS_WORKFLOW_ONLY" >> "$GITHUB_OUTPUT"
49
80
  if [ "$IS_WORKFLOW_ONLY" = "true" ]; then
50
- echo "::notice title=Clud Bug 🐛::Skipping LLM review — workflow-only PR."
81
+ echo "::notice title=Clud Bug 🐛::Skipping LLM review — clud-bug update output."
51
82
  echo "model=$MODEL" >> "$GITHUB_OUTPUT"
52
83
  exit 0
53
84
  fi
@@ -81,22 +112,57 @@ jobs:
81
112
  fi
82
113
  echo "model=$MODEL" >> "$GITHUB_OUTPUT"
83
114
 
84
- # Adaptive max-turns (v0.6.23 / §5) — see workflow.yml.tmpl for design notes.
85
- MAX_TURNS=15
115
+ # Smart budget (v0.6.25 / §5.5 Layer 1) — see workflow.yml.tmpl for design notes + formula.
116
+ FILE_COUNT=$(echo "$CHANGED" | wc -l | tr -d ' ')
117
+ THREAD_COUNT=$(gh api graphql -f query='{repository(owner:"'"$(echo "$REPO" | cut -d/ -f1)"'",name:"'"$(echo "$REPO" | cut -d/ -f2)"'"){pullRequest(number:'"$PR_NUMBER"'){reviewThreads(first:50){nodes{isResolved comments(first:1){nodes{author{login}}}}}}}}' --jq '[.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false and (.comments.nodes[0].author.login == "claude" or .comments.nodes[0].author.login == "claude[bot]"))] | length' 2>/dev/null || echo 0)
118
+ THREAD_COUNT=${THREAD_COUNT:-0}
119
+
86
120
  if [ "$IS_TRIVIAL" = "true" ]; then
87
121
  MAX_TURNS=10
122
+ TURNS_ESTIMATED=10
123
+ LINES_ADDED=0
124
+ LINES_DELETED=0
125
+ echo "::notice title=Clud Bug 🐛::Trivial (Haiku) budget: max_turns=10."
88
126
  else
89
- FILE_COUNT=$(echo "$CHANGED" | wc -l | tr -d ' ')
90
- THREAD_COUNT=$(gh api graphql -f query='{repository(owner:"'"$(echo "$REPO" | cut -d/ -f1)"'",name:"'"$(echo "$REPO" | cut -d/ -f2)"'"){pullRequest(number:'"$PR_NUMBER"'){reviewThreads(first:50){nodes{isResolved comments(first:1){nodes{author{login}}}}}}}}' --jq '[.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false and (.comments.nodes[0].author.login == "claude" or .comments.nodes[0].author.login == "claude[bot]"))] | length' 2>/dev/null || echo 0)
91
- THREAD_COUNT=${THREAD_COUNT:-0}
92
- if [ "$FILE_COUNT" -ge 30 ] || [ "$THREAD_COUNT" -ge 6 ]; then
93
- MAX_TURNS=40
94
- elif [ "$FILE_COUNT" -ge 10 ] || [ "$THREAD_COUNT" -ge 3 ]; then
95
- MAX_TURNS=25
96
- fi
97
- echo "::notice title=Clud Bug 🐛::Turn budget: $MAX_TURNS ($FILE_COUNT files, $THREAD_COUNT prior threads)."
127
+ FILES_JSON=$(gh pr view "$PR_NUMBER" -R "$REPO" --json files --jq '.files' 2>/dev/null || echo '[]')
128
+ LINES_ADDED=$(echo "$FILES_JSON" | jq '[.[].additions] | add // 0' 2>/dev/null || echo 0)
129
+ LINES_DELETED=$(echo "$FILES_JSON" | jq '[.[].deletions] | add // 0' 2>/dev/null || echo 0)
130
+ LINES_ADDED=${LINES_ADDED:-0}
131
+ LINES_DELETED=${LINES_DELETED:-0}
132
+
133
+ # jq implements the §5.5 Layer 1 formula — see workflow.yml.tmpl.
134
+ TURNS_ESTIMATED=$(echo "$FILES_JSON" | jq --argjson threads "$THREAD_COUNT" '
135
+ def per_line(path):
136
+ if (path | test("\\.(test|spec)\\.|__tests__|^tests?/")) then 0.01
137
+ elif (path | test("\\.(md|txt|rst|adoc|mdx)$")) then 0.00666667
138
+ elif (path | test("\\.(yml|yaml|toml|json|cfg|ini|conf|tmpl)$")) then 0.01
139
+ elif (path | test("\\.(ts|tsx|py|js|jsx|mjs|cjs|go|rs|java|kt|rb|php|cs|c|cpp|h|hpp|swift|scala)$")) then 0.02
140
+ else 0.0125 end;
141
+ map(
142
+ select(.path != "docs/timeline.md"
143
+ and .path != "docs/file-structure.md"
144
+ and .path != "docs/decisions.md")
145
+ | (.additions) as $add | (.deletions) as $del
146
+ | (if $add < $del then $add else $del end) as $mod
147
+ | ($add - $mod) as $pa | ($del - $mod) as $pd
148
+ | per_line(.path) as $tw
149
+ | 0.3 + ($pa * $tw) + ($mod * 1.5 * $tw) + ($pd * 0.1 * $tw)
150
+ ) | (add // 0) + 10 + ($threads * 1.5) | (. + 0.9999999) | floor
151
+ ' 2>/dev/null || echo 15)
152
+ TURNS_ESTIMATED=${TURNS_ESTIMATED:-15}
153
+ MAX_TURNS=$(( (TURNS_ESTIMATED * 12 + 9) / 10 ))
154
+ [ "$MAX_TURNS" -lt 15 ] && MAX_TURNS=15
155
+ [ "$MAX_TURNS" -gt 60 ] && MAX_TURNS=60
156
+ echo "::notice title=Clud Bug 🐛::Smart budget: estimated $TURNS_ESTIMATED turns → max_turns=$MAX_TURNS ($FILE_COUNT files, +$LINES_ADDED/-$LINES_DELETED lines, $THREAD_COUNT prior threads)."
98
157
  fi
99
- echo "max_turns=$MAX_TURNS" >> "$GITHUB_OUTPUT"
158
+ {
159
+ echo "max_turns=$MAX_TURNS"
160
+ echo "turns_estimated=$TURNS_ESTIMATED"
161
+ echo "files_count=$FILE_COUNT"
162
+ echo "lines_added=$LINES_ADDED"
163
+ echo "lines_deleted=$LINES_DELETED"
164
+ echo "threads_count=$THREAD_COUNT"
165
+ } >> "$GITHUB_OUTPUT"
100
166
 
101
167
  clud-bug-review:
102
168
  needs: paths-check
@@ -180,43 +246,100 @@ jobs:
180
246
  Review this pull request following the discipline in your
181
247
  system prompt — every rule about skill routing, comment
182
248
  format, the strict-mode header, the two-surface review
183
- shape, and the FIX-PUSH FLOW applies.
249
+ shape, the FIX-PUSH FLOW, and (v0.6.25+) turn-budget
250
+ self-rationing applies.
251
+
252
+ ## Your turn budget for this PR (v0.6.25 / §5.5 Layer 2)
253
+
254
+ max_turns=${{ needs.paths-check.outputs.max_turns }},
255
+ estimated=${{ needs.paths-check.outputs.turns_estimated }},
256
+ files=${{ needs.paths-check.outputs.files_count }},
257
+ +${{ needs.paths-check.outputs.lines_added }}/-${{ needs.paths-check.outputs.lines_deleted }} lines,
258
+ prior_threads=${{ needs.paths-check.outputs.threads_count }}.
259
+
260
+ Plan accordingly per the "Turn budget self-rationing"
261
+ section in your system prompt. Reserve 5 turns for emit.
184
262
  id: clud-bug-review
185
263
 
186
264
  # v0.6.22 / 0.0.O: render structured output → post via gh pr comment.
265
+ # v0.6.25 / §5.5 Layer 1.5: append calibration marker.
187
266
  - name: Render + post structured review
188
267
  if: success() && steps.clud-bug-review.outputs.structured_output != ''
189
268
  env:
190
269
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
191
270
  PR_NUMBER: ${{ github.event.pull_request.number }}
192
271
  STRUCTURED: ${{ steps.clud-bug-review.outputs.structured_output }}
272
+ TURNS_ESTIMATED: ${{ needs.paths-check.outputs.turns_estimated }}
273
+ MAX_TURNS: ${{ needs.paths-check.outputs.max_turns }}
274
+ FILES_COUNT: ${{ needs.paths-check.outputs.files_count }}
275
+ LINES_ADDED: ${{ needs.paths-check.outputs.lines_added }}
276
+ LINES_DELETED: ${{ needs.paths-check.outputs.lines_deleted }}
277
+ THREADS_COUNT: ${{ needs.paths-check.outputs.threads_count }}
193
278
  run: |
194
279
  set -euo pipefail
195
280
  BODY=$(printf '%s\n' "$STRUCTURED" | npx --yes clud-bug@{{CLUD_BUG_VERSION}} render --stdin)
196
- gh pr comment "$PR_NUMBER" --body "$BODY"
281
+ CALIBRATION="<!-- clud-bug-calibration: turns_estimated=$TURNS_ESTIMATED, max_turns=$MAX_TURNS, files=$FILES_COUNT, lines_added=$LINES_ADDED, lines_deleted=$LINES_DELETED, threads=$THREADS_COUNT -->"
282
+ gh pr comment "$PR_NUMBER" --body "$BODY
197
283
 
198
- # Fallback when structured_output is empty (max-retries hit).
284
+ $CALIBRATION"
285
+
286
+ # v0.6.26 / §5.5 Layer 6 fallback render-from-inlines — see workflow.yml.tmpl for design notes.
199
287
  - name: Fallback summary (structured_output empty)
200
288
  if: success() && steps.clud-bug-review.outputs.structured_output == ''
201
289
  env:
202
290
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
203
291
  PR_NUMBER: ${{ github.event.pull_request.number }}
292
+ REPO: ${{ github.repository }}
293
+ HEAD_SHA: ${{ github.event.pull_request.head.sha }}
294
+ TURNS_ESTIMATED: ${{ needs.paths-check.outputs.turns_estimated }}
295
+ MAX_TURNS: ${{ needs.paths-check.outputs.max_turns }}
296
+ FILES_COUNT: ${{ needs.paths-check.outputs.files_count }}
297
+ LINES_ADDED: ${{ needs.paths-check.outputs.lines_added }}
298
+ LINES_DELETED: ${{ needs.paths-check.outputs.lines_deleted }}
299
+ THREADS_COUNT: ${{ needs.paths-check.outputs.threads_count }}
204
300
  run: |
205
301
  set -euo pipefail
206
- gh pr comment "$PR_NUMBER" --body "## 🐛 Clud Bug review
302
+ INLINES=$(gh api "repos/$REPO/pulls/$PR_NUMBER/comments?per_page=100" \
303
+ --jq "[.[] | select(.user.login == \"claude[bot]\" and .commit_id == \"$HEAD_SHA\")]")
304
+ INLINE_COUNT=$(echo "$INLINES" | jq 'length')
305
+ CRITICAL=$(echo "$INLINES" | jq '[.[] | select(.body | test("🔴"))] | length')
306
+ MINOR=$(echo "$INLINES" | jq '[.[] | select(.body | test("🟡"))] | length')
307
+ PREEXISTING=$(echo "$INLINES" | jq '[.[] | select(.body | test("🟣"))] | length')
308
+ CALIBRATION="<!-- clud-bug-calibration: turns_estimated=$TURNS_ESTIMATED, max_turns=$MAX_TURNS, files=$FILES_COUNT, lines_added=$LINES_ADDED, lines_deleted=$LINES_DELETED, threads=$THREADS_COUNT, structured_output=empty, inline_findings=$INLINE_COUNT -->"
309
+ if [ "$INLINE_COUNT" -gt 0 ]; then
310
+ STATUS=""
311
+ [ "$CRITICAL" -gt 0 ] && STATUS=" — critical findings"
312
+ gh pr comment "$PR_NUMBER" --body "## 🐛 Clud Bug review${STATUS}
313
+
314
+ **This round:** $CRITICAL critical · $MINOR minor · 0 resolved from prior · 0 still open
315
+
316
+ Found: $CRITICAL 🔴 / $MINOR 🟡 / $PREEXISTING 🟣
317
+
318
+ ⚠️ **Synthetic summary** (v0.6.26 §5.5 Layer 6 fallback). Inline findings above ARE the substantive review.
319
+
320
+ <!-- last-reviewed-sha: $HEAD_SHA -->
321
+
322
+ $CALIBRATION"
323
+ else
324
+ gh pr comment "$PR_NUMBER" --body "## 🐛 Clud Bug review
207
325
 
208
326
  **This round:** 0 critical · 0 minor · 0 resolved from prior · 0 still open
209
327
 
210
328
  Found: 0 🔴 / 0 🟡 / 0 🟣
211
329
 
212
- ⚠️ Structured output (\`--json-schema\`) returned empty likely max-retries hit on schema validation. Investigate the action logs.
330
+ ⚠️ Structured output (\`--json-schema\`) returned empty AND no inline findings posted. Investigate run logs.
331
+
332
+ Skills referenced: [none]
213
333
 
214
- Skills referenced: [none]"
334
+ <!-- last-reviewed-sha: $HEAD_SHA -->
335
+
336
+ $CALIBRATION"
337
+ fi
215
338
 
216
339
  # Strict-mode gate — composite action; see workflow.yml.tmpl for design notes.
217
340
  - name: Strict mode — fail check on critical findings
218
341
  if: success()
219
- uses: thrillmade/clud-bug/.github/actions/strict-mode-gate@v0.6.24
342
+ uses: thrillmade/clud-bug/.github/actions/strict-mode-gate@v0.6.26
220
343
  with:
221
344
  github-token: ${{ secrets.GITHUB_TOKEN }}
222
345
  # v0.6.22 / 0.0.O: summary now posted by github-actions[bot].
@@ -1,20 +1,62 @@
1
- # clud-bug-template-version: v10
1
+ # clud-bug-template-version: v12
2
2
  name: Clud Bug 🐛 Crawls Your Code
3
3
 
4
4
  on:
5
5
  pull_request:
6
6
  types: [opened, synchronize]
7
7
 
8
+ # v0.6.25: workflow-level concurrency — newer pushes cancel older
9
+ # in-flight runs on the same PR. Avoids the duplicate-review failure
10
+ # mode hit on tokenomics #21 where my merge-of-main and logmind's
11
+ # auto-regen-derived-docs follow-up fired two concurrent reviews
12
+ # (each posted its own "in-progress" todo comment).
13
+ concurrency:
14
+ group: clud-bug-review-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
15
+ cancel-in-progress: true
16
+
8
17
  jobs:
9
18
  # Pre-flight: classify the PR diff to decide (a) whether to skip the
10
19
  # LLM review entirely, and (b) which model to route to.
11
20
  #
12
- # (a) Workflow-only PRs (v0.6.14 / 0.0.W): if every changed file
13
- # matches `.github/workflows/clud-bug-*.yml` or
14
- # `.github/actions/strict-mode-gate/**`, the LLM call is skipped
15
- # entirely claude-code-action would refuse anyway (self-modification
16
- # guard), and template re-renders have no useful review surface.
17
- # Skipping converts what were admin-bypass merges into normal ones.
21
+ # (a) Workflow-only / clud-bug-update PRs (v0.6.14 / 0.0.W;
22
+ # v0.6.26 / 0.0.W²): the LLM call is skipped when EITHER condition:
23
+ #
24
+ # - Every changed file matches `.github/workflows/clud-bug-*.yml`
25
+ # or `.github/actions/strict-mode-gate/**` (original 0.0.W
26
+ # pure workflow PRs that claude-code-action would refuse anyway
27
+ # via its self-modification guard).
28
+ #
29
+ # - The PR is a recognized `clud-bug update` propagation: every
30
+ # changed file is in the clud-bug-update output allowlist AND a
31
+ # workflow file (or strict-mode-gate composite) is part of the
32
+ # change. v0.6.26 / 0.0.W² added this branch so the propagation
33
+ # dance doesn't require admin-bypass every cycle.
34
+ #
35
+ # Allowlist (files `clud-bug update` legitimately touches):
36
+ # .github/workflows/clud-bug-*.yml (workflow re-render)
37
+ # .github/actions/strict-mode-gate/* (composite re-render)
38
+ # AGENTS.md (logmind block + clud-bug stanza)
39
+ # .cursorrules / .clinerules / .windsurfrules / .continuerules
40
+ # .github/copilot-instructions.md
41
+ # .claude/skills/.clud-bug.json (manifest)
42
+ # .claude/skills/{critical-issues-only,evidence-based-review,
43
+ # respect-existing-conventions}/SKILL.md (baselines)
44
+ # docs/timeline.md / docs/file-structure.md / docs/decisions.md
45
+ # docs/decisions-branches/*.md (logmind side-effects)
46
+ #
47
+ # The workflow-change requirement is the SIGNATURE that distinguishes
48
+ # "agent ran clud-bug update" from "user is editing AGENTS.md by hand."
49
+ # An AGENTS.md-only PR (no workflow change) still goes through normal
50
+ # review — this catches the prompt-injection-via-AGENTS.md attack
51
+ # surface that a naive allowlist would open.
52
+ #
53
+ # Safety envelope:
54
+ # - Workflow file is still protected by the App-side guard (it just
55
+ # never runs in this branch).
56
+ # - Files in the allowlist are non-executable; modifying them can't
57
+ # grant code execution.
58
+ # - strictMode toggle is read from BASE ref so a PR can't disable
59
+ # strict-mode on itself.
18
60
  #
19
61
  # (b) Trivial PRs (v0.6.15 / 0.0.R): if the PR author is a dep-bumping
20
62
  # bot (dependabot, renovate) OR the diff is small (<2KB) AND only
@@ -30,6 +72,17 @@ jobs:
30
72
  is_workflow_only: ${{ steps.classify.outputs.is_workflow_only }}
31
73
  model: ${{ steps.classify.outputs.model }}
32
74
  max_turns: ${{ steps.classify.outputs.max_turns }}
75
+ # v0.6.25 / §5.5 Layer 1.5 (calibration measurement): emit the
76
+ # raw estimated-turns alongside the actual max_turns the workflow
77
+ # passes to claude-code-action. The post-step records both into
78
+ # the summary comment as a hidden HTML marker so we can tune the
79
+ # per-file coefficients (Layer 1) from real consumer-review data
80
+ # over the Step 4 30-day window. Re-tune in v0.6.26+.
81
+ turns_estimated: ${{ steps.classify.outputs.turns_estimated }}
82
+ files_count: ${{ steps.classify.outputs.files_count }}
83
+ lines_added: ${{ steps.classify.outputs.lines_added }}
84
+ lines_deleted: ${{ steps.classify.outputs.lines_deleted }}
85
+ threads_count: ${{ steps.classify.outputs.threads_count }}
33
86
  steps:
34
87
  - name: Classify PR diff
35
88
  id: classify
@@ -53,22 +106,58 @@ jobs:
53
106
  echo "is_workflow_only=false"
54
107
  echo "model=$MODEL"
55
108
  echo "max_turns=15"
109
+ echo "turns_estimated=0"
110
+ echo "files_count=0"
111
+ echo "lines_added=0"
112
+ echo "lines_deleted=0"
113
+ echo "threads_count=0"
56
114
  } >> "$GITHUB_OUTPUT"
57
115
  exit 0
58
116
  fi
59
117
 
60
- # --- (a) workflow-only classifier ---
61
- IS_WORKFLOW_ONLY=true
118
+ # --- (a) workflow-only / clud-bug-update classifier
119
+ # (v0.6.14 / 0.0.W; widened in v0.6.26 / 0.0.W²) ---
120
+ # Two-track check:
121
+ # ALL_IN_ALLOWLIST = every changed file is in the
122
+ # clud-bug-update output allowlist (see header design notes
123
+ # for the full list + rationale).
124
+ # HAS_WORKFLOW_CHANGE = at least one workflow-file or
125
+ # strict-mode-gate change is present. This is the signature
126
+ # that distinguishes "clud-bug update output" from "user
127
+ # edited AGENTS.md by hand" — naked AGENTS.md edits go
128
+ # through normal review.
129
+ #
130
+ # Skip when both are true. Backward-compat with v0.6.14's
131
+ # `is_workflow_only` output name preserved (semantic widened to
132
+ # "skip-review-eligible"; the dependent clud-bug-review job
133
+ # gates on this output without needing a rename).
134
+ ALL_IN_ALLOWLIST=true
135
+ HAS_WORKFLOW_CHANGE=false
62
136
  while IFS= read -r f; do
63
137
  case "$f" in
64
- .github/workflows/clud-bug-*.yml) ;;
65
- .github/actions/strict-mode-gate/*) ;;
66
- *) IS_WORKFLOW_ONLY=false; break ;;
138
+ .github/workflows/clud-bug-*.yml) HAS_WORKFLOW_CHANGE=true ;;
139
+ .github/actions/strict-mode-gate/*) HAS_WORKFLOW_CHANGE=true ;;
140
+ # v0.6.26 / 0.0.W² additions — files clud-bug update produces.
141
+ AGENTS.md) ;;
142
+ .cursorrules|.clinerules|.windsurfrules|.continuerules) ;;
143
+ .github/copilot-instructions.md) ;;
144
+ .claude/skills/.clud-bug.json) ;;
145
+ .claude/skills/critical-issues-only/SKILL.md) ;;
146
+ .claude/skills/evidence-based-review/SKILL.md) ;;
147
+ .claude/skills/respect-existing-conventions/SKILL.md) ;;
148
+ # logmind side-effects of `logmind log` on the same commit.
149
+ docs/timeline.md|docs/file-structure.md|docs/decisions.md) ;;
150
+ docs/decisions-branches/*.md) ;;
151
+ *) ALL_IN_ALLOWLIST=false; break ;;
67
152
  esac
68
153
  done <<< "$CHANGED"
154
+ IS_WORKFLOW_ONLY=false
155
+ if [ "$ALL_IN_ALLOWLIST" = "true" ] && [ "$HAS_WORKFLOW_CHANGE" = "true" ]; then
156
+ IS_WORKFLOW_ONLY=true
157
+ fi
69
158
  echo "is_workflow_only=$IS_WORKFLOW_ONLY" >> "$GITHUB_OUTPUT"
70
159
  if [ "$IS_WORKFLOW_ONLY" = "true" ]; then
71
- echo "::notice title=Clud Bug 🐛::Skipping LLM review — PR only touches workflow files."
160
+ echo "::notice title=Clud Bug 🐛::Skipping LLM review — clud-bug update output (workflow + allowlist files, no review surface)."
72
161
  echo "model=$MODEL" >> "$GITHUB_OUTPUT"
73
162
  exit 0
74
163
  fi
@@ -113,37 +202,106 @@ jobs:
113
202
  fi
114
203
  echo "model=$MODEL" >> "$GITHUB_OUTPUT"
115
204
 
116
- # --- (c) adaptive max-turns (v0.6.23 / §5) ---
117
- # Scope-based turn budget so large PRs (many files OR many prior
118
- # unresolved threads to walk in FIX-PUSH FLOW) don't exhaust the
119
- # default 15-turn budget. Concrete failure that motivated this:
120
- # tokenomics PR #18 (23 docs files + 6 prior claude[bot] threads)
121
- # exhausted the cap under v0.6.12 AND under v0.6.22's
122
- # structured-output flow.
205
+ # --- (c) smart budget estimation (v0.6.25 / §5.5 Layer 1) ---
206
+ # Replaces v0.6.23's 4-bucket if-elif with a line-based
207
+ # formula. File count was a crude proxy; lines + edit-type +
208
+ # file-class is much better. Per-file cost in turns:
209
+ #
210
+ # per_file_cost = 0.3 + added × tw × 1.0
211
+ # + modified × tw × 1.5 # context-heavy
212
+ # + deleted × tw × 0.1 # trivial
123
213
  #
124
- # Buckets:
125
- # Trivial (Haiku) 10
126
- # Standard (<10 files AND <3 prior threads) → 15 (current default)
127
- # Larger (≥10 files OR ≥3 prior threads) → 25
128
- # Very large (≥30 files OR ≥6 prior threads) → 40
129
- MAX_TURNS=15
214
+ # type_weight tw (turns per line):
215
+ # code (.ts/.py/.js/.go/.rs/.java/...) : 1/50
216
+ # docs (.md/.txt/.rst/.adoc/...) : 1/150
217
+ # tests (.test.*/.spec.*/__tests__/*) : 1/100
218
+ # config (.yml/.toml/.json/.cfg) : 1/100
219
+ # derived (timeline.md/file-structure.md/decisions.md) : 0
220
+ #
221
+ # estimated_turns = 10 + sum(per_file_cost) + 1.5 × prior_threads
222
+ # max_turns = max(estimated × 1.2, 15) # 20% safety margin
223
+ # max_turns = min(max_turns, 60) # ceiling; L5 retry above
224
+ #
225
+ # Emit overhead 10 (raised from initial 5 design): tokenomics
226
+ # #21 (26 docs files, +310/-235) used ~25 turns vs old-formula
227
+ # 16 — structured-output emit + JSON-schema retries + initial
228
+ # context loading cost ~5-10 turns themselves. Will retune from
229
+ # Layer 1.5 calibration data in v0.6.26+.
230
+ #
231
+ # `gh` is the source for additions/deletions + threads.
232
+ # `jq` is the inline estimator (preinstalled on ubuntu-latest
233
+ # runners; an earlier design used python3 but jq sidesteps the
234
+ # YAML-block-indent vs heredoc-content-indent dance).
235
+ # Calibration via the turns_estimated + turns_actually_used data
236
+ # points the post-step records (Layer 1.5).
237
+ FILE_COUNT=$(echo "$CHANGED" | wc -l | tr -d ' ')
238
+ # Count unresolved claude-bot threads. Best-effort: rate-limit
239
+ # or auth failures default to 0 (no escalation, fall back to
240
+ # the line-count-only estimate).
241
+ THREAD_COUNT=$(gh api graphql -f query='{repository(owner:"'"$(echo "$REPO" | cut -d/ -f1)"'",name:"'"$(echo "$REPO" | cut -d/ -f2)"'"){pullRequest(number:'"$PR_NUMBER"'){reviewThreads(first:50){nodes{isResolved comments(first:1){nodes{author{login}}}}}}}}' --jq '[.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false and (.comments.nodes[0].author.login == "claude" or .comments.nodes[0].author.login == "claude[bot]"))] | length' 2>/dev/null || echo 0)
242
+ THREAD_COUNT=${THREAD_COUNT:-0}
243
+
130
244
  if [ "$IS_TRIVIAL" = "true" ]; then
245
+ # Haiku route stays on a small flat budget — trivial PRs
246
+ # need ~5 turns; 10 leaves margin.
131
247
  MAX_TURNS=10
248
+ TURNS_ESTIMATED=10
249
+ LINES_ADDED=0
250
+ LINES_DELETED=0
251
+ echo "::notice title=Clud Bug 🐛::Trivial (Haiku) budget: max_turns=10."
132
252
  else
133
- FILE_COUNT=$(echo "$CHANGED" | wc -l | tr -d ' ')
134
- # Count unresolved claude-bot threads. Best-effort: rate-limit
135
- # or auth failures default to 0 (no escalation, fall back to
136
- # file-count tier).
137
- THREAD_COUNT=$(gh api graphql -f query='{repository(owner:"'"$(echo "$REPO" | cut -d/ -f1)"'",name:"'"$(echo "$REPO" | cut -d/ -f2)"'"){pullRequest(number:'"$PR_NUMBER"'){reviewThreads(first:50){nodes{isResolved comments(first:1){nodes{author{login}}}}}}}}' --jq '[.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false and (.comments.nodes[0].author.login == "claude" or .comments.nodes[0].author.login == "claude[bot]"))] | length' 2>/dev/null || echo 0)
138
- THREAD_COUNT=${THREAD_COUNT:-0}
139
- if [ "$FILE_COUNT" -ge 30 ] || [ "$THREAD_COUNT" -ge 6 ]; then
140
- MAX_TURNS=40
141
- elif [ "$FILE_COUNT" -ge 10 ] || [ "$THREAD_COUNT" -ge 3 ]; then
142
- MAX_TURNS=25
143
- fi
144
- echo "::notice title=Clud Bug 🐛::Turn budget: $MAX_TURNS ($FILE_COUNT files, $THREAD_COUNT prior unresolved claude threads)."
253
+ FILES_JSON=$(gh pr view "$PR_NUMBER" -R "$REPO" --json files --jq '.files' 2>/dev/null || echo '[]')
254
+ LINES_ADDED=$(echo "$FILES_JSON" | jq '[.[].additions] | add // 0' 2>/dev/null || echo 0)
255
+ LINES_DELETED=$(echo "$FILES_JSON" | jq '[.[].deletions] | add // 0' 2>/dev/null || echo 0)
256
+ LINES_ADDED=${LINES_ADDED:-0}
257
+ LINES_DELETED=${LINES_DELETED:-0}
258
+
259
+ # jq implements the formula from §5.5 Layer 1:
260
+ # per_file_cost = 0.3 + pa×tw + mod×1.5×tw + pd×0.1×tw
261
+ # estimated = 10 + sum(per_file_cost) + 1.5×threads
262
+ # type_weight tw (turns per line; reciprocal of lines-per-turn):
263
+ # code 1/50 = 0.02, docs 1/150 ≈ 0.00667,
264
+ # tests/config 1/100 = 0.01, unknown 1/80 = 0.0125
265
+ # Emit overhead 10 (raised from 5 in initial design): empirical
266
+ # tokenomics #21 (26 docs files, +310/-235) used ~25 turns vs
267
+ # formula-predicted 16 — structured-output emit + JSON-schema
268
+ # retries + initial context loading cost ~5-10 turns themselves.
269
+ # 10 matches better; will retune from Layer 1.5 calibration data.
270
+ # ceil via (+ 0.9999999 | floor) since jq has no ceil.
271
+ TURNS_ESTIMATED=$(echo "$FILES_JSON" | jq --argjson threads "$THREAD_COUNT" '
272
+ def per_line(path):
273
+ if (path | test("\\.(test|spec)\\.|__tests__|^tests?/")) then 0.01
274
+ elif (path | test("\\.(md|txt|rst|adoc|mdx)$")) then 0.00666667
275
+ elif (path | test("\\.(yml|yaml|toml|json|cfg|ini|conf|tmpl)$")) then 0.01
276
+ elif (path | test("\\.(ts|tsx|py|js|jsx|mjs|cjs|go|rs|java|kt|rb|php|cs|c|cpp|h|hpp|swift|scala)$")) then 0.02
277
+ else 0.0125 end;
278
+ map(
279
+ select(.path != "docs/timeline.md"
280
+ and .path != "docs/file-structure.md"
281
+ and .path != "docs/decisions.md")
282
+ | (.additions) as $add | (.deletions) as $del
283
+ | (if $add < $del then $add else $del end) as $mod
284
+ | ($add - $mod) as $pa | ($del - $mod) as $pd
285
+ | per_line(.path) as $tw
286
+ | 0.3 + ($pa * $tw) + ($mod * 1.5 * $tw) + ($pd * 0.1 * $tw)
287
+ ) | (add // 0) + 10 + ($threads * 1.5) | (. + 0.9999999) | floor
288
+ ' 2>/dev/null || echo 15)
289
+ TURNS_ESTIMATED=${TURNS_ESTIMATED:-15}
290
+
291
+ # max_turns = ceil(estimated × 1.2), floor 15, ceiling 60
292
+ MAX_TURNS=$(( (TURNS_ESTIMATED * 12 + 9) / 10 ))
293
+ [ "$MAX_TURNS" -lt 15 ] && MAX_TURNS=15
294
+ [ "$MAX_TURNS" -gt 60 ] && MAX_TURNS=60
295
+ echo "::notice title=Clud Bug 🐛::Smart budget: estimated $TURNS_ESTIMATED turns → max_turns=$MAX_TURNS ($FILE_COUNT files, +$LINES_ADDED/-$LINES_DELETED lines, $THREAD_COUNT prior threads). v0.6.25 Phase 1; coefficients TBD from calibration data."
145
296
  fi
146
- echo "max_turns=$MAX_TURNS" >> "$GITHUB_OUTPUT"
297
+ {
298
+ echo "max_turns=$MAX_TURNS"
299
+ echo "turns_estimated=$TURNS_ESTIMATED"
300
+ echo "files_count=$FILE_COUNT"
301
+ echo "lines_added=$LINES_ADDED"
302
+ echo "lines_deleted=$LINES_DELETED"
303
+ echo "threads_count=$THREAD_COUNT"
304
+ } >> "$GITHUB_OUTPUT"
147
305
 
148
306
  clud-bug-review:
149
307
  needs: paths-check
@@ -288,44 +446,133 @@ jobs:
288
446
  Review this pull request following the discipline in your
289
447
  system prompt — every rule about skill routing, comment
290
448
  format, the strict-mode header, the two-surface review
291
- shape, and the FIX-PUSH FLOW applies.
449
+ shape, the FIX-PUSH FLOW, and (v0.6.25+) turn-budget
450
+ self-rationing applies.
451
+
452
+ ## Your turn budget for this PR (v0.6.25 / §5.5 Layer 2)
453
+
454
+ max_turns=${{ needs.paths-check.outputs.max_turns }},
455
+ estimated=${{ needs.paths-check.outputs.turns_estimated }},
456
+ files=${{ needs.paths-check.outputs.files_count }},
457
+ +${{ needs.paths-check.outputs.lines_added }}/-${{ needs.paths-check.outputs.lines_deleted }} lines,
458
+ prior_threads=${{ needs.paths-check.outputs.threads_count }}.
459
+
460
+ Plan accordingly per the "Turn budget self-rationing"
461
+ section in your system prompt. The estimate is the
462
+ pre-flight's best guess; the cap is the workflow's hard
463
+ stop. Reserve 5 turns for emit.
292
464
  id: clud-bug-review
293
465
 
294
466
  # v0.6.22 / 0.0.O: render the structured output to the summary
295
467
  # comment shape, post via gh pr comment. Guarded so we only run
296
468
  # when the action returned a non-empty structured payload
297
469
  # (max-retries hit → empty → fall through to the next step).
470
+ #
471
+ # v0.6.25 / §5.5 Layer 1.5 (calibration measurement): append a
472
+ # hidden HTML marker carrying the pre-flight estimate + the
473
+ # actual max_turns we passed to claude-code-action. Aggregation
474
+ # script lives in clud-bug usage --calibration (TBD v0.6.26+);
475
+ # for now, the marker is just an audit trail visible via
476
+ # `gh api ...issues/N/comments`.
298
477
  - name: Render + post structured review
299
478
  if: success() && steps.clud-bug-review.outputs.structured_output != ''
300
479
  env:
301
480
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
302
481
  PR_NUMBER: ${{ github.event.pull_request.number }}
303
482
  STRUCTURED: ${{ steps.clud-bug-review.outputs.structured_output }}
483
+ TURNS_ESTIMATED: ${{ needs.paths-check.outputs.turns_estimated }}
484
+ MAX_TURNS: ${{ needs.paths-check.outputs.max_turns }}
485
+ FILES_COUNT: ${{ needs.paths-check.outputs.files_count }}
486
+ LINES_ADDED: ${{ needs.paths-check.outputs.lines_added }}
487
+ LINES_DELETED: ${{ needs.paths-check.outputs.lines_deleted }}
488
+ THREADS_COUNT: ${{ needs.paths-check.outputs.threads_count }}
304
489
  run: |
305
490
  set -euo pipefail
306
491
  BODY=$(printf '%s\n' "$STRUCTURED" | npx --yes clud-bug@{{CLUD_BUG_VERSION}} render --stdin)
307
- gh pr comment "$PR_NUMBER" --body "$BODY"
492
+ CALIBRATION="<!-- clud-bug-calibration: turns_estimated=$TURNS_ESTIMATED, max_turns=$MAX_TURNS, files=$FILES_COUNT, lines_added=$LINES_ADDED, lines_deleted=$LINES_DELETED, threads=$THREADS_COUNT -->"
493
+ gh pr comment "$PR_NUMBER" --body "$BODY
494
+
495
+ $CALIBRATION"
308
496
 
309
497
  # Fallback comment when the model couldn't produce schema-valid
310
498
  # output after max retries (structured_output is empty). Keeps a
311
499
  # bare H2 header so the strict-mode gate sees a comment and falls
312
500
  # open (advisory) rather than panicking on a missing summary.
501
+ # v0.6.26 / §5.5 Layer 6: when structured_output is empty BUT inline
502
+ # findings were posted before the budget exhausted (tokenomics #21
503
+ # pattern), scrape the inline findings via gh api and render a
504
+ # synthetic summary that cites the real findings. Avoids the
505
+ # "0-findings-shown / 5-inline-posted" failure mode where the
506
+ # maintainer reading the PR sees a meaningless empty summary even
507
+ # though the review actually surfaced issues.
508
+ #
509
+ # If NO inline findings were posted either, falls through to the
510
+ # original advisory bare-H2 (legacy v0.6.22 behaviour).
313
511
  - name: Fallback summary (structured_output empty)
314
512
  if: success() && steps.clud-bug-review.outputs.structured_output == ''
315
513
  env:
316
514
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
317
515
  PR_NUMBER: ${{ github.event.pull_request.number }}
516
+ REPO: ${{ github.repository }}
517
+ HEAD_SHA: ${{ github.event.pull_request.head.sha }}
518
+ TURNS_ESTIMATED: ${{ needs.paths-check.outputs.turns_estimated }}
519
+ MAX_TURNS: ${{ needs.paths-check.outputs.max_turns }}
520
+ FILES_COUNT: ${{ needs.paths-check.outputs.files_count }}
521
+ LINES_ADDED: ${{ needs.paths-check.outputs.lines_added }}
522
+ LINES_DELETED: ${{ needs.paths-check.outputs.lines_deleted }}
523
+ THREADS_COUNT: ${{ needs.paths-check.outputs.threads_count }}
318
524
  run: |
319
525
  set -euo pipefail
320
- gh pr comment "$PR_NUMBER" --body "## 🐛 Clud Bug review
526
+
527
+ # Layer 6: count inline findings claude[bot] posted on THIS
528
+ # head SHA via the inline-review-thread endpoint. Filter to
529
+ # the current SHA so older review-pass findings don't inflate
530
+ # the count.
531
+ INLINES=$(gh api "repos/$REPO/pulls/$PR_NUMBER/comments?per_page=100" \
532
+ --jq "[.[] | select(.user.login == \"claude[bot]\" and .commit_id == \"$HEAD_SHA\")]")
533
+ INLINE_COUNT=$(echo "$INLINES" | jq 'length')
534
+ CRITICAL=$(echo "$INLINES" | jq '[.[] | select(.body | test("🔴"))] | length')
535
+ MINOR=$(echo "$INLINES" | jq '[.[] | select(.body | test("🟡"))] | length')
536
+ PREEXISTING=$(echo "$INLINES" | jq '[.[] | select(.body | test("🟣"))] | length')
537
+ # Findings with no emoji prefix (cross-cutting / un-prefixed) — count once.
538
+ UNPREFIXED=$(( INLINE_COUNT - CRITICAL - MINOR - PREEXISTING ))
539
+ [ "$UNPREFIXED" -lt 0 ] && UNPREFIXED=0
540
+
541
+ CALIBRATION="<!-- clud-bug-calibration: turns_estimated=$TURNS_ESTIMATED, max_turns=$MAX_TURNS, files=$FILES_COUNT, lines_added=$LINES_ADDED, lines_deleted=$LINES_DELETED, threads=$THREADS_COUNT, structured_output=empty, inline_findings=$INLINE_COUNT -->"
542
+
543
+ if [ "$INLINE_COUNT" -gt 0 ]; then
544
+ # L6 synthetic summary — cites the real inline findings the
545
+ # action managed to post before structured-output emit failed.
546
+ STATUS=""
547
+ [ "$CRITICAL" -gt 0 ] && STATUS=" — critical findings"
548
+ gh pr comment "$PR_NUMBER" --body "## 🐛 Clud Bug review${STATUS}
549
+
550
+ **This round:** $CRITICAL critical · $MINOR minor · 0 resolved from prior · 0 still open
551
+
552
+ Found: $CRITICAL 🔴 / $MINOR 🟡 / $PREEXISTING 🟣
553
+
554
+ ⚠️ **Synthetic summary** (v0.6.26 §5.5 Layer 6 fallback) — the action posted $INLINE_COUNT inline finding(s) before the structured-output emit step exhausted its turn budget (or hit schema-validation retries). The inline findings above ARE the substantive review; this summary is reconstructed from them. v0.6.26's Layer 5 (auto-retry on cap-hit) is the more permanent fix; this is the safety net.
555
+
556
+ <!-- last-reviewed-sha: $HEAD_SHA -->
557
+
558
+ $CALIBRATION"
559
+ else
560
+ # No inline findings either — legacy bare-H2 advisory. The
561
+ # action errored before it could post anything substantive.
562
+ gh pr comment "$PR_NUMBER" --body "## 🐛 Clud Bug review
321
563
 
322
564
  **This round:** 0 critical · 0 minor · 0 resolved from prior · 0 still open
323
565
 
324
566
  Found: 0 🔴 / 0 🟡 / 0 🟣
325
567
 
326
- ⚠️ Structured output (\`--json-schema\`) returned empty likely max-retries hit on schema validation. Investigate the action logs.
568
+ ⚠️ Structured output (\`--json-schema\`) returned empty AND no inline findings were posted. The action likely errored before producing review output investigate the run logs.
569
+
570
+ Skills referenced: [none]
327
571
 
328
- Skills referenced: [none]"
572
+ <!-- last-reviewed-sha: $HEAD_SHA -->
573
+
574
+ $CALIBRATION"
575
+ fi
329
576
 
330
577
  # Strict-mode gate. Fails the check when the BASE ref's manifest
331
578
  # has { "strictMode": true } AND the latest clud-bug review's first
@@ -342,7 +589,7 @@ jobs:
342
589
  # Letting the action's own failure fail the check is louder and right.
343
590
  - name: Strict mode — fail check on critical findings
344
591
  if: success()
345
- uses: thrillmade/clud-bug/.github/actions/strict-mode-gate@v0.6.24
592
+ uses: thrillmade/clud-bug/.github/actions/strict-mode-gate@v0.6.26
346
593
  with:
347
594
  github-token: ${{ secrets.GITHUB_TOKEN }}
348
595
  # v0.6.22 / 0.0.O: the summary is now posted by the workflow