@webpieces/ai-hook-rules 0.3.220 → 0.3.222

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/ai-hook-rules",
3
- "version": "0.3.220",
3
+ "version": "0.3.222",
4
4
  "description": "Pluggable write-time validation framework for AI coding agents (@webpieces/ai-hook-rules). Claude Code PreToolUse + openclaw before_tool_call adapters share one rule engine.",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -35,7 +35,7 @@
35
35
  "directory": "packages/tooling/ai-hook-rules"
36
36
  },
37
37
  "dependencies": {
38
- "@webpieces/rules-config": "0.3.220"
38
+ "@webpieces/rules-config": "0.3.222"
39
39
  },
40
40
  "publishConfig": {
41
41
  "access": "public"
@@ -0,0 +1,3 @@
1
+ export declare function denyJson(reason: string): string;
2
+ export declare function emitDeny(reason: string): never;
3
+ export declare function emitAllow(): never;
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ // The single place that knows Claude Code's PreToolUse decision protocol, so every deny in the
3
+ // Claude Code adapter is emitted identically — and identically to the checked-in shim
4
+ // (.claude/webpieces/ai-hook.sh, rendered by renderShim() in ../bin/setup.ts), which already emits
5
+ // this exact JSON.
6
+ //
7
+ // A block is signalled by `permissionDecision: "deny"` JSON on STDOUT with exit 0 — NOT exit 2.
8
+ // Claude Code only parses the JSON on exit 0; exit 2 would ignore stdout and the reason would not
9
+ // surface in the terminal UI (the original bug). "deny" still blocks the tool, so this remains
10
+ // fail-closed: it is not the silent-allow a bare exit 0 with no JSON would be.
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.denyJson = denyJson;
13
+ exports.emitDeny = emitDeny;
14
+ exports.emitAllow = emitAllow;
15
+ function denyJson(reason) {
16
+ return JSON.stringify({
17
+ hookSpecificOutput: {
18
+ hookEventName: 'PreToolUse',
19
+ permissionDecision: 'deny',
20
+ permissionDecisionReason: reason,
21
+ },
22
+ });
23
+ }
24
+ // Block the tool call and surface `reason` to both the user (terminal UI) and the model.
25
+ function emitDeny(reason) {
26
+ process.stdout.write(denyJson(reason) + '\n');
27
+ process.exit(0);
28
+ }
29
+ // Allow the tool call. No JSON needed — a silent exit 0 is "allow" in the PreToolUse protocol.
30
+ function emitAllow() {
31
+ process.exit(0);
32
+ }
33
+ //# sourceMappingURL=claude-code-response.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude-code-response.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/adapters/claude-code-response.ts"],"names":[],"mappings":";AAAA,+FAA+F;AAC/F,sFAAsF;AACtF,mGAAmG;AACnG,mBAAmB;AACnB,EAAE;AACF,gGAAgG;AAChG,kGAAkG;AAClG,+FAA+F;AAC/F,+EAA+E;;AAE/E,4BAQC;AAGD,4BAGC;AAGD,8BAEC;AAnBD,SAAgB,QAAQ,CAAC,MAAc;IACnC,OAAO,IAAI,CAAC,SAAS,CAAC;QAClB,kBAAkB,EAAE;YAChB,aAAa,EAAE,YAAY;YAC3B,kBAAkB,EAAE,MAAM;YAC1B,wBAAwB,EAAE,MAAM;SACnC;KACJ,CAAC,CAAC;AACP,CAAC;AAED,yFAAyF;AACzF,SAAgB,QAAQ,CAAC,MAAc;IACnC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC;IAC9C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACpB,CAAC;AAED,+FAA+F;AAC/F,SAAgB,SAAS;IACrB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACpB,CAAC","sourcesContent":["// The single place that knows Claude Code's PreToolUse decision protocol, so every deny in the\n// Claude Code adapter is emitted identically — and identically to the checked-in shim\n// (.claude/webpieces/ai-hook.sh, rendered by renderShim() in ../bin/setup.ts), which already emits\n// this exact JSON.\n//\n// A block is signalled by `permissionDecision: \"deny\"` JSON on STDOUT with exit 0 — NOT exit 2.\n// Claude Code only parses the JSON on exit 0; exit 2 would ignore stdout and the reason would not\n// surface in the terminal UI (the original bug). \"deny\" still blocks the tool, so this remains\n// fail-closed: it is not the silent-allow a bare exit 0 with no JSON would be.\n\nexport function denyJson(reason: string): string {\n return JSON.stringify({\n hookSpecificOutput: {\n hookEventName: 'PreToolUse',\n permissionDecision: 'deny',\n permissionDecisionReason: reason,\n },\n });\n}\n\n// Block the tool call and surface `reason` to both the user (terminal UI) and the model.\nexport function emitDeny(reason: string): never {\n process.stdout.write(denyJson(reason) + '\\n');\n process.exit(0);\n}\n\n// Allow the tool call. No JSON needed — a silent exit 0 is \"allow\" in the PreToolUse protocol.\nexport function emitAllow(): never {\n process.exit(0);\n}\n"]}
@@ -2,7 +2,9 @@ import { HookMode } from '../core/types';
2
2
  export type { HookMode };
3
3
  /**
4
4
  * Shared entry point for all three Claude Code PreToolUse adapters. `mode` selects which tool kinds
5
- * to validate; payloads outside the mode's scope pass through (exit 0). Fails CLOSED (exit 2) on any
6
- * unexpected crash so a broken hook never silently lets an edit through.
5
+ * to validate; payloads outside the mode's scope pass through (emitAllow). Blocks by emitting a
6
+ * PreToolUse `permissionDecision:"deny"` JSON on stdout (exit 0) see claude-code-response.ts. Fails
7
+ * CLOSED on any unexpected crash (emits a deny) so a broken hook never silently lets an edit through,
8
+ * and the reason now surfaces in the Claude Code UI instead of being hidden on a stderr+exit-2 block.
7
9
  */
8
10
  export declare function runMain(mode: HookMode): Promise<void>;
@@ -10,6 +10,8 @@ const main_sync_refresh_1 = require("../core/main-sync-refresh");
10
10
  const load_config_1 = require("../core/load-config");
11
11
  const types_1 = require("../core/types");
12
12
  const to_error_1 = require("../core/to-error");
13
+ const claude_code_response_1 = require("./claude-code-response");
14
+ const shim_1 = require("../bin/shim");
13
15
  const HANDLED_FILE_TOOLS = new Set(['Write', 'Edit', 'MultiEdit']);
14
16
  function readStdin() {
15
17
  return new Promise((resolve) => {
@@ -63,27 +65,22 @@ function normalizeToolInput(toolKind, toolInput) {
63
65
  function handleBash(payload, cwd, mode) {
64
66
  const command = payload.tool_input.command;
65
67
  if (!command || command.trim() === '') {
66
- process.exit(0);
67
- return;
68
+ (0, claude_code_response_1.emitAllow)();
68
69
  }
69
70
  const result = (0, runner_1.runBash)(command, cwd, mode);
70
71
  if (!result) {
71
- process.exit(0);
72
- return;
72
+ (0, claude_code_response_1.emitAllow)();
73
73
  }
74
- process.stderr.write(result.report);
75
- process.exit(2);
74
+ (0, claude_code_response_1.emitDeny)(result.report);
76
75
  }
77
76
  function handleFileTool(payload, cwd, mode) {
78
77
  const toolKind = normalizeToolKind(payload.tool_name);
79
78
  if (!toolKind) {
80
- process.exit(0);
81
- return;
79
+ (0, claude_code_response_1.emitAllow)();
82
80
  }
83
81
  const input = normalizeToolInput(toolKind, payload.tool_input);
84
82
  if (!input) {
85
- process.exit(0);
86
- return;
83
+ (0, claude_code_response_1.emitAllow)();
87
84
  }
88
85
  // Always allow edits to webpieces.config.json — it's the fix target when the config is broken.
89
86
  // This exits BEFORE run(), so feature-branch-guard never sees a config edit; record that so the
@@ -96,22 +93,21 @@ function handleFileTool(payload, cwd, mode) {
96
93
  // refreshes the sync status. Fire-and-forget; never blocks the edit.
97
94
  (0, main_sync_refresh_1.triggerMainSyncRefresh)(cwd);
98
95
  }
99
- process.exit(0);
100
- return;
96
+ (0, claude_code_response_1.emitAllow)();
101
97
  }
102
98
  const result = (0, runner_1.run)(toolKind, input, cwd, mode);
103
99
  if (!result) {
104
- process.exit(0);
105
- return;
100
+ (0, claude_code_response_1.emitAllow)();
106
101
  }
107
102
  (0, rejection_log_1.logRejection)(toolKind, input, result, cwd);
108
- process.stderr.write(result.report);
109
- process.exit(2);
103
+ (0, claude_code_response_1.emitDeny)(result.report);
110
104
  }
111
105
  /**
112
106
  * Shared entry point for all three Claude Code PreToolUse adapters. `mode` selects which tool kinds
113
- * to validate; payloads outside the mode's scope pass through (exit 0). Fails CLOSED (exit 2) on any
114
- * unexpected crash so a broken hook never silently lets an edit through.
107
+ * to validate; payloads outside the mode's scope pass through (emitAllow). Blocks by emitting a
108
+ * PreToolUse `permissionDecision:"deny"` JSON on stdout (exit 0) see claude-code-response.ts. Fails
109
+ * CLOSED on any unexpected crash (emits a deny) so a broken hook never silently lets an edit through,
110
+ * and the reason now surfaces in the Claude Code UI instead of being hidden on a stderr+exit-2 block.
115
111
  */
116
112
  async function runMain(mode) {
117
113
  // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
@@ -119,18 +115,22 @@ async function runMain(mode) {
119
115
  const raw = await readStdin();
120
116
  const payload = safeParse(raw);
121
117
  if (!payload) {
122
- process.exit(0);
123
- return;
118
+ (0, claude_code_response_1.emitAllow)();
124
119
  }
125
120
  // Prefer the payload cwd (the AI's actual working dir, follows a persisted `cd`) over
126
121
  // process.cwd(); they match today, but the payload is the authoritative signal and stays
127
122
  // correct if the hook is ever invoked from a fixed dir (e.g. via $CLAUDE_PROJECT_DIR).
128
123
  const cwd = payload.cwd ?? process.cwd();
124
+ // Keep the committed shim (.claude/webpieces/ai-hook.sh) identical to renderShim() so its
125
+ // fail-closed escape hatch + installer allowlist never go stale — no human hand-edits it.
126
+ // Runs only when the guards binary is actually installed (i.e. now), is best-effort, and
127
+ // never throws into the decision below. 'rules' hook skips it (guards owns the shim).
128
+ if (mode !== 'rules')
129
+ (0, shim_1.healShim)(cwd);
129
130
  if (payload.tool_name === 'Bash') {
130
131
  // No code-style rule is bash-scoped, so the rules hook ignores Bash.
131
132
  if (mode === 'rules') {
132
- process.exit(0);
133
- return;
133
+ (0, claude_code_response_1.emitAllow)();
134
134
  }
135
135
  handleBash(payload, cwd, mode);
136
136
  return;
@@ -141,13 +141,18 @@ async function runMain(mode) {
141
141
  }
142
142
  catch (err) {
143
143
  const error = (0, to_error_1.toError)(err);
144
- if (err instanceof types_1.InformAiError) {
145
- process.stderr.write(error.message + '\n');
144
+ // An escaped RuleFailError (a rule that threw past the runner's per-rule catch) or an
145
+ // InformAiError (bad config/stdin) both carry an AI-readable message; anything else is an
146
+ // unexpected bug. All three deny (fail closed) and surface their reason to the AI.
147
+ if (error instanceof types_1.RuleFailError) {
148
+ (0, claude_code_response_1.emitDeny)(error.aiMessage);
149
+ }
150
+ else if (error instanceof types_1.InformAiError) {
151
+ (0, claude_code_response_1.emitDeny)(error.message);
146
152
  }
147
153
  else {
148
- process.stderr.write(`[ai-hooks] hook crashed unexpectedly — failing closed: ${error.message}\n`);
154
+ (0, claude_code_response_1.emitDeny)(`[ai-hooks] hook crashed unexpectedly — failing closed: ${error.message}`);
149
155
  }
150
- process.exit(2);
151
156
  }
152
157
  }
153
158
  //# sourceMappingURL=hook-core.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"hook-core.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/adapters/hook-core.ts"],"names":[],"mappings":"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","sourcesContent":["import * as path from 'path';\n\nimport { run, runBash } from '../core/runner';\nimport { logRejection } from '../core/rejection-log';\nimport { logGuardDecision, GuardDecision, branchForLog } from '../core/decision-log';\nimport { triggerMainSyncRefresh } from '../core/main-sync-refresh';\nimport { CONFIG_FILENAME } from '../core/load-config';\nimport { NormalizedToolInput, NormalizedEdit, ToolKind, InformAiError, HookMode } from '../core/types';\nimport { toError } from '../core/to-error';\n\n// Which category of rules this hook invocation runs. The hook is split into two independently\n// installable PreToolUse hooks; each runs ONE category (the runner filters by it), and both can\n// receive file AND bash payloads:\n// - 'rules' → code-style rules (file/edit scope). Bash payloads pass through (no code rules apply).\n// - 'guards' → hookGuards section: bash git/PR guards on Bash AND file guards (feature-branch-guard)\n// on Write/Edit. Matcher is Write|Edit|MultiEdit|Bash.\n// - 'all' → both categories, for the combined back-compat `wp-ai-hook` bin.\nexport type { HookMode };\n\nconst HANDLED_FILE_TOOLS = new Set(['Write', 'Edit', 'MultiEdit']);\n\ninterface ClaudeCodePayload {\n tool_name: string;\n tool_input: ClaudeCodeToolInput;\n // Claude Code sends the session's current working directory (follows a persisted `cd`). Used to\n // scope guards to the git repo the AI is actually in — see runner git-repo-boundary governance.\n cwd?: string;\n}\n\ninterface ClaudeCodeToolInput {\n file_path?: string;\n content?: string;\n old_string?: string;\n new_string?: string;\n edits?: ClaudeCodeEditEntry[];\n command?: string;\n}\n\ninterface ClaudeCodeEditEntry {\n old_string?: string;\n new_string?: string;\n}\n\nfunction readStdin(): Promise<string> {\n return new Promise((resolve: (value: string) => void) => {\n let data = '';\n process.stdin.setEncoding('utf8');\n process.stdin.on('data', (chunk: string) => { data += chunk; });\n process.stdin.on('end', () => resolve(data));\n process.stdin.on('error', () => resolve(''));\n if (process.stdin.isTTY) resolve('');\n });\n}\n\nfunction safeParse(raw: string): ClaudeCodePayload | null {\n if (!raw || raw.trim() === '') return null;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return JSON.parse(raw) as ClaudeCodePayload;\n } catch (err: unknown) {\n const error = toError(err);\n throw new InformAiError(`Malformed hook input from Claude Code stdin: ${error.message}`, { cause: error });\n }\n}\n\nfunction normalizeToolKind(toolName: string): ToolKind | null {\n if (HANDLED_FILE_TOOLS.has(toolName)) return toolName as ToolKind;\n return null;\n}\n\nfunction normalizeToolInput(toolKind: ToolKind, toolInput: ClaudeCodeToolInput): NormalizedToolInput | null {\n const filePath = toolInput.file_path;\n if (!filePath) return null;\n\n if (toolKind === 'Write') {\n return new NormalizedToolInput(filePath, [\n new NormalizedEdit('', toolInput.content || ''),\n ]);\n }\n if (toolKind === 'Edit') {\n return new NormalizedToolInput(filePath, [\n new NormalizedEdit(toolInput.old_string || '', toolInput.new_string || ''),\n ]);\n }\n if (toolKind === 'MultiEdit') {\n const raw = Array.isArray(toolInput.edits) ? toolInput.edits : [];\n const edits = raw.map((e: ClaudeCodeEditEntry) => new NormalizedEdit(e.old_string || '', e.new_string || ''));\n return new NormalizedToolInput(filePath, edits);\n }\n return null;\n}\n\nfunction handleBash(payload: ClaudeCodePayload, cwd: string, mode: HookMode): void {\n const command = payload.tool_input.command;\n if (!command || command.trim() === '') { process.exit(0); return; }\n const result = runBash(command, cwd, mode);\n if (!result) { process.exit(0); return; }\n process.stderr.write(result.report);\n process.exit(2);\n}\n\nfunction handleFileTool(payload: ClaudeCodePayload, cwd: string, mode: HookMode): void {\n const toolKind = normalizeToolKind(payload.tool_name);\n if (!toolKind) { process.exit(0); return; }\n\n const input = normalizeToolInput(toolKind, payload.tool_input);\n if (!input) { process.exit(0); return; }\n\n // Always allow edits to webpieces.config.json — it's the fix target when the config is broken.\n // This exits BEFORE run(), so feature-branch-guard never sees a config edit; record that so the\n // audit trail explains why a config edit on a bad branch was not blocked (see decision-log.ts).\n if (path.basename(input.filePath) === CONFIG_FILENAME) {\n if (mode !== 'rules') {\n logGuardDecision(\n cwd,\n new GuardDecision('feature-branch-guard', toolKind, input.filePath, branchForLog(cwd), 'ALLOW', 'config-bypass (feature-branch-guard skipped)'),\n );\n // The guard's own refresh trigger lives inside its check(), which we skip here — so warm\n // the cache directly, otherwise a session that only edits webpieces.config.json never\n // refreshes the sync status. Fire-and-forget; never blocks the edit.\n triggerMainSyncRefresh(cwd);\n }\n process.exit(0);\n return;\n }\n\n const result = run(toolKind, input, cwd, mode);\n if (!result) { process.exit(0); return; }\n\n logRejection(toolKind, input, result, cwd);\n process.stderr.write(result.report);\n process.exit(2);\n}\n\n/**\n * Shared entry point for all three Claude Code PreToolUse adapters. `mode` selects which tool kinds\n * to validate; payloads outside the mode's scope pass through (exit 0). Fails CLOSED (exit 2) on any\n * unexpected crash so a broken hook never silently lets an edit through.\n */\nexport async function runMain(mode: HookMode): Promise<void> {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const raw = await readStdin();\n const payload = safeParse(raw);\n if (!payload) { process.exit(0); return; }\n\n // Prefer the payload cwd (the AI's actual working dir, follows a persisted `cd`) over\n // process.cwd(); they match today, but the payload is the authoritative signal and stays\n // correct if the hook is ever invoked from a fixed dir (e.g. via $CLAUDE_PROJECT_DIR).\n const cwd = payload.cwd ?? process.cwd();\n\n if (payload.tool_name === 'Bash') {\n // No code-style rule is bash-scoped, so the rules hook ignores Bash.\n if (mode === 'rules') { process.exit(0); return; }\n handleBash(payload, cwd, mode);\n return;\n }\n\n // File payloads run in 'rules' (code-style), 'guards' (file-scoped guards like\n // feature-branch-guard), and 'all'. The runner filters to the right category.\n handleFileTool(payload, cwd, mode);\n } catch (err: unknown) {\n const error = toError(err);\n if (err instanceof InformAiError) {\n process.stderr.write(error.message + '\\n');\n } else {\n process.stderr.write(`[ai-hooks] hook crashed unexpectedly — failing closed: ${error.message}\\n`);\n }\n process.exit(2);\n }\n}\n"]}
1
+ {"version":3,"file":"hook-core.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/adapters/hook-core.ts"],"names":[],"mappings":"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","sourcesContent":["import * as path from 'path';\n\nimport { run, runBash } from '../core/runner';\nimport { logRejection } from '../core/rejection-log';\nimport { logGuardDecision, GuardDecision, branchForLog } from '../core/decision-log';\nimport { triggerMainSyncRefresh } from '../core/main-sync-refresh';\nimport { CONFIG_FILENAME } from '../core/load-config';\nimport { NormalizedToolInput, NormalizedEdit, ToolKind, InformAiError, RuleFailError, HookMode } from '../core/types';\nimport { toError } from '../core/to-error';\nimport { emitDeny, emitAllow } from './claude-code-response';\nimport { healShim } from '../bin/shim';\n\n// Which category of rules this hook invocation runs. The hook is split into two independently\n// installable PreToolUse hooks; each runs ONE category (the runner filters by it), and both can\n// receive file AND bash payloads:\n// - 'rules' → code-style rules (file/edit scope). Bash payloads pass through (no code rules apply).\n// - 'guards' → hookGuards section: bash git/PR guards on Bash AND file guards (feature-branch-guard)\n// on Write/Edit. Matcher is Write|Edit|MultiEdit|Bash.\n// - 'all' → both categories, for the combined back-compat `wp-ai-hook` bin.\nexport type { HookMode };\n\nconst HANDLED_FILE_TOOLS = new Set(['Write', 'Edit', 'MultiEdit']);\n\ninterface ClaudeCodePayload {\n tool_name: string;\n tool_input: ClaudeCodeToolInput;\n // Claude Code sends the session's current working directory (follows a persisted `cd`). Used to\n // scope guards to the git repo the AI is actually in — see runner git-repo-boundary governance.\n cwd?: string;\n}\n\ninterface ClaudeCodeToolInput {\n file_path?: string;\n content?: string;\n old_string?: string;\n new_string?: string;\n edits?: ClaudeCodeEditEntry[];\n command?: string;\n}\n\ninterface ClaudeCodeEditEntry {\n old_string?: string;\n new_string?: string;\n}\n\nfunction readStdin(): Promise<string> {\n return new Promise((resolve: (value: string) => void) => {\n let data = '';\n process.stdin.setEncoding('utf8');\n process.stdin.on('data', (chunk: string) => { data += chunk; });\n process.stdin.on('end', () => resolve(data));\n process.stdin.on('error', () => resolve(''));\n if (process.stdin.isTTY) resolve('');\n });\n}\n\nfunction safeParse(raw: string): ClaudeCodePayload | null {\n if (!raw || raw.trim() === '') return null;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return JSON.parse(raw) as ClaudeCodePayload;\n } catch (err: unknown) {\n const error = toError(err);\n throw new InformAiError(`Malformed hook input from Claude Code stdin: ${error.message}`, { cause: error });\n }\n}\n\nfunction normalizeToolKind(toolName: string): ToolKind | null {\n if (HANDLED_FILE_TOOLS.has(toolName)) return toolName as ToolKind;\n return null;\n}\n\nfunction normalizeToolInput(toolKind: ToolKind, toolInput: ClaudeCodeToolInput): NormalizedToolInput | null {\n const filePath = toolInput.file_path;\n if (!filePath) return null;\n\n if (toolKind === 'Write') {\n return new NormalizedToolInput(filePath, [\n new NormalizedEdit('', toolInput.content || ''),\n ]);\n }\n if (toolKind === 'Edit') {\n return new NormalizedToolInput(filePath, [\n new NormalizedEdit(toolInput.old_string || '', toolInput.new_string || ''),\n ]);\n }\n if (toolKind === 'MultiEdit') {\n const raw = Array.isArray(toolInput.edits) ? toolInput.edits : [];\n const edits = raw.map((e: ClaudeCodeEditEntry) => new NormalizedEdit(e.old_string || '', e.new_string || ''));\n return new NormalizedToolInput(filePath, edits);\n }\n return null;\n}\n\nfunction handleBash(payload: ClaudeCodePayload, cwd: string, mode: HookMode): void {\n const command = payload.tool_input.command;\n if (!command || command.trim() === '') { emitAllow(); }\n const result = runBash(command, cwd, mode);\n if (!result) { emitAllow(); }\n emitDeny(result.report);\n}\n\nfunction handleFileTool(payload: ClaudeCodePayload, cwd: string, mode: HookMode): void {\n const toolKind = normalizeToolKind(payload.tool_name);\n if (!toolKind) { emitAllow(); }\n\n const input = normalizeToolInput(toolKind, payload.tool_input);\n if (!input) { emitAllow(); }\n\n // Always allow edits to webpieces.config.json — it's the fix target when the config is broken.\n // This exits BEFORE run(), so feature-branch-guard never sees a config edit; record that so the\n // audit trail explains why a config edit on a bad branch was not blocked (see decision-log.ts).\n if (path.basename(input.filePath) === CONFIG_FILENAME) {\n if (mode !== 'rules') {\n logGuardDecision(\n cwd,\n new GuardDecision('feature-branch-guard', toolKind, input.filePath, branchForLog(cwd), 'ALLOW', 'config-bypass (feature-branch-guard skipped)'),\n );\n // The guard's own refresh trigger lives inside its check(), which we skip here — so warm\n // the cache directly, otherwise a session that only edits webpieces.config.json never\n // refreshes the sync status. Fire-and-forget; never blocks the edit.\n triggerMainSyncRefresh(cwd);\n }\n emitAllow();\n }\n\n const result = run(toolKind, input, cwd, mode);\n if (!result) { emitAllow(); }\n\n logRejection(toolKind, input, result, cwd);\n emitDeny(result.report);\n}\n\n/**\n * Shared entry point for all three Claude Code PreToolUse adapters. `mode` selects which tool kinds\n * to validate; payloads outside the mode's scope pass through (emitAllow). Blocks by emitting a\n * PreToolUse `permissionDecision:\"deny\"` JSON on stdout (exit 0) — see claude-code-response.ts. Fails\n * CLOSED on any unexpected crash (emits a deny) so a broken hook never silently lets an edit through,\n * and the reason now surfaces in the Claude Code UI instead of being hidden on a stderr+exit-2 block.\n */\nexport async function runMain(mode: HookMode): Promise<void> {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const raw = await readStdin();\n const payload = safeParse(raw);\n if (!payload) { emitAllow(); }\n\n // Prefer the payload cwd (the AI's actual working dir, follows a persisted `cd`) over\n // process.cwd(); they match today, but the payload is the authoritative signal and stays\n // correct if the hook is ever invoked from a fixed dir (e.g. via $CLAUDE_PROJECT_DIR).\n const cwd = payload.cwd ?? process.cwd();\n\n // Keep the committed shim (.claude/webpieces/ai-hook.sh) identical to renderShim() so its\n // fail-closed escape hatch + installer allowlist never go stale — no human hand-edits it.\n // Runs only when the guards binary is actually installed (i.e. now), is best-effort, and\n // never throws into the decision below. 'rules' hook skips it (guards owns the shim).\n if (mode !== 'rules') healShim(cwd);\n\n if (payload.tool_name === 'Bash') {\n // No code-style rule is bash-scoped, so the rules hook ignores Bash.\n if (mode === 'rules') { emitAllow(); }\n handleBash(payload, cwd, mode);\n return;\n }\n\n // File payloads run in 'rules' (code-style), 'guards' (file-scoped guards like\n // feature-branch-guard), and 'all'. The runner filters to the right category.\n handleFileTool(payload, cwd, mode);\n } catch (err: unknown) {\n const error = toError(err);\n // An escaped RuleFailError (a rule that threw past the runner's per-rule catch) or an\n // InformAiError (bad config/stdin) both carry an AI-readable message; anything else is an\n // unexpected bug. All three deny (fail closed) and surface their reason to the AI.\n if (error instanceof RuleFailError) {\n emitDeny(error.aiMessage);\n } else if (error instanceof InformAiError) {\n emitDeny(error.message);\n } else {\n emitDeny(`[ai-hooks] hook crashed unexpectedly — failing closed: ${error.message}`);\n }\n }\n}\n"]}
@@ -69,9 +69,18 @@ async function handler(event, _context) {
69
69
  }
70
70
  catch (err) {
71
71
  const error = (0, to_error_1.toError)(err);
72
- const msg = err instanceof types_1.InformAiError
73
- ? error.message
74
- : `[ai-hooks] openclaw adapter crashed — failing closed: ${error.message}`;
72
+ // An escaped RuleFailError or InformAiError carries an AI-readable message; anything else is
73
+ // an unexpected bug. All reject (fail closed).
74
+ let msg;
75
+ if (error instanceof types_1.RuleFailError) {
76
+ msg = error.aiMessage;
77
+ }
78
+ else if (error instanceof types_1.InformAiError) {
79
+ msg = error.message;
80
+ }
81
+ else {
82
+ msg = `[ai-hooks] openclaw adapter crashed — failing closed: ${error.message}`;
83
+ }
75
84
  return new OpenclawHandlerResult('rejected', msg);
76
85
  }
77
86
  }
@@ -1 +1 @@
1
- {"version":3,"file":"openclaw-plugin.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/adapters/openclaw-plugin.ts"],"names":[],"mappings":"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","sourcesContent":["import * as path from 'path';\nimport * as fs from 'fs';\n\nimport { CONFIG_FILENAME } from '@webpieces/rules-config';\nimport { run } from '../core/runner';\nimport { NormalizedToolInput, NormalizedEdit, ToolKind, InformAiError } from '../core/types';\nimport { toError } from '../core/to-error';\n\ninterface ToolCallEvent {\n toolName: string;\n // webpieces-disable no-any-unknown -- openclaw SDK passes opaque tool arguments\n arguments: Record<string, unknown>;\n}\n\ninterface HookContext {\n // webpieces-disable no-any-unknown -- openclaw SDK context shape is opaque\n [key: string]: unknown;\n}\n\nclass OpenclawHandlerResult {\n readonly status: 'approved' | 'rejected';\n readonly reason: string | undefined;\n\n constructor(status: 'approved' | 'rejected', reason?: string) {\n this.status = status;\n this.reason = reason;\n }\n}\n\nconst TOOL_MAP: Record<string, ToolKind> = {\n 'write': 'Write',\n 'edit': 'Edit',\n};\n\nfunction mapToolName(openclawName: string): ToolKind | null {\n return TOOL_MAP[openclawName] || null;\n}\n\n// webpieces-disable no-any-unknown -- openclaw SDK passes opaque tool arguments\nfunction mapToolInput(toolName: string, args: Record<string, unknown>): NormalizedToolInput | null {\n const filePath = typeof args['path'] === 'string' ? args['path'] as string : null;\n if (!filePath) return null;\n\n if (toolName === 'write') {\n const content = typeof args['content'] === 'string' ? args['content'] as string : '';\n return new NormalizedToolInput(filePath, [new NormalizedEdit('', content)]);\n }\n if (toolName === 'edit') {\n const oldStr = typeof args['old_string'] === 'string' ? args['old_string'] as string : '';\n const newStr = typeof args['new_string'] === 'string' ? args['new_string'] as string : '';\n return new NormalizedToolInput(filePath, [new NormalizedEdit(oldStr, newStr)]);\n }\n return null;\n}\n\nfunction findWorkspaceRoot(filePath: string): string | null {\n let dir = path.dirname(filePath);\n while (true) {\n if (fs.existsSync(path.join(dir, CONFIG_FILENAME))) return dir;\n const parent = path.dirname(dir);\n if (parent === dir) return null;\n dir = parent;\n }\n}\n\nexport default async function handler(\n event: ToolCallEvent,\n _context: HookContext,\n): Promise<OpenclawHandlerResult | undefined> {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const toolKind = mapToolName(event.toolName);\n if (!toolKind) return undefined;\n\n const input = mapToolInput(event.toolName, event.arguments);\n if (!input) return undefined;\n\n const wsRoot = findWorkspaceRoot(input.filePath);\n if (!wsRoot) return undefined;\n\n const result = run(toolKind, input, wsRoot);\n if (!result) return new OpenclawHandlerResult('approved');\n return new OpenclawHandlerResult('rejected', result.report);\n } catch (err: unknown) {\n const error = toError(err);\n const msg = err instanceof InformAiError\n ? error.message\n : `[ai-hooks] openclaw adapter crashed — failing closed: ${error.message}`;\n return new OpenclawHandlerResult('rejected', msg);\n }\n}\n"]}
1
+ {"version":3,"file":"openclaw-plugin.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/adapters/openclaw-plugin.ts"],"names":[],"mappings":"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","sourcesContent":["import * as path from 'path';\nimport * as fs from 'fs';\n\nimport { CONFIG_FILENAME } from '@webpieces/rules-config';\nimport { run } from '../core/runner';\nimport { NormalizedToolInput, NormalizedEdit, ToolKind, InformAiError, RuleFailError } from '../core/types';\nimport { toError } from '../core/to-error';\n\ninterface ToolCallEvent {\n toolName: string;\n // webpieces-disable no-any-unknown -- openclaw SDK passes opaque tool arguments\n arguments: Record<string, unknown>;\n}\n\ninterface HookContext {\n // webpieces-disable no-any-unknown -- openclaw SDK context shape is opaque\n [key: string]: unknown;\n}\n\nclass OpenclawHandlerResult {\n readonly status: 'approved' | 'rejected';\n readonly reason: string | undefined;\n\n constructor(status: 'approved' | 'rejected', reason?: string) {\n this.status = status;\n this.reason = reason;\n }\n}\n\nconst TOOL_MAP: Record<string, ToolKind> = {\n 'write': 'Write',\n 'edit': 'Edit',\n};\n\nfunction mapToolName(openclawName: string): ToolKind | null {\n return TOOL_MAP[openclawName] || null;\n}\n\n// webpieces-disable no-any-unknown -- openclaw SDK passes opaque tool arguments\nfunction mapToolInput(toolName: string, args: Record<string, unknown>): NormalizedToolInput | null {\n const filePath = typeof args['path'] === 'string' ? args['path'] as string : null;\n if (!filePath) return null;\n\n if (toolName === 'write') {\n const content = typeof args['content'] === 'string' ? args['content'] as string : '';\n return new NormalizedToolInput(filePath, [new NormalizedEdit('', content)]);\n }\n if (toolName === 'edit') {\n const oldStr = typeof args['old_string'] === 'string' ? args['old_string'] as string : '';\n const newStr = typeof args['new_string'] === 'string' ? args['new_string'] as string : '';\n return new NormalizedToolInput(filePath, [new NormalizedEdit(oldStr, newStr)]);\n }\n return null;\n}\n\nfunction findWorkspaceRoot(filePath: string): string | null {\n let dir = path.dirname(filePath);\n while (true) {\n if (fs.existsSync(path.join(dir, CONFIG_FILENAME))) return dir;\n const parent = path.dirname(dir);\n if (parent === dir) return null;\n dir = parent;\n }\n}\n\nexport default async function handler(\n event: ToolCallEvent,\n _context: HookContext,\n): Promise<OpenclawHandlerResult | undefined> {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const toolKind = mapToolName(event.toolName);\n if (!toolKind) return undefined;\n\n const input = mapToolInput(event.toolName, event.arguments);\n if (!input) return undefined;\n\n const wsRoot = findWorkspaceRoot(input.filePath);\n if (!wsRoot) return undefined;\n\n const result = run(toolKind, input, wsRoot);\n if (!result) return new OpenclawHandlerResult('approved');\n return new OpenclawHandlerResult('rejected', result.report);\n } catch (err: unknown) {\n const error = toError(err);\n // An escaped RuleFailError or InformAiError carries an AI-readable message; anything else is\n // an unexpected bug. All reject (fail closed).\n let msg: string;\n if (error instanceof RuleFailError) {\n msg = error.aiMessage;\n } else if (error instanceof InformAiError) {\n msg = error.message;\n } else {\n msg = `[ai-hooks] openclaw adapter crashed — failing closed: ${error.message}`;\n }\n return new OpenclawHandlerResult('rejected', msg);\n }\n}\n"]}
@@ -1,3 +1,5 @@
1
+ import { renderShim } from './shim';
2
+ export { renderShim };
1
3
  declare class HookSpec {
2
4
  readonly key: string;
3
5
  readonly label: string;
@@ -6,7 +8,6 @@ declare class HookSpec {
6
8
  constructor(key: string, label: string, matcher: string, bin: string);
7
9
  commandFor(target: InstallTarget, projectRoot: string): string;
8
10
  }
9
- export declare function renderShim(): string;
10
11
  export declare class InstallTarget {
11
12
  readonly choice: string;
12
13
  readonly label: string;
@@ -53,4 +54,3 @@ export declare function applyHook(hook: HookSpec, chosen: InstallTarget | null,
53
54
  export declare function resolveTargetChoice(name: string): string | null;
54
55
  export declare function parseTargetArg(args: string[]): string | null;
55
56
  export declare function main(): Promise<void>;
56
- export {};
package/src/bin/setup.js CHANGED
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.GUARDS_HOOK = exports.RULES_HOOK = exports.InstallTarget = void 0;
4
- exports.renderShim = renderShim;
3
+ exports.GUARDS_HOOK = exports.RULES_HOOK = exports.InstallTarget = exports.renderShim = void 0;
5
4
  exports.installTargets = installTargets;
6
5
  exports.migrate = migrate;
7
6
  exports.readSettings = readSettings;
@@ -17,6 +16,8 @@ const os_1 = require("os");
17
16
  const readline_1 = require("readline");
18
17
  const rules_config_1 = require("@webpieces/rules-config");
19
18
  const to_error_1 = require("../core/to-error");
19
+ const shim_1 = require("./shim");
20
+ Object.defineProperty(exports, "renderShim", { enumerable: true, get: function () { return shim_1.renderShim; } });
20
21
  const CONFIG_FILENAME = 'webpieces.config.json';
21
22
  const DEFAULT_BUILD_COMMAND = 'pnpm nx affected --target=ci --base=origin/main';
22
23
  const DEFAULT_UPSERT_PR = 'pnpm wp-start-upsert-pr';
@@ -58,58 +59,27 @@ class HookSpec {
58
59
  // `sh: No such file or directory` on every Write/Edit/Bash tool call. The bin name rides along in
59
60
  // the command string, so `command.includes(bin)` still detects/uninstalls each hook (hasHook /
60
61
  // removeHook). `.claude` is committed, so the shim survives even when node_modules does not.
62
+ // The shim body + path live in ./shim (shared with the runtime self-heal in hook-core); only the
63
+ // settings.json command string is built here.
61
64
  // ---------------------------------------------------------------------------
62
- const SHIM_MARKER = '.claude/webpieces/ai-hook.sh';
63
- function shimPath(projectRoot) {
64
- return path.join(projectRoot, '.claude', 'webpieces', 'ai-hook.sh');
65
- }
66
65
  function shimCommand(bin) {
67
- // $CLAUDE_PROJECT_DIR (exported to hooks by Claude Code) = the project root, so the shim resolves
68
- // from any cwd. Quoted to survive spaces in the path.
69
- return `"$CLAUDE_PROJECT_DIR/${SHIM_MARKER}" ${bin}`;
70
- }
71
- function renderShim() {
72
- return `#!/bin/sh
73
- # Managed by @webpieces/ai-hook-rules (wp-setup-ai-hooks) — do not edit; re-running the installer
74
- # overwrites this file. Checked in on purpose so the hook has a stable, committed entry point even
75
- # when node_modules is absent. Safe to delete along with the matching .claude/settings.json entries
76
- # if you remove @webpieces/ai-hook-rules.
77
- #
78
- # Usage (wired into .claude/settings.json): "$CLAUDE_PROJECT_DIR/.claude/webpieces/ai-hook.sh" <bin-name>
79
- BIN_NAME="$1"
80
- shift
81
- # Resolve the bin relative to THIS script (…/<root>/.claude/webpieces/ai-hook.sh → <root>), not the
82
- # caller's cwd — the hook can be invoked from any directory (a subdir, or a nested clone).
83
- ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
84
- BIN="$ROOT/node_modules/.bin/$BIN_NAME"
85
- if [ -x "$BIN" ]; then
86
- exec "$BIN" "$@" # exec preserves stdin — hooks receive the tool payload as JSON on stdin
87
- fi
88
- # Bin missing (fresh clone before install, or a broken install). The webpieces guards CANNOT run, so
89
- # FAIL CLOSED — deny the tool call. We deny via Claude Code's PreToolUse JSON protocol
90
- # (permissionDecision "deny" on stdout, then exit 0) rather than a bare "exit 2". BOTH block the call,
91
- # but only the JSON's permissionDecisionReason is surfaced to the human in the terminal UI (and to the
92
- # model) — an exit-2 stderr message is NOT reliably shown on a blocked call, so the user would never
93
- # see the "run pnpm install" fix (they'd just see the tool's optimistic action summary). This still
94
- # fails closed: permissionDecision "deny" blocks the tool; it is not silently allowed like a plain
95
- # exit 0 would be. Tell the human to install; their own terminal 'pnpm install' does not go through
96
- # this hook. The reason is a single JSON string with no double-quotes/backslashes, so it stays valid
97
- # JSON after \${BIN_NAME} (always wp-ai-rules-hook / wp-ai-guards-hook) is substituted in.
98
- REASON="❌ @webpieces/ai-hook-rules is declared in package.json but is not installed (\${BIN_NAME} not found). Run 'pnpm install' (or this repo's installer) to enable the webpieces AI guards, then retry. (If you removed @webpieces/ai-hook-rules on purpose, delete its hooks from .claude/settings.json.)"
99
- printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"%s"}}\\n' "\$REASON"
100
- exit 0 # decision is carried by permissionDecision "deny", not the exit code
101
- `;
66
+ // Invoke via `sh <file>` rather than executing the shim directly: `sh` reads a 0644 file fine, so
67
+ // a missing executable bit on the checked-in shim (fresh clone, a filesystem that drops the bit,
68
+ // git core.fileMode quirks) can NEVER break the hook with a raw `Permission denied` on every tool
69
+ // call. $CLAUDE_PROJECT_DIR (exported to hooks by Claude Code) = the project root, so the shim
70
+ // resolves from any cwd. Quoted to survive spaces in the path.
71
+ return `sh "$CLAUDE_PROJECT_DIR/${shim_1.SHIM_MARKER}" ${bin}`;
102
72
  }
103
73
  // Idempotent: re-running the installer overwrites the managed shim in place.
104
74
  function writeShim(projectRoot) {
105
- const target = shimPath(projectRoot);
75
+ const target = (0, shim_1.shimPath)(projectRoot);
106
76
  fs.mkdirSync(path.dirname(target), { recursive: true });
107
- fs.writeFileSync(target, renderShim(), { mode: 0o755 });
77
+ fs.writeFileSync(target, (0, shim_1.renderShim)(), { mode: 0o755 });
108
78
  // writeFileSync's mode is only applied when creating the file; force it on overwrite too.
109
79
  fs.chmodSync(target, 0o755);
110
80
  }
111
81
  function removeShim(projectRoot) {
112
- const target = shimPath(projectRoot);
82
+ const target = (0, shim_1.shimPath)(projectRoot);
113
83
  if (fs.existsSync(target))
114
84
  fs.rmSync(target);
115
85
  }
@@ -118,7 +88,7 @@ function removeShim(projectRoot) {
118
88
  function shimReferenced(targets) {
119
89
  return targets.some((t) => {
120
90
  const entries = readSettings(t.settingsPath).hooks?.PreToolUse ?? [];
121
- return entries.some((e) => e.hooks.some((h) => h.command.includes(SHIM_MARKER)));
91
+ return entries.some((e) => e.hooks.some((h) => h.command.includes(shim_1.SHIM_MARKER)));
122
92
  });
123
93
  }
124
94
  class InstallTarget {
@@ -1 +1 @@
1
- {"version":3,"file":"setup.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/setup.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { homedir } from 'os';\nimport { createInterface } from 'readline';\n\nimport { allRuleNames, sectionForRule, isHookGuard } from '@webpieces/rules-config';\n\nimport { toError } from '../core/to-error';\n\nconst CONFIG_FILENAME = 'webpieces.config.json';\nconst DEFAULT_BUILD_COMMAND = 'pnpm nx affected --target=ci --base=origin/main';\nconst DEFAULT_UPSERT_PR = 'pnpm wp-start-upsert-pr';\nconst DEFAULT_MERGE_COMPLETE = 'pnpm wp-finish-upsert-pr';\n\n// ---------------------------------------------------------------------------\n// The two independently-installable hooks. Each can land in a different settings\n// file (see InstallTarget) so a team can ship the guards while a developer keeps\n// the code-style rules local while iterating.\n// ---------------------------------------------------------------------------\nclass HookSpec {\n constructor(\n readonly key: string,\n readonly label: string,\n readonly matcher: string,\n readonly bin: string,\n ) {}\n\n // Absolute targets (global) need the exact path to this repo's bin — no ~/.webpieces bridge.\n // Project (relative) targets point at the checked-in shim via $CLAUDE_PROJECT_DIR (the project\n // root Claude Code exports to hooks). Using $CLAUDE_PROJECT_DIR — NOT a bare `./…` — means the\n // hook resolves from ANY cwd (a monorepo subdir, or a nested clone under repositories/) instead\n // of `command not found` (exit 127) silently skipping the guard. It stays portable (no hardcoded\n // absolute path), and the shim still degrades gracefully when node_modules is absent. See\n // writeShim(); the git-repo-boundary decision (foreign clone → allow) then happens in the binary.\n commandFor(target: InstallTarget, projectRoot: string): string {\n if (target.absolute) {\n return `node ${path.join(projectRoot, 'node_modules', '.bin', this.bin)}`;\n }\n return shimCommand(this.bin);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Single checked-in shim (.claude/webpieces/ai-hook.sh). Both project hooks point at it, passing\n// their bin name as the first arg. settings.json points here (not at the bare bin) so a missing bin\n// (fresh clone, package removed) yields a friendly \"run pnpm install\" line instead of the raw\n// `sh: No such file or directory` on every Write/Edit/Bash tool call. The bin name rides along in\n// the command string, so `command.includes(bin)` still detects/uninstalls each hook (hasHook /\n// removeHook). `.claude` is committed, so the shim survives even when node_modules does not.\n// ---------------------------------------------------------------------------\nconst SHIM_MARKER = '.claude/webpieces/ai-hook.sh';\n\nfunction shimPath(projectRoot: string): string {\n return path.join(projectRoot, '.claude', 'webpieces', 'ai-hook.sh');\n}\n\nfunction shimCommand(bin: string): string {\n // $CLAUDE_PROJECT_DIR (exported to hooks by Claude Code) = the project root, so the shim resolves\n // from any cwd. Quoted to survive spaces in the path.\n return `\"$CLAUDE_PROJECT_DIR/${SHIM_MARKER}\" ${bin}`;\n}\n\nexport function renderShim(): string {\n return `#!/bin/sh\n# Managed by @webpieces/ai-hook-rules (wp-setup-ai-hooks) — do not edit; re-running the installer\n# overwrites this file. Checked in on purpose so the hook has a stable, committed entry point even\n# when node_modules is absent. Safe to delete along with the matching .claude/settings.json entries\n# if you remove @webpieces/ai-hook-rules.\n#\n# Usage (wired into .claude/settings.json): \"$CLAUDE_PROJECT_DIR/.claude/webpieces/ai-hook.sh\" <bin-name>\nBIN_NAME=\"$1\"\nshift\n# Resolve the bin relative to THIS script (…/<root>/.claude/webpieces/ai-hook.sh → <root>), not the\n# caller's cwd — the hook can be invoked from any directory (a subdir, or a nested clone).\nROOT=\"$(CDPATH= cd -- \"$(dirname -- \"$0\")/../..\" && pwd)\"\nBIN=\"$ROOT/node_modules/.bin/$BIN_NAME\"\nif [ -x \"$BIN\" ]; then\n exec \"$BIN\" \"$@\" # exec preserves stdin — hooks receive the tool payload as JSON on stdin\nfi\n# Bin missing (fresh clone before install, or a broken install). The webpieces guards CANNOT run, so\n# FAIL CLOSED — deny the tool call. We deny via Claude Code's PreToolUse JSON protocol\n# (permissionDecision \"deny\" on stdout, then exit 0) rather than a bare \"exit 2\". BOTH block the call,\n# but only the JSON's permissionDecisionReason is surfaced to the human in the terminal UI (and to the\n# model) — an exit-2 stderr message is NOT reliably shown on a blocked call, so the user would never\n# see the \"run pnpm install\" fix (they'd just see the tool's optimistic action summary). This still\n# fails closed: permissionDecision \"deny\" blocks the tool; it is not silently allowed like a plain\n# exit 0 would be. Tell the human to install; their own terminal 'pnpm install' does not go through\n# this hook. The reason is a single JSON string with no double-quotes/backslashes, so it stays valid\n# JSON after \\${BIN_NAME} (always wp-ai-rules-hook / wp-ai-guards-hook) is substituted in.\nREASON=\"❌ @webpieces/ai-hook-rules is declared in package.json but is not installed (\\${BIN_NAME} not found). Run 'pnpm install' (or this repo's installer) to enable the webpieces AI guards, then retry. (If you removed @webpieces/ai-hook-rules on purpose, delete its hooks from .claude/settings.json.)\"\nprintf '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"%s\"}}\\\\n' \"\\$REASON\"\nexit 0 # decision is carried by permissionDecision \"deny\", not the exit code\n`;\n}\n\n// Idempotent: re-running the installer overwrites the managed shim in place.\nfunction writeShim(projectRoot: string): void {\n const target = shimPath(projectRoot);\n fs.mkdirSync(path.dirname(target), { recursive: true });\n fs.writeFileSync(target, renderShim(), { mode: 0o755 });\n // writeFileSync's mode is only applied when creating the file; force it on overwrite too.\n fs.chmodSync(target, 0o755);\n}\n\nfunction removeShim(projectRoot: string): void {\n const target = shimPath(projectRoot);\n if (fs.existsSync(target)) fs.rmSync(target);\n}\n\n// The shim is shared by both hooks — only safe to delete once no project settings file references\n// it anymore (i.e. the other hook was moved to global or uninstalled too).\nfunction shimReferenced(targets: InstallTarget[]): boolean {\n return targets.some((t: InstallTarget) => {\n const entries = readSettings(t.settingsPath).hooks?.PreToolUse ?? [];\n return entries.some((e: HookEntry) => e.hooks.some((h: HookCommand) => h.command.includes(SHIM_MARKER)));\n });\n}\n\nexport class InstallTarget {\n constructor(\n readonly choice: string,\n readonly label: string,\n readonly settingsPath: string,\n readonly absolute: boolean,\n ) {}\n}\n\nexport const RULES_HOOK = new HookSpec('rules', 'Rules hook (code-style validation)', 'Write|Edit|MultiEdit', 'wp-ai-rules-hook');\n// Guards match BOTH Bash (git/PR guards) and Write|Edit|MultiEdit (file-scoped guards like\n// feature-branch-guard), so ALL guards run in the guards hook regardless of scope.\nexport const GUARDS_HOOK = new HookSpec('guards', 'Guards hook (git/PR/branch protection)', 'Write|Edit|MultiEdit|Bash', 'wp-ai-guards-hook');\n\n// `homeDir` is injectable so tests can point the global target at a temp dir instead of the real\n// ~/.claude/settings.json (a unit test must never write the user's actual global settings).\nexport function installTargets(projectRoot: string, homeDir: string = homedir()): InstallTarget[] {\n return [\n new InstallTarget('1', 'project (.claude/settings.json — committed, for the team)',\n path.join(projectRoot, '.claude', 'settings.json'), false),\n new InstallTarget('2', 'project for you (.claude/settings.local.json — personal)',\n path.join(projectRoot, '.claude', 'settings.local.json'), false),\n new InstallTarget('3', 'global (~/.claude/settings.json — exact path, this repo only)',\n path.join(homeDir, '.claude', 'settings.json'), true),\n ];\n}\n\n// ---------------------------------------------------------------------------\n// webpieces.config.json seeding + migration to the rules / hookGuards / commands layout.\n// ---------------------------------------------------------------------------\n// webpieces-disable no-any-unknown -- webpieces.config.json / settings.json are opaque consumer JSON\ntype Json = Record<string, unknown>;\ntype RuleEntry = Json;\ntype Section = Record<string, RuleEntry>;\n\ninterface ConfigFile {\n extends?: string;\n rules: Section;\n hookGuards: Section;\n commands: Json;\n excludePaths: Json;\n rulesDir: string[];\n}\n\ninterface MigrateResult {\n config: ConfigFile;\n changes: string[];\n}\n\nfunction seedRule(): RuleEntry {\n return { mode: 'OFF', ignoreModifiedUntilEpoch: 0 };\n}\n\nfunction seedCommands(): Json {\n return {\n 'pr-gate': { mode: 'OFF', buildCommand: DEFAULT_BUILD_COMMAND, gates: [] },\n upsertPr: DEFAULT_UPSERT_PR,\n mergeComplete: DEFAULT_MERGE_COMPLETE,\n };\n}\n\n// Required excludePaths block: two glob lists that suppress hook enforcement per file path. Seeded\n// empty (enforce everywhere) — a client adds paths (e.g. \"repositories/**\") to exempt vendored trees.\nfunction seedExcludePaths(): Json {\n return { rules: [], guards: [] };\n}\n\nfunction buildSeedConfig(): ConfigFile {\n const rules: Section = {};\n const hookGuards: Section = {};\n for (const name of allRuleNames()) {\n if (sectionForRule(name) === 'hookGuards') hookGuards[name] = seedRule();\n else rules[name] = seedRule();\n }\n return { rules, hookGuards, commands: seedCommands(), excludePaths: seedExcludePaths(), rulesDir: [] };\n}\n\nfunction writeConfig(configPath: string, config: ConfigFile): void {\n fs.writeFileSync(configPath, JSON.stringify(config, null, 4) + '\\n');\n}\n\nfunction readConfig(configPath: string): Json {\n const raw = fs.readFileSync(configPath, 'utf8');\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return JSON.parse(raw) as Json;\n } catch (err: unknown) {\n const error = toError(err);\n throw new Error(`${CONFIG_FILENAME} has invalid JSON — fix it, then retry: ${error.message}`, { cause: error });\n }\n}\n\nfunction asSection(value: Json[string]): Section {\n return (typeof value === 'object' && value !== null && !Array.isArray(value)) ? (value as Section) : {};\n}\n\n// Migrate an existing config to the rules / hookGuards / commands layout and add any missing rules.\n// Returns a human-readable list of what changed (empty = already up to date).\nexport function migrate(existing: Json): MigrateResult {\n const changes: string[] = [];\n const rules: Section = asSection(existing['rules']);\n const hookGuards: Section = asSection(existing['hookGuards']);\n const commands: Json = (typeof existing['commands'] === 'object' && existing['commands'] !== null)\n ? (existing['commands'] as Json) : {};\n\n // Move a deprecated top-level pr-gate block under commands.\n if (existing['pr-gate'] !== undefined && commands['pr-gate'] === undefined) {\n commands['pr-gate'] = existing['pr-gate'];\n changes.push('moved top-level \"pr-gate\" → commands[\"pr-gate\"]');\n }\n // Move guards mistakenly left in rules into hookGuards.\n for (const name of Object.keys(rules)) {\n if (isHookGuard(name)) {\n hookGuards[name] = rules[name];\n delete rules[name];\n changes.push(`moved \"${name}\" from rules → hookGuards`);\n }\n }\n // Move code rules mistakenly placed in hookGuards back into rules.\n for (const name of Object.keys(hookGuards)) {\n if (!isHookGuard(name) && allRuleNames().includes(name)) {\n rules[name] = hookGuards[name];\n delete hookGuards[name];\n changes.push(`moved \"${name}\" from hookGuards → rules`);\n }\n }\n // Add any missing built-in into its correct section (OFF).\n for (const name of allRuleNames()) {\n const target = sectionForRule(name) === 'hookGuards' ? hookGuards : rules;\n if (!(name in target)) {\n target[name] = seedRule();\n changes.push(`added \"${name}\" (OFF) to ${sectionForRule(name)}`);\n }\n }\n // Fill command defaults.\n if (commands['pr-gate'] === undefined) {\n commands['pr-gate'] = { mode: 'OFF', buildCommand: DEFAULT_BUILD_COMMAND, gates: [] };\n changes.push('added commands[\"pr-gate\"] (OFF)');\n }\n if (commands['upsertPr'] === undefined) { commands['upsertPr'] = DEFAULT_UPSERT_PR; changes.push('added commands.upsertPr'); }\n if (commands['mergeComplete'] === undefined) { commands['mergeComplete'] = DEFAULT_MERGE_COMPLETE; changes.push('added commands.mergeComplete'); }\n\n // Seed the now-required excludePaths block (empty = enforce everywhere) if the config predates it.\n const excludePaths: Json = (typeof existing['excludePaths'] === 'object' && existing['excludePaths'] !== null && !Array.isArray(existing['excludePaths']))\n ? (existing['excludePaths'] as Json) : {};\n if (excludePaths['rules'] === undefined) { excludePaths['rules'] = []; changes.push('added excludePaths.rules ([])'); }\n if (excludePaths['guards'] === undefined) { excludePaths['guards'] = []; changes.push('added excludePaths.guards ([])'); }\n\n const rulesDir: string[] = Array.isArray(existing['rulesDir']) ? (existing['rulesDir'] as string[]) : [];\n const config: ConfigFile = { rules, hookGuards, commands, excludePaths, rulesDir };\n if (typeof existing['extends'] === 'string') config.extends = existing['extends'];\n return { config, changes };\n}\n\nfunction seedOrSyncConfig(projectRoot: string, syncOnly: boolean): void {\n const configPath = path.join(projectRoot, CONFIG_FILENAME);\n if (!fs.existsSync(configPath)) {\n if (syncOnly) {\n console.log(` [ai-hooks] No ${CONFIG_FILENAME} found — nothing to sync.`);\n return;\n }\n writeConfig(configPath, buildSeedConfig());\n console.log(` [ai-hooks] Created ${CONFIG_FILENAME} (rules / hookGuards / commands), all rules OFF.`);\n console.log(' Enable the ones you want by changing \"mode\".');\n return;\n }\n const result = migrate(readConfig(configPath));\n if (result.changes.length === 0) {\n console.log(` [ai-hooks] ${CONFIG_FILENAME} already uses the rules / hookGuards / commands layout — no changes.`);\n return;\n }\n writeConfig(configPath, result.config);\n console.log(` [ai-hooks] Migrated ${CONFIG_FILENAME}:`);\n for (const change of result.changes) console.log(` - ${change}`);\n}\n\n// ---------------------------------------------------------------------------\n// Claude Code settings.json hook wiring.\n// ---------------------------------------------------------------------------\ninterface HookCommand { type: string; command: string; }\ninterface HookEntry { matcher: string; hooks: HookCommand[]; }\ninterface ClaudeSettings {\n hooks?: { PreToolUse?: HookEntry[] };\n // webpieces-disable no-any-unknown -- opaque settings bag; arbitrary keys allowed\n [key: string]: unknown;\n}\n\nexport function readSettings(settingsPath: string): ClaudeSettings {\n if (!fs.existsSync(settingsPath)) return {};\n const raw = fs.readFileSync(settingsPath, 'utf8');\n if (raw.trim() === '') return {};\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return JSON.parse(raw) as ClaudeSettings;\n } catch (err: unknown) {\n const error = toError(err);\n throw new Error(`${settingsPath} has invalid JSON — fix it, then retry: ${error.message}`, { cause: error });\n }\n}\n\nfunction writeSettings(settingsPath: string, settings: ClaudeSettings): void {\n fs.mkdirSync(path.dirname(settingsPath), { recursive: true });\n fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 4) + '\\n');\n}\n\nexport function hasHook(settings: ClaudeSettings, bin: string): boolean {\n const entries = settings.hooks?.PreToolUse ?? [];\n return entries.some((e: HookEntry) => e.hooks.some((h: HookCommand) => h.command.includes(bin)));\n}\n\n// Drop every PreToolUse command referencing `bin`; returns true if anything was removed.\nfunction removeHook(settings: ClaudeSettings, bin: string): boolean {\n const entries = settings.hooks?.PreToolUse;\n if (!entries) return false;\n let changed = false;\n const kept: HookEntry[] = [];\n for (const entry of entries) {\n const hooks = entry.hooks.filter((h: HookCommand) => !h.command.includes(bin));\n if (hooks.length !== entry.hooks.length) changed = true;\n if (hooks.length > 0) kept.push({ matcher: entry.matcher, hooks });\n }\n if (changed) settings.hooks!.PreToolUse = kept;\n return changed;\n}\n\nfunction addHook(settings: ClaudeSettings, matcher: string, command: string): void {\n if (!settings.hooks) settings.hooks = {};\n if (!Array.isArray(settings.hooks.PreToolUse)) settings.hooks.PreToolUse = [];\n settings.hooks.PreToolUse.push({ matcher, hooks: [{ type: 'command', command }] });\n}\n\n// Apply the chosen install for one hook: remove it from every target file, then add it back to the\n// chosen one (or nowhere, for uninstall). Writes only the files that changed.\nexport function applyHook(hook: HookSpec, chosen: InstallTarget | null, targets: InstallTarget[], projectRoot: string): void {\n for (const target of targets) {\n const settings = readSettings(target.settingsPath);\n const removed = removeHook(settings, hook.bin);\n const isChosen = chosen !== null && chosen.settingsPath === target.settingsPath;\n if (isChosen) {\n addHook(settings, hook.matcher, hook.commandFor(target, projectRoot));\n writeSettings(target.settingsPath, settings);\n console.log(` ✅ ${hook.label} → ${target.label}`);\n } else if (removed) {\n writeSettings(target.settingsPath, settings);\n }\n }\n // Manage the shared checked-in shim: (re)write it whenever a project (relative) install exists,\n // otherwise clean it up once neither hook references it anymore.\n if (chosen !== null && !chosen.absolute) {\n writeShim(projectRoot);\n } else if (!shimReferenced(targets)) {\n removeShim(projectRoot);\n }\n if (chosen === null) console.log(` ⛔ ${hook.label} not installed (removed from all locations).`);\n}\n\nfunction currentLocation(hook: HookSpec, targets: InstallTarget[]): string {\n const here = targets.filter((t: InstallTarget) => hasHook(readSettings(t.settingsPath), hook.bin));\n return here.length === 0 ? 'none' : here.map((t: InstallTarget) => t.label.split(' (')[0]).join(', ');\n}\n\nfunction prompt(question: string): Promise<string> {\n return new Promise((resolve: (answer: string) => void) => {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n rl.question(question, (answer: string) => { rl.close(); resolve(answer.trim()); });\n });\n}\n\n// Map a friendly `--target` name to an InstallTarget choice id (see installTargets). Returns null\n// for an unknown name so the caller can error out. Kept separate + exported for unit testing.\nexport function resolveTargetChoice(name: string): string | null {\n switch (name) {\n case 'project': return '1';\n case 'project-personal':\n case 'projectpersonal':\n case 'local': return '2';\n case 'global': return '3';\n case 'none':\n case 'uninstall': return '4';\n default: return null;\n }\n}\n\n// Extract the value of `--target=<name>` from argv (null if the flag is absent).\nexport function parseTargetArg(args: string[]): string | null {\n const flag = args.find((a: string): boolean => a.startsWith('--target='));\n return flag ? flag.slice('--target='.length) : null;\n}\n\nasync function wireHook(hook: HookSpec, targets: InstallTarget[], projectRoot: string): Promise<void> {\n console.log('');\n console.log(`${hook.label} [matcher: ${hook.matcher}]`);\n console.log(` currently installed in: ${currentLocation(hook, targets)}`);\n for (const target of targets) console.log(` ${target.choice}) ${target.label}`);\n console.log(' 4) none / uninstall');\n const answer = await prompt(' Where should it live? [1/2/3/4, default 4]: ');\n const chosen = targets.find((t: InstallTarget) => t.choice === answer) ?? null;\n applyHook(hook, chosen, targets, projectRoot);\n}\n\nexport async function main(): Promise<void> {\n const args = process.argv.slice(2);\n const syncOnly = args.includes('--sync');\n const projectRoot = process.cwd();\n\n seedOrSyncConfig(projectRoot, syncOnly);\n if (syncOnly) return;\n\n const targets = installTargets(projectRoot);\n\n // Non-interactive: `--target=project|project-personal|global|none` installs BOTH hooks at that\n // location without prompting, so an agent or CI can run the installer unattended (e.g. after a\n // @webpieces upgrade that changed the hook entry). Omit the flag for the interactive per-hook chooser.\n const targetName = parseTargetArg(args);\n if (targetName !== null) {\n const choice = resolveTargetChoice(targetName);\n if (choice === null) {\n console.error(`❌ Unknown --target '${targetName}'. Use one of: project | project-personal | global | none`);\n process.exitCode = 1;\n return;\n }\n const chosen = targets.find((t: InstallTarget): boolean => t.choice === choice) ?? null;\n applyHook(RULES_HOOK, chosen, targets, projectRoot);\n applyHook(GUARDS_HOOK, chosen, targets, projectRoot);\n console.log(`\\nDone. Both hooks set to: ${targetName}.`);\n return;\n }\n\n console.log('');\n console.log('Two webpieces hooks can be installed independently — choose a location for each:');\n await wireHook(RULES_HOOK, targets, projectRoot);\n await wireHook(GUARDS_HOOK, targets, projectRoot);\n console.log('');\n console.log('Done. Re-run wp-setup-ai-hooks anytime to move or uninstall a hook.');\n console.log('(Non-interactive: wp-setup-ai-hooks --target=project|project-personal|global|none)');\n}\n\nif (require.main === module) {\n void main();\n}\n"]}
1
+ {"version":3,"file":"setup.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/setup.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { homedir } from 'os';\nimport { createInterface } from 'readline';\n\nimport { allRuleNames, sectionForRule, isHookGuard } from '@webpieces/rules-config';\n\nimport { toError } from '../core/to-error';\nimport { SHIM_MARKER, shimPath, renderShim } from './shim';\n\n// Re-exported for back-compat (setup.spec.ts + external callers). The shim body + path now live in\n// ./shim (shared with the runtime self-heal in hook-core). See shim.ts for the single source of truth.\nexport { renderShim };\n\nconst CONFIG_FILENAME = 'webpieces.config.json';\nconst DEFAULT_BUILD_COMMAND = 'pnpm nx affected --target=ci --base=origin/main';\nconst DEFAULT_UPSERT_PR = 'pnpm wp-start-upsert-pr';\nconst DEFAULT_MERGE_COMPLETE = 'pnpm wp-finish-upsert-pr';\n\n// ---------------------------------------------------------------------------\n// The two independently-installable hooks. Each can land in a different settings\n// file (see InstallTarget) so a team can ship the guards while a developer keeps\n// the code-style rules local while iterating.\n// ---------------------------------------------------------------------------\nclass HookSpec {\n constructor(\n readonly key: string,\n readonly label: string,\n readonly matcher: string,\n readonly bin: string,\n ) {}\n\n // Absolute targets (global) need the exact path to this repo's bin — no ~/.webpieces bridge.\n // Project (relative) targets point at the checked-in shim via $CLAUDE_PROJECT_DIR (the project\n // root Claude Code exports to hooks). Using $CLAUDE_PROJECT_DIR — NOT a bare `./…` — means the\n // hook resolves from ANY cwd (a monorepo subdir, or a nested clone under repositories/) instead\n // of `command not found` (exit 127) silently skipping the guard. It stays portable (no hardcoded\n // absolute path), and the shim still degrades gracefully when node_modules is absent. See\n // writeShim(); the git-repo-boundary decision (foreign clone → allow) then happens in the binary.\n commandFor(target: InstallTarget, projectRoot: string): string {\n if (target.absolute) {\n return `node ${path.join(projectRoot, 'node_modules', '.bin', this.bin)}`;\n }\n return shimCommand(this.bin);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Single checked-in shim (.claude/webpieces/ai-hook.sh). Both project hooks point at it, passing\n// their bin name as the first arg. settings.json points here (not at the bare bin) so a missing bin\n// (fresh clone, package removed) yields a friendly \"run pnpm install\" line instead of the raw\n// `sh: No such file or directory` on every Write/Edit/Bash tool call. The bin name rides along in\n// the command string, so `command.includes(bin)` still detects/uninstalls each hook (hasHook /\n// removeHook). `.claude` is committed, so the shim survives even when node_modules does not.\n// The shim body + path live in ./shim (shared with the runtime self-heal in hook-core); only the\n// settings.json command string is built here.\n// ---------------------------------------------------------------------------\nfunction shimCommand(bin: string): string {\n // Invoke via `sh <file>` rather than executing the shim directly: `sh` reads a 0644 file fine, so\n // a missing executable bit on the checked-in shim (fresh clone, a filesystem that drops the bit,\n // git core.fileMode quirks) can NEVER break the hook with a raw `Permission denied` on every tool\n // call. $CLAUDE_PROJECT_DIR (exported to hooks by Claude Code) = the project root, so the shim\n // resolves from any cwd. Quoted to survive spaces in the path.\n return `sh \"$CLAUDE_PROJECT_DIR/${SHIM_MARKER}\" ${bin}`;\n}\n\n// Idempotent: re-running the installer overwrites the managed shim in place.\nfunction writeShim(projectRoot: string): void {\n const target = shimPath(projectRoot);\n fs.mkdirSync(path.dirname(target), { recursive: true });\n fs.writeFileSync(target, renderShim(), { mode: 0o755 });\n // writeFileSync's mode is only applied when creating the file; force it on overwrite too.\n fs.chmodSync(target, 0o755);\n}\n\nfunction removeShim(projectRoot: string): void {\n const target = shimPath(projectRoot);\n if (fs.existsSync(target)) fs.rmSync(target);\n}\n\n// The shim is shared by both hooks — only safe to delete once no project settings file references\n// it anymore (i.e. the other hook was moved to global or uninstalled too).\nfunction shimReferenced(targets: InstallTarget[]): boolean {\n return targets.some((t: InstallTarget) => {\n const entries = readSettings(t.settingsPath).hooks?.PreToolUse ?? [];\n return entries.some((e: HookEntry) => e.hooks.some((h: HookCommand) => h.command.includes(SHIM_MARKER)));\n });\n}\n\nexport class InstallTarget {\n constructor(\n readonly choice: string,\n readonly label: string,\n readonly settingsPath: string,\n readonly absolute: boolean,\n ) {}\n}\n\nexport const RULES_HOOK = new HookSpec('rules', 'Rules hook (code-style validation)', 'Write|Edit|MultiEdit', 'wp-ai-rules-hook');\n// Guards match BOTH Bash (git/PR guards) and Write|Edit|MultiEdit (file-scoped guards like\n// feature-branch-guard), so ALL guards run in the guards hook regardless of scope.\nexport const GUARDS_HOOK = new HookSpec('guards', 'Guards hook (git/PR/branch protection)', 'Write|Edit|MultiEdit|Bash', 'wp-ai-guards-hook');\n\n// `homeDir` is injectable so tests can point the global target at a temp dir instead of the real\n// ~/.claude/settings.json (a unit test must never write the user's actual global settings).\nexport function installTargets(projectRoot: string, homeDir: string = homedir()): InstallTarget[] {\n return [\n new InstallTarget('1', 'project (.claude/settings.json — committed, for the team)',\n path.join(projectRoot, '.claude', 'settings.json'), false),\n new InstallTarget('2', 'project for you (.claude/settings.local.json — personal)',\n path.join(projectRoot, '.claude', 'settings.local.json'), false),\n new InstallTarget('3', 'global (~/.claude/settings.json — exact path, this repo only)',\n path.join(homeDir, '.claude', 'settings.json'), true),\n ];\n}\n\n// ---------------------------------------------------------------------------\n// webpieces.config.json seeding + migration to the rules / hookGuards / commands layout.\n// ---------------------------------------------------------------------------\n// webpieces-disable no-any-unknown -- webpieces.config.json / settings.json are opaque consumer JSON\ntype Json = Record<string, unknown>;\ntype RuleEntry = Json;\ntype Section = Record<string, RuleEntry>;\n\ninterface ConfigFile {\n extends?: string;\n rules: Section;\n hookGuards: Section;\n commands: Json;\n excludePaths: Json;\n rulesDir: string[];\n}\n\ninterface MigrateResult {\n config: ConfigFile;\n changes: string[];\n}\n\nfunction seedRule(): RuleEntry {\n return { mode: 'OFF', ignoreModifiedUntilEpoch: 0 };\n}\n\nfunction seedCommands(): Json {\n return {\n 'pr-gate': { mode: 'OFF', buildCommand: DEFAULT_BUILD_COMMAND, gates: [] },\n upsertPr: DEFAULT_UPSERT_PR,\n mergeComplete: DEFAULT_MERGE_COMPLETE,\n };\n}\n\n// Required excludePaths block: two glob lists that suppress hook enforcement per file path. Seeded\n// empty (enforce everywhere) — a client adds paths (e.g. \"repositories/**\") to exempt vendored trees.\nfunction seedExcludePaths(): Json {\n return { rules: [], guards: [] };\n}\n\nfunction buildSeedConfig(): ConfigFile {\n const rules: Section = {};\n const hookGuards: Section = {};\n for (const name of allRuleNames()) {\n if (sectionForRule(name) === 'hookGuards') hookGuards[name] = seedRule();\n else rules[name] = seedRule();\n }\n return { rules, hookGuards, commands: seedCommands(), excludePaths: seedExcludePaths(), rulesDir: [] };\n}\n\nfunction writeConfig(configPath: string, config: ConfigFile): void {\n fs.writeFileSync(configPath, JSON.stringify(config, null, 4) + '\\n');\n}\n\nfunction readConfig(configPath: string): Json {\n const raw = fs.readFileSync(configPath, 'utf8');\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return JSON.parse(raw) as Json;\n } catch (err: unknown) {\n const error = toError(err);\n throw new Error(`${CONFIG_FILENAME} has invalid JSON — fix it, then retry: ${error.message}`, { cause: error });\n }\n}\n\nfunction asSection(value: Json[string]): Section {\n return (typeof value === 'object' && value !== null && !Array.isArray(value)) ? (value as Section) : {};\n}\n\n// Migrate an existing config to the rules / hookGuards / commands layout and add any missing rules.\n// Returns a human-readable list of what changed (empty = already up to date).\nexport function migrate(existing: Json): MigrateResult {\n const changes: string[] = [];\n const rules: Section = asSection(existing['rules']);\n const hookGuards: Section = asSection(existing['hookGuards']);\n const commands: Json = (typeof existing['commands'] === 'object' && existing['commands'] !== null)\n ? (existing['commands'] as Json) : {};\n\n // Move a deprecated top-level pr-gate block under commands.\n if (existing['pr-gate'] !== undefined && commands['pr-gate'] === undefined) {\n commands['pr-gate'] = existing['pr-gate'];\n changes.push('moved top-level \"pr-gate\" → commands[\"pr-gate\"]');\n }\n // Move guards mistakenly left in rules into hookGuards.\n for (const name of Object.keys(rules)) {\n if (isHookGuard(name)) {\n hookGuards[name] = rules[name];\n delete rules[name];\n changes.push(`moved \"${name}\" from rules → hookGuards`);\n }\n }\n // Move code rules mistakenly placed in hookGuards back into rules.\n for (const name of Object.keys(hookGuards)) {\n if (!isHookGuard(name) && allRuleNames().includes(name)) {\n rules[name] = hookGuards[name];\n delete hookGuards[name];\n changes.push(`moved \"${name}\" from hookGuards → rules`);\n }\n }\n // Add any missing built-in into its correct section (OFF).\n for (const name of allRuleNames()) {\n const target = sectionForRule(name) === 'hookGuards' ? hookGuards : rules;\n if (!(name in target)) {\n target[name] = seedRule();\n changes.push(`added \"${name}\" (OFF) to ${sectionForRule(name)}`);\n }\n }\n // Fill command defaults.\n if (commands['pr-gate'] === undefined) {\n commands['pr-gate'] = { mode: 'OFF', buildCommand: DEFAULT_BUILD_COMMAND, gates: [] };\n changes.push('added commands[\"pr-gate\"] (OFF)');\n }\n if (commands['upsertPr'] === undefined) { commands['upsertPr'] = DEFAULT_UPSERT_PR; changes.push('added commands.upsertPr'); }\n if (commands['mergeComplete'] === undefined) { commands['mergeComplete'] = DEFAULT_MERGE_COMPLETE; changes.push('added commands.mergeComplete'); }\n\n // Seed the now-required excludePaths block (empty = enforce everywhere) if the config predates it.\n const excludePaths: Json = (typeof existing['excludePaths'] === 'object' && existing['excludePaths'] !== null && !Array.isArray(existing['excludePaths']))\n ? (existing['excludePaths'] as Json) : {};\n if (excludePaths['rules'] === undefined) { excludePaths['rules'] = []; changes.push('added excludePaths.rules ([])'); }\n if (excludePaths['guards'] === undefined) { excludePaths['guards'] = []; changes.push('added excludePaths.guards ([])'); }\n\n const rulesDir: string[] = Array.isArray(existing['rulesDir']) ? (existing['rulesDir'] as string[]) : [];\n const config: ConfigFile = { rules, hookGuards, commands, excludePaths, rulesDir };\n if (typeof existing['extends'] === 'string') config.extends = existing['extends'];\n return { config, changes };\n}\n\nfunction seedOrSyncConfig(projectRoot: string, syncOnly: boolean): void {\n const configPath = path.join(projectRoot, CONFIG_FILENAME);\n if (!fs.existsSync(configPath)) {\n if (syncOnly) {\n console.log(` [ai-hooks] No ${CONFIG_FILENAME} found — nothing to sync.`);\n return;\n }\n writeConfig(configPath, buildSeedConfig());\n console.log(` [ai-hooks] Created ${CONFIG_FILENAME} (rules / hookGuards / commands), all rules OFF.`);\n console.log(' Enable the ones you want by changing \"mode\".');\n return;\n }\n const result = migrate(readConfig(configPath));\n if (result.changes.length === 0) {\n console.log(` [ai-hooks] ${CONFIG_FILENAME} already uses the rules / hookGuards / commands layout — no changes.`);\n return;\n }\n writeConfig(configPath, result.config);\n console.log(` [ai-hooks] Migrated ${CONFIG_FILENAME}:`);\n for (const change of result.changes) console.log(` - ${change}`);\n}\n\n// ---------------------------------------------------------------------------\n// Claude Code settings.json hook wiring.\n// ---------------------------------------------------------------------------\ninterface HookCommand { type: string; command: string; }\ninterface HookEntry { matcher: string; hooks: HookCommand[]; }\ninterface ClaudeSettings {\n hooks?: { PreToolUse?: HookEntry[] };\n // webpieces-disable no-any-unknown -- opaque settings bag; arbitrary keys allowed\n [key: string]: unknown;\n}\n\nexport function readSettings(settingsPath: string): ClaudeSettings {\n if (!fs.existsSync(settingsPath)) return {};\n const raw = fs.readFileSync(settingsPath, 'utf8');\n if (raw.trim() === '') return {};\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return JSON.parse(raw) as ClaudeSettings;\n } catch (err: unknown) {\n const error = toError(err);\n throw new Error(`${settingsPath} has invalid JSON — fix it, then retry: ${error.message}`, { cause: error });\n }\n}\n\nfunction writeSettings(settingsPath: string, settings: ClaudeSettings): void {\n fs.mkdirSync(path.dirname(settingsPath), { recursive: true });\n fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 4) + '\\n');\n}\n\nexport function hasHook(settings: ClaudeSettings, bin: string): boolean {\n const entries = settings.hooks?.PreToolUse ?? [];\n return entries.some((e: HookEntry) => e.hooks.some((h: HookCommand) => h.command.includes(bin)));\n}\n\n// Drop every PreToolUse command referencing `bin`; returns true if anything was removed.\nfunction removeHook(settings: ClaudeSettings, bin: string): boolean {\n const entries = settings.hooks?.PreToolUse;\n if (!entries) return false;\n let changed = false;\n const kept: HookEntry[] = [];\n for (const entry of entries) {\n const hooks = entry.hooks.filter((h: HookCommand) => !h.command.includes(bin));\n if (hooks.length !== entry.hooks.length) changed = true;\n if (hooks.length > 0) kept.push({ matcher: entry.matcher, hooks });\n }\n if (changed) settings.hooks!.PreToolUse = kept;\n return changed;\n}\n\nfunction addHook(settings: ClaudeSettings, matcher: string, command: string): void {\n if (!settings.hooks) settings.hooks = {};\n if (!Array.isArray(settings.hooks.PreToolUse)) settings.hooks.PreToolUse = [];\n settings.hooks.PreToolUse.push({ matcher, hooks: [{ type: 'command', command }] });\n}\n\n// Apply the chosen install for one hook: remove it from every target file, then add it back to the\n// chosen one (or nowhere, for uninstall). Writes only the files that changed.\nexport function applyHook(hook: HookSpec, chosen: InstallTarget | null, targets: InstallTarget[], projectRoot: string): void {\n for (const target of targets) {\n const settings = readSettings(target.settingsPath);\n const removed = removeHook(settings, hook.bin);\n const isChosen = chosen !== null && chosen.settingsPath === target.settingsPath;\n if (isChosen) {\n addHook(settings, hook.matcher, hook.commandFor(target, projectRoot));\n writeSettings(target.settingsPath, settings);\n console.log(` ✅ ${hook.label} → ${target.label}`);\n } else if (removed) {\n writeSettings(target.settingsPath, settings);\n }\n }\n // Manage the shared checked-in shim: (re)write it whenever a project (relative) install exists,\n // otherwise clean it up once neither hook references it anymore.\n if (chosen !== null && !chosen.absolute) {\n writeShim(projectRoot);\n } else if (!shimReferenced(targets)) {\n removeShim(projectRoot);\n }\n if (chosen === null) console.log(` ⛔ ${hook.label} not installed (removed from all locations).`);\n}\n\nfunction currentLocation(hook: HookSpec, targets: InstallTarget[]): string {\n const here = targets.filter((t: InstallTarget) => hasHook(readSettings(t.settingsPath), hook.bin));\n return here.length === 0 ? 'none' : here.map((t: InstallTarget) => t.label.split(' (')[0]).join(', ');\n}\n\nfunction prompt(question: string): Promise<string> {\n return new Promise((resolve: (answer: string) => void) => {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n rl.question(question, (answer: string) => { rl.close(); resolve(answer.trim()); });\n });\n}\n\n// Map a friendly `--target` name to an InstallTarget choice id (see installTargets). Returns null\n// for an unknown name so the caller can error out. Kept separate + exported for unit testing.\nexport function resolveTargetChoice(name: string): string | null {\n switch (name) {\n case 'project': return '1';\n case 'project-personal':\n case 'projectpersonal':\n case 'local': return '2';\n case 'global': return '3';\n case 'none':\n case 'uninstall': return '4';\n default: return null;\n }\n}\n\n// Extract the value of `--target=<name>` from argv (null if the flag is absent).\nexport function parseTargetArg(args: string[]): string | null {\n const flag = args.find((a: string): boolean => a.startsWith('--target='));\n return flag ? flag.slice('--target='.length) : null;\n}\n\nasync function wireHook(hook: HookSpec, targets: InstallTarget[], projectRoot: string): Promise<void> {\n console.log('');\n console.log(`${hook.label} [matcher: ${hook.matcher}]`);\n console.log(` currently installed in: ${currentLocation(hook, targets)}`);\n for (const target of targets) console.log(` ${target.choice}) ${target.label}`);\n console.log(' 4) none / uninstall');\n const answer = await prompt(' Where should it live? [1/2/3/4, default 4]: ');\n const chosen = targets.find((t: InstallTarget) => t.choice === answer) ?? null;\n applyHook(hook, chosen, targets, projectRoot);\n}\n\nexport async function main(): Promise<void> {\n const args = process.argv.slice(2);\n const syncOnly = args.includes('--sync');\n const projectRoot = process.cwd();\n\n seedOrSyncConfig(projectRoot, syncOnly);\n if (syncOnly) return;\n\n const targets = installTargets(projectRoot);\n\n // Non-interactive: `--target=project|project-personal|global|none` installs BOTH hooks at that\n // location without prompting, so an agent or CI can run the installer unattended (e.g. after a\n // @webpieces upgrade that changed the hook entry). Omit the flag for the interactive per-hook chooser.\n const targetName = parseTargetArg(args);\n if (targetName !== null) {\n const choice = resolveTargetChoice(targetName);\n if (choice === null) {\n console.error(`❌ Unknown --target '${targetName}'. Use one of: project | project-personal | global | none`);\n process.exitCode = 1;\n return;\n }\n const chosen = targets.find((t: InstallTarget): boolean => t.choice === choice) ?? null;\n applyHook(RULES_HOOK, chosen, targets, projectRoot);\n applyHook(GUARDS_HOOK, chosen, targets, projectRoot);\n console.log(`\\nDone. Both hooks set to: ${targetName}.`);\n return;\n }\n\n console.log('');\n console.log('Two webpieces hooks can be installed independently — choose a location for each:');\n await wireHook(RULES_HOOK, targets, projectRoot);\n await wireHook(GUARDS_HOOK, targets, projectRoot);\n console.log('');\n console.log('Done. Re-run wp-setup-ai-hooks anytime to move or uninstall a hook.');\n console.log('(Non-interactive: wp-setup-ai-hooks --target=project|project-personal|global|none)');\n}\n\nif (require.main === module) {\n void main();\n}\n"]}
@@ -0,0 +1,6 @@
1
+ export declare const SHIM_MARKER = ".claude/webpieces/ai-hook.sh";
2
+ export declare function shimPath(projectRoot: string): string;
3
+ export declare const INSTALLER_ALLOW_ERE = "^(pnpm|npm) install([[:space:]]+--[A-Za-z][A-Za-z-]*)*[[:space:]]*$";
4
+ export declare const INSTALLER_ALLOW_JS: RegExp;
5
+ export declare function renderShim(): string;
6
+ export declare function healShim(cwd: string): void;
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.INSTALLER_ALLOW_JS = exports.INSTALLER_ALLOW_ERE = exports.SHIM_MARKER = void 0;
4
+ exports.shimPath = shimPath;
5
+ exports.renderShim = renderShim;
6
+ exports.healShim = healShim;
7
+ const tslib_1 = require("tslib");
8
+ const fs = tslib_1.__importStar(require("fs"));
9
+ const path = tslib_1.__importStar(require("path"));
10
+ // ---------------------------------------------------------------------------
11
+ // The single checked-in shim (.claude/webpieces/ai-hook.sh). Both project hooks point at it, passing
12
+ // their bin name as the first arg. settings.json points here (not at the bare bin) so a missing bin
13
+ // (fresh clone, package removed) yields a friendly message instead of the raw `sh: No such file or
14
+ // directory` on every Write/Edit/Bash tool call. `.claude` is committed, so the shim survives even
15
+ // when node_modules does not.
16
+ //
17
+ // This module is the SINGLE SOURCE OF TRUTH for the shim body + the installer allowlist. The
18
+ // installer (setup.ts) renders it on install; the running guards binary re-renders and self-heals it
19
+ // (healShim) so the committed .sh can never go stale — no human ever hand-edits it.
20
+ // ---------------------------------------------------------------------------
21
+ exports.SHIM_MARKER = '.claude/webpieces/ai-hook.sh';
22
+ function shimPath(projectRoot) {
23
+ return path.join(projectRoot, '.claude', 'webpieces', 'ai-hook.sh');
24
+ }
25
+ // Package-manager install commands allowed to pass the fail-closed shim so the assistant can
26
+ // self-heal the guards (run `pnpm install`) when node_modules is absent — otherwise the guard blocks
27
+ // the very command that re-enables it (deadlock). nx/pnpm monorepo only. POSIX ERE (fed to `grep -E`).
28
+ //
29
+ // Base command + `--flags` only. Because nothing but `--word` tokens may follow `install`, shell
30
+ // operators (`;`, `&&`, `|`, backticks, `$()`, `>`, `<`) cannot match — so nothing can be smuggled
31
+ // alongside the install. Keep in sync with INSTALLER_ALLOW_JS below (locked by a unit test).
32
+ exports.INSTALLER_ALLOW_ERE = '^(pnpm|npm) install([[:space:]]+--[A-Za-z][A-Za-z-]*)*[[:space:]]*$';
33
+ // JS-regex twin of INSTALLER_ALLOW_ERE (POSIX `[[:space:]]` → `\s`). Not used by the fail-closed
34
+ // shim (which is pure sh), but kept as the single JS-side definition should a future guard ever need
35
+ // to recognise installer commands in the runner. A unit test asserts the two agree on a sample set.
36
+ exports.INSTALLER_ALLOW_JS = /^(pnpm|npm) install(\s+--[A-Za-z][A-Za-z-]*)*\s*$/;
37
+ // Normal template literal (not String.raw): it carries #235's shell escapes verbatim (\${BIN_NAME},
38
+ // \$REASON, \\n for the deny JSON) AND my sed backslashes (doubled: \\(, \\), \\1, [^"\\\\]). The
39
+ // grep pattern is interpolated from INSTALLER_ALLOW_ERE (its value has no backslashes).
40
+ function renderShim() {
41
+ return `#!/bin/sh
42
+ # Managed by @webpieces/ai-hook-rules (wp-setup-ai-hooks) — do not edit; the installer AND the running
43
+ # guards binary both overwrite this file (self-healing) from renderShim(). Checked in on purpose so the
44
+ # hook has a stable, committed entry point even when node_modules is absent. Safe to delete along with
45
+ # the matching .claude/settings.json entries if you remove @webpieces/ai-hook-rules.
46
+ #
47
+ # Usage (wired into .claude/settings.json): sh "$CLAUDE_PROJECT_DIR/.claude/webpieces/ai-hook.sh" <bin-name>
48
+ BIN_NAME="$1"
49
+ shift
50
+ # Resolve the bin relative to THIS script (…/<root>/.claude/webpieces/ai-hook.sh → <root>), not the
51
+ # caller's cwd — the hook can be invoked from any directory (a subdir, or a nested clone).
52
+ ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
53
+ BIN="$ROOT/node_modules/.bin/$BIN_NAME"
54
+ if [ -x "$BIN" ]; then
55
+ exec "$BIN" "$@" # exec preserves stdin — hooks receive the tool payload as JSON on stdin
56
+ fi
57
+ # Bin missing (fresh clone before install, or a broken install). The webpieces guards CANNOT run.
58
+ # Before failing closed, peek at the tool payload and let ONLY package-manager install commands
59
+ # through: the assistant's own Bash tool routes through this hook too, so blocking everything would
60
+ # deadlock the one command (pnpm/npm install) that re-enables the guards. A silent exit 0 = "allow"
61
+ # in the PreToolUse protocol; the guards resume automatically once node_modules is present.
62
+ PAYLOAD="$(cat)"
63
+ CMD="$(printf '%s' "$PAYLOAD" | sed -n 's/.*"command"[[:space:]]*:[[:space:]]*"\\([^"\\\\]*\\)".*/\\1/p')"
64
+ if printf '%s' "$CMD" | grep -Eq '${exports.INSTALLER_ALLOW_ERE}'; then
65
+ exit 0 # allow the installer so the assistant can self-heal the deadlock
66
+ fi
67
+ # Not an installer command → FAIL CLOSED. Deny via Claude Code's PreToolUse JSON protocol
68
+ # (permissionDecision "deny" on stdout, then exit 0) rather than a bare "exit 2". BOTH block the call,
69
+ # but only the JSON's permissionDecisionReason is surfaced to the human in the terminal UI (and to the
70
+ # model) — an exit-2 stderr message is NOT reliably shown on a blocked call, so the user would never
71
+ # see the "run pnpm install" fix. This still fails closed: "deny" blocks the tool; it is not the silent
72
+ # allow a plain exit 0 with no JSON would be. The reason is a single JSON string with no
73
+ # double-quotes/backslashes, so it stays valid JSON after \${BIN_NAME} is substituted in.
74
+ REASON="❌ @webpieces/ai-hook-rules is declared in package.json but is not installed (\${BIN_NAME} not found). Run 'pnpm install' (or this repo's installer) to enable the webpieces AI guards, then retry. (If you removed @webpieces/ai-hook-rules on purpose, delete its hooks from .claude/settings.json.)"
75
+ printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"%s"}}\\n' "\$REASON"
76
+ exit 0 # decision is carried by permissionDecision "deny", not the exit code
77
+ `;
78
+ }
79
+ // Find the repo root that owns the committed shim to heal: walk up from `cwd` (the invocation's
80
+ // actual dir) to the nearest ancestor holding a shim, falling back to $CLAUDE_PROJECT_DIR (which
81
+ // Claude Code exports to hooks) only if the walk finds nothing. cwd-first keeps this correct for a
82
+ // nested clone and testable (a temp root is honoured over the ambient project env). Returns null when
83
+ // no committed shim exists (e.g. a global / absolute install, which has none to heal).
84
+ function findShimRoot(cwd) {
85
+ let dir = cwd;
86
+ for (;;) {
87
+ if (fs.existsSync(shimPath(dir)))
88
+ return dir;
89
+ const parent = path.dirname(dir);
90
+ if (parent === dir)
91
+ break;
92
+ dir = parent;
93
+ }
94
+ const env = process.env['CLAUDE_PROJECT_DIR'];
95
+ if (env && fs.existsSync(shimPath(env)))
96
+ return env;
97
+ return null;
98
+ }
99
+ // Best-effort: keep the committed shim identical to renderShim() so the fail-closed escape hatch and
100
+ // allowlist never drift. Only rewrites an EXISTING shim (never creates one) so global installs are
101
+ // untouched. NEVER throws — a self-heal must never block or crash a tool call.
102
+ function healShim(cwd) {
103
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
104
+ try {
105
+ const root = findShimRoot(cwd);
106
+ if (!root)
107
+ return;
108
+ const target = shimPath(root);
109
+ const desired = renderShim();
110
+ if (fs.readFileSync(target, 'utf8') === desired)
111
+ return;
112
+ fs.writeFileSync(target, desired, { mode: 0o755 });
113
+ fs.chmodSync(target, 0o755);
114
+ }
115
+ catch (err) {
116
+ //const error = toError(err);
117
+ // Ignore: healing is a convenience, not part of the guard decision.
118
+ }
119
+ }
120
+ //# sourceMappingURL=shim.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shim.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/shim.ts"],"names":[],"mappings":";;;AAgBA,4BAEC;AAoBD,gCAsCC;AAuBD,4BAcC;;AAjHD,+CAAyB;AACzB,mDAA6B;AAE7B,8EAA8E;AAC9E,qGAAqG;AACrG,oGAAoG;AACpG,mGAAmG;AACnG,mGAAmG;AACnG,8BAA8B;AAC9B,EAAE;AACF,6FAA6F;AAC7F,qGAAqG;AACrG,oFAAoF;AACpF,8EAA8E;AACjE,QAAA,WAAW,GAAG,8BAA8B,CAAC;AAE1D,SAAgB,QAAQ,CAAC,WAAmB;IACxC,OAAO,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,SAAS,EAAE,WAAW,EAAE,YAAY,CAAC,CAAC;AACxE,CAAC;AAED,6FAA6F;AAC7F,qGAAqG;AACrG,uGAAuG;AACvG,EAAE;AACF,iGAAiG;AACjG,mGAAmG;AACnG,6FAA6F;AAChF,QAAA,mBAAmB,GAC5B,qEAAqE,CAAC;AAE1E,iGAAiG;AACjG,qGAAqG;AACrG,oGAAoG;AACvF,QAAA,kBAAkB,GAAG,mDAAmD,CAAC;AAEtF,oGAAoG;AACpG,kGAAkG;AAClG,wFAAwF;AACxF,SAAgB,UAAU;IACtB,OAAO;;;;;;;;;;;;;;;;;;;;;;;oCAuByB,2BAAmB;;;;;;;;;;;;;CAatD,CAAC;AACF,CAAC;AAED,gGAAgG;AAChG,iGAAiG;AACjG,mGAAmG;AACnG,sGAAsG;AACtG,uFAAuF;AACvF,SAAS,YAAY,CAAC,GAAW;IAC7B,IAAI,GAAG,GAAG,GAAG,CAAC;IACd,SAAS,CAAC;QACN,IAAI,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;YAAE,OAAO,GAAG,CAAC;QAC7C,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACjC,IAAI,MAAM,KAAK,GAAG;YAAE,MAAM;QAC1B,GAAG,GAAG,MAAM,CAAC;IACjB,CAAC;IACD,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;IAC9C,IAAI,GAAG,IAAI,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QAAE,OAAO,GAAG,CAAC;IACpD,OAAO,IAAI,CAAC;AAChB,CAAC;AAED,qGAAqG;AACrG,mGAAmG;AACnG,+EAA+E;AAC/E,SAAgB,QAAQ,CAAC,GAAW;IAChC,8DAA8D;IAC9D,IAAI,CAAC;QACD,MAAM,IAAI,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAI,CAAC,IAAI;YAAE,OAAO;QAClB,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC9B,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;QAC7B,IAAI,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,OAAO;YAAE,OAAO;QACxD,EAAE,CAAC,aAAa,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QACnD,EAAE,CAAC,SAAS,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAChC,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACpB,6BAA6B;QAC7B,oEAAoE;IACxE,CAAC;AACL,CAAC","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\n// ---------------------------------------------------------------------------\n// The single checked-in shim (.claude/webpieces/ai-hook.sh). Both project hooks point at it, passing\n// their bin name as the first arg. settings.json points here (not at the bare bin) so a missing bin\n// (fresh clone, package removed) yields a friendly message instead of the raw `sh: No such file or\n// directory` on every Write/Edit/Bash tool call. `.claude` is committed, so the shim survives even\n// when node_modules does not.\n//\n// This module is the SINGLE SOURCE OF TRUTH for the shim body + the installer allowlist. The\n// installer (setup.ts) renders it on install; the running guards binary re-renders and self-heals it\n// (healShim) so the committed .sh can never go stale — no human ever hand-edits it.\n// ---------------------------------------------------------------------------\nexport const SHIM_MARKER = '.claude/webpieces/ai-hook.sh';\n\nexport function shimPath(projectRoot: string): string {\n return path.join(projectRoot, '.claude', 'webpieces', 'ai-hook.sh');\n}\n\n// Package-manager install commands allowed to pass the fail-closed shim so the assistant can\n// self-heal the guards (run `pnpm install`) when node_modules is absent — otherwise the guard blocks\n// the very command that re-enables it (deadlock). nx/pnpm monorepo only. POSIX ERE (fed to `grep -E`).\n//\n// Base command + `--flags` only. Because nothing but `--word` tokens may follow `install`, shell\n// operators (`;`, `&&`, `|`, backticks, `$()`, `>`, `<`) cannot match — so nothing can be smuggled\n// alongside the install. Keep in sync with INSTALLER_ALLOW_JS below (locked by a unit test).\nexport const INSTALLER_ALLOW_ERE =\n '^(pnpm|npm) install([[:space:]]+--[A-Za-z][A-Za-z-]*)*[[:space:]]*$';\n\n// JS-regex twin of INSTALLER_ALLOW_ERE (POSIX `[[:space:]]` → `\\s`). Not used by the fail-closed\n// shim (which is pure sh), but kept as the single JS-side definition should a future guard ever need\n// to recognise installer commands in the runner. A unit test asserts the two agree on a sample set.\nexport const INSTALLER_ALLOW_JS = /^(pnpm|npm) install(\\s+--[A-Za-z][A-Za-z-]*)*\\s*$/;\n\n// Normal template literal (not String.raw): it carries #235's shell escapes verbatim (\\${BIN_NAME},\n// \\$REASON, \\\\n for the deny JSON) AND my sed backslashes (doubled: \\\\(, \\\\), \\\\1, [^\"\\\\\\\\]). The\n// grep pattern is interpolated from INSTALLER_ALLOW_ERE (its value has no backslashes).\nexport function renderShim(): string {\n return `#!/bin/sh\n# Managed by @webpieces/ai-hook-rules (wp-setup-ai-hooks) — do not edit; the installer AND the running\n# guards binary both overwrite this file (self-healing) from renderShim(). Checked in on purpose so the\n# hook has a stable, committed entry point even when node_modules is absent. Safe to delete along with\n# the matching .claude/settings.json entries if you remove @webpieces/ai-hook-rules.\n#\n# Usage (wired into .claude/settings.json): sh \"$CLAUDE_PROJECT_DIR/.claude/webpieces/ai-hook.sh\" <bin-name>\nBIN_NAME=\"$1\"\nshift\n# Resolve the bin relative to THIS script (…/<root>/.claude/webpieces/ai-hook.sh → <root>), not the\n# caller's cwd — the hook can be invoked from any directory (a subdir, or a nested clone).\nROOT=\"$(CDPATH= cd -- \"$(dirname -- \"$0\")/../..\" && pwd)\"\nBIN=\"$ROOT/node_modules/.bin/$BIN_NAME\"\nif [ -x \"$BIN\" ]; then\n exec \"$BIN\" \"$@\" # exec preserves stdin — hooks receive the tool payload as JSON on stdin\nfi\n# Bin missing (fresh clone before install, or a broken install). The webpieces guards CANNOT run.\n# Before failing closed, peek at the tool payload and let ONLY package-manager install commands\n# through: the assistant's own Bash tool routes through this hook too, so blocking everything would\n# deadlock the one command (pnpm/npm install) that re-enables the guards. A silent exit 0 = \"allow\"\n# in the PreToolUse protocol; the guards resume automatically once node_modules is present.\nPAYLOAD=\"$(cat)\"\nCMD=\"$(printf '%s' \"$PAYLOAD\" | sed -n 's/.*\"command\"[[:space:]]*:[[:space:]]*\"\\\\([^\"\\\\\\\\]*\\\\)\".*/\\\\1/p')\"\nif printf '%s' \"$CMD\" | grep -Eq '${INSTALLER_ALLOW_ERE}'; then\n exit 0 # allow the installer so the assistant can self-heal the deadlock\nfi\n# Not an installer command → FAIL CLOSED. Deny via Claude Code's PreToolUse JSON protocol\n# (permissionDecision \"deny\" on stdout, then exit 0) rather than a bare \"exit 2\". BOTH block the call,\n# but only the JSON's permissionDecisionReason is surfaced to the human in the terminal UI (and to the\n# model) — an exit-2 stderr message is NOT reliably shown on a blocked call, so the user would never\n# see the \"run pnpm install\" fix. This still fails closed: \"deny\" blocks the tool; it is not the silent\n# allow a plain exit 0 with no JSON would be. The reason is a single JSON string with no\n# double-quotes/backslashes, so it stays valid JSON after \\${BIN_NAME} is substituted in.\nREASON=\"❌ @webpieces/ai-hook-rules is declared in package.json but is not installed (\\${BIN_NAME} not found). Run 'pnpm install' (or this repo's installer) to enable the webpieces AI guards, then retry. (If you removed @webpieces/ai-hook-rules on purpose, delete its hooks from .claude/settings.json.)\"\nprintf '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"%s\"}}\\\\n' \"\\$REASON\"\nexit 0 # decision is carried by permissionDecision \"deny\", not the exit code\n`;\n}\n\n// Find the repo root that owns the committed shim to heal: walk up from `cwd` (the invocation's\n// actual dir) to the nearest ancestor holding a shim, falling back to $CLAUDE_PROJECT_DIR (which\n// Claude Code exports to hooks) only if the walk finds nothing. cwd-first keeps this correct for a\n// nested clone and testable (a temp root is honoured over the ambient project env). Returns null when\n// no committed shim exists (e.g. a global / absolute install, which has none to heal).\nfunction findShimRoot(cwd: string): string | null {\n let dir = cwd;\n for (;;) {\n if (fs.existsSync(shimPath(dir))) return dir;\n const parent = path.dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n const env = process.env['CLAUDE_PROJECT_DIR'];\n if (env && fs.existsSync(shimPath(env))) return env;\n return null;\n}\n\n// Best-effort: keep the committed shim identical to renderShim() so the fail-closed escape hatch and\n// allowlist never drift. Only rewrites an EXISTING shim (never creates one) so global installs are\n// untouched. NEVER throws — a self-heal must never block or crash a tool call.\nexport function healShim(cwd: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const root = findShimRoot(cwd);\n if (!root) return;\n const target = shimPath(root);\n const desired = renderShim();\n if (fs.readFileSync(target, 'utf8') === desired) return;\n fs.writeFileSync(target, desired, { mode: 0o755 });\n fs.chmodSync(target, 0o755);\n } catch (err: unknown) {\n //const error = toError(err);\n // Ignore: healing is a convenience, not part of the guard decision.\n }\n}\n"]}
@@ -1,6 +1,7 @@
1
1
  import { ExcludePaths } from '@webpieces/rules-config';
2
- import { ToolKind, NormalizedToolInput, BlockedResult, HookMode, Rule } from './types';
2
+ import { ToolKind, NormalizedToolInput, BlockedResult, HookMode, Rule, Violation, EditContext, FileContext, BashContext } from './types';
3
3
  export declare function filterByExcludedPaths(rules: readonly Rule[], relativePath: string, ex: ExcludePaths): readonly Rule[];
4
4
  export declare function isGitOrGhCommand(command: string): boolean;
5
5
  export declare function run(toolKind: ToolKind, input: NormalizedToolInput, cwd: string, mode?: HookMode): BlockedResult | null;
6
6
  export declare function runBash(command: string, cwd: string, mode?: HookMode): BlockedResult | null;
7
+ export declare function runRuleCheck(rule: Rule, ctx: EditContext | FileContext | BashContext): readonly Violation[];
@@ -4,6 +4,7 @@ exports.filterByExcludedPaths = filterByExcludedPaths;
4
4
  exports.isGitOrGhCommand = isGitOrGhCommand;
5
5
  exports.run = run;
6
6
  exports.runBash = runBash;
7
+ exports.runRuleCheck = runRuleCheck;
7
8
  const tslib_1 = require("tslib");
8
9
  const path = tslib_1.__importStar(require("path"));
9
10
  const child_process_1 = require("child_process");
@@ -188,7 +189,10 @@ function checkConfigSync(rules, config) {
188
189
  }
189
190
  return new types_1.BlockedResult(lines.join('\n'));
190
191
  }
191
- // N-legs pattern: each rule runs independently; crash visible violation so AI sees it, not silent []
192
+ // N-legs pattern: each rule runs independently so one rule can never abort the others. A rule may
193
+ // EITHER return Violation[] OR throw — both accumulate here into visible violations the AI sees:
194
+ // - a thrown RuleFailError → an expected, well-formed violation (its line/snippet/fixHints kept);
195
+ // - a thrown plain Error → a "crashed" violation (a bug, surfaced not swallowed).
192
196
  function runRuleCheck(rule, ctx) {
193
197
  // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
194
198
  try {
@@ -196,9 +200,19 @@ function runRuleCheck(rule, ctx) {
196
200
  }
197
201
  catch (err) {
198
202
  const error = (0, to_error_1.toError)(err);
203
+ if (error instanceof types_1.RuleFailError) {
204
+ return [violationFromRuleFail(error)];
205
+ }
199
206
  return [new types_1.Violation(0, '', `Rule '${rule.name}' crashed: ${error.message}`)];
200
207
  }
201
208
  }
209
+ // A thrown RuleFailError carries its own AI-facing message + optional location and fix hints. Fold the
210
+ // fix hints into the message because Violation has no fixHint field (RuleGroup's fixHint comes from the
211
+ // rule definition, not a per-throw value).
212
+ function violationFromRuleFail(error) {
213
+ const hints = error.fixHints.length > 0 ? `\n Fix: ${error.fixHints.join('\n Fix: ')}` : '';
214
+ return new types_1.Violation(error.line ?? 0, error.snippet ?? '', error.aiMessage + hints);
215
+ }
202
216
  function ruleMatchesFile(rule, relativePath) {
203
217
  for (const pattern of rule.files) {
204
218
  if ((0, load_rules_1.globMatches)(pattern, relativePath))
@@ -1 +1 @@
1
- {"version":3,"file":"runner.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/runner.ts"],"names":[],"mappings":"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","sourcesContent":["import * as path from 'path';\nimport { spawnSync } from 'child_process';\n\nimport { loadAndValidate, WebpiecesRulesConfig, ExcludePaths, isHookGuard, DEFAULT_HANG_TIMEOUT_MINUTES } from '@webpieces/rules-config';\n\nimport { buildContexts, buildBashContext } from './build-context';\nimport { loadRules, globMatches } from './load-rules';\nimport { triggerMainSyncRefresh } from './main-sync-refresh';\nimport { logGuardDecision, GuardDecision, branchForLog } from './decision-log';\nimport { toError } from './to-error';\nimport { formatReport } from './report';\nimport {\n ToolKind, NormalizedToolInput, BlockedResult, HookMode,\n Rule, Violation, RuleGroup,\n EditContext, FileContext, BashContext,\n} from './types';\n\n// Restrict loaded rules to the category this hook invocation runs. The two split hooks each pass a\n// disjoint category ('rules' = code-style, 'guards' = the hookGuards section); 'all' runs both (the\n// combined back-compat bin). isHookGuard is the shared classifier in @webpieces/rules-config.\nfunction filterByMode(rules: readonly Rule[], mode: HookMode): readonly Rule[] {\n if (mode === 'all') return rules;\n if (mode === 'guards') return rules.filter((r: Rule): boolean => isHookGuard(r.name));\n return rules.filter((r: Rule): boolean => !isHookGuard(r.name));\n}\n\n// Drop rules whose category is excluded for this file path (webpieces.config.json → excludePaths).\n// Two independent glob lists: `guards` suppresses file-scoped guards (e.g. feature-branch-guard),\n// `rules` suppresses code-style rules — so a vendored tree can be exempt from one but not the other.\n// Only file tools reach here; bash git/PR guards (no file path) are never affected.\nexport function filterByExcludedPaths(rules: readonly Rule[], relativePath: string, ex: ExcludePaths): readonly Rule[] {\n return rules.filter((r: Rule): boolean => {\n const patterns = isHookGuard(r.name) ? ex.guards : ex.rules;\n return !patterns.some((p: string): boolean => globMatches(p, relativePath));\n });\n}\n\n// The git repo root of `cwd`, or null if cwd is not in a git repo / git is unavailable. This is the\n// repo-boundary signal: the guards only govern commands whose repo IS the one this webpieces.config\n// governs; a command run inside a nested clone (different git root) is out of scope.\nfunction gitToplevel(cwd: string): string | null {\n const r = spawnSync('git', ['-C', cwd, 'rev-parse', '--show-toplevel'], { encoding: 'utf8' });\n return r.status === 0 ? (r.stdout ?? '').trim() : null;\n}\n\n// A git or gh invocation anywhere in the command (start, or after a ;/&&/|| separator or pipe).\nconst GIT_OR_GH_RE = /(?:^|[;&|]\\s*)(?:git|gh)\\b/;\nexport function isGitOrGhCommand(command: string): boolean {\n return GIT_OR_GH_RE.test(command);\n}\n\n// Fire-and-forget the detached refresher when feature-branch-guard is loaded and active, so the\n// cache (.webpieces/main-sync-status.json) stays fresh as the AI works. The guard rule itself also\n// triggers this on Write/Edit; this covers the Bash path so the cache is warm on every command.\nfunction maybeRefreshMainSync(rules: readonly Rule[], workspaceRoot: string): void {\n const guard = rules.find((r: Rule): boolean => r.name === 'feature-branch-guard');\n if (guard && guard.shouldRun()) {\n triggerMainSyncRefresh(workspaceRoot, DEFAULT_HANG_TIMEOUT_MINUTES);\n }\n}\n\nconst CONFIG_MISSING_REPORT =\n 'webpieces.config.json not found.\\n' +\n 'Tell the human: run `./node_modules/.bin/wp-setup-ai-hooks` to initialize the project configuration.\\n' +\n 'Do not proceed until the human has done this.';\n\nexport function run(\n toolKind: ToolKind,\n input: NormalizedToolInput,\n cwd: string,\n mode: HookMode = 'all',\n): BlockedResult | null {\n return runInternal(toolKind, input, cwd, mode);\n}\n\nfunction runInternal(\n toolKind: ToolKind,\n input: NormalizedToolInput,\n cwd: string,\n mode: HookMode,\n): BlockedResult | null {\n const loaded = loadAndValidate(cwd);\n if (loaded.configPath === null) return new BlockedResult(CONFIG_MISSING_REPORT);\n\n const workspaceRoot = path.dirname(loaded.configPath);\n\n // Always allow edits to webpieces.config.json — it's the fix target when out of sync\n if (path.resolve(input.filePath) === path.resolve(loaded.configPath)) {\n return null;\n }\n\n const modeRules = filterByMode(loadRules(loaded.rulesConfig, workspaceRoot), mode);\n if (modeRules.length === 0) return null;\n\n // Suppress enforcement for files under this category's excludePaths (e.g. vendored repos under\n // repositories/**). Exclusion is all-or-nothing per category, so an excluded file drops the whole\n // rule set and is fully hands-off — no violations AND no config-sync nag on those files.\n const relativePath = path.relative(workspaceRoot, input.filePath);\n const rules = filterByExcludedPaths(modeRules, relativePath, loaded.excludePaths);\n if (rules.length === 0) return null;\n\n const outOfSync = checkConfigSync(rules, loaded.rulesConfig);\n if (outOfSync) return outOfSync;\n\n const contexts = buildContexts(toolKind, input, workspaceRoot);\n\n const editGroups = runEditRules(rules, contexts.editContexts);\n const fileGroups = runFileRules(rules, contexts.fileContext);\n const allGroups = [...editGroups, ...fileGroups];\n\n if (allGroups.length === 0) return null;\n\n const report = formatReport(relativePath, allGroups);\n return new BlockedResult(report);\n}\n\nexport function runBash(command: string, cwd: string, mode: HookMode = 'all'): BlockedResult | null {\n return runBashInternal(command, cwd, mode);\n}\n\nfunction runBashInternal(command: string, cwd: string, mode: HookMode): BlockedResult | null {\n const loaded = loadAndValidate(cwd);\n if (loaded.configPath === null) return new BlockedResult(CONFIG_MISSING_REPORT);\n\n const workspaceRoot = path.dirname(loaded.configPath);\n\n // Git-repo-boundary governance. The hook now always runs (via $CLAUDE_PROJECT_DIR), so this is\n // where out-of-scope work is let through deliberately instead of the old accidental 127.\n const gitRoot = gitToplevel(cwd);\n if (gitRoot !== null && path.resolve(gitRoot) !== path.resolve(workspaceRoot)) {\n // cwd is inside a DIFFERENT git repo than this webpieces.config governs (e.g. a clone under\n // repositories/). Out of scope → allow, hands-off. Intentional, not a silent hole.\n logGuardDecision(workspaceRoot, new GuardDecision('-', 'Bash', command, branchForLog(workspaceRoot), 'ALLOW', 'foreign git repo (out of scope)'));\n return null;\n }\n\n const rules = filterByMode(loadRules(loaded.rulesConfig, workspaceRoot), mode);\n if (rules.length === 0) return null;\n\n const outOfSync = checkConfigSync(rules, loaded.rulesConfig);\n if (outOfSync) return outOfSync;\n\n // Force-to-root: git/gh commands must run from the repo root, where the guards can reason about\n // git state coherently. From a subdir, BLOCK with an actionable cd message — never silently skip.\n if (isGitOrGhCommand(command) && path.resolve(cwd) !== path.resolve(workspaceRoot)) {\n const report =\n `❌ Run git/gh commands from the repo root, not a subdirectory.\\n` +\n ` You are in: ${cwd}\\n` +\n ` cd to the repo root first: cd ${workspaceRoot}\\n` +\n ` Then re-run your command. (The webpieces guards evaluate the repo's git state at its root.)`;\n logGuardDecision(workspaceRoot, new GuardDecision('force-to-root', 'Bash', command, branchForLog(workspaceRoot), 'BLOCK', 'git/gh from subdir'));\n return new BlockedResult(report);\n }\n\n // Keep the feature-branch-guard cache warm on EVERY command (not just Write/Edit): the AI runs\n // far more bash than edits, so refreshing here means the guard's next file-edit check reads a\n // fresh status. Detached + fire-and-forget — never blocks the command. Only when the guard is\n // loaded (guards/all mode) and enabled, so a project that opted out never triggers git fetches.\n maybeRefreshMainSync(rules, workspaceRoot);\n\n const ctx = buildBashContext(command, workspaceRoot);\n const groups = runBashRules(rules, ctx);\n if (groups.length === 0) {\n // Record the ALLOW only for git/gh commands — the operations the bash guards actually reason\n // about (branch create, commit, push, merge, PR). Skipping ls/cat/grep keeps the audit log\n // focused (the whole point of the log is \"why did/didn't a guard fire?\"). Blocks are always\n // logged below.\n if (/\\b(?:git|gh)\\b/.test(command)) {\n logGuardDecision(workspaceRoot, new GuardDecision('-', 'Bash', command, branchForLog(workspaceRoot), 'ALLOW', 'no bash-guard block'));\n }\n return null;\n }\n\n const ruleNames = groups.map((g: RuleGroup): string => g.ruleName).join(',');\n logGuardDecision(workspaceRoot, new GuardDecision(ruleNames, 'Bash', command, branchForLog(workspaceRoot), 'BLOCK', 'bash-guard block'));\n const report = formatReport('<bash>', groups);\n return new BlockedResult(report);\n}\n\n// The set of rule names explicitly present in webpieces.config.json (every key except rulesDir).\nfunction configuredRuleNames(config: WebpiecesRulesConfig): ReadonlySet<string> {\n return new Set(Object.keys(config).filter((k: string) => k !== 'rulesDir'));\n}\n\nfunction checkConfigSync(rules: readonly Rule[], config: WebpiecesRulesConfig): BlockedResult | null {\n const configured = configuredRuleNames(config);\n const unconfiguredRules = rules.filter((r: Rule) => !configured.has(r.name));\n if (unconfiguredRules.length === 0) return null;\n\n const lines = [\n 'webpieces.config.json is out of sync — new built-in rules are present that have no entry in webpieces.config.json.',\n '',\n 'Tell the human: the following rules need to be configured. Ask for each one:',\n ' - Should this rule be ON, OFF, NEW_AND_MODIFIED_CODE, or NEW_AND_MODIFIED_FILES?',\n ' - What values do you want for the options listed below?',\n 'Then update webpieces.config.json and retry.',\n '',\n 'Do NOT proceed until webpieces.config.json has an entry for every rule below.',\n '',\n ];\n\n for (const rule of unconfiguredRules) {\n lines.push(`--- ${rule.name} ---`);\n lines.push(`Description: ${rule.description}`);\n const opts = rule.defaultOptions;\n const optKeys = Object.keys(opts);\n if (optKeys.length > 0) {\n lines.push(`Available options (suggested defaults shown):`);\n for (const key of optKeys) {\n lines.push(` ${key}: ${JSON.stringify(opts[key])}`);\n }\n } else {\n lines.push('Available options: none beyond mode');\n }\n lines.push(`Example entry for webpieces.config.json:`);\n lines.push(` \"${rule.name}\": { \"mode\": \"ON\" }`);\n lines.push('');\n }\n\n return new BlockedResult(lines.join('\\n'));\n}\n\n// N-legs pattern: each rule runs independently; crash → visible violation so AI sees it, not silent []\nfunction runRuleCheck(rule: Rule, ctx: EditContext | FileContext | BashContext): readonly Violation[] {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return rule.check(ctx);\n } catch (err: unknown) {\n const error = toError(err);\n return [new Violation(0, '', `Rule '${rule.name}' crashed: ${error.message}`)];\n }\n}\n\nfunction ruleMatchesFile(rule: Rule, relativePath: string): boolean {\n for (const pattern of rule.files) {\n if (globMatches(pattern, relativePath)) return true;\n }\n return false;\n}\n\nfunction runBashRules(rules: readonly Rule[], bashContext: BashContext): readonly RuleGroup[] {\n const groups: RuleGroup[] = [];\n for (const rule of rules) {\n if (rule.scope !== 'bash') continue;\n if (!rule.shouldRun()) continue;\n const vs = runRuleCheck(rule, bashContext);\n if (vs.length > 0) {\n groups.push(new RuleGroup(\n rule.name, rule.description, [...rule.fixHint], [...vs],\n ));\n }\n }\n return groups;\n}\n\nfunction runEditRules(rules: readonly Rule[], editContexts: readonly EditContext[]): readonly RuleGroup[] {\n const groups: RuleGroup[] = [];\n for (const rule of rules) {\n if (rule.scope !== 'edit') continue;\n if (!rule.shouldRun()) continue;\n const allViolations: Violation[] = [];\n for (const ctx of editContexts) {\n if (!ruleMatchesFile(rule, ctx.relativePath)) continue;\n const vs = runRuleCheck(rule, ctx);\n for (const v of vs) {\n const copy = new Violation(v.line, v.snippet, v.message);\n copy.editIndex = ctx.editIndex;\n copy.editCount = ctx.editCount;\n allViolations.push(copy);\n }\n }\n if (allViolations.length > 0) {\n groups.push(new RuleGroup(\n rule.name, rule.description, [...rule.fixHint], allViolations,\n ));\n }\n }\n return groups;\n}\n\nfunction runFileRules(rules: readonly Rule[], fileContext: FileContext): readonly RuleGroup[] {\n const groups: RuleGroup[] = [];\n for (const rule of rules) {\n if (rule.scope !== 'file') continue;\n if (!rule.shouldRun()) continue;\n if (!ruleMatchesFile(rule, fileContext.relativePath)) continue;\n const vs = runRuleCheck(rule, fileContext);\n if (vs.length > 0) {\n groups.push(new RuleGroup(\n rule.name, rule.description, [...rule.fixHint], [...vs],\n ));\n }\n }\n return groups;\n}\n"]}
1
+ {"version":3,"file":"runner.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/runner.ts"],"names":[],"mappings":"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","sourcesContent":["import * as path from 'path';\nimport { spawnSync } from 'child_process';\n\nimport { loadAndValidate, WebpiecesRulesConfig, ExcludePaths, isHookGuard, DEFAULT_HANG_TIMEOUT_MINUTES } from '@webpieces/rules-config';\n\nimport { buildContexts, buildBashContext } from './build-context';\nimport { loadRules, globMatches } from './load-rules';\nimport { triggerMainSyncRefresh } from './main-sync-refresh';\nimport { logGuardDecision, GuardDecision, branchForLog } from './decision-log';\nimport { toError } from './to-error';\nimport { formatReport } from './report';\nimport {\n ToolKind, NormalizedToolInput, BlockedResult, HookMode,\n Rule, Violation, RuleGroup, RuleFailError,\n EditContext, FileContext, BashContext,\n} from './types';\n\n// Restrict loaded rules to the category this hook invocation runs. The two split hooks each pass a\n// disjoint category ('rules' = code-style, 'guards' = the hookGuards section); 'all' runs both (the\n// combined back-compat bin). isHookGuard is the shared classifier in @webpieces/rules-config.\nfunction filterByMode(rules: readonly Rule[], mode: HookMode): readonly Rule[] {\n if (mode === 'all') return rules;\n if (mode === 'guards') return rules.filter((r: Rule): boolean => isHookGuard(r.name));\n return rules.filter((r: Rule): boolean => !isHookGuard(r.name));\n}\n\n// Drop rules whose category is excluded for this file path (webpieces.config.json → excludePaths).\n// Two independent glob lists: `guards` suppresses file-scoped guards (e.g. feature-branch-guard),\n// `rules` suppresses code-style rules — so a vendored tree can be exempt from one but not the other.\n// Only file tools reach here; bash git/PR guards (no file path) are never affected.\nexport function filterByExcludedPaths(rules: readonly Rule[], relativePath: string, ex: ExcludePaths): readonly Rule[] {\n return rules.filter((r: Rule): boolean => {\n const patterns = isHookGuard(r.name) ? ex.guards : ex.rules;\n return !patterns.some((p: string): boolean => globMatches(p, relativePath));\n });\n}\n\n// The git repo root of `cwd`, or null if cwd is not in a git repo / git is unavailable. This is the\n// repo-boundary signal: the guards only govern commands whose repo IS the one this webpieces.config\n// governs; a command run inside a nested clone (different git root) is out of scope.\nfunction gitToplevel(cwd: string): string | null {\n const r = spawnSync('git', ['-C', cwd, 'rev-parse', '--show-toplevel'], { encoding: 'utf8' });\n return r.status === 0 ? (r.stdout ?? '').trim() : null;\n}\n\n// A git or gh invocation anywhere in the command (start, or after a ;/&&/|| separator or pipe).\nconst GIT_OR_GH_RE = /(?:^|[;&|]\\s*)(?:git|gh)\\b/;\nexport function isGitOrGhCommand(command: string): boolean {\n return GIT_OR_GH_RE.test(command);\n}\n\n// Fire-and-forget the detached refresher when feature-branch-guard is loaded and active, so the\n// cache (.webpieces/main-sync-status.json) stays fresh as the AI works. The guard rule itself also\n// triggers this on Write/Edit; this covers the Bash path so the cache is warm on every command.\nfunction maybeRefreshMainSync(rules: readonly Rule[], workspaceRoot: string): void {\n const guard = rules.find((r: Rule): boolean => r.name === 'feature-branch-guard');\n if (guard && guard.shouldRun()) {\n triggerMainSyncRefresh(workspaceRoot, DEFAULT_HANG_TIMEOUT_MINUTES);\n }\n}\n\nconst CONFIG_MISSING_REPORT =\n 'webpieces.config.json not found.\\n' +\n 'Tell the human: run `./node_modules/.bin/wp-setup-ai-hooks` to initialize the project configuration.\\n' +\n 'Do not proceed until the human has done this.';\n\nexport function run(\n toolKind: ToolKind,\n input: NormalizedToolInput,\n cwd: string,\n mode: HookMode = 'all',\n): BlockedResult | null {\n return runInternal(toolKind, input, cwd, mode);\n}\n\nfunction runInternal(\n toolKind: ToolKind,\n input: NormalizedToolInput,\n cwd: string,\n mode: HookMode,\n): BlockedResult | null {\n const loaded = loadAndValidate(cwd);\n if (loaded.configPath === null) return new BlockedResult(CONFIG_MISSING_REPORT);\n\n const workspaceRoot = path.dirname(loaded.configPath);\n\n // Always allow edits to webpieces.config.json — it's the fix target when out of sync\n if (path.resolve(input.filePath) === path.resolve(loaded.configPath)) {\n return null;\n }\n\n const modeRules = filterByMode(loadRules(loaded.rulesConfig, workspaceRoot), mode);\n if (modeRules.length === 0) return null;\n\n // Suppress enforcement for files under this category's excludePaths (e.g. vendored repos under\n // repositories/**). Exclusion is all-or-nothing per category, so an excluded file drops the whole\n // rule set and is fully hands-off — no violations AND no config-sync nag on those files.\n const relativePath = path.relative(workspaceRoot, input.filePath);\n const rules = filterByExcludedPaths(modeRules, relativePath, loaded.excludePaths);\n if (rules.length === 0) return null;\n\n const outOfSync = checkConfigSync(rules, loaded.rulesConfig);\n if (outOfSync) return outOfSync;\n\n const contexts = buildContexts(toolKind, input, workspaceRoot);\n\n const editGroups = runEditRules(rules, contexts.editContexts);\n const fileGroups = runFileRules(rules, contexts.fileContext);\n const allGroups = [...editGroups, ...fileGroups];\n\n if (allGroups.length === 0) return null;\n\n const report = formatReport(relativePath, allGroups);\n return new BlockedResult(report);\n}\n\nexport function runBash(command: string, cwd: string, mode: HookMode = 'all'): BlockedResult | null {\n return runBashInternal(command, cwd, mode);\n}\n\nfunction runBashInternal(command: string, cwd: string, mode: HookMode): BlockedResult | null {\n const loaded = loadAndValidate(cwd);\n if (loaded.configPath === null) return new BlockedResult(CONFIG_MISSING_REPORT);\n\n const workspaceRoot = path.dirname(loaded.configPath);\n\n // Git-repo-boundary governance. The hook now always runs (via $CLAUDE_PROJECT_DIR), so this is\n // where out-of-scope work is let through deliberately instead of the old accidental 127.\n const gitRoot = gitToplevel(cwd);\n if (gitRoot !== null && path.resolve(gitRoot) !== path.resolve(workspaceRoot)) {\n // cwd is inside a DIFFERENT git repo than this webpieces.config governs (e.g. a clone under\n // repositories/). Out of scope → allow, hands-off. Intentional, not a silent hole.\n logGuardDecision(workspaceRoot, new GuardDecision('-', 'Bash', command, branchForLog(workspaceRoot), 'ALLOW', 'foreign git repo (out of scope)'));\n return null;\n }\n\n const rules = filterByMode(loadRules(loaded.rulesConfig, workspaceRoot), mode);\n if (rules.length === 0) return null;\n\n const outOfSync = checkConfigSync(rules, loaded.rulesConfig);\n if (outOfSync) return outOfSync;\n\n // Force-to-root: git/gh commands must run from the repo root, where the guards can reason about\n // git state coherently. From a subdir, BLOCK with an actionable cd message — never silently skip.\n if (isGitOrGhCommand(command) && path.resolve(cwd) !== path.resolve(workspaceRoot)) {\n const report =\n `❌ Run git/gh commands from the repo root, not a subdirectory.\\n` +\n ` You are in: ${cwd}\\n` +\n ` cd to the repo root first: cd ${workspaceRoot}\\n` +\n ` Then re-run your command. (The webpieces guards evaluate the repo's git state at its root.)`;\n logGuardDecision(workspaceRoot, new GuardDecision('force-to-root', 'Bash', command, branchForLog(workspaceRoot), 'BLOCK', 'git/gh from subdir'));\n return new BlockedResult(report);\n }\n\n // Keep the feature-branch-guard cache warm on EVERY command (not just Write/Edit): the AI runs\n // far more bash than edits, so refreshing here means the guard's next file-edit check reads a\n // fresh status. Detached + fire-and-forget — never blocks the command. Only when the guard is\n // loaded (guards/all mode) and enabled, so a project that opted out never triggers git fetches.\n maybeRefreshMainSync(rules, workspaceRoot);\n\n const ctx = buildBashContext(command, workspaceRoot);\n const groups = runBashRules(rules, ctx);\n if (groups.length === 0) {\n // Record the ALLOW only for git/gh commands — the operations the bash guards actually reason\n // about (branch create, commit, push, merge, PR). Skipping ls/cat/grep keeps the audit log\n // focused (the whole point of the log is \"why did/didn't a guard fire?\"). Blocks are always\n // logged below.\n if (/\\b(?:git|gh)\\b/.test(command)) {\n logGuardDecision(workspaceRoot, new GuardDecision('-', 'Bash', command, branchForLog(workspaceRoot), 'ALLOW', 'no bash-guard block'));\n }\n return null;\n }\n\n const ruleNames = groups.map((g: RuleGroup): string => g.ruleName).join(',');\n logGuardDecision(workspaceRoot, new GuardDecision(ruleNames, 'Bash', command, branchForLog(workspaceRoot), 'BLOCK', 'bash-guard block'));\n const report = formatReport('<bash>', groups);\n return new BlockedResult(report);\n}\n\n// The set of rule names explicitly present in webpieces.config.json (every key except rulesDir).\nfunction configuredRuleNames(config: WebpiecesRulesConfig): ReadonlySet<string> {\n return new Set(Object.keys(config).filter((k: string) => k !== 'rulesDir'));\n}\n\nfunction checkConfigSync(rules: readonly Rule[], config: WebpiecesRulesConfig): BlockedResult | null {\n const configured = configuredRuleNames(config);\n const unconfiguredRules = rules.filter((r: Rule) => !configured.has(r.name));\n if (unconfiguredRules.length === 0) return null;\n\n const lines = [\n 'webpieces.config.json is out of sync — new built-in rules are present that have no entry in webpieces.config.json.',\n '',\n 'Tell the human: the following rules need to be configured. Ask for each one:',\n ' - Should this rule be ON, OFF, NEW_AND_MODIFIED_CODE, or NEW_AND_MODIFIED_FILES?',\n ' - What values do you want for the options listed below?',\n 'Then update webpieces.config.json and retry.',\n '',\n 'Do NOT proceed until webpieces.config.json has an entry for every rule below.',\n '',\n ];\n\n for (const rule of unconfiguredRules) {\n lines.push(`--- ${rule.name} ---`);\n lines.push(`Description: ${rule.description}`);\n const opts = rule.defaultOptions;\n const optKeys = Object.keys(opts);\n if (optKeys.length > 0) {\n lines.push(`Available options (suggested defaults shown):`);\n for (const key of optKeys) {\n lines.push(` ${key}: ${JSON.stringify(opts[key])}`);\n }\n } else {\n lines.push('Available options: none beyond mode');\n }\n lines.push(`Example entry for webpieces.config.json:`);\n lines.push(` \"${rule.name}\": { \"mode\": \"ON\" }`);\n lines.push('');\n }\n\n return new BlockedResult(lines.join('\\n'));\n}\n\n// N-legs pattern: each rule runs independently so one rule can never abort the others. A rule may\n// EITHER return Violation[] OR throw — both accumulate here into visible violations the AI sees:\n// - a thrown RuleFailError → an expected, well-formed violation (its line/snippet/fixHints kept);\n// - a thrown plain Error → a \"crashed\" violation (a bug, surfaced not swallowed).\nexport function runRuleCheck(rule: Rule, ctx: EditContext | FileContext | BashContext): readonly Violation[] {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return rule.check(ctx);\n } catch (err: unknown) {\n const error = toError(err);\n if (error instanceof RuleFailError) {\n return [violationFromRuleFail(error)];\n }\n return [new Violation(0, '', `Rule '${rule.name}' crashed: ${error.message}`)];\n }\n}\n\n// A thrown RuleFailError carries its own AI-facing message + optional location and fix hints. Fold the\n// fix hints into the message because Violation has no fixHint field (RuleGroup's fixHint comes from the\n// rule definition, not a per-throw value).\nfunction violationFromRuleFail(error: RuleFailError): Violation {\n const hints = error.fixHints.length > 0 ? `\\n Fix: ${error.fixHints.join('\\n Fix: ')}` : '';\n return new Violation(error.line ?? 0, error.snippet ?? '', error.aiMessage + hints);\n}\n\nfunction ruleMatchesFile(rule: Rule, relativePath: string): boolean {\n for (const pattern of rule.files) {\n if (globMatches(pattern, relativePath)) return true;\n }\n return false;\n}\n\nfunction runBashRules(rules: readonly Rule[], bashContext: BashContext): readonly RuleGroup[] {\n const groups: RuleGroup[] = [];\n for (const rule of rules) {\n if (rule.scope !== 'bash') continue;\n if (!rule.shouldRun()) continue;\n const vs = runRuleCheck(rule, bashContext);\n if (vs.length > 0) {\n groups.push(new RuleGroup(\n rule.name, rule.description, [...rule.fixHint], [...vs],\n ));\n }\n }\n return groups;\n}\n\nfunction runEditRules(rules: readonly Rule[], editContexts: readonly EditContext[]): readonly RuleGroup[] {\n const groups: RuleGroup[] = [];\n for (const rule of rules) {\n if (rule.scope !== 'edit') continue;\n if (!rule.shouldRun()) continue;\n const allViolations: Violation[] = [];\n for (const ctx of editContexts) {\n if (!ruleMatchesFile(rule, ctx.relativePath)) continue;\n const vs = runRuleCheck(rule, ctx);\n for (const v of vs) {\n const copy = new Violation(v.line, v.snippet, v.message);\n copy.editIndex = ctx.editIndex;\n copy.editCount = ctx.editCount;\n allViolations.push(copy);\n }\n }\n if (allViolations.length > 0) {\n groups.push(new RuleGroup(\n rule.name, rule.description, [...rule.fixHint], allViolations,\n ));\n }\n }\n return groups;\n}\n\nfunction runFileRules(rules: readonly Rule[], fileContext: FileContext): readonly RuleGroup[] {\n const groups: RuleGroup[] = [];\n for (const rule of rules) {\n if (rule.scope !== 'file') continue;\n if (!rule.shouldRun()) continue;\n if (!ruleMatchesFile(rule, fileContext.relativePath)) continue;\n const vs = runRuleCheck(rule, fileContext);\n if (vs.length > 0) {\n groups.push(new RuleGroup(\n rule.name, rule.description, [...rule.fixHint], [...vs],\n ));\n }\n }\n return groups;\n}\n"]}
@@ -1,5 +1,5 @@
1
1
  import { RuleOptions } from '@webpieces/rules-config';
2
- export { ResolvedConfig, ResolvedRuleConfig, RuleOptions, InformAiError } from '@webpieces/rules-config';
2
+ export { ResolvedConfig, ResolvedRuleConfig, RuleOptions, InformAiError, RuleFailError } from '@webpieces/rules-config';
3
3
  export type ToolKind = 'Write' | 'Edit' | 'MultiEdit';
4
4
  export type RuleScope = 'edit' | 'file' | 'bash';
5
5
  export type HookMode = 'rules' | 'guards' | 'all';
package/src/core/types.js CHANGED
@@ -1,10 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.BlockedResult = exports.RuleGroup = exports.FileContext = exports.BashContext = exports.EditContext = exports.NormalizedBashInput = exports.NormalizedToolInput = exports.NormalizedEdit = exports.Violation = exports.InformAiError = exports.ResolvedRuleConfig = exports.ResolvedConfig = void 0;
3
+ exports.BlockedResult = exports.RuleGroup = exports.FileContext = exports.BashContext = exports.EditContext = exports.NormalizedBashInput = exports.NormalizedToolInput = exports.NormalizedEdit = exports.Violation = exports.RuleFailError = exports.InformAiError = exports.ResolvedRuleConfig = exports.ResolvedConfig = void 0;
4
4
  var rules_config_1 = require("@webpieces/rules-config");
5
5
  Object.defineProperty(exports, "ResolvedConfig", { enumerable: true, get: function () { return rules_config_1.ResolvedConfig; } });
6
6
  Object.defineProperty(exports, "ResolvedRuleConfig", { enumerable: true, get: function () { return rules_config_1.ResolvedRuleConfig; } });
7
7
  Object.defineProperty(exports, "InformAiError", { enumerable: true, get: function () { return rules_config_1.InformAiError; } });
8
+ Object.defineProperty(exports, "RuleFailError", { enumerable: true, get: function () { return rules_config_1.RuleFailError; } });
8
9
  class Violation {
9
10
  line;
10
11
  snippet;
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/types.ts"],"names":[],"mappings":"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","sourcesContent":["// ResolvedConfig / ResolvedRuleConfig / RuleOptions now live in @webpieces/rules-config\n// so ai-hooks and the Nx validate-code executor share one loader and one config file.\nimport { RuleOptions } from '@webpieces/rules-config';\nexport { ResolvedConfig, ResolvedRuleConfig, RuleOptions, InformAiError } from '@webpieces/rules-config';\n\nexport type ToolKind = 'Write' | 'Edit' | 'MultiEdit';\nexport type RuleScope = 'edit' | 'file' | 'bash';\n// Which category of built-in rules a hook invocation runs: code-style 'rules', git/PR/branch\n// 'guards' (the hookGuards section), or 'all' (the combined back-compat bin).\nexport type HookMode = 'rules' | 'guards' | 'all';\nexport type IsLineDisabled = (lineNum: number, ruleName: string) => boolean;\n\nexport class Violation {\n readonly line: number;\n readonly snippet: string;\n readonly message: string;\n editIndex: number | undefined;\n editCount: number | undefined;\n\n constructor(line: number, snippet: string, message: string) {\n this.line = line;\n this.snippet = snippet;\n this.message = message;\n this.editIndex = undefined;\n this.editCount = undefined;\n }\n}\n\nexport class NormalizedEdit {\n readonly oldString: string;\n readonly newString: string;\n\n constructor(oldString: string, newString: string) {\n this.oldString = oldString;\n this.newString = newString;\n }\n}\n\nexport class NormalizedToolInput {\n readonly filePath: string;\n readonly edits: readonly NormalizedEdit[];\n\n constructor(filePath: string, edits: readonly NormalizedEdit[]) {\n this.filePath = filePath;\n this.edits = edits;\n }\n}\n\nexport class NormalizedBashInput {\n readonly command: string;\n\n constructor(command: string) {\n this.command = command;\n }\n}\n\nexport class EditContext {\n readonly tool: ToolKind;\n readonly editIndex: number;\n readonly editCount: number;\n readonly filePath: string;\n readonly relativePath: string;\n readonly workspaceRoot: string;\n readonly addedContent: string;\n readonly strippedContent: string;\n readonly lines: readonly string[];\n readonly strippedLines: readonly string[];\n readonly removedContent: string;\n readonly isLineDisabled: IsLineDisabled;\n options: RuleOptions;\n\n constructor(\n tool: ToolKind,\n editIndex: number,\n editCount: number,\n filePath: string,\n relativePath: string,\n workspaceRoot: string,\n addedContent: string,\n strippedContent: string,\n lines: readonly string[],\n strippedLines: readonly string[],\n removedContent: string,\n isLineDisabled: IsLineDisabled,\n ) {\n this.tool = tool;\n this.editIndex = editIndex;\n this.editCount = editCount;\n this.filePath = filePath;\n this.relativePath = relativePath;\n this.workspaceRoot = workspaceRoot;\n this.addedContent = addedContent;\n this.strippedContent = strippedContent;\n this.lines = lines;\n this.strippedLines = strippedLines;\n this.removedContent = removedContent;\n this.isLineDisabled = isLineDisabled;\n this.options = {};\n }\n}\n\nexport class BashContext {\n readonly tool: 'Bash';\n readonly command: string;\n readonly workspaceRoot: string;\n options: RuleOptions;\n\n constructor(command: string, workspaceRoot: string) {\n this.tool = 'Bash';\n this.command = command;\n this.workspaceRoot = workspaceRoot;\n this.options = {};\n }\n}\n\nexport class FileContext {\n readonly tool: ToolKind;\n readonly filePath: string;\n readonly relativePath: string;\n readonly workspaceRoot: string;\n readonly currentFileLines: number;\n readonly linesAdded: number;\n readonly linesRemoved: number;\n readonly projectedFileLines: number;\n options: RuleOptions;\n\n constructor(\n tool: ToolKind,\n filePath: string,\n relativePath: string,\n workspaceRoot: string,\n currentFileLines: number,\n linesAdded: number,\n linesRemoved: number,\n projectedFileLines: number,\n ) {\n this.tool = tool;\n this.filePath = filePath;\n this.relativePath = relativePath;\n this.workspaceRoot = workspaceRoot;\n this.currentFileLines = currentFileLines;\n this.linesAdded = linesAdded;\n this.linesRemoved = linesRemoved;\n this.projectedFileLines = projectedFileLines;\n this.options = {};\n }\n}\n\n/**\n * The shape of a custom rule loaded from a `rulesDir` (a plain object returned by require()).\n * It carries only metadata + a `check` method — no on/off logic of its own.\n */\nexport interface PlainRule {\n readonly name: string;\n readonly description: string;\n readonly scope: RuleScope;\n readonly files: readonly string[];\n readonly defaultOptions: RuleOptions;\n readonly fixHint: readonly string[];\n check(ctx: EditContext | FileContext | BashContext): readonly Violation[];\n}\n\n/**\n * The runtime contract the runner iterates. Both the built-in rule classes (which extend\n * EditRuleBase/FileRuleBase/BashRuleBase) and the custom-rule adapter satisfy this: they add\n * `shouldRun()` (mode + escape-hatch decision) on top of the PlainRule metadata.\n */\nexport interface Rule extends PlainRule {\n shouldRun(): boolean;\n}\n\nexport class RuleGroup {\n readonly ruleName: string;\n readonly ruleDescription: string;\n readonly fixHint: readonly string[];\n readonly violations: readonly Violation[];\n\n constructor(\n ruleName: string,\n ruleDescription: string,\n fixHint: readonly string[],\n violations: readonly Violation[],\n ) {\n this.ruleName = ruleName;\n this.ruleDescription = ruleDescription;\n this.fixHint = fixHint;\n this.violations = violations;\n }\n}\n\nexport class BlockedResult {\n readonly report: string;\n\n constructor(report: string) {\n this.report = report;\n }\n}\n\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/types.ts"],"names":[],"mappings":";;;AAGA,wDAAwH;AAA/G,8GAAA,cAAc,OAAA;AAAE,kHAAA,kBAAkB,OAAA;AAAe,6GAAA,aAAa,OAAA;AAAE,6GAAA,aAAa,OAAA;AAStF,MAAa,SAAS;IACT,IAAI,CAAS;IACb,OAAO,CAAS;IAChB,OAAO,CAAS;IACzB,SAAS,CAAqB;IAC9B,SAAS,CAAqB;IAE9B,YAAY,IAAY,EAAE,OAAe,EAAE,OAAe;QACtD,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC/B,CAAC;CACJ;AAdD,8BAcC;AAED,MAAa,cAAc;IACd,SAAS,CAAS;IAClB,SAAS,CAAS;IAE3B,YAAY,SAAiB,EAAE,SAAiB;QAC5C,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC/B,CAAC;CACJ;AARD,wCAQC;AAED,MAAa,mBAAmB;IACnB,QAAQ,CAAS;IACjB,KAAK,CAA4B;IAE1C,YAAY,QAAgB,EAAE,KAAgC;QAC1D,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACvB,CAAC;CACJ;AARD,kDAQC;AAED,MAAa,mBAAmB;IACnB,OAAO,CAAS;IAEzB,YAAY,OAAe;QACvB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IAC3B,CAAC;CACJ;AAND,kDAMC;AAED,MAAa,WAAW;IACX,IAAI,CAAW;IACf,SAAS,CAAS;IAClB,SAAS,CAAS;IAClB,QAAQ,CAAS;IACjB,YAAY,CAAS;IACrB,aAAa,CAAS;IACtB,YAAY,CAAS;IACrB,eAAe,CAAS;IACxB,KAAK,CAAoB;IACzB,aAAa,CAAoB;IACjC,cAAc,CAAS;IACvB,cAAc,CAAiB;IACxC,OAAO,CAAc;IAErB,YACI,IAAc,EACd,SAAiB,EACjB,SAAiB,EACjB,QAAgB,EAChB,YAAoB,EACpB,aAAqB,EACrB,YAAoB,EACpB,eAAuB,EACvB,KAAwB,EACxB,aAAgC,EAChC,cAAsB,EACtB,cAA8B;QAE9B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;QACnC,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,CAAC,eAAe,GAAG,eAAe,CAAC;QACvC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;QACnC,IAAI,CAAC,cAAc,GAAG,cAAc,CAAC;QACrC,IAAI,CAAC,cAAc,GAAG,cAAc,CAAC;QACrC,IAAI,CAAC,OAAO,GAAG,EAAE,CAAC;IACtB,CAAC;CACJ;AA3CD,kCA2CC;AAED,MAAa,WAAW;IACX,IAAI,CAAS;IACb,OAAO,CAAS;IAChB,aAAa,CAAS;IAC/B,OAAO,CAAc;IAErB,YAAY,OAAe,EAAE,aAAqB;QAC9C,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC;QACnB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;QACnC,IAAI,CAAC,OAAO,GAAG,EAAE,CAAC;IACtB,CAAC;CACJ;AAZD,kCAYC;AAED,MAAa,WAAW;IACX,IAAI,CAAW;IACf,QAAQ,CAAS;IACjB,YAAY,CAAS;IACrB,aAAa,CAAS;IACtB,gBAAgB,CAAS;IACzB,UAAU,CAAS;IACnB,YAAY,CAAS;IACrB,kBAAkB,CAAS;IACpC,OAAO,CAAc;IAErB,YACI,IAAc,EACd,QAAgB,EAChB,YAAoB,EACpB,aAAqB,EACrB,gBAAwB,EACxB,UAAkB,EAClB,YAAoB,EACpB,kBAA0B;QAE1B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;QACnC,IAAI,CAAC,gBAAgB,GAAG,gBAAgB,CAAC;QACzC,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,CAAC,kBAAkB,GAAG,kBAAkB,CAAC;QAC7C,IAAI,CAAC,OAAO,GAAG,EAAE,CAAC;IACtB,CAAC;CACJ;AA/BD,kCA+BC;AAyBD,MAAa,SAAS;IACT,QAAQ,CAAS;IACjB,eAAe,CAAS;IACxB,OAAO,CAAoB;IAC3B,UAAU,CAAuB;IAE1C,YACI,QAAgB,EAChB,eAAuB,EACvB,OAA0B,EAC1B,UAAgC;QAEhC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,eAAe,GAAG,eAAe,CAAC;QACvC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;IACjC,CAAC;CACJ;AAjBD,8BAiBC;AAED,MAAa,aAAa;IACb,MAAM,CAAS;IAExB,YAAY,MAAc;QACtB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACzB,CAAC;CACJ;AAND,sCAMC","sourcesContent":["// ResolvedConfig / ResolvedRuleConfig / RuleOptions now live in @webpieces/rules-config\n// so ai-hooks and the Nx validate-code executor share one loader and one config file.\nimport { RuleOptions } from '@webpieces/rules-config';\nexport { ResolvedConfig, ResolvedRuleConfig, RuleOptions, InformAiError, RuleFailError } from '@webpieces/rules-config';\n\nexport type ToolKind = 'Write' | 'Edit' | 'MultiEdit';\nexport type RuleScope = 'edit' | 'file' | 'bash';\n// Which category of built-in rules a hook invocation runs: code-style 'rules', git/PR/branch\n// 'guards' (the hookGuards section), or 'all' (the combined back-compat bin).\nexport type HookMode = 'rules' | 'guards' | 'all';\nexport type IsLineDisabled = (lineNum: number, ruleName: string) => boolean;\n\nexport class Violation {\n readonly line: number;\n readonly snippet: string;\n readonly message: string;\n editIndex: number | undefined;\n editCount: number | undefined;\n\n constructor(line: number, snippet: string, message: string) {\n this.line = line;\n this.snippet = snippet;\n this.message = message;\n this.editIndex = undefined;\n this.editCount = undefined;\n }\n}\n\nexport class NormalizedEdit {\n readonly oldString: string;\n readonly newString: string;\n\n constructor(oldString: string, newString: string) {\n this.oldString = oldString;\n this.newString = newString;\n }\n}\n\nexport class NormalizedToolInput {\n readonly filePath: string;\n readonly edits: readonly NormalizedEdit[];\n\n constructor(filePath: string, edits: readonly NormalizedEdit[]) {\n this.filePath = filePath;\n this.edits = edits;\n }\n}\n\nexport class NormalizedBashInput {\n readonly command: string;\n\n constructor(command: string) {\n this.command = command;\n }\n}\n\nexport class EditContext {\n readonly tool: ToolKind;\n readonly editIndex: number;\n readonly editCount: number;\n readonly filePath: string;\n readonly relativePath: string;\n readonly workspaceRoot: string;\n readonly addedContent: string;\n readonly strippedContent: string;\n readonly lines: readonly string[];\n readonly strippedLines: readonly string[];\n readonly removedContent: string;\n readonly isLineDisabled: IsLineDisabled;\n options: RuleOptions;\n\n constructor(\n tool: ToolKind,\n editIndex: number,\n editCount: number,\n filePath: string,\n relativePath: string,\n workspaceRoot: string,\n addedContent: string,\n strippedContent: string,\n lines: readonly string[],\n strippedLines: readonly string[],\n removedContent: string,\n isLineDisabled: IsLineDisabled,\n ) {\n this.tool = tool;\n this.editIndex = editIndex;\n this.editCount = editCount;\n this.filePath = filePath;\n this.relativePath = relativePath;\n this.workspaceRoot = workspaceRoot;\n this.addedContent = addedContent;\n this.strippedContent = strippedContent;\n this.lines = lines;\n this.strippedLines = strippedLines;\n this.removedContent = removedContent;\n this.isLineDisabled = isLineDisabled;\n this.options = {};\n }\n}\n\nexport class BashContext {\n readonly tool: 'Bash';\n readonly command: string;\n readonly workspaceRoot: string;\n options: RuleOptions;\n\n constructor(command: string, workspaceRoot: string) {\n this.tool = 'Bash';\n this.command = command;\n this.workspaceRoot = workspaceRoot;\n this.options = {};\n }\n}\n\nexport class FileContext {\n readonly tool: ToolKind;\n readonly filePath: string;\n readonly relativePath: string;\n readonly workspaceRoot: string;\n readonly currentFileLines: number;\n readonly linesAdded: number;\n readonly linesRemoved: number;\n readonly projectedFileLines: number;\n options: RuleOptions;\n\n constructor(\n tool: ToolKind,\n filePath: string,\n relativePath: string,\n workspaceRoot: string,\n currentFileLines: number,\n linesAdded: number,\n linesRemoved: number,\n projectedFileLines: number,\n ) {\n this.tool = tool;\n this.filePath = filePath;\n this.relativePath = relativePath;\n this.workspaceRoot = workspaceRoot;\n this.currentFileLines = currentFileLines;\n this.linesAdded = linesAdded;\n this.linesRemoved = linesRemoved;\n this.projectedFileLines = projectedFileLines;\n this.options = {};\n }\n}\n\n/**\n * The shape of a custom rule loaded from a `rulesDir` (a plain object returned by require()).\n * It carries only metadata + a `check` method — no on/off logic of its own.\n */\nexport interface PlainRule {\n readonly name: string;\n readonly description: string;\n readonly scope: RuleScope;\n readonly files: readonly string[];\n readonly defaultOptions: RuleOptions;\n readonly fixHint: readonly string[];\n check(ctx: EditContext | FileContext | BashContext): readonly Violation[];\n}\n\n/**\n * The runtime contract the runner iterates. Both the built-in rule classes (which extend\n * EditRuleBase/FileRuleBase/BashRuleBase) and the custom-rule adapter satisfy this: they add\n * `shouldRun()` (mode + escape-hatch decision) on top of the PlainRule metadata.\n */\nexport interface Rule extends PlainRule {\n shouldRun(): boolean;\n}\n\nexport class RuleGroup {\n readonly ruleName: string;\n readonly ruleDescription: string;\n readonly fixHint: readonly string[];\n readonly violations: readonly Violation[];\n\n constructor(\n ruleName: string,\n ruleDescription: string,\n fixHint: readonly string[],\n violations: readonly Violation[],\n ) {\n this.ruleName = ruleName;\n this.ruleDescription = ruleDescription;\n this.fixHint = fixHint;\n this.violations = violations;\n }\n}\n\nexport class BlockedResult {\n readonly report: string;\n\n constructor(report: string) {\n this.report = report;\n }\n}\n\n"]}
@@ -0,0 +1,36 @@
1
+ #!/bin/sh
2
+ # Managed by @webpieces/ai-hook-rules (wp-setup-ai-hooks) — do not edit; the installer AND the running
3
+ # guards binary both overwrite this file (self-healing) from renderShim(). Checked in on purpose so the
4
+ # hook has a stable, committed entry point even when node_modules is absent. Safe to delete along with
5
+ # the matching .claude/settings.json entries if you remove @webpieces/ai-hook-rules.
6
+ #
7
+ # Usage (wired into .claude/settings.json): sh "$CLAUDE_PROJECT_DIR/.claude/webpieces/ai-hook.sh" <bin-name>
8
+ BIN_NAME="$1"
9
+ shift
10
+ # Resolve the bin relative to THIS script (…/<root>/.claude/webpieces/ai-hook.sh → <root>), not the
11
+ # caller's cwd — the hook can be invoked from any directory (a subdir, or a nested clone).
12
+ ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
13
+ BIN="$ROOT/node_modules/.bin/$BIN_NAME"
14
+ if [ -x "$BIN" ]; then
15
+ exec "$BIN" "$@" # exec preserves stdin — hooks receive the tool payload as JSON on stdin
16
+ fi
17
+ # Bin missing (fresh clone before install, or a broken install). The webpieces guards CANNOT run.
18
+ # Before failing closed, peek at the tool payload and let ONLY package-manager install commands
19
+ # through: the assistant's own Bash tool routes through this hook too, so blocking everything would
20
+ # deadlock the one command (pnpm/npm install) that re-enables the guards. A silent exit 0 = "allow"
21
+ # in the PreToolUse protocol; the guards resume automatically once node_modules is present.
22
+ PAYLOAD="$(cat)"
23
+ CMD="$(printf '%s' "$PAYLOAD" | sed -n 's/.*"command"[[:space:]]*:[[:space:]]*"\([^"\\]*\)".*/\1/p')"
24
+ if printf '%s' "$CMD" | grep -Eq '^(pnpm|npm) install([[:space:]]+--[A-Za-z][A-Za-z-]*)*[[:space:]]*$'; then
25
+ exit 0 # allow the installer so the assistant can self-heal the deadlock
26
+ fi
27
+ # Not an installer command → FAIL CLOSED. Deny via Claude Code's PreToolUse JSON protocol
28
+ # (permissionDecision "deny" on stdout, then exit 0) rather than a bare "exit 2". BOTH block the call,
29
+ # but only the JSON's permissionDecisionReason is surfaced to the human in the terminal UI (and to the
30
+ # model) — an exit-2 stderr message is NOT reliably shown on a blocked call, so the user would never
31
+ # see the "run pnpm install" fix. This still fails closed: "deny" blocks the tool; it is not the silent
32
+ # allow a plain exit 0 with no JSON would be. The reason is a single JSON string with no
33
+ # double-quotes/backslashes, so it stays valid JSON after ${BIN_NAME} is substituted in.
34
+ REASON="❌ @webpieces/ai-hook-rules is declared in package.json but is not installed (${BIN_NAME} not found). Run 'pnpm install' (or this repo's installer) to enable the webpieces AI guards, then retry. (If you removed @webpieces/ai-hook-rules on purpose, delete its hooks from .claude/settings.json.)"
35
+ printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"%s"}}\n' "$REASON"
36
+ exit 0 # decision is carried by permissionDecision "deny", not the exit code
@@ -1,32 +0,0 @@
1
- #!/bin/sh
2
- # Managed by @webpieces/ai-hook-rules (wp-setup-ai-hooks) — do not edit; re-running the installer
3
- # overwrites this file. Checked in on purpose so the hook has a stable, committed entry point even
4
- # when node_modules is absent. Safe to delete along with the matching .claude/settings.json entries
5
- # if you remove @webpieces/ai-hook-rules.
6
- #
7
- # Usage (wired into .claude/settings.json): "$CLAUDE_PROJECT_DIR/.claude/webpieces/ai-hook.sh" <bin-name>
8
- #
9
- # This template mirrors renderShim() in src/bin/setup.ts, which is the source of truth the installer
10
- # actually writes into a consumer repo. It lives here for reference / code review; keep it in sync.
11
- BIN_NAME="$1"
12
- shift
13
- # Resolve the bin relative to THIS script (…/<root>/.claude/webpieces/ai-hook.sh → <root>), not the
14
- # caller's cwd — the hook can be invoked from any directory (a subdir, or a nested clone).
15
- ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
16
- BIN="$ROOT/node_modules/.bin/$BIN_NAME"
17
- if [ -x "$BIN" ]; then
18
- exec "$BIN" "$@" # exec preserves stdin — hooks receive the tool payload as JSON on stdin
19
- fi
20
- # Bin missing (fresh clone before install, or a broken install). The webpieces guards CANNOT run, so
21
- # FAIL CLOSED — deny the tool call. We deny via Claude Code's PreToolUse JSON protocol
22
- # (permissionDecision "deny" on stdout, then exit 0) rather than a bare "exit 2". BOTH block the call,
23
- # but only the JSON's permissionDecisionReason is surfaced to the human in the terminal UI (and to the
24
- # model) — an exit-2 stderr message is NOT reliably shown on a blocked call, so the user would never
25
- # see the "run pnpm install" fix (they'd just see the tool's optimistic action summary). This still
26
- # fails closed: permissionDecision "deny" blocks the tool; it is not silently allowed like a plain
27
- # exit 0 would be. Tell the human to install; their own terminal 'pnpm install' does not go through
28
- # this hook. The reason is a single JSON string with no double-quotes/backslashes, so it stays valid
29
- # JSON after ${BIN_NAME} (always wp-ai-rules-hook / wp-ai-guards-hook) is substituted in.
30
- REASON="❌ @webpieces/ai-hook-rules is declared in package.json but is not installed (${BIN_NAME} not found). Run 'pnpm install' (or this repo's installer) to enable the webpieces AI guards, then retry. (If you removed @webpieces/ai-hook-rules on purpose, delete its hooks from .claude/settings.json.)"
31
- printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"%s"}}\n' "$REASON"
32
- exit 0 # decision is carried by permissionDecision "deny", not the exit code