clud-bug 0.6.23 → 0.6.25

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.23",
3
+ "version": "0.6.25",
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: v11
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
@@ -28,10 +39,18 @@ jobs:
28
39
  CHANGED=$(gh pr diff "$PR_NUMBER" -R "$REPO" --name-only)
29
40
  MODEL=claude-sonnet-4-6
30
41
  if [ -z "$CHANGED" ]; then
31
- echo "is_workflow_only=false" >> "$GITHUB_OUTPUT"
32
- echo "model=$MODEL" >> "$GITHUB_OUTPUT"
33
42
  # v0.6.23 / §5: max_turns must always be emitted — see workflow.yml.tmpl for design notes.
34
- echo "max_turns=15" >> "$GITHUB_OUTPUT"
43
+ # Grouped redirect (v0.6.24) silences the SC2129 style warning.
44
+ {
45
+ echo "is_workflow_only=false"
46
+ echo "model=$MODEL"
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"
53
+ } >> "$GITHUB_OUTPUT"
35
54
  exit 0
36
55
  fi
37
56
  IS_WORKFLOW_ONLY=true
@@ -78,22 +97,57 @@ jobs:
78
97
  fi
79
98
  echo "model=$MODEL" >> "$GITHUB_OUTPUT"
80
99
 
81
- # Adaptive max-turns (v0.6.23 / §5) — see workflow.yml.tmpl for design notes.
82
- MAX_TURNS=15
100
+ # Smart budget (v0.6.25 / §5.5 Layer 1) — see workflow.yml.tmpl for design notes + formula.
101
+ FILE_COUNT=$(echo "$CHANGED" | wc -l | tr -d ' ')
102
+ 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)
103
+ THREAD_COUNT=${THREAD_COUNT:-0}
104
+
83
105
  if [ "$IS_TRIVIAL" = "true" ]; then
84
106
  MAX_TURNS=10
107
+ TURNS_ESTIMATED=10
108
+ LINES_ADDED=0
109
+ LINES_DELETED=0
110
+ echo "::notice title=Clud Bug 🐛::Trivial (Haiku) budget: max_turns=10."
85
111
  else
86
- FILE_COUNT=$(echo "$CHANGED" | wc -l | tr -d ' ')
87
- 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)
88
- THREAD_COUNT=${THREAD_COUNT:-0}
89
- if [ "$FILE_COUNT" -ge 30 ] || [ "$THREAD_COUNT" -ge 6 ]; then
90
- MAX_TURNS=40
91
- elif [ "$FILE_COUNT" -ge 10 ] || [ "$THREAD_COUNT" -ge 3 ]; then
92
- MAX_TURNS=25
93
- fi
94
- echo "::notice title=Clud Bug 🐛::Turn budget: $MAX_TURNS ($FILE_COUNT files, $THREAD_COUNT prior threads)."
112
+ FILES_JSON=$(gh pr view "$PR_NUMBER" -R "$REPO" --json files --jq '.files' 2>/dev/null || echo '[]')
113
+ LINES_ADDED=$(echo "$FILES_JSON" | jq '[.[].additions] | add // 0' 2>/dev/null || echo 0)
114
+ LINES_DELETED=$(echo "$FILES_JSON" | jq '[.[].deletions] | add // 0' 2>/dev/null || echo 0)
115
+ LINES_ADDED=${LINES_ADDED:-0}
116
+ LINES_DELETED=${LINES_DELETED:-0}
117
+
118
+ # jq implements the §5.5 Layer 1 formula — see workflow.yml.tmpl.
119
+ TURNS_ESTIMATED=$(echo "$FILES_JSON" | jq --argjson threads "$THREAD_COUNT" '
120
+ def per_line(path):
121
+ if (path | test("\\.(test|spec)\\.|__tests__|^tests?/")) then 0.01
122
+ elif (path | test("\\.(md|txt|rst|adoc|mdx)$")) then 0.00666667
123
+ elif (path | test("\\.(yml|yaml|toml|json|cfg|ini|conf|tmpl)$")) then 0.01
124
+ 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
125
+ else 0.0125 end;
126
+ map(
127
+ select(.path != "docs/timeline.md"
128
+ and .path != "docs/file-structure.md"
129
+ and .path != "docs/decisions.md")
130
+ | (.additions) as $add | (.deletions) as $del
131
+ | (if $add < $del then $add else $del end) as $mod
132
+ | ($add - $mod) as $pa | ($del - $mod) as $pd
133
+ | per_line(.path) as $tw
134
+ | 0.3 + ($pa * $tw) + ($mod * 1.5 * $tw) + ($pd * 0.1 * $tw)
135
+ ) | (add // 0) + 10 + ($threads * 1.5) | (. + 0.9999999) | floor
136
+ ' 2>/dev/null || echo 15)
137
+ TURNS_ESTIMATED=${TURNS_ESTIMATED:-15}
138
+ MAX_TURNS=$(( (TURNS_ESTIMATED * 12 + 9) / 10 ))
139
+ [ "$MAX_TURNS" -lt 15 ] && MAX_TURNS=15
140
+ [ "$MAX_TURNS" -gt 60 ] && MAX_TURNS=60
141
+ 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)."
95
142
  fi
96
- echo "max_turns=$MAX_TURNS" >> "$GITHUB_OUTPUT"
143
+ {
144
+ echo "max_turns=$MAX_TURNS"
145
+ echo "turns_estimated=$TURNS_ESTIMATED"
146
+ echo "files_count=$FILE_COUNT"
147
+ echo "lines_added=$LINES_ADDED"
148
+ echo "lines_deleted=$LINES_DELETED"
149
+ echo "threads_count=$THREAD_COUNT"
150
+ } >> "$GITHUB_OUTPUT"
97
151
 
98
152
  clud-bug-review:
99
153
  needs: paths-check
@@ -105,8 +159,9 @@ jobs:
105
159
  id-token: write
106
160
  # checks: write — composite emits per-skill check-runs (BB.3).
107
161
  checks: write
108
- # actions: read (v0.6.23 / §5)github_ci MCP server. See workflow.yml.tmpl for design notes.
109
- actions: read
162
+ # v0.6.24: `actions: read` (added in v0.6.23) backed outbroke
163
+ # `pull_request` trigger firing on private consumer repos. See
164
+ # workflow.yml.tmpl for the diagnosis.
110
165
 
111
166
  steps:
112
167
  - uses: actions/checkout@v6
@@ -176,20 +231,42 @@ jobs:
176
231
  Review this pull request following the discipline in your
177
232
  system prompt — every rule about skill routing, comment
178
233
  format, the strict-mode header, the two-surface review
179
- shape, and the FIX-PUSH FLOW applies.
234
+ shape, the FIX-PUSH FLOW, and (v0.6.25+) turn-budget
235
+ self-rationing applies.
236
+
237
+ ## Your turn budget for this PR (v0.6.25 / §5.5 Layer 2)
238
+
239
+ max_turns=${{ needs.paths-check.outputs.max_turns }},
240
+ estimated=${{ needs.paths-check.outputs.turns_estimated }},
241
+ files=${{ needs.paths-check.outputs.files_count }},
242
+ +${{ needs.paths-check.outputs.lines_added }}/-${{ needs.paths-check.outputs.lines_deleted }} lines,
243
+ prior_threads=${{ needs.paths-check.outputs.threads_count }}.
244
+
245
+ Plan accordingly per the "Turn budget self-rationing"
246
+ section in your system prompt. Reserve 5 turns for emit.
180
247
  id: clud-bug-review
181
248
 
182
249
  # v0.6.22 / 0.0.O: render structured output → post via gh pr comment.
250
+ # v0.6.25 / §5.5 Layer 1.5: append calibration marker.
183
251
  - name: Render + post structured review
184
252
  if: success() && steps.clud-bug-review.outputs.structured_output != ''
185
253
  env:
186
254
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
187
255
  PR_NUMBER: ${{ github.event.pull_request.number }}
188
256
  STRUCTURED: ${{ steps.clud-bug-review.outputs.structured_output }}
257
+ TURNS_ESTIMATED: ${{ needs.paths-check.outputs.turns_estimated }}
258
+ MAX_TURNS: ${{ needs.paths-check.outputs.max_turns }}
259
+ FILES_COUNT: ${{ needs.paths-check.outputs.files_count }}
260
+ LINES_ADDED: ${{ needs.paths-check.outputs.lines_added }}
261
+ LINES_DELETED: ${{ needs.paths-check.outputs.lines_deleted }}
262
+ THREADS_COUNT: ${{ needs.paths-check.outputs.threads_count }}
189
263
  run: |
190
264
  set -euo pipefail
191
265
  BODY=$(printf '%s\n' "$STRUCTURED" | npx --yes clud-bug@{{CLUD_BUG_VERSION}} render --stdin)
192
- gh pr comment "$PR_NUMBER" --body "$BODY"
266
+ 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 -->"
267
+ gh pr comment "$PR_NUMBER" --body "$BODY
268
+
269
+ $CALIBRATION"
193
270
 
194
271
  # Fallback when structured_output is empty (max-retries hit).
195
272
  - name: Fallback summary (structured_output empty)
@@ -212,7 +289,7 @@ jobs:
212
289
  # Strict-mode gate — composite action; see workflow.yml.tmpl for design notes.
213
290
  - name: Strict mode — fail check on critical findings
214
291
  if: success()
215
- uses: thrillmade/clud-bug/.github/actions/strict-mode-gate@v0.6.23
292
+ uses: thrillmade/clud-bug/.github/actions/strict-mode-gate@v0.6.25
216
293
  with:
217
294
  github-token: ${{ secrets.GITHUB_TOKEN }}
218
295
  # 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: v11
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
@@ -28,10 +39,18 @@ jobs:
28
39
  CHANGED=$(gh pr diff "$PR_NUMBER" -R "$REPO" --name-only)
29
40
  MODEL=claude-sonnet-4-6
30
41
  if [ -z "$CHANGED" ]; then
31
- echo "is_workflow_only=false" >> "$GITHUB_OUTPUT"
32
- echo "model=$MODEL" >> "$GITHUB_OUTPUT"
33
42
  # v0.6.23 / §5: max_turns must always be emitted — see workflow.yml.tmpl for design notes.
34
- echo "max_turns=15" >> "$GITHUB_OUTPUT"
43
+ # Grouped redirect (v0.6.24) silences the SC2129 style warning.
44
+ {
45
+ echo "is_workflow_only=false"
46
+ echo "model=$MODEL"
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"
53
+ } >> "$GITHUB_OUTPUT"
35
54
  exit 0
36
55
  fi
37
56
  IS_WORKFLOW_ONLY=true
@@ -78,22 +97,57 @@ jobs:
78
97
  fi
79
98
  echo "model=$MODEL" >> "$GITHUB_OUTPUT"
80
99
 
81
- # Adaptive max-turns (v0.6.23 / §5) — see workflow.yml.tmpl for design notes.
82
- MAX_TURNS=15
100
+ # Smart budget (v0.6.25 / §5.5 Layer 1) — see workflow.yml.tmpl for design notes + formula.
101
+ FILE_COUNT=$(echo "$CHANGED" | wc -l | tr -d ' ')
102
+ 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)
103
+ THREAD_COUNT=${THREAD_COUNT:-0}
104
+
83
105
  if [ "$IS_TRIVIAL" = "true" ]; then
84
106
  MAX_TURNS=10
107
+ TURNS_ESTIMATED=10
108
+ LINES_ADDED=0
109
+ LINES_DELETED=0
110
+ echo "::notice title=Clud Bug 🐛::Trivial (Haiku) budget: max_turns=10."
85
111
  else
86
- FILE_COUNT=$(echo "$CHANGED" | wc -l | tr -d ' ')
87
- 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)
88
- THREAD_COUNT=${THREAD_COUNT:-0}
89
- if [ "$FILE_COUNT" -ge 30 ] || [ "$THREAD_COUNT" -ge 6 ]; then
90
- MAX_TURNS=40
91
- elif [ "$FILE_COUNT" -ge 10 ] || [ "$THREAD_COUNT" -ge 3 ]; then
92
- MAX_TURNS=25
93
- fi
94
- echo "::notice title=Clud Bug 🐛::Turn budget: $MAX_TURNS ($FILE_COUNT files, $THREAD_COUNT prior threads)."
112
+ FILES_JSON=$(gh pr view "$PR_NUMBER" -R "$REPO" --json files --jq '.files' 2>/dev/null || echo '[]')
113
+ LINES_ADDED=$(echo "$FILES_JSON" | jq '[.[].additions] | add // 0' 2>/dev/null || echo 0)
114
+ LINES_DELETED=$(echo "$FILES_JSON" | jq '[.[].deletions] | add // 0' 2>/dev/null || echo 0)
115
+ LINES_ADDED=${LINES_ADDED:-0}
116
+ LINES_DELETED=${LINES_DELETED:-0}
117
+
118
+ # jq implements the §5.5 Layer 1 formula — see workflow.yml.tmpl.
119
+ TURNS_ESTIMATED=$(echo "$FILES_JSON" | jq --argjson threads "$THREAD_COUNT" '
120
+ def per_line(path):
121
+ if (path | test("\\.(test|spec)\\.|__tests__|^tests?/")) then 0.01
122
+ elif (path | test("\\.(md|txt|rst|adoc|mdx)$")) then 0.00666667
123
+ elif (path | test("\\.(yml|yaml|toml|json|cfg|ini|conf|tmpl)$")) then 0.01
124
+ 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
125
+ else 0.0125 end;
126
+ map(
127
+ select(.path != "docs/timeline.md"
128
+ and .path != "docs/file-structure.md"
129
+ and .path != "docs/decisions.md")
130
+ | (.additions) as $add | (.deletions) as $del
131
+ | (if $add < $del then $add else $del end) as $mod
132
+ | ($add - $mod) as $pa | ($del - $mod) as $pd
133
+ | per_line(.path) as $tw
134
+ | 0.3 + ($pa * $tw) + ($mod * 1.5 * $tw) + ($pd * 0.1 * $tw)
135
+ ) | (add // 0) + 10 + ($threads * 1.5) | (. + 0.9999999) | floor
136
+ ' 2>/dev/null || echo 15)
137
+ TURNS_ESTIMATED=${TURNS_ESTIMATED:-15}
138
+ MAX_TURNS=$(( (TURNS_ESTIMATED * 12 + 9) / 10 ))
139
+ [ "$MAX_TURNS" -lt 15 ] && MAX_TURNS=15
140
+ [ "$MAX_TURNS" -gt 60 ] && MAX_TURNS=60
141
+ 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)."
95
142
  fi
96
- echo "max_turns=$MAX_TURNS" >> "$GITHUB_OUTPUT"
143
+ {
144
+ echo "max_turns=$MAX_TURNS"
145
+ echo "turns_estimated=$TURNS_ESTIMATED"
146
+ echo "files_count=$FILE_COUNT"
147
+ echo "lines_added=$LINES_ADDED"
148
+ echo "lines_deleted=$LINES_DELETED"
149
+ echo "threads_count=$THREAD_COUNT"
150
+ } >> "$GITHUB_OUTPUT"
97
151
 
98
152
  clud-bug-review:
99
153
  needs: paths-check
@@ -105,8 +159,9 @@ jobs:
105
159
  id-token: write
106
160
  # checks: write — composite emits per-skill check-runs (BB.3).
107
161
  checks: write
108
- # actions: read (v0.6.23 / §5)github_ci MCP server. See workflow.yml.tmpl for design notes.
109
- actions: read
162
+ # v0.6.24: `actions: read` (added in v0.6.23) backed outbroke
163
+ # `pull_request` trigger firing on private consumer repos. See
164
+ # workflow.yml.tmpl for the diagnosis.
110
165
 
111
166
  steps:
112
167
  - uses: actions/checkout@v6
@@ -176,20 +231,42 @@ jobs:
176
231
  Review this pull request following the discipline in your
177
232
  system prompt — every rule about skill routing, comment
178
233
  format, the strict-mode header, the two-surface review
179
- shape, and the FIX-PUSH FLOW applies.
234
+ shape, the FIX-PUSH FLOW, and (v0.6.25+) turn-budget
235
+ self-rationing applies.
236
+
237
+ ## Your turn budget for this PR (v0.6.25 / §5.5 Layer 2)
238
+
239
+ max_turns=${{ needs.paths-check.outputs.max_turns }},
240
+ estimated=${{ needs.paths-check.outputs.turns_estimated }},
241
+ files=${{ needs.paths-check.outputs.files_count }},
242
+ +${{ needs.paths-check.outputs.lines_added }}/-${{ needs.paths-check.outputs.lines_deleted }} lines,
243
+ prior_threads=${{ needs.paths-check.outputs.threads_count }}.
244
+
245
+ Plan accordingly per the "Turn budget self-rationing"
246
+ section in your system prompt. Reserve 5 turns for emit.
180
247
  id: clud-bug-review
181
248
 
182
249
  # v0.6.22 / 0.0.O: render structured output → post via gh pr comment.
250
+ # v0.6.25 / §5.5 Layer 1.5: append calibration marker.
183
251
  - name: Render + post structured review
184
252
  if: success() && steps.clud-bug-review.outputs.structured_output != ''
185
253
  env:
186
254
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
187
255
  PR_NUMBER: ${{ github.event.pull_request.number }}
188
256
  STRUCTURED: ${{ steps.clud-bug-review.outputs.structured_output }}
257
+ TURNS_ESTIMATED: ${{ needs.paths-check.outputs.turns_estimated }}
258
+ MAX_TURNS: ${{ needs.paths-check.outputs.max_turns }}
259
+ FILES_COUNT: ${{ needs.paths-check.outputs.files_count }}
260
+ LINES_ADDED: ${{ needs.paths-check.outputs.lines_added }}
261
+ LINES_DELETED: ${{ needs.paths-check.outputs.lines_deleted }}
262
+ THREADS_COUNT: ${{ needs.paths-check.outputs.threads_count }}
189
263
  run: |
190
264
  set -euo pipefail
191
265
  BODY=$(printf '%s\n' "$STRUCTURED" | npx --yes clud-bug@{{CLUD_BUG_VERSION}} render --stdin)
192
- gh pr comment "$PR_NUMBER" --body "$BODY"
266
+ 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 -->"
267
+ gh pr comment "$PR_NUMBER" --body "$BODY
268
+
269
+ $CALIBRATION"
193
270
 
194
271
  # Fallback when structured_output is empty (max-retries hit).
195
272
  - name: Fallback summary (structured_output empty)
@@ -212,7 +289,7 @@ jobs:
212
289
  # Strict-mode gate — composite action; see workflow.yml.tmpl for design notes.
213
290
  - name: Strict mode — fail check on critical findings
214
291
  if: success()
215
- uses: thrillmade/clud-bug/.github/actions/strict-mode-gate@v0.6.23
292
+ uses: thrillmade/clud-bug/.github/actions/strict-mode-gate@v0.6.25
216
293
  with:
217
294
  github-token: ${{ secrets.GITHUB_TOKEN }}
218
295
  # v0.6.22 / 0.0.O: summary now posted by github-actions[bot].
@@ -1,10 +1,19 @@
1
- # clud-bug-template-version: v10
1
+ # clud-bug-template-version: v11
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.
@@ -30,6 +39,17 @@ jobs:
30
39
  is_workflow_only: ${{ steps.classify.outputs.is_workflow_only }}
31
40
  model: ${{ steps.classify.outputs.model }}
32
41
  max_turns: ${{ steps.classify.outputs.max_turns }}
42
+ # v0.6.25 / §5.5 Layer 1.5 (calibration measurement): emit the
43
+ # raw estimated-turns alongside the actual max_turns the workflow
44
+ # passes to claude-code-action. The post-step records both into
45
+ # the summary comment as a hidden HTML marker so we can tune the
46
+ # per-file coefficients (Layer 1) from real consumer-review data
47
+ # over the Step 4 30-day window. Re-tune in v0.6.26+.
48
+ turns_estimated: ${{ steps.classify.outputs.turns_estimated }}
49
+ files_count: ${{ steps.classify.outputs.files_count }}
50
+ lines_added: ${{ steps.classify.outputs.lines_added }}
51
+ lines_deleted: ${{ steps.classify.outputs.lines_deleted }}
52
+ threads_count: ${{ steps.classify.outputs.threads_count }}
33
53
  steps:
34
54
  - name: Classify PR diff
35
55
  id: classify
@@ -42,15 +62,23 @@ jobs:
42
62
  CHANGED=$(gh pr diff "$PR_NUMBER" -R "$REPO" --name-only)
43
63
  MODEL=claude-sonnet-4-6 # default
44
64
  if [ -z "$CHANGED" ]; then
45
- echo "is_workflow_only=false" >> "$GITHUB_OUTPUT"
46
- echo "model=$MODEL" >> "$GITHUB_OUTPUT"
47
65
  # v0.6.23 / §5: max_turns must always be emitted because
48
66
  # clud-bug-review runs (is_workflow_only=false). Without
49
- # this, --max-turns ${{ ... }} expands to '--max-turns '
50
- # (empty), failing the CLI invocation. Empty-CHANGED
51
- # fires on gh pr diff auth/network failures + the
52
- # (theoretical) no-changed-files PR.
53
- echo "max_turns=15" >> "$GITHUB_OUTPUT"
67
+ # this, --max-turns $-{{ ... }} expands to '--max-turns '
68
+ # (empty), failing the CLI invocation. Empty-CHANGED fires
69
+ # on gh pr diff auth/network failures + the (theoretical)
70
+ # no-changed-files PR. Grouped redirect (v0.6.24) silences
71
+ # the SC2129 style warning.
72
+ {
73
+ echo "is_workflow_only=false"
74
+ echo "model=$MODEL"
75
+ echo "max_turns=15"
76
+ echo "turns_estimated=0"
77
+ echo "files_count=0"
78
+ echo "lines_added=0"
79
+ echo "lines_deleted=0"
80
+ echo "threads_count=0"
81
+ } >> "$GITHUB_OUTPUT"
54
82
  exit 0
55
83
  fi
56
84
 
@@ -110,37 +138,106 @@ jobs:
110
138
  fi
111
139
  echo "model=$MODEL" >> "$GITHUB_OUTPUT"
112
140
 
113
- # --- (c) adaptive max-turns (v0.6.23 / §5) ---
114
- # Scope-based turn budget so large PRs (many files OR many prior
115
- # unresolved threads to walk in FIX-PUSH FLOW) don't exhaust the
116
- # default 15-turn budget. Concrete failure that motivated this:
117
- # tokenomics PR #18 (23 docs files + 6 prior claude[bot] threads)
118
- # exhausted the cap under v0.6.12 AND under v0.6.22's
119
- # structured-output flow.
141
+ # --- (c) smart budget estimation (v0.6.25 / §5.5 Layer 1) ---
142
+ # Replaces v0.6.23's 4-bucket if-elif with a line-based
143
+ # formula. File count was a crude proxy; lines + edit-type +
144
+ # file-class is much better. Per-file cost in turns:
145
+ #
146
+ # per_file_cost = 0.3 + added × tw × 1.0
147
+ # + modified × tw × 1.5 # context-heavy
148
+ # + deleted × tw × 0.1 # trivial
149
+ #
150
+ # type_weight tw (turns per line):
151
+ # code (.ts/.py/.js/.go/.rs/.java/...) : 1/50
152
+ # docs (.md/.txt/.rst/.adoc/...) : 1/150
153
+ # tests (.test.*/.spec.*/__tests__/*) : 1/100
154
+ # config (.yml/.toml/.json/.cfg) : 1/100
155
+ # derived (timeline.md/file-structure.md/decisions.md) : 0
120
156
  #
121
- # Buckets:
122
- # Trivial (Haiku) 10
123
- # Standard (<10 files AND <3 prior threads) 15 (current default)
124
- # Larger (≥10 files OR ≥3 prior threads) → 25
125
- # Very large (≥30 files OR ≥6 prior threads) → 40
126
- MAX_TURNS=15
157
+ # estimated_turns = 10 + sum(per_file_cost) + 1.5 × prior_threads
158
+ # max_turns = max(estimated × 1.2, 15) # 20% safety margin
159
+ # max_turns = min(max_turns, 60) # ceiling; L5 retry above
160
+ #
161
+ # Emit overhead 10 (raised from initial 5 design): tokenomics
162
+ # #21 (26 docs files, +310/-235) used ~25 turns vs old-formula
163
+ # 16 — structured-output emit + JSON-schema retries + initial
164
+ # context loading cost ~5-10 turns themselves. Will retune from
165
+ # Layer 1.5 calibration data in v0.6.26+.
166
+ #
167
+ # `gh` is the source for additions/deletions + threads.
168
+ # `jq` is the inline estimator (preinstalled on ubuntu-latest
169
+ # runners; an earlier design used python3 but jq sidesteps the
170
+ # YAML-block-indent vs heredoc-content-indent dance).
171
+ # Calibration via the turns_estimated + turns_actually_used data
172
+ # points the post-step records (Layer 1.5).
173
+ FILE_COUNT=$(echo "$CHANGED" | wc -l | tr -d ' ')
174
+ # Count unresolved claude-bot threads. Best-effort: rate-limit
175
+ # or auth failures default to 0 (no escalation, fall back to
176
+ # the line-count-only estimate).
177
+ 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)
178
+ THREAD_COUNT=${THREAD_COUNT:-0}
179
+
127
180
  if [ "$IS_TRIVIAL" = "true" ]; then
181
+ # Haiku route stays on a small flat budget — trivial PRs
182
+ # need ~5 turns; 10 leaves margin.
128
183
  MAX_TURNS=10
184
+ TURNS_ESTIMATED=10
185
+ LINES_ADDED=0
186
+ LINES_DELETED=0
187
+ echo "::notice title=Clud Bug 🐛::Trivial (Haiku) budget: max_turns=10."
129
188
  else
130
- FILE_COUNT=$(echo "$CHANGED" | wc -l | tr -d ' ')
131
- # Count unresolved claude-bot threads. Best-effort: rate-limit
132
- # or auth failures default to 0 (no escalation, fall back to
133
- # file-count tier).
134
- 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)
135
- THREAD_COUNT=${THREAD_COUNT:-0}
136
- if [ "$FILE_COUNT" -ge 30 ] || [ "$THREAD_COUNT" -ge 6 ]; then
137
- MAX_TURNS=40
138
- elif [ "$FILE_COUNT" -ge 10 ] || [ "$THREAD_COUNT" -ge 3 ]; then
139
- MAX_TURNS=25
140
- fi
141
- echo "::notice title=Clud Bug 🐛::Turn budget: $MAX_TURNS ($FILE_COUNT files, $THREAD_COUNT prior unresolved claude threads)."
189
+ FILES_JSON=$(gh pr view "$PR_NUMBER" -R "$REPO" --json files --jq '.files' 2>/dev/null || echo '[]')
190
+ LINES_ADDED=$(echo "$FILES_JSON" | jq '[.[].additions] | add // 0' 2>/dev/null || echo 0)
191
+ LINES_DELETED=$(echo "$FILES_JSON" | jq '[.[].deletions] | add // 0' 2>/dev/null || echo 0)
192
+ LINES_ADDED=${LINES_ADDED:-0}
193
+ LINES_DELETED=${LINES_DELETED:-0}
194
+
195
+ # jq implements the formula from §5.5 Layer 1:
196
+ # per_file_cost = 0.3 + pa×tw + mod×1.5×tw + pd×0.1×tw
197
+ # estimated = 10 + sum(per_file_cost) + 1.5×threads
198
+ # type_weight tw (turns per line; reciprocal of lines-per-turn):
199
+ # code 1/50 = 0.02, docs 1/150 ≈ 0.00667,
200
+ # tests/config 1/100 = 0.01, unknown 1/80 = 0.0125
201
+ # Emit overhead 10 (raised from 5 in initial design): empirical
202
+ # tokenomics #21 (26 docs files, +310/-235) used ~25 turns vs
203
+ # formula-predicted 16 — structured-output emit + JSON-schema
204
+ # retries + initial context loading cost ~5-10 turns themselves.
205
+ # 10 matches better; will retune from Layer 1.5 calibration data.
206
+ # ceil via (+ 0.9999999 | floor) since jq has no ceil.
207
+ TURNS_ESTIMATED=$(echo "$FILES_JSON" | jq --argjson threads "$THREAD_COUNT" '
208
+ def per_line(path):
209
+ if (path | test("\\.(test|spec)\\.|__tests__|^tests?/")) then 0.01
210
+ elif (path | test("\\.(md|txt|rst|adoc|mdx)$")) then 0.00666667
211
+ elif (path | test("\\.(yml|yaml|toml|json|cfg|ini|conf|tmpl)$")) then 0.01
212
+ 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
213
+ else 0.0125 end;
214
+ map(
215
+ select(.path != "docs/timeline.md"
216
+ and .path != "docs/file-structure.md"
217
+ and .path != "docs/decisions.md")
218
+ | (.additions) as $add | (.deletions) as $del
219
+ | (if $add < $del then $add else $del end) as $mod
220
+ | ($add - $mod) as $pa | ($del - $mod) as $pd
221
+ | per_line(.path) as $tw
222
+ | 0.3 + ($pa * $tw) + ($mod * 1.5 * $tw) + ($pd * 0.1 * $tw)
223
+ ) | (add // 0) + 10 + ($threads * 1.5) | (. + 0.9999999) | floor
224
+ ' 2>/dev/null || echo 15)
225
+ TURNS_ESTIMATED=${TURNS_ESTIMATED:-15}
226
+
227
+ # max_turns = ceil(estimated × 1.2), floor 15, ceiling 60
228
+ MAX_TURNS=$(( (TURNS_ESTIMATED * 12 + 9) / 10 ))
229
+ [ "$MAX_TURNS" -lt 15 ] && MAX_TURNS=15
230
+ [ "$MAX_TURNS" -gt 60 ] && MAX_TURNS=60
231
+ 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."
142
232
  fi
143
- echo "max_turns=$MAX_TURNS" >> "$GITHUB_OUTPUT"
233
+ {
234
+ echo "max_turns=$MAX_TURNS"
235
+ echo "turns_estimated=$TURNS_ESTIMATED"
236
+ echo "files_count=$FILE_COUNT"
237
+ echo "lines_added=$LINES_ADDED"
238
+ echo "lines_deleted=$LINES_DELETED"
239
+ echo "threads_count=$THREAD_COUNT"
240
+ } >> "$GITHUB_OUTPUT"
144
241
 
145
242
  clud-bug-review:
146
243
  needs: paths-check
@@ -154,11 +251,15 @@ jobs:
154
251
  # the GitHub Checks API for any skill in .clud-bug.json's strictSkills
155
252
  # list (BB.3, v0.5.10+). No-op when strictSkills is unset.
156
253
  checks: write
157
- # actions: read (v0.6.23 / §5) claude-code-action's bundled
158
- # github_ci MCP server needs this to introspect recent CI runs.
159
- # Per-job GITHUB_TOKEN permissions aren't inherited, so this
160
- # MUST be on the clud-bug-review job, not paths-check.
161
- actions: read
254
+ # v0.6.23 attempted to add `actions: read` here for the github_ci
255
+ # MCP server bundled with claude-code-action. v0.6.24 backed it
256
+ # out: on private consumer repos the `pull_request` trigger
257
+ # silently stopped firing the workflow after the permissions
258
+ # block changed (validated against tokenomics — public agent-skills
259
+ # kept firing, private tokenomics/rezgen did not). claude-code-action
260
+ # warns about the missing `actions: read` but reviews still run
261
+ # correctly. Re-add via a separate path once we understand the
262
+ # private-repo trigger-registration semantics.
162
263
 
163
264
  steps:
164
265
  - uses: actions/checkout@v6
@@ -281,23 +382,53 @@ jobs:
281
382
  Review this pull request following the discipline in your
282
383
  system prompt — every rule about skill routing, comment
283
384
  format, the strict-mode header, the two-surface review
284
- shape, and the FIX-PUSH FLOW applies.
385
+ shape, the FIX-PUSH FLOW, and (v0.6.25+) turn-budget
386
+ self-rationing applies.
387
+
388
+ ## Your turn budget for this PR (v0.6.25 / §5.5 Layer 2)
389
+
390
+ max_turns=${{ needs.paths-check.outputs.max_turns }},
391
+ estimated=${{ needs.paths-check.outputs.turns_estimated }},
392
+ files=${{ needs.paths-check.outputs.files_count }},
393
+ +${{ needs.paths-check.outputs.lines_added }}/-${{ needs.paths-check.outputs.lines_deleted }} lines,
394
+ prior_threads=${{ needs.paths-check.outputs.threads_count }}.
395
+
396
+ Plan accordingly per the "Turn budget self-rationing"
397
+ section in your system prompt. The estimate is the
398
+ pre-flight's best guess; the cap is the workflow's hard
399
+ stop. Reserve 5 turns for emit.
285
400
  id: clud-bug-review
286
401
 
287
402
  # v0.6.22 / 0.0.O: render the structured output to the summary
288
403
  # comment shape, post via gh pr comment. Guarded so we only run
289
404
  # when the action returned a non-empty structured payload
290
405
  # (max-retries hit → empty → fall through to the next step).
406
+ #
407
+ # v0.6.25 / §5.5 Layer 1.5 (calibration measurement): append a
408
+ # hidden HTML marker carrying the pre-flight estimate + the
409
+ # actual max_turns we passed to claude-code-action. Aggregation
410
+ # script lives in clud-bug usage --calibration (TBD v0.6.26+);
411
+ # for now, the marker is just an audit trail visible via
412
+ # `gh api ...issues/N/comments`.
291
413
  - name: Render + post structured review
292
414
  if: success() && steps.clud-bug-review.outputs.structured_output != ''
293
415
  env:
294
416
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
295
417
  PR_NUMBER: ${{ github.event.pull_request.number }}
296
418
  STRUCTURED: ${{ steps.clud-bug-review.outputs.structured_output }}
419
+ TURNS_ESTIMATED: ${{ needs.paths-check.outputs.turns_estimated }}
420
+ MAX_TURNS: ${{ needs.paths-check.outputs.max_turns }}
421
+ FILES_COUNT: ${{ needs.paths-check.outputs.files_count }}
422
+ LINES_ADDED: ${{ needs.paths-check.outputs.lines_added }}
423
+ LINES_DELETED: ${{ needs.paths-check.outputs.lines_deleted }}
424
+ THREADS_COUNT: ${{ needs.paths-check.outputs.threads_count }}
297
425
  run: |
298
426
  set -euo pipefail
299
427
  BODY=$(printf '%s\n' "$STRUCTURED" | npx --yes clud-bug@{{CLUD_BUG_VERSION}} render --stdin)
300
- gh pr comment "$PR_NUMBER" --body "$BODY"
428
+ 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 -->"
429
+ gh pr comment "$PR_NUMBER" --body "$BODY
430
+
431
+ $CALIBRATION"
301
432
 
302
433
  # Fallback comment when the model couldn't produce schema-valid
303
434
  # output after max retries (structured_output is empty). Keeps a
@@ -335,7 +466,7 @@ jobs:
335
466
  # Letting the action's own failure fail the check is louder and right.
336
467
  - name: Strict mode — fail check on critical findings
337
468
  if: success()
338
- uses: thrillmade/clud-bug/.github/actions/strict-mode-gate@v0.6.23
469
+ uses: thrillmade/clud-bug/.github/actions/strict-mode-gate@v0.6.25
339
470
  with:
340
471
  github-token: ${{ secrets.GITHUB_TOKEN }}
341
472
  # v0.6.22 / 0.0.O: the summary is now posted by the workflow