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.
Files changed (55) hide show
  1. package/dist/cli/hooks.d.ts +40 -0
  2. package/dist/cli/hooks.d.ts.map +1 -0
  3. package/dist/cli/hooks.js +105 -0
  4. package/dist/cli/hooks.js.map +1 -0
  5. package/dist/cli/main.d.ts.map +1 -1
  6. package/dist/cli/main.js +61 -0
  7. package/dist/cli/main.js.map +1 -1
  8. package/dist/cli/review-prompt.d.ts +28 -0
  9. package/dist/cli/review-prompt.d.ts.map +1 -0
  10. package/dist/cli/review-prompt.js +169 -0
  11. package/dist/cli/review-prompt.js.map +1 -0
  12. package/dist/cli/update.d.ts.map +1 -1
  13. package/dist/cli/update.js +22 -0
  14. package/dist/cli/update.js.map +1 -1
  15. package/dist/core/budget-plan.d.ts +125 -0
  16. package/dist/core/budget-plan.d.ts.map +1 -0
  17. package/dist/core/budget-plan.js +211 -0
  18. package/dist/core/budget-plan.js.map +1 -0
  19. package/dist/core/index.d.ts +5 -1
  20. package/dist/core/index.d.ts.map +1 -1
  21. package/dist/core/index.js +14 -0
  22. package/dist/core/index.js.map +1 -1
  23. package/dist/core/multi-pass-aggregate.d.ts +131 -0
  24. package/dist/core/multi-pass-aggregate.d.ts.map +1 -0
  25. package/dist/core/multi-pass-aggregate.js +427 -0
  26. package/dist/core/multi-pass-aggregate.js.map +1 -0
  27. package/dist/core/plan-review.d.ts +57 -0
  28. package/dist/core/plan-review.d.ts.map +1 -0
  29. package/dist/core/plan-review.js +77 -0
  30. package/dist/core/plan-review.js.map +1 -0
  31. package/dist/core/review-plan.d.ts +219 -0
  32. package/dist/core/review-plan.d.ts.map +1 -0
  33. package/dist/core/review-plan.js +274 -0
  34. package/dist/core/review-plan.js.map +1 -0
  35. package/dist/core/review-writeback.d.ts +20 -0
  36. package/dist/core/review-writeback.d.ts.map +1 -1
  37. package/dist/core/review-writeback.js +16 -0
  38. package/dist/core/review-writeback.js.map +1 -1
  39. package/dist/core/version.d.ts +1 -1
  40. package/dist/core/version.js +1 -1
  41. package/package.json +1 -1
  42. package/src/cli/hooks.ts +122 -0
  43. package/src/cli/main.ts +56 -0
  44. package/src/cli/review-prompt.ts +206 -0
  45. package/src/cli/update.ts +22 -0
  46. package/src/core/budget-plan.ts +324 -0
  47. package/src/core/index.ts +64 -0
  48. package/src/core/multi-pass-aggregate.ts +545 -0
  49. package/src/core/plan-review.ts +136 -0
  50. package/src/core/review-plan.ts +485 -0
  51. package/src/core/review-writeback.ts +38 -0
  52. package/src/core/version.ts +1 -1
  53. package/templates/workflow-py.yml.tmpl +1 -1
  54. package/templates/workflow-ts.yml.tmpl +1 -1
  55. package/templates/workflow.yml.tmpl +1 -1
@@ -0,0 +1,40 @@
1
+ /** Stable marker embedded in our hook's prompt so re-runs replace it in place. */
2
+ export declare const CLUD_BUG_HOOK_MARKER = "clud-bug-local-review";
3
+ /**
4
+ * Build the `prompt` of the Claude Code `type: agent` commit-review hook,
5
+ * pinned to the clud-bug VERSION that scaffolded it. Pinning matters: a bare
6
+ * `npx clud-bug` resolves to the `latest` dist-tag, which can predate the
7
+ * `review-prompt` verb (e.g. while v0.7 is prerelease on `next`); `@${version}`
8
+ * guarantees the verb exists. `clud-bug update` refreshes the pin in place.
9
+ *
10
+ * Rather than baking a static recipe (which would drift from the repo's
11
+ * skills/config), it tells the subagent to run `clud-bug review-prompt` — the
12
+ * engine-driven verb that emits a recipe tailored to THIS repo's resolved plan
13
+ * — and follow it. The recipe is generated fresh at commit time, always current.
14
+ */
15
+ export declare function buildCommitReviewPrompt(version: string): string;
16
+ /** One Claude Code hook entry: a tool matcher plus its hook list. */
17
+ export interface HookMatcherEntry {
18
+ matcher?: string;
19
+ hooks: Array<Record<string, unknown>>;
20
+ }
21
+ /** Minimal shape of the `.claude/settings.json` we read + merge into. */
22
+ export interface ClaudeSettings {
23
+ hooks?: Record<string, HookMatcherEntry[]>;
24
+ [key: string]: unknown;
25
+ }
26
+ /**
27
+ * Builds the PostToolUse entry that spawns the commit-review subagent: a native
28
+ * `type: agent` hook that is backgrounded (`async`), surfaces findings
29
+ * (`asyncRewake`), and fires only on `git commit` (`if`).
30
+ */
31
+ export declare function buildLocalReviewHook(recipe: string): HookMatcherEntry;
32
+ /**
33
+ * Merges the clud-bug commit-review hook into an existing `.claude/settings.json`
34
+ * object. **Idempotent** (replaces any prior clud-bug entry rather than
35
+ * duplicating) and **non-clobbering** (preserves every other top-level key,
36
+ * every other event, and every other hook). Tolerates a missing/malformed
37
+ * `existing` value.
38
+ */
39
+ export declare function mergeLocalReviewHook(existing: unknown, recipe: string): ClaudeSettings;
40
+ //# sourceMappingURL=hooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../src/cli/hooks.ts"],"names":[],"mappings":"AAcA,kFAAkF;AAClF,eAAO,MAAM,oBAAoB,0BAA0B,CAAC;AAE5D;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAkB/D;AAED,qEAAqE;AACrE,MAAM,WAAW,gBAAgB;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACvC;AAED,yEAAyE;AACzE,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,EAAE,CAAC,CAAC;IAC3C,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,CAcrE;AAUD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,cAAc,CAwBtF"}
@@ -0,0 +1,105 @@
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
+ /** Stable marker embedded in our hook's prompt so re-runs replace it in place. */
15
+ export const CLUD_BUG_HOOK_MARKER = 'clud-bug-local-review';
16
+ /**
17
+ * Build the `prompt` of the Claude Code `type: agent` commit-review hook,
18
+ * pinned to the clud-bug VERSION that scaffolded it. Pinning matters: a bare
19
+ * `npx clud-bug` resolves to the `latest` dist-tag, which can predate the
20
+ * `review-prompt` verb (e.g. while v0.7 is prerelease on `next`); `@${version}`
21
+ * guarantees the verb exists. `clud-bug update` refreshes the pin in place.
22
+ *
23
+ * Rather than baking a static recipe (which would drift from the repo's
24
+ * skills/config), it tells the subagent to run `clud-bug review-prompt` — the
25
+ * engine-driven verb that emits a recipe tailored to THIS repo's resolved plan
26
+ * — and follow it. The recipe is generated fresh at commit time, always current.
27
+ */
28
+ export function buildCommitReviewPrompt(version) {
29
+ return `<!-- ${CLUD_BUG_HOOK_MARKER} v1 -->
30
+ You are clud-bug's local commit review, running as a background subagent on this
31
+ session's own subscription (no extra auth — you have git, gh, and file access).
32
+
33
+ Step 1 — get your review recipe from clud-bug's engine:
34
+
35
+ npx clud-bug@${version} review-prompt --trigger commit
36
+
37
+ That prints a structured review recipe tailored to THIS repo's skills + config (a
38
+ fast single pass for a commit). If that command isn't available, run the repo's
39
+ installed clud-bug CLI's \`review-prompt --trigger commit\` instead.
40
+
41
+ Step 2 — follow that recipe exactly: review the commit that was just made against
42
+ the skills it names, and surface any findings back into the session so they can be
43
+ fixed. A clean commit needs only a one-line note.
44
+
45
+ Keep it tight — this is the commit-time safety net; the deeper review runs at PR time.`;
46
+ }
47
+ /**
48
+ * Builds the PostToolUse entry that spawns the commit-review subagent: a native
49
+ * `type: agent` hook that is backgrounded (`async`), surfaces findings
50
+ * (`asyncRewake`), and fires only on `git commit` (`if`).
51
+ */
52
+ export function buildLocalReviewHook(recipe) {
53
+ return {
54
+ matcher: 'Bash',
55
+ hooks: [
56
+ {
57
+ type: 'agent',
58
+ if: 'Bash(git commit *)',
59
+ async: true,
60
+ asyncRewake: true,
61
+ timeout: 180,
62
+ prompt: recipe,
63
+ },
64
+ ],
65
+ };
66
+ }
67
+ function isOurHook(h) {
68
+ return typeof h?.['prompt'] === 'string' && h['prompt'].includes(CLUD_BUG_HOOK_MARKER);
69
+ }
70
+ function isCludBugReviewEntry(entry) {
71
+ return !!entry && Array.isArray(entry.hooks) && entry.hooks.some(isOurHook);
72
+ }
73
+ /**
74
+ * Merges the clud-bug commit-review hook into an existing `.claude/settings.json`
75
+ * object. **Idempotent** (replaces any prior clud-bug entry rather than
76
+ * duplicating) and **non-clobbering** (preserves every other top-level key,
77
+ * every other event, and every other hook). Tolerates a missing/malformed
78
+ * `existing` value.
79
+ */
80
+ export function mergeLocalReviewHook(existing, recipe) {
81
+ const base = existing && typeof existing === 'object' ? { ...existing } : {};
82
+ const hooks = { ...(base.hooks ?? {}) };
83
+ const priorPost = Array.isArray(hooks.PostToolUse) ? hooks.PostToolUse : [];
84
+ // Preserve every non-clud-bug hook — including any the user co-located INSIDE
85
+ // our own matcher entry: drop only the hook(s) carrying our marker, never the
86
+ // whole entry.
87
+ const ours = buildLocalReviewHook(recipe);
88
+ const otherEntries = [];
89
+ const coLocatedUserHooks = [];
90
+ for (const entry of priorPost) {
91
+ if (isCludBugReviewEntry(entry)) {
92
+ for (const h of entry.hooks)
93
+ if (!isOurHook(h))
94
+ coLocatedUserHooks.push(h);
95
+ }
96
+ else {
97
+ otherEntries.push(entry);
98
+ }
99
+ }
100
+ const ourEntry = coLocatedUserHooks.length > 0 ? { ...ours, hooks: [...coLocatedUserHooks, ...ours.hooks] } : ours;
101
+ hooks.PostToolUse = [...otherEntries, ourEntry];
102
+ base.hooks = hooks;
103
+ return base;
104
+ }
105
+ //# sourceMappingURL=hooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.js","sourceRoot":"","sources":["../../src/cli/hooks.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,EAAE;AACF,+EAA+E;AAC/E,8EAA8E;AAC9E,0EAA0E;AAC1E,iFAAiF;AACjF,EAAE;AACF,wEAAwE;AACxE,gFAAgF;AAChF,iEAAiE;AACjE,EAAE;AACF,0EAA0E;AAC1E,gFAAgF;AAEhF,kFAAkF;AAClF,MAAM,CAAC,MAAM,oBAAoB,GAAG,uBAAuB,CAAC;AAE5D;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,uBAAuB,CAAC,OAAe;IACrD,OAAO,QAAQ,oBAAoB;;;;;;mBAMlB,OAAO;;;;;;;;;;uFAU6D,CAAC;AACxF,CAAC;AAcD;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,MAAc;IACjD,OAAO;QACL,OAAO,EAAE,MAAM;QACf,KAAK,EAAE;YACL;gBACE,IAAI,EAAE,OAAO;gBACb,EAAE,EAAE,oBAAoB;gBACxB,KAAK,EAAE,IAAI;gBACX,WAAW,EAAE,IAAI;gBACjB,OAAO,EAAE,GAAG;gBACZ,MAAM,EAAE,MAAM;aACf;SACF;KACF,CAAC;AACJ,CAAC;AAED,SAAS,SAAS,CAAC,CAAsC;IACvD,OAAO,OAAO,CAAC,EAAE,CAAC,QAAQ,CAAC,KAAK,QAAQ,IAAK,CAAC,CAAC,QAAQ,CAAY,CAAC,QAAQ,CAAC,oBAAoB,CAAC,CAAC;AACrG,CAAC;AAED,SAAS,oBAAoB,CAAC,KAAmC;IAC/D,OAAO,CAAC,CAAC,KAAK,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;AAC9E,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAAC,QAAiB,EAAE,MAAc;IACpE,MAAM,IAAI,GACR,QAAQ,IAAI,OAAO,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAI,QAA2B,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IACtF,MAAM,KAAK,GAAuC,EAAE,GAAG,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,EAAE,CAAC;IAC5E,MAAM,SAAS,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC;IAE5E,8EAA8E;IAC9E,8EAA8E;IAC9E,eAAe;IACf,MAAM,IAAI,GAAG,oBAAoB,CAAC,MAAM,CAAC,CAAC;IAC1C,MAAM,YAAY,GAAuB,EAAE,CAAC;IAC5C,MAAM,kBAAkB,GAAmC,EAAE,CAAC;IAC9D,KAAK,MAAM,KAAK,IAAI,SAAS,EAAE,CAAC;QAC9B,IAAI,oBAAoB,CAAC,KAAK,CAAC,EAAE,CAAC;YAChC,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,KAAK;gBAAE,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;oBAAE,kBAAkB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC7E,CAAC;aAAM,CAAC;YACN,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC3B,CAAC;IACH,CAAC;IACD,MAAM,QAAQ,GACZ,kBAAkB,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,CAAC,GAAG,kBAAkB,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IACpG,KAAK,CAAC,WAAW,GAAG,CAAC,GAAG,YAAY,EAAE,QAAQ,CAAC,CAAC;IAChD,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACnB,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/cli/main.ts"],"names":[],"mappings":"AAmOA,iBAAe,IAAI,kBA4BlB;AA45DD,OAAO,EAAE,IAAI,EAAE,CAAC"}
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/cli/main.ts"],"names":[],"mappings":"AAmPA,iBAAe,IAAI,kBA6BlB;AAm8DD,OAAO,EAAE,IAAI,EAAE,CAAC"}
package/dist/cli/main.js CHANGED
@@ -26,6 +26,7 @@ import { writeSkills, writeSkill, loadBaseline, readManifest, writeManifest, rem
26
26
  import { computeAuditFileSet } from './audit.js';
27
27
  import { renderAuditHeader } from '../core/audit.js';
28
28
  import { runUpdate } from './update.js';
29
+ import { runReviewPrompt } from './review-prompt.js';
29
30
  import { getPendingWorkflowEdits, makeBranchName, git as gitCmd } from './edit-workflow.js';
30
31
  import { applyToRepo as applyAgentDocs } from './agents-md.js';
31
32
  import { detectRepo, detectDefaultBranch, getProtectionState, enableConversationResolution } from './branch-protection.js';
@@ -59,6 +60,7 @@ function parseArgs(argv) {
59
60
  // .claude/commands/clud-bug-review.md so `/clud-bug-review` works in a
60
61
  // Claude Code session (reviews the current PR with the session's tokens).
61
62
  withLocalReview: false,
63
+ withHooks: false,
62
64
  // v0.7.0-rc.4: `clud-bug configure-github` flags.
63
65
  // --dry-run prints the diff but skips PATCH; --branch overrides "main".
64
66
  dryRun: false,
@@ -106,10 +108,16 @@ function parseArgs(argv) {
106
108
  args.withSkdd = true;
107
109
  else if (a === '--with-local-review')
108
110
  args.withLocalReview = true;
111
+ else if (a === '--with-hooks')
112
+ args.withHooks = true;
109
113
  else if (a === '--dry-run')
110
114
  args.dryRun = true;
111
115
  else if (a === '--branch')
112
116
  args.branch = argv[++i];
117
+ else if (a === '--trigger')
118
+ args.trigger = argv[++i];
119
+ else if (a === '--diff-size')
120
+ args.diffSizeBytes = Number(argv[++i]);
113
121
  else
114
122
  args._.push(a);
115
123
  }
@@ -200,6 +208,11 @@ Commands:
200
208
  Required env vars: GH_TOKEN, ANTHROPIC_API_KEY,
201
209
  REPO ("owner/name"), PR_NUMBER. Output: JSON
202
210
  \`{actions, verifierCallCount, shouldRequestChanges}\`.
211
+ review-prompt Emit the local-review recipe — the structured prompt a
212
+ Claude Code agent/hook runs to review the current diff on
213
+ the session's own subscription. Plans via the shared
214
+ engine: --trigger commit (default) → a fast single pass;
215
+ push/pr → the full multi-pass plan. Prints to stdout.
203
216
 
204
217
  Options:
205
218
  --offline Skip skills.sh; pin only the bundled baseline specimens.
@@ -209,6 +222,11 @@ Options:
209
222
  \`/clud-bug-review\` works in a Claude Code session —
210
223
  reviews the current branch's PR using that session's
211
224
  own tokens (no hosted App, no extra auth).
225
+ --with-hooks (init) Also scaffold a native Claude Code \`type: agent\`
226
+ commit-review hook into .claude/settings.json — on every
227
+ \`git commit\` the agent makes, a backgrounded clud-bug
228
+ review subagent runs on the session's subscription.
229
+ Implies --with-local-review. Off by default.
212
230
  --quiet,-q Token-frugal mode for agent invocations. Suppresses
213
231
  progress chatter; emits exactly one final
214
232
  \`ok <key-value>\` summary line per command. Errors
@@ -221,6 +239,7 @@ Options:
221
239
  --dry-run Print the canonical-v1 diff without PATCH-ing
222
240
  (configure-github only).
223
241
  --branch <name> Target branch for configure-github (default: main).
242
+ --trigger <ctx> review-prompt context: commit (default) | push | pr.
224
243
  --repo <owner/name> Restrict \`usage\` to a single repo. Default: all repos
225
244
  with clud-bug-review.yml in the gh user's auth scope.
226
245
  --pr <N> Restrict \`usage\` to a single PR.
@@ -268,6 +287,7 @@ async function main() {
268
287
  case 'select-review-event': return runSelectReviewEvent(args);
269
288
  case 'post-inline-threads': return runPostInlineThreads(args);
270
289
  case 'resolve-threads': return runResolveThreads(args);
290
+ case 'review-prompt': return runReviewPrompt(args);
271
291
  default:
272
292
  process.stderr.write(`Unknown command: ${cmd || '(none)'}\n\n${HELP}`);
273
293
  process.exit(2);
@@ -1147,6 +1167,11 @@ async function runInit(args) {
1147
1167
  // Claude Code session — the agent reviews the current branch's open PR using
1148
1168
  // THAT session's own tokens (Max or API), no hosted App or new auth required.
1149
1169
  // `.claude/` is already in the --commit add-list below, so it's staged too.
1170
+ // `--with-hooks` implies `--with-local-review` — the auto hook and the manual
1171
+ // `/clud-bug-review` command are complementary (the hook auto-runs the engine
1172
+ // recipe on each commit; the command runs it on demand against the PR).
1173
+ if (args.withHooks)
1174
+ args.withLocalReview = true;
1150
1175
  if (args.withLocalReview) {
1151
1176
  const commandPath = join(cwd, '.claude', 'commands', 'clud-bug-review.md');
1152
1177
  await mkdir(dirname(commandPath), { recursive: true });
@@ -1154,6 +1179,42 @@ async function runInit(args) {
1154
1179
  await writeFile(commandPath, commandContent);
1155
1180
  log(` wrote ${rel(cwd, commandPath)}`);
1156
1181
  }
1182
+ // v0.7.0 (Wave 6b): optional native commit-review hook. Merges a Claude Code
1183
+ // `type: agent` PostToolUse hook into `.claude/settings.json` (preserving any
1184
+ // existing settings + hooks) that, on every `git commit` the agent makes,
1185
+ // spawns a clud-bug review subagent on the session's subscription, in the
1186
+ // background — the hook's prompt runs `clud-bug review-prompt` and follows it.
1187
+ if (args.withHooks) {
1188
+ const { mergeLocalReviewHook, buildCommitReviewPrompt } = await import('./hooks.js');
1189
+ const hookPrompt = buildCommitReviewPrompt(await readPkgVersion());
1190
+ const settingsPath = join(cwd, '.claude', 'settings.json');
1191
+ await mkdir(dirname(settingsPath), { recursive: true });
1192
+ // Read-then-parse so we can tell "no file yet" (fresh merge) from "file
1193
+ // exists but is invalid JSON" (leave it ALONE — never clobber user content).
1194
+ let raw;
1195
+ try {
1196
+ raw = await readFile(settingsPath, 'utf8');
1197
+ }
1198
+ catch {
1199
+ raw = undefined; // no settings.json yet → fresh
1200
+ }
1201
+ let existing;
1202
+ let proceed = true;
1203
+ if (raw !== undefined) {
1204
+ try {
1205
+ existing = JSON.parse(raw);
1206
+ }
1207
+ catch {
1208
+ proceed = false;
1209
+ log(` skipped ${rel(cwd, settingsPath)} (existing file is not valid JSON; left untouched)`);
1210
+ }
1211
+ }
1212
+ if (proceed) {
1213
+ const merged = mergeLocalReviewHook(existing, hookPrompt);
1214
+ await writeFile(settingsPath, JSON.stringify(merged, null, 2) + '\n');
1215
+ log(` wrote ${rel(cwd, settingsPath)} (commit-review hook)`);
1216
+ }
1217
+ }
1157
1218
  // Stamp the manifest. Sets strictMode: true ONLY on fresh installs —
1158
1219
  // a manifest that's never been touched by clud-bug init/update has no
1159
1220
  // lastUpdate field. Existing v0.3.x advisory installs (where strictMode