clud-bug 0.7.0-rc.11 → 0.7.0-rc.13
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/dist/cli/hooks.d.ts +40 -0
- package/dist/cli/hooks.d.ts.map +1 -0
- package/dist/cli/hooks.js +105 -0
- package/dist/cli/hooks.js.map +1 -0
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +61 -0
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/review-prompt.d.ts +28 -0
- package/dist/cli/review-prompt.d.ts.map +1 -0
- package/dist/cli/review-prompt.js +169 -0
- package/dist/cli/review-prompt.js.map +1 -0
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +22 -0
- package/dist/cli/update.js.map +1 -1
- package/dist/core/budget-plan.d.ts +125 -0
- package/dist/core/budget-plan.d.ts.map +1 -0
- package/dist/core/budget-plan.js +211 -0
- package/dist/core/budget-plan.js.map +1 -0
- package/dist/core/index.d.ts +5 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +14 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/multi-pass-aggregate.d.ts +131 -0
- package/dist/core/multi-pass-aggregate.d.ts.map +1 -0
- package/dist/core/multi-pass-aggregate.js +427 -0
- package/dist/core/multi-pass-aggregate.js.map +1 -0
- package/dist/core/plan-review.d.ts +57 -0
- package/dist/core/plan-review.d.ts.map +1 -0
- package/dist/core/plan-review.js +77 -0
- package/dist/core/plan-review.js.map +1 -0
- package/dist/core/review-plan.d.ts +219 -0
- package/dist/core/review-plan.d.ts.map +1 -0
- package/dist/core/review-plan.js +274 -0
- package/dist/core/review-plan.js.map +1 -0
- package/dist/core/review-writeback.d.ts +20 -0
- package/dist/core/review-writeback.d.ts.map +1 -1
- package/dist/core/review-writeback.js +16 -0
- package/dist/core/review-writeback.js.map +1 -1
- package/dist/core/version.d.ts +1 -1
- package/dist/core/version.js +1 -1
- package/package.json +1 -1
- package/src/cli/hooks.ts +122 -0
- package/src/cli/main.ts +56 -0
- package/src/cli/review-prompt.ts +206 -0
- package/src/cli/update.ts +22 -0
- package/src/core/budget-plan.ts +324 -0
- package/src/core/index.ts +64 -0
- package/src/core/multi-pass-aggregate.ts +545 -0
- package/src/core/plan-review.ts +136 -0
- package/src/core/review-plan.ts +485 -0
- package/src/core/review-writeback.ts +38 -0
- package/src/core/version.ts +1 -1
- package/templates/workflow-py.yml.tmpl +1 -1
- package/templates/workflow-ts.yml.tmpl +1 -1
- package/templates/workflow.yml.tmpl +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clud-bug",
|
|
3
|
-
"version": "0.7.0-rc.
|
|
3
|
+
"version": "0.7.0-rc.13",
|
|
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",
|
package/src/cli/hooks.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Wave 6b — Claude Code `type: agent` commit-review hook scaffolding.
|
|
2
|
+
//
|
|
3
|
+
// `clud-bug init --with-hooks` writes a native Claude Code hook that, on every
|
|
4
|
+
// `git commit` the agent makes, spawns a clud-bug review SUBAGENT in the same
|
|
5
|
+
// session — so it runs on the session's subscription (no API key), in the
|
|
6
|
+
// background (the commit never blocks), and surfaces findings back to the agent.
|
|
7
|
+
//
|
|
8
|
+
// The model (see the Wave 6b plan): clud-bug supplies the *recipe* (the
|
|
9
|
+
// agent-hook `prompt`); Claude Code runs it as a subagent. This is the dynamic,
|
|
10
|
+
// always-on counterpart to the `/clud-bug-review` slash command.
|
|
11
|
+
//
|
|
12
|
+
// `type: agent` hooks are documented as experimental; the same outcome is
|
|
13
|
+
// reachable via a `type: command` hook + `additionalContext` if the API shifts.
|
|
14
|
+
|
|
15
|
+
/** Stable marker embedded in our hook's prompt so re-runs replace it in place. */
|
|
16
|
+
export const CLUD_BUG_HOOK_MARKER = 'clud-bug-local-review';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Build the `prompt` of the Claude Code `type: agent` commit-review hook,
|
|
20
|
+
* pinned to the clud-bug VERSION that scaffolded it. Pinning matters: a bare
|
|
21
|
+
* `npx clud-bug` resolves to the `latest` dist-tag, which can predate the
|
|
22
|
+
* `review-prompt` verb (e.g. while v0.7 is prerelease on `next`); `@${version}`
|
|
23
|
+
* guarantees the verb exists. `clud-bug update` refreshes the pin in place.
|
|
24
|
+
*
|
|
25
|
+
* Rather than baking a static recipe (which would drift from the repo's
|
|
26
|
+
* skills/config), it tells the subagent to run `clud-bug review-prompt` — the
|
|
27
|
+
* engine-driven verb that emits a recipe tailored to THIS repo's resolved plan
|
|
28
|
+
* — and follow it. The recipe is generated fresh at commit time, always current.
|
|
29
|
+
*/
|
|
30
|
+
export function buildCommitReviewPrompt(version: string): string {
|
|
31
|
+
return `<!-- ${CLUD_BUG_HOOK_MARKER} v1 -->
|
|
32
|
+
You are clud-bug's local commit review, running as a background subagent on this
|
|
33
|
+
session's own subscription (no extra auth — you have git, gh, and file access).
|
|
34
|
+
|
|
35
|
+
Step 1 — get your review recipe from clud-bug's engine:
|
|
36
|
+
|
|
37
|
+
npx clud-bug@${version} review-prompt --trigger commit
|
|
38
|
+
|
|
39
|
+
That prints a structured review recipe tailored to THIS repo's skills + config (a
|
|
40
|
+
fast single pass for a commit). If that command isn't available, run the repo's
|
|
41
|
+
installed clud-bug CLI's \`review-prompt --trigger commit\` instead.
|
|
42
|
+
|
|
43
|
+
Step 2 — follow that recipe exactly: review the commit that was just made against
|
|
44
|
+
the skills it names, and surface any findings back into the session so they can be
|
|
45
|
+
fixed. A clean commit needs only a one-line note.
|
|
46
|
+
|
|
47
|
+
Keep it tight — this is the commit-time safety net; the deeper review runs at PR time.`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** One Claude Code hook entry: a tool matcher plus its hook list. */
|
|
51
|
+
export interface HookMatcherEntry {
|
|
52
|
+
matcher?: string;
|
|
53
|
+
hooks: Array<Record<string, unknown>>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Minimal shape of the `.claude/settings.json` we read + merge into. */
|
|
57
|
+
export interface ClaudeSettings {
|
|
58
|
+
hooks?: Record<string, HookMatcherEntry[]>;
|
|
59
|
+
[key: string]: unknown;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Builds the PostToolUse entry that spawns the commit-review subagent: a native
|
|
64
|
+
* `type: agent` hook that is backgrounded (`async`), surfaces findings
|
|
65
|
+
* (`asyncRewake`), and fires only on `git commit` (`if`).
|
|
66
|
+
*/
|
|
67
|
+
export function buildLocalReviewHook(recipe: string): HookMatcherEntry {
|
|
68
|
+
return {
|
|
69
|
+
matcher: 'Bash',
|
|
70
|
+
hooks: [
|
|
71
|
+
{
|
|
72
|
+
type: 'agent',
|
|
73
|
+
if: 'Bash(git commit *)',
|
|
74
|
+
async: true,
|
|
75
|
+
asyncRewake: true,
|
|
76
|
+
timeout: 180,
|
|
77
|
+
prompt: recipe,
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isOurHook(h: Record<string, unknown> | undefined): boolean {
|
|
84
|
+
return typeof h?.['prompt'] === 'string' && (h['prompt'] as string).includes(CLUD_BUG_HOOK_MARKER);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isCludBugReviewEntry(entry: HookMatcherEntry | undefined): boolean {
|
|
88
|
+
return !!entry && Array.isArray(entry.hooks) && entry.hooks.some(isOurHook);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Merges the clud-bug commit-review hook into an existing `.claude/settings.json`
|
|
93
|
+
* object. **Idempotent** (replaces any prior clud-bug entry rather than
|
|
94
|
+
* duplicating) and **non-clobbering** (preserves every other top-level key,
|
|
95
|
+
* every other event, and every other hook). Tolerates a missing/malformed
|
|
96
|
+
* `existing` value.
|
|
97
|
+
*/
|
|
98
|
+
export function mergeLocalReviewHook(existing: unknown, recipe: string): ClaudeSettings {
|
|
99
|
+
const base: ClaudeSettings =
|
|
100
|
+
existing && typeof existing === 'object' ? { ...(existing as ClaudeSettings) } : {};
|
|
101
|
+
const hooks: Record<string, HookMatcherEntry[]> = { ...(base.hooks ?? {}) };
|
|
102
|
+
const priorPost = Array.isArray(hooks.PostToolUse) ? hooks.PostToolUse : [];
|
|
103
|
+
|
|
104
|
+
// Preserve every non-clud-bug hook — including any the user co-located INSIDE
|
|
105
|
+
// our own matcher entry: drop only the hook(s) carrying our marker, never the
|
|
106
|
+
// whole entry.
|
|
107
|
+
const ours = buildLocalReviewHook(recipe);
|
|
108
|
+
const otherEntries: HookMatcherEntry[] = [];
|
|
109
|
+
const coLocatedUserHooks: Array<Record<string, unknown>> = [];
|
|
110
|
+
for (const entry of priorPost) {
|
|
111
|
+
if (isCludBugReviewEntry(entry)) {
|
|
112
|
+
for (const h of entry.hooks) if (!isOurHook(h)) coLocatedUserHooks.push(h);
|
|
113
|
+
} else {
|
|
114
|
+
otherEntries.push(entry);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const ourEntry: HookMatcherEntry =
|
|
118
|
+
coLocatedUserHooks.length > 0 ? { ...ours, hooks: [...coLocatedUserHooks, ...ours.hooks] } : ours;
|
|
119
|
+
hooks.PostToolUse = [...otherEntries, ourEntry];
|
|
120
|
+
base.hooks = hooks;
|
|
121
|
+
return base;
|
|
122
|
+
}
|
package/src/cli/main.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
import { computeAuditFileSet } from './audit.js';
|
|
32
32
|
import { renderAuditHeader } from '../core/audit.js';
|
|
33
33
|
import { runUpdate } from './update.js';
|
|
34
|
+
import { runReviewPrompt } from './review-prompt.js';
|
|
34
35
|
import { getPendingWorkflowEdits, makeBranchName, git as gitCmd } from './edit-workflow.js';
|
|
35
36
|
import { applyToRepo as applyAgentDocs } from './agents-md.js';
|
|
36
37
|
import { detectRepo, detectDefaultBranch, getProtectionState, enableConversationResolution } from './branch-protection.js';
|
|
@@ -66,6 +67,7 @@ function parseArgs(argv) {
|
|
|
66
67
|
// .claude/commands/clud-bug-review.md so `/clud-bug-review` works in a
|
|
67
68
|
// Claude Code session (reviews the current PR with the session's tokens).
|
|
68
69
|
withLocalReview: false,
|
|
70
|
+
withHooks: false,
|
|
69
71
|
// v0.7.0-rc.4: `clud-bug configure-github` flags.
|
|
70
72
|
// --dry-run prints the diff but skips PATCH; --branch overrides "main".
|
|
71
73
|
dryRun: false,
|
|
@@ -93,8 +95,11 @@ function parseArgs(argv) {
|
|
|
93
95
|
else if (a === '--no-artifacts') args.artifacts = false;
|
|
94
96
|
else if (a === '--with-skdd') args.withSkdd = true;
|
|
95
97
|
else if (a === '--with-local-review') args.withLocalReview = true;
|
|
98
|
+
else if (a === '--with-hooks') args.withHooks = true;
|
|
96
99
|
else if (a === '--dry-run') args.dryRun = true;
|
|
97
100
|
else if (a === '--branch') args.branch = argv[++i];
|
|
101
|
+
else if (a === '--trigger') args.trigger = argv[++i];
|
|
102
|
+
else if (a === '--diff-size') args.diffSizeBytes = Number(argv[++i]);
|
|
98
103
|
else args._.push(a);
|
|
99
104
|
}
|
|
100
105
|
return args;
|
|
@@ -185,6 +190,11 @@ Commands:
|
|
|
185
190
|
Required env vars: GH_TOKEN, ANTHROPIC_API_KEY,
|
|
186
191
|
REPO ("owner/name"), PR_NUMBER. Output: JSON
|
|
187
192
|
\`{actions, verifierCallCount, shouldRequestChanges}\`.
|
|
193
|
+
review-prompt Emit the local-review recipe — the structured prompt a
|
|
194
|
+
Claude Code agent/hook runs to review the current diff on
|
|
195
|
+
the session's own subscription. Plans via the shared
|
|
196
|
+
engine: --trigger commit (default) → a fast single pass;
|
|
197
|
+
push/pr → the full multi-pass plan. Prints to stdout.
|
|
188
198
|
|
|
189
199
|
Options:
|
|
190
200
|
--offline Skip skills.sh; pin only the bundled baseline specimens.
|
|
@@ -194,6 +204,11 @@ Options:
|
|
|
194
204
|
\`/clud-bug-review\` works in a Claude Code session —
|
|
195
205
|
reviews the current branch's PR using that session's
|
|
196
206
|
own tokens (no hosted App, no extra auth).
|
|
207
|
+
--with-hooks (init) Also scaffold a native Claude Code \`type: agent\`
|
|
208
|
+
commit-review hook into .claude/settings.json — on every
|
|
209
|
+
\`git commit\` the agent makes, a backgrounded clud-bug
|
|
210
|
+
review subagent runs on the session's subscription.
|
|
211
|
+
Implies --with-local-review. Off by default.
|
|
197
212
|
--quiet,-q Token-frugal mode for agent invocations. Suppresses
|
|
198
213
|
progress chatter; emits exactly one final
|
|
199
214
|
\`ok <key-value>\` summary line per command. Errors
|
|
@@ -206,6 +221,7 @@ Options:
|
|
|
206
221
|
--dry-run Print the canonical-v1 diff without PATCH-ing
|
|
207
222
|
(configure-github only).
|
|
208
223
|
--branch <name> Target branch for configure-github (default: main).
|
|
224
|
+
--trigger <ctx> review-prompt context: commit (default) | push | pr.
|
|
209
225
|
--repo <owner/name> Restrict \`usage\` to a single repo. Default: all repos
|
|
210
226
|
with clud-bug-review.yml in the gh user's auth scope.
|
|
211
227
|
--pr <N> Restrict \`usage\` to a single PR.
|
|
@@ -249,6 +265,7 @@ async function main() {
|
|
|
249
265
|
case 'select-review-event': return runSelectReviewEvent(args);
|
|
250
266
|
case 'post-inline-threads': return runPostInlineThreads(args);
|
|
251
267
|
case 'resolve-threads': return runResolveThreads(args);
|
|
268
|
+
case 'review-prompt': return runReviewPrompt(args);
|
|
252
269
|
default:
|
|
253
270
|
process.stderr.write(`Unknown command: ${cmd || '(none)'}\n\n${HELP}`);
|
|
254
271
|
process.exit(2);
|
|
@@ -1251,6 +1268,10 @@ async function runInit(args) {
|
|
|
1251
1268
|
// Claude Code session — the agent reviews the current branch's open PR using
|
|
1252
1269
|
// THAT session's own tokens (Max or API), no hosted App or new auth required.
|
|
1253
1270
|
// `.claude/` is already in the --commit add-list below, so it's staged too.
|
|
1271
|
+
// `--with-hooks` implies `--with-local-review` — the auto hook and the manual
|
|
1272
|
+
// `/clud-bug-review` command are complementary (the hook auto-runs the engine
|
|
1273
|
+
// recipe on each commit; the command runs it on demand against the PR).
|
|
1274
|
+
if (args.withHooks) args.withLocalReview = true;
|
|
1254
1275
|
if (args.withLocalReview) {
|
|
1255
1276
|
const commandPath = join(cwd, '.claude', 'commands', 'clud-bug-review.md');
|
|
1256
1277
|
await mkdir(dirname(commandPath), { recursive: true });
|
|
@@ -1259,6 +1280,41 @@ async function runInit(args) {
|
|
|
1259
1280
|
log(` wrote ${rel(cwd, commandPath)}`);
|
|
1260
1281
|
}
|
|
1261
1282
|
|
|
1283
|
+
// v0.7.0 (Wave 6b): optional native commit-review hook. Merges a Claude Code
|
|
1284
|
+
// `type: agent` PostToolUse hook into `.claude/settings.json` (preserving any
|
|
1285
|
+
// existing settings + hooks) that, on every `git commit` the agent makes,
|
|
1286
|
+
// spawns a clud-bug review subagent on the session's subscription, in the
|
|
1287
|
+
// background — the hook's prompt runs `clud-bug review-prompt` and follows it.
|
|
1288
|
+
if (args.withHooks) {
|
|
1289
|
+
const { mergeLocalReviewHook, buildCommitReviewPrompt } = await import('./hooks.js');
|
|
1290
|
+
const hookPrompt = buildCommitReviewPrompt(await readPkgVersion());
|
|
1291
|
+
const settingsPath = join(cwd, '.claude', 'settings.json');
|
|
1292
|
+
await mkdir(dirname(settingsPath), { recursive: true });
|
|
1293
|
+
// Read-then-parse so we can tell "no file yet" (fresh merge) from "file
|
|
1294
|
+
// exists but is invalid JSON" (leave it ALONE — never clobber user content).
|
|
1295
|
+
let raw: string | undefined;
|
|
1296
|
+
try {
|
|
1297
|
+
raw = await readFile(settingsPath, 'utf8');
|
|
1298
|
+
} catch {
|
|
1299
|
+
raw = undefined; // no settings.json yet → fresh
|
|
1300
|
+
}
|
|
1301
|
+
let existing: unknown;
|
|
1302
|
+
let proceed = true;
|
|
1303
|
+
if (raw !== undefined) {
|
|
1304
|
+
try {
|
|
1305
|
+
existing = JSON.parse(raw);
|
|
1306
|
+
} catch {
|
|
1307
|
+
proceed = false;
|
|
1308
|
+
log(` skipped ${rel(cwd, settingsPath)} (existing file is not valid JSON; left untouched)`);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
if (proceed) {
|
|
1312
|
+
const merged = mergeLocalReviewHook(existing, hookPrompt);
|
|
1313
|
+
await writeFile(settingsPath, JSON.stringify(merged, null, 2) + '\n');
|
|
1314
|
+
log(` wrote ${rel(cwd, settingsPath)} (commit-review hook)`);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1262
1318
|
// Stamp the manifest. Sets strictMode: true ONLY on fresh installs —
|
|
1263
1319
|
// a manifest that's never been touched by clud-bug init/update has no
|
|
1264
1320
|
// lastUpdate field. Existing v0.3.x advisory installs (where strictMode
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// `clud-bug review-prompt` — emits the local-review RECIPE: a highly-structured
|
|
2
|
+
// prompt the in-session Claude Code agent (or a `type: agent` hook) runs to
|
|
3
|
+
// review the current diff, on the session's own subscription. It is the dynamic,
|
|
4
|
+
// planReview-driven counterpart of the rc.11 static slash-command prompt: the
|
|
5
|
+
// recipe is rendered FROM the shared engine (`core/planReview`), so a commit
|
|
6
|
+
// gets a single fast pass and a PR gets the full multi-pass plan — clud-bug
|
|
7
|
+
// writes the recipe, Claude Code's subagent is the runtime.
|
|
8
|
+
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { readFile } from 'node:fs/promises';
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
planReview,
|
|
14
|
+
roleForPass,
|
|
15
|
+
readReviewPassesConfig,
|
|
16
|
+
parseFrontmatter,
|
|
17
|
+
type ReviewPlan,
|
|
18
|
+
type ReviewPlanSkill,
|
|
19
|
+
type ReviewTrigger,
|
|
20
|
+
type ReviewPassMode,
|
|
21
|
+
} from '../core/index.js';
|
|
22
|
+
import { readManifest } from './skills.js';
|
|
23
|
+
|
|
24
|
+
/** Marker that identifies a clud-bug local-review recipe (idempotency + hook detection). */
|
|
25
|
+
export const CLUD_BUG_RECIPE_MARKER = 'clud-bug-local-review';
|
|
26
|
+
|
|
27
|
+
const MODE_AGGREGATION: Record<ReviewPassMode, string> = {
|
|
28
|
+
'cross-check':
|
|
29
|
+
"Pass 1 reviews the diff against all the skills; each later pass re-reviews AND checks pass 1's findings (agree / disagree), adding any new ones it finds.",
|
|
30
|
+
consensus:
|
|
31
|
+
'Run all passes independently against all the skills, then keep only findings that two or more passes agree on.',
|
|
32
|
+
independent:
|
|
33
|
+
'Run all passes independently against all the skills, then take the union of their findings, each attributed to its pass.',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const TRIGGER_INTRO: Record<ReviewTrigger, string> = {
|
|
37
|
+
commit: 'a fast review of the commit you just made',
|
|
38
|
+
push: 'a review of the branch you are about to push',
|
|
39
|
+
pr: "a review of this branch's open PR",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Render the local-review recipe from a resolved plan. Pure — all I/O (loading
|
|
44
|
+
* skills + config) happens in `runReviewPrompt`; this turns a `ReviewPlan` into
|
|
45
|
+
* the prompt text. The number of passes, the aggregation mode, the role tiers,
|
|
46
|
+
* and the skills all come from the plan, so the recipe scales from a single
|
|
47
|
+
* fast commit pass up to the full multi-pass PR review without branching here.
|
|
48
|
+
*/
|
|
49
|
+
export function renderReviewRecipe(input: { plan: ReviewPlan; trigger: ReviewTrigger }): string {
|
|
50
|
+
const { plan, trigger } = input;
|
|
51
|
+
const slugs = plan.perSkill.map((p) => p.slug);
|
|
52
|
+
const maxPasses = plan.perSkill.length
|
|
53
|
+
? Math.max(...plan.perSkill.map((p) => p.count))
|
|
54
|
+
: 1;
|
|
55
|
+
// Use the aggregation mode of the skill that drives the pass depth, not just
|
|
56
|
+
// the first skill (which may be configured for a single pass).
|
|
57
|
+
const mode: ReviewPassMode =
|
|
58
|
+
plan.perSkill.find((p) => p.count === maxPasses)?.mode ?? 'cross-check';
|
|
59
|
+
|
|
60
|
+
const diffStep =
|
|
61
|
+
trigger === 'commit'
|
|
62
|
+
? 'The commit you just made:\n\n```bash\ngit show --no-color --format=medium HEAD\n```'
|
|
63
|
+
: 'If an open PR exists for this branch, review it; otherwise diff the branch against its base:\n\n' +
|
|
64
|
+
'```bash\n' +
|
|
65
|
+
'PR=$(gh pr list --head "$(git branch --show-current)" --state open --json number --jq \'.[0].number\')\n' +
|
|
66
|
+
'if [ -n "$PR" ]; then\n' +
|
|
67
|
+
' gh pr diff "$PR"\n' +
|
|
68
|
+
'else\n' +
|
|
69
|
+
' git remote set-head origin --auto >/dev/null 2>&1 || true # make sure origin/HEAD resolves\n' +
|
|
70
|
+
' git diff --no-color origin/HEAD...HEAD\n' +
|
|
71
|
+
'fi\n' +
|
|
72
|
+
'```';
|
|
73
|
+
|
|
74
|
+
const skillsList =
|
|
75
|
+
slugs.length > 0
|
|
76
|
+
? slugs.map((s) => ` - \`.claude/skills/${s}/SKILL.md\``).join('\n')
|
|
77
|
+
: ' - (no skills resolved — apply the baseline discipline below)';
|
|
78
|
+
|
|
79
|
+
let reviewStep: string;
|
|
80
|
+
if (maxPasses <= 1) {
|
|
81
|
+
reviewStep =
|
|
82
|
+
'Review the diff against every loaded skill in a single pass. For each REAL issue, ' +
|
|
83
|
+
'record `file`, `line`, `severity` (`critical` | `minor` | `preexisting`), the `skill` ' +
|
|
84
|
+
'that motivated it, and a one-line `summary` with the quoted offending line. Finding ' +
|
|
85
|
+
'nothing is the normal outcome — be precise, not exhaustive.';
|
|
86
|
+
} else {
|
|
87
|
+
const passLines = Array.from({ length: maxPasses }, (_, i) => {
|
|
88
|
+
const role = roleForPass(plan.roles, i, 'Reviewer');
|
|
89
|
+
const tier = role.tier ? ` · ${role.tier} tier` : '';
|
|
90
|
+
return ` ${i + 1}. **${role.name}**${tier}`;
|
|
91
|
+
}).join('\n');
|
|
92
|
+
reviewStep =
|
|
93
|
+
`Dispatch ${maxPasses} reviewer sub-agents — a ${maxPasses}-pass **${mode}** review on this ` +
|
|
94
|
+
`session's subscription (bind each tier to a Claude Code model: a fast model for \`beetle\`, ` +
|
|
95
|
+
`a strong model for \`wasp\`/\`mantis\`):\n\n${passLines}\n\n${MODE_AGGREGATION[mode]}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const surface =
|
|
99
|
+
trigger === 'commit'
|
|
100
|
+
? 'Surface the findings back into this session so the agent can fix them immediately. ' +
|
|
101
|
+
'If the commit is clean, report a single line: `clud-bug: commit <short-sha> — clean.`'
|
|
102
|
+
: 'Surface the findings into the session, and — if an open PR exists — post or edit (in ' +
|
|
103
|
+
'place, by integer comment id) the clud-bug summary comment on it.';
|
|
104
|
+
|
|
105
|
+
return `<!-- ${CLUD_BUG_RECIPE_MARKER} v1 -->
|
|
106
|
+
You are **clud-bug**, running ${TRIGGER_INTRO[trigger]} inside this Claude Code session, on
|
|
107
|
+
this session's own model tokens — no hosted App, no extra auth (you already have \`git\`,
|
|
108
|
+
\`gh\`, and file access).
|
|
109
|
+
|
|
110
|
+
## The plan
|
|
111
|
+
clud-bug resolved this review from the repo's skills + \`.clud-bug.json\`:
|
|
112
|
+
**${plan.summary}**
|
|
113
|
+
|
|
114
|
+
## 1. Get the diff
|
|
115
|
+
${diffStep}
|
|
116
|
+
|
|
117
|
+
## 2. Load the review skills
|
|
118
|
+
Read each skill's discipline from the checkout:
|
|
119
|
+
${skillsList}
|
|
120
|
+
|
|
121
|
+
Apply them strictly — at minimum **critical-issues-only** (flag only correctness, security,
|
|
122
|
+
or performance bugs — skip nits), **evidence-based-review** (quote the exact line you flag),
|
|
123
|
+
and **respect-existing-conventions** (don't fight the codebase's patterns).
|
|
124
|
+
|
|
125
|
+
## 3. Review
|
|
126
|
+
${reviewStep}
|
|
127
|
+
|
|
128
|
+
## 4. Report
|
|
129
|
+
Render the body in clud-bug's standard shape (§1.8.1) — omit any empty section:
|
|
130
|
+
|
|
131
|
+
\`\`\`
|
|
132
|
+
## 🐛 Clud Bug review — <clean | critical findings>
|
|
133
|
+
|
|
134
|
+
**This round:** N critical · N minor · N resolved from prior · N still open
|
|
135
|
+
|
|
136
|
+
Found: N 🔴 / N 🟡 / N 🟣
|
|
137
|
+
|
|
138
|
+
<per-finding: 🔴 [skill]: <summary> (file:line) — with the quoted line + a one-line fix>
|
|
139
|
+
|
|
140
|
+
Skills referenced: [<the skills you applied>]
|
|
141
|
+
|
|
142
|
+
<!-- written-by: @<login> (clud-bug local-mode) -->
|
|
143
|
+
\`\`\`
|
|
144
|
+
|
|
145
|
+
${surface}
|
|
146
|
+
|
|
147
|
+
Keep it tight — this is the local safety net; the deeper review still happens at PR time.
|
|
148
|
+
`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
interface ReviewPromptArgs {
|
|
152
|
+
trigger?: string;
|
|
153
|
+
cwd?: string;
|
|
154
|
+
diffSizeBytes?: number;
|
|
155
|
+
_?: string[];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* `clud-bug review-prompt [--trigger commit|push|pr]` — load the repo's skills +
|
|
160
|
+
* config, plan the review through `core/planReview`, and print the recipe to
|
|
161
|
+
* stdout. Defaults to the `commit` trigger (the primary hook consumer).
|
|
162
|
+
*/
|
|
163
|
+
export async function runReviewPrompt(args: ReviewPromptArgs): Promise<void> {
|
|
164
|
+
const cwd = args.cwd ?? process.cwd();
|
|
165
|
+
const trigger = normalizeTrigger(args.trigger);
|
|
166
|
+
|
|
167
|
+
const skillsDir = join(cwd, '.claude', 'skills');
|
|
168
|
+
const manifest = await readManifest(skillsDir);
|
|
169
|
+
|
|
170
|
+
// Load each installed skill's frontmatter (for `review_mode`), tolerating a
|
|
171
|
+
// skill whose SKILL.md is missing or unparseable (skip it, don't crash).
|
|
172
|
+
const skills: ReviewPlanSkill[] = [];
|
|
173
|
+
// Keep the raw SKILL.md text so `planReview` can honor a skill author's
|
|
174
|
+
// `review_passes:` frontmatter override (precedence layer 2) — without this
|
|
175
|
+
// that layer is silently dead in local mode.
|
|
176
|
+
const rawSkillMd: Record<string, string> = {};
|
|
177
|
+
for (const entry of manifest.installed) {
|
|
178
|
+
try {
|
|
179
|
+
const raw = await readFile(join(skillsDir, entry.slug, 'SKILL.md'), 'utf8');
|
|
180
|
+
skills.push({ slug: entry.slug, frontmatter: parseFrontmatter(raw) });
|
|
181
|
+
rawSkillMd[entry.slug] = raw;
|
|
182
|
+
} catch {
|
|
183
|
+
// Skill body unreadable/unparseable — omit from the plan.
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const config = readReviewPassesConfig(manifest);
|
|
188
|
+
const plan = planReview({
|
|
189
|
+
skills,
|
|
190
|
+
config,
|
|
191
|
+
trigger,
|
|
192
|
+
rawSkillMd,
|
|
193
|
+
...(args.diffSizeBytes !== undefined ? { diffSizeBytes: args.diffSizeBytes } : {}),
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
process.stdout.write(renderReviewRecipe({ plan, trigger }) + '\n');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function normalizeTrigger(raw: string | undefined): ReviewTrigger {
|
|
200
|
+
if (raw === undefined || raw === 'commit') return 'commit';
|
|
201
|
+
if (raw === 'push' || raw === 'pr') return raw;
|
|
202
|
+
process.stderr.write(
|
|
203
|
+
`clud-bug review-prompt: unrecognized --trigger "${raw}" (expected commit|push|pr); using commit.\n`,
|
|
204
|
+
);
|
|
205
|
+
return 'commit';
|
|
206
|
+
}
|
package/src/cli/update.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { reviewPrompt } from '../core/prompts.js';
|
|
|
5
5
|
import { detect, buildDescriptionLine } from '../core/detect.js';
|
|
6
6
|
import { loadBaseline, readManifest, writeManifest, type LoadBaselineOptions } from './skills.js';
|
|
7
7
|
import { applyToRepo as applyAgentDocs } from './agents-md.js';
|
|
8
|
+
import { mergeLocalReviewHook, buildCommitReviewPrompt, CLUD_BUG_HOOK_MARKER } from './hooks.js';
|
|
8
9
|
|
|
9
10
|
// Re-render the user's workflow + refresh baseline skills using the
|
|
10
11
|
// templates / baseline shipped with the currently-installed clud-bug.
|
|
@@ -162,6 +163,27 @@ export async function runUpdate(opts: RunUpdateOptions): Promise<RunUpdateResult
|
|
|
162
163
|
}
|
|
163
164
|
}
|
|
164
165
|
|
|
166
|
+
// 5c. Refresh the native commit-review hook (Wave 6b) in place when it was
|
|
167
|
+
// scaffolded via `clud-bug init --with-hooks` and our entry is intact (the
|
|
168
|
+
// `clud-bug-local-review` marker). settings.json is user-managed — we only
|
|
169
|
+
// re-merge OUR marked hook, never touching the user's other hooks/settings.
|
|
170
|
+
const settingsPath = join(cwd, '.claude', 'settings.json');
|
|
171
|
+
if (await pathExists(settingsPath)) {
|
|
172
|
+
const prior = await readSafe(settingsPath);
|
|
173
|
+
if (prior && prior.includes(CLUD_BUG_HOOK_MARKER)) {
|
|
174
|
+
try {
|
|
175
|
+
const merged = mergeLocalReviewHook(JSON.parse(prior), buildCommitReviewPrompt(ourVersion));
|
|
176
|
+
await maybeWrite(settingsPath, JSON.stringify(merged, null, 2) + '\n', changed, unchanged, 'commit-review hook');
|
|
177
|
+
} catch {
|
|
178
|
+
skipped.push({
|
|
179
|
+
path: settingsPath,
|
|
180
|
+
label: 'commit-review hook',
|
|
181
|
+
reason: 'settings.json is not valid JSON; left untouched',
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
165
187
|
// 6. Stamp the manifest with the version that ran the update.
|
|
166
188
|
manifest['lastUpdate'] = new Date().toISOString();
|
|
167
189
|
manifest['lastUpdateVersion'] = ourVersion;
|