clud-bug 0.6.28 → 0.6.29

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/bin/clud-bug.js CHANGED
@@ -84,12 +84,18 @@ Commands:
84
84
  \`.claude/skills/.clud-bug.json\` usage block + renders
85
85
  archive-candidate / stale / new / healthy status per skill.
86
86
  Read-only — no automation acts on the output. Humans
87
- decide which skills to prune. Workflow integration ships
88
- in v0.6.29; today this command surfaces whatever data
89
- has been written manually or by future runs.
87
+ decide which skills to prune. v0.6.29 wires the workflow
88
+ post-step that auto-writes per-review deltas; v0.6.30
89
+ will aggregate across runs into the dashboard read path.
90
90
  eval Run the golden-set regression gate against the rendered review
91
91
  prompt (must-contain / must-not-contain / byte-budget). Same as
92
92
  \`node --test test/prompts.eval.test.js\` but works from any cwd.
93
+ update-skill-usage Update the .claude/skills/.clud-bug.json usage block from
94
+ a structured-output JSON payload (the action's
95
+ \`outputs.structured_output\`). Called as a workflow
96
+ post-step alongside \`render\` (v0.6.29 / Component 4).
97
+ Pipe the JSON to stdin. Idempotent + atomic write.
98
+ Silent no-op on empty stdin (parity with \`render\`).
93
99
  render --stdin Render a structured-output JSON payload (the action's
94
100
  \`outputs.structured_output\`, piped via stdin) to the
95
101
  GitHub-markdown summary comment shape. Invoked by the
@@ -147,6 +153,7 @@ async function main() {
147
153
  case 'usage': return runUsage(args);
148
154
  case 'eval': return runEval();
149
155
  case 'render': return runRender(args);
156
+ case 'update-skill-usage': return runUpdateSkillUsage(args);
150
157
  default:
151
158
  process.stderr.write(`Unknown command: ${cmd || '(none)'}\n\n${HELP}`);
152
159
  process.exit(2);
@@ -193,6 +200,104 @@ async function runRender(args) {
193
200
  }
194
201
  }
195
202
 
203
+ // v0.6.29 — Component 4. Pipe the action's structured_output through
204
+ // the skill-usage data layer (v0.6.28) + write the merged result back
205
+ // to .claude/skills/.clud-bug.json atomically.
206
+ //
207
+ // Workflow integration (post-step in workflow.yml.tmpl):
208
+ //
209
+ // echo "${{ steps.review.outputs.structured_output }}" \
210
+ // | npx clud-bug@latest update-skill-usage --stdin
211
+ //
212
+ // Runs AFTER the render post-step. Silent no-op on empty stdin
213
+ // (same contract as `render` — preserves the workflow's existing
214
+ // "skip both if empty" branch). Idempotent: running on the same JSON
215
+ // twice produces the same result.
216
+ async function runUpdateSkillUsage(args) {
217
+ const fs = await import('node:fs/promises');
218
+ const path = await import('node:path');
219
+ const {
220
+ computeSkillUsageDelta,
221
+ mergeSkillUsage,
222
+ } = await import('../lib/skill-usage.js');
223
+
224
+ if (!args.stdin) {
225
+ process.stderr.write('clud-bug update-skill-usage: --stdin is required.\n');
226
+ process.exit(2);
227
+ }
228
+
229
+ let raw = '';
230
+ for await (const chunk of process.stdin) raw += chunk;
231
+ raw = raw.trim();
232
+ if (!raw) {
233
+ // Empty structured_output → render is also skipped → nothing to
234
+ // update. Match the render contract: exit 0 with a stderr note.
235
+ process.stderr.write('clud-bug update-skill-usage: stdin empty — no usage update.\n');
236
+ return;
237
+ }
238
+
239
+ let reviewJson;
240
+ try {
241
+ reviewJson = JSON.parse(raw);
242
+ } catch (e) {
243
+ process.stderr.write(`clud-bug update-skill-usage: invalid JSON: ${e.message}\n`);
244
+ process.exit(2);
245
+ }
246
+ if (!reviewJson || typeof reviewJson !== 'object') {
247
+ process.stderr.write('clud-bug update-skill-usage: payload must be a JSON object.\n');
248
+ process.exit(2);
249
+ }
250
+
251
+ // Compute per-review delta. Empty delta is fine — just means no
252
+ // skills loaded or cited (workflow-only PRs, e.g.).
253
+ const delta = computeSkillUsageDelta(reviewJson);
254
+ if (Object.keys(delta).length === 0) {
255
+ process.stderr.write('clud-bug update-skill-usage: no skills in payload — nothing to record.\n');
256
+ return;
257
+ }
258
+
259
+ // Read existing .clud-bug.json. The path is canonical:
260
+ // .claude/skills/.clud-bug.json relative to cwd (the workflow runs
261
+ // from the repo root).
262
+ const jsonPath = path.resolve(process.cwd(), '.claude', 'skills', '.clud-bug.json');
263
+ let parsed;
264
+ try {
265
+ const existingRaw = await fs.readFile(jsonPath, 'utf-8');
266
+ parsed = JSON.parse(existingRaw);
267
+ } catch (err) {
268
+ if (err.code === 'ENOENT') {
269
+ process.stderr.write(
270
+ `clud-bug update-skill-usage: no .clud-bug.json at ${jsonPath} — skipping. ` +
271
+ `Run \`npx clud-bug init\` first.\n`
272
+ );
273
+ return;
274
+ }
275
+ process.stderr.write(`clud-bug update-skill-usage: parse failed: ${err.message}\n`);
276
+ process.exit(2);
277
+ }
278
+ if (!parsed || typeof parsed !== 'object') {
279
+ process.stderr.write('clud-bug update-skill-usage: .clud-bug.json malformed.\n');
280
+ process.exit(2);
281
+ }
282
+
283
+ const existingUsage = parsed.usage || {};
284
+ const timestamp = new Date().toISOString();
285
+ const mergedUsage = mergeSkillUsage(existingUsage, delta, timestamp);
286
+ parsed.usage = mergedUsage;
287
+
288
+ // Write back ATOMICALLY: temp file + rename. Guards against a
289
+ // crashed write leaving the JSON half-written + unparseable on next
290
+ // read (which would brick the entire skill catalog).
291
+ const tmpPath = jsonPath + '.tmp';
292
+ const serialized = JSON.stringify(parsed, null, 2) + '\n';
293
+ await fs.writeFile(tmpPath, serialized, 'utf-8');
294
+ await fs.rename(tmpPath, jsonPath);
295
+
296
+ const skillCount = Object.keys(delta).length;
297
+ ok(`update-skill-usage: merged ${skillCount} skill${skillCount === 1 ? '' : 's'} from review`);
298
+ }
299
+
300
+
196
301
  // 0.0.E (v0.6.17): thin wrapper around the golden-set test file. Devs
197
302
  // who follow the README invoke `clud-bug eval` — this routes to the
198
303
  // same `node --test` runner CI uses, so dev and CI verdicts match.
@@ -225,8 +225,9 @@ export function formatHealthDashboard(rows) {
225
225
  return (
226
226
  'Skill health: no usage data yet.\n\n' +
227
227
  'Usage data accumulates after clud-bug reviews land in your repo.\n' +
228
- 'Workflow integration ships in v0.6.29 until then this command is\n' +
229
- 'a structural placeholder.'
228
+ 'v0.6.29 wires up per-review delta writes via the workflow post-step\n' +
229
+ 'and uploads them as 90-day artifacts. v0.6.30 will add cross-review\n' +
230
+ 'aggregation so this dashboard reads the full artifact stream.'
230
231
  );
231
232
  }
232
233
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clud-bug",
3
- "version": "0.6.28",
3
+ "version": "0.6.29",
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",
@@ -283,6 +283,30 @@ jobs:
283
283
 
284
284
  $CALIBRATION"
285
285
 
286
+ # v0.6.29 / Component 4 — see workflow.yml.tmpl for design notes.
287
+ - name: Update skill-usage tracking
288
+ if: success() && steps.clud-bug-review.outputs.structured_output != ''
289
+ continue-on-error: true
290
+ env:
291
+ STRUCTURED: ${{ steps.clud-bug-review.outputs.structured_output }}
292
+ run: |
293
+ set -euo pipefail
294
+ mkdir -p .claude/skills
295
+ if [ ! -f .claude/skills/.clud-bug.json ]; then
296
+ echo '{"version": 1}' > .claude/skills/.clud-bug.json
297
+ fi
298
+ printf '%s\n' "$STRUCTURED" | npx --yes clud-bug@{{CLUD_BUG_VERSION}} update-skill-usage --stdin
299
+ echo "::notice title=skill-usage::recorded review delta to ephemeral .clud-bug.json (v0.6.30 will add cross-review aggregation)"
300
+
301
+ - name: Upload skill-usage artifact
302
+ if: success() && steps.clud-bug-review.outputs.structured_output != ''
303
+ continue-on-error: true
304
+ uses: actions/upload-artifact@v4
305
+ with:
306
+ name: clud-bug-skill-usage-pr-${{ github.event.pull_request.number }}
307
+ path: .claude/skills/.clud-bug.json
308
+ retention-days: 90
309
+
286
310
  # v0.6.26 / §5.5 Layer 6 fallback render-from-inlines — see workflow.yml.tmpl for design notes.
287
311
  - name: Fallback summary (structured_output empty)
288
312
  if: success() && steps.clud-bug-review.outputs.structured_output == ''
@@ -339,7 +363,7 @@ jobs:
339
363
  # Strict-mode gate — composite action; see workflow.yml.tmpl for design notes.
340
364
  - name: Strict mode — fail check on critical findings
341
365
  if: success()
342
- uses: thrillmade/clud-bug/.github/actions/strict-mode-gate@v0.6.28
366
+ uses: thrillmade/clud-bug/.github/actions/strict-mode-gate@v0.6.29
343
367
  with:
344
368
  github-token: ${{ secrets.GITHUB_TOKEN }}
345
369
  # v0.6.22 / 0.0.O: summary now posted by github-actions[bot].
@@ -283,6 +283,30 @@ jobs:
283
283
 
284
284
  $CALIBRATION"
285
285
 
286
+ # v0.6.29 / Component 4 — see workflow.yml.tmpl for design notes.
287
+ - name: Update skill-usage tracking
288
+ if: success() && steps.clud-bug-review.outputs.structured_output != ''
289
+ continue-on-error: true
290
+ env:
291
+ STRUCTURED: ${{ steps.clud-bug-review.outputs.structured_output }}
292
+ run: |
293
+ set -euo pipefail
294
+ mkdir -p .claude/skills
295
+ if [ ! -f .claude/skills/.clud-bug.json ]; then
296
+ echo '{"version": 1}' > .claude/skills/.clud-bug.json
297
+ fi
298
+ printf '%s\n' "$STRUCTURED" | npx --yes clud-bug@{{CLUD_BUG_VERSION}} update-skill-usage --stdin
299
+ echo "::notice title=skill-usage::recorded review delta to ephemeral .clud-bug.json (v0.6.30 will add cross-review aggregation)"
300
+
301
+ - name: Upload skill-usage artifact
302
+ if: success() && steps.clud-bug-review.outputs.structured_output != ''
303
+ continue-on-error: true
304
+ uses: actions/upload-artifact@v4
305
+ with:
306
+ name: clud-bug-skill-usage-pr-${{ github.event.pull_request.number }}
307
+ path: .claude/skills/.clud-bug.json
308
+ retention-days: 90
309
+
286
310
  # v0.6.26 / §5.5 Layer 6 fallback render-from-inlines — see workflow.yml.tmpl for design notes.
287
311
  - name: Fallback summary (structured_output empty)
288
312
  if: success() && steps.clud-bug-review.outputs.structured_output == ''
@@ -339,7 +363,7 @@ jobs:
339
363
  # Strict-mode gate — composite action; see workflow.yml.tmpl for design notes.
340
364
  - name: Strict mode — fail check on critical findings
341
365
  if: success()
342
- uses: thrillmade/clud-bug/.github/actions/strict-mode-gate@v0.6.28
366
+ uses: thrillmade/clud-bug/.github/actions/strict-mode-gate@v0.6.29
343
367
  with:
344
368
  github-token: ${{ secrets.GITHUB_TOKEN }}
345
369
  # v0.6.22 / 0.0.O: summary now posted by github-actions[bot].
@@ -494,6 +494,44 @@ jobs:
494
494
 
495
495
  $CALIBRATION"
496
496
 
497
+ # v0.6.29 / Component 4: pipe structured_output through
498
+ # `clud-bug update-skill-usage` to compute the per-skill loads +
499
+ # citations delta from this one review. Writes to the ephemeral
500
+ # workspace .clud-bug.json AND uploads as a workflow artifact
501
+ # (90-day retention). v0.6.30 will add aggregation logic so
502
+ # `clud-bug usage --health` reads + merges artifacts across runs.
503
+ #
504
+ # Why artifact instead of committing back to main: committing
505
+ # would require `contents: write` permission (private-repo
506
+ # trigger-firing regression risk per v0.6.23 hotfix) + race
507
+ # handling. Artifacts are GitHub-native persistence with zero
508
+ # permission expansion + zero commit noise.
509
+ - name: Update skill-usage tracking
510
+ if: success() && steps.clud-bug-review.outputs.structured_output != ''
511
+ continue-on-error: true # don't fail the whole review if usage update flakes
512
+ env:
513
+ STRUCTURED: ${{ steps.clud-bug-review.outputs.structured_output }}
514
+ run: |
515
+ set -euo pipefail
516
+ mkdir -p .claude/skills
517
+ # If a .clud-bug.json already exists in the workspace (PR's
518
+ # snapshot), keep it. Otherwise initialize an empty shell so
519
+ # update-skill-usage has something to merge into.
520
+ if [ ! -f .claude/skills/.clud-bug.json ]; then
521
+ echo '{"version": 1}' > .claude/skills/.clud-bug.json
522
+ fi
523
+ printf '%s\n' "$STRUCTURED" | npx --yes clud-bug@{{CLUD_BUG_VERSION}} update-skill-usage --stdin
524
+ echo "::notice title=skill-usage::recorded review delta to ephemeral .clud-bug.json (v0.6.30 will add cross-review aggregation)"
525
+
526
+ - name: Upload skill-usage artifact
527
+ if: success() && steps.clud-bug-review.outputs.structured_output != ''
528
+ continue-on-error: true
529
+ uses: actions/upload-artifact@v4
530
+ with:
531
+ name: clud-bug-skill-usage-pr-${{ github.event.pull_request.number }}
532
+ path: .claude/skills/.clud-bug.json
533
+ retention-days: 90
534
+
497
535
  # Fallback comment when the model couldn't produce schema-valid
498
536
  # output after max retries (structured_output is empty). Keeps a
499
537
  # bare H2 header so the strict-mode gate sees a comment and falls
@@ -589,7 +627,7 @@ jobs:
589
627
  # Letting the action's own failure fail the check is louder and right.
590
628
  - name: Strict mode — fail check on critical findings
591
629
  if: success()
592
- uses: thrillmade/clud-bug/.github/actions/strict-mode-gate@v0.6.28
630
+ uses: thrillmade/clud-bug/.github/actions/strict-mode-gate@v0.6.29
593
631
  with:
594
632
  github-token: ${{ secrets.GITHUB_TOKEN }}
595
633
  # v0.6.22 / 0.0.O: the summary is now posted by the workflow