@webpieces/ai-hook-rules 0.3.160 → 0.3.162

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.
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ // Plain JS shim — delegates to compiled TypeScript.
3
+ // Must NOT be converted to TypeScript (needs to exist pre-build for pnpm bin symlinks).
4
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
5
+ 'use strict';
6
+
7
+ const path = require('path');
8
+ const fs = require('fs');
9
+ const compiled = path.join(__dirname, '..', 'src', 'adapters', 'claude-code-hook.js');
10
+
11
+ if (fs.existsSync(compiled)) {
12
+ require(compiled).main();
13
+ } else {
14
+ console.error(' [ai-hook-rules] Package not built yet. Run the build first, or install from npm.');
15
+ process.exit(1);
16
+ }
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ // Plain JS shim — delegates to compiled TypeScript.
3
+ // Must NOT be converted to TypeScript (needs to exist pre-build for pnpm bin symlinks).
4
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
5
+ 'use strict';
6
+
7
+ const path = require('path');
8
+ const fs = require('fs');
9
+ const compiled = path.join(__dirname, '..', 'src', 'bin', 'dev-hook-install.js');
10
+
11
+ if (fs.existsSync(compiled)) {
12
+ require(compiled).main();
13
+ } else {
14
+ console.error(' [ai-hook-rules] Package not built yet. Run `nx build ai-hook-rules` first.');
15
+ process.exit(1);
16
+ }
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ // Plain JS shim — delegates to compiled TypeScript.
3
+ // Must NOT be converted to TypeScript (needs to exist pre-build for pnpm bin symlinks).
4
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
5
+ 'use strict';
6
+
7
+ const path = require('path');
8
+ const fs = require('fs');
9
+ const compiled = path.join(__dirname, '..', 'src', 'bin', 'dev-hook-uninstall.js');
10
+
11
+ if (fs.existsSync(compiled)) {
12
+ require(compiled).main();
13
+ } else {
14
+ console.error(' [ai-hook-rules] Package not built yet. Run `nx build ai-hook-rules` first.');
15
+ process.exit(1);
16
+ }
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "@webpieces/ai-hook-rules",
3
- "version": "0.3.160",
3
+ "version": "0.3.162",
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",
7
7
  "bin": {
8
- "wp-ai-hook": "./src/adapters/claude-code-hook.js",
8
+ "wp-ai-hook": "./bin/wp-ai-hook.js",
9
9
  "wp-setup-ai-hooks": "./bin/wp-setup-ai-hooks.js",
10
10
  "wp-setup-global-ai-hooks": "./bin/wp-setup-global-ai-hooks.js",
11
+ "wp-dev-hook-install": "./bin/wp-dev-hook-install.js",
12
+ "wp-dev-hook-uninstall": "./bin/wp-dev-hook-uninstall.js",
11
13
  "wp-git-update": "./src/scripts/git-updateFromMain.js",
12
14
  "wp-git-gather": "./src/scripts/git-gatherInfo.js",
13
15
  "wp-git-merge-complete": "./src/scripts/git-mergeComplete.js"
@@ -34,7 +36,7 @@
34
36
  "directory": "packages/tooling/ai-hook-rules"
35
37
  },
36
38
  "dependencies": {
37
- "@webpieces/rules-config": "0.3.160"
39
+ "@webpieces/rules-config": "0.3.162"
38
40
  },
39
41
  "publishConfig": {
40
42
  "access": "public"
@@ -0,0 +1 @@
1
+ export declare function main(): void;
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.main = main;
4
+ const fs_1 = require("fs");
5
+ const os_1 = require("os");
6
+ const path_1 = require("path");
7
+ function wireLocalRulesConfig(cwd, distRulesConfigPath) {
8
+ // Place the symlink in dist/packages/tooling/node_modules/ — one level above
9
+ // the ai-hook-rules package output but still below the workspace root node_modules.
10
+ // Node's require() resolution walks up from the requiring file and finds this
11
+ // node_modules entry before reaching the workspace root's stale published package.
12
+ // This directory is NOT wiped by any individual package build (each build only
13
+ // cleans its own output subfolder), so it survives pnpm run build-all.
14
+ const overrideDir = (0, path_1.join)(cwd, 'dist', 'packages', 'tooling', 'node_modules', '@webpieces');
15
+ const overrideLink = (0, path_1.join)(overrideDir, 'rules-config');
16
+ (0, fs_1.mkdirSync)(overrideDir, { recursive: true });
17
+ if ((0, fs_1.existsSync)(overrideLink)) {
18
+ (0, fs_1.rmSync)(overrideLink, { recursive: true });
19
+ }
20
+ (0, fs_1.symlinkSync)(distRulesConfigPath, overrideLink);
21
+ }
22
+ function main() {
23
+ const cwd = process.cwd();
24
+ const distHookPath = (0, path_1.join)(cwd, 'dist', 'packages', 'tooling', 'ai-hook-rules', 'src', 'adapters', 'claude-code-hook.js');
25
+ if (!(0, fs_1.existsSync)(distHookPath)) {
26
+ console.error(`[wp-dev-hook-install] Local build not found at: ${distHookPath}`);
27
+ console.error(' Run `pnpm run build-all` first.');
28
+ process.exit(1);
29
+ }
30
+ const distRulesConfigPath = (0, path_1.join)(cwd, 'dist', 'packages', 'tooling', 'rules-config');
31
+ if (!(0, fs_1.existsSync)(distRulesConfigPath)) {
32
+ console.error(`[wp-dev-hook-install] Local rules-config build not found at: ${distRulesConfigPath}`);
33
+ console.error(' Run `pnpm run build-all` first.');
34
+ process.exit(1);
35
+ }
36
+ const homeDir = (0, os_1.homedir)();
37
+ const webpiecesDir = (0, path_1.join)(homeDir, '.webpieces');
38
+ const backupPath = (0, path_1.join)(webpiecesDir, 'dev-hook-backup.json');
39
+ const claudeSettingsPath = (0, path_1.join)(homeDir, '.claude', 'settings.json');
40
+ if ((0, fs_1.existsSync)(backupPath)) {
41
+ console.error('[wp-dev-hook-install] Dev hook is already installed (backup file exists).');
42
+ console.error(' Run `wp-dev-hook-uninstall` first to remove the current dev hook.');
43
+ process.exit(1);
44
+ }
45
+ wireLocalRulesConfig(cwd, distRulesConfigPath);
46
+ let settings = {};
47
+ if ((0, fs_1.existsSync)(claudeSettingsPath)) {
48
+ settings = JSON.parse((0, fs_1.readFileSync)(claudeSettingsPath, 'utf8'));
49
+ }
50
+ // Save whatever hooks exist now (may be undefined/null) so uninstall can restore them
51
+ const backup = { previousHooks: settings.hooks ?? null };
52
+ if (!(0, fs_1.existsSync)(webpiecesDir)) {
53
+ (0, fs_1.mkdirSync)(webpiecesDir, { recursive: true });
54
+ }
55
+ (0, fs_1.writeFileSync)(backupPath, JSON.stringify(backup, null, 4) + '\n');
56
+ const hookCommand = `node ${distHookPath}`;
57
+ settings.hooks = {
58
+ PreToolUse: [
59
+ {
60
+ matcher: 'Write|Edit|MultiEdit|Bash',
61
+ hooks: [{ type: 'command', command: hookCommand }],
62
+ },
63
+ ],
64
+ };
65
+ (0, fs_1.mkdirSync)((0, path_1.dirname)(claudeSettingsPath), { recursive: true });
66
+ (0, fs_1.writeFileSync)(claudeSettingsPath, JSON.stringify(settings, null, 4) + '\n');
67
+ console.log(` Dev hook installed → ${hookCommand}`);
68
+ console.log(' Run `wp-dev-hook-uninstall` when done testing.');
69
+ }
70
+ if (require.main === module) {
71
+ main();
72
+ }
73
+ //# sourceMappingURL=dev-hook-install.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev-hook-install.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/dev-hook-install.ts"],"names":[],"mappings":";;AAqCA,oBAyDC;AA9FD,2BAA6F;AAC7F,2BAA6B;AAC7B,+BAAqC;AAmBrC,SAAS,oBAAoB,CAAC,GAAW,EAAE,mBAA2B;IAClE,6EAA6E;IAC7E,oFAAoF;IACpF,8EAA8E;IAC9E,mFAAmF;IACnF,+EAA+E;IAC/E,uEAAuE;IACvE,MAAM,WAAW,GAAG,IAAA,WAAI,EAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,cAAc,EAAE,YAAY,CAAC,CAAC;IAC3F,MAAM,YAAY,GAAG,IAAA,WAAI,EAAC,WAAW,EAAE,cAAc,CAAC,CAAC;IACvD,IAAA,cAAS,EAAC,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC5C,IAAI,IAAA,eAAU,EAAC,YAAY,CAAC,EAAE,CAAC;QAC3B,IAAA,WAAM,EAAC,YAAY,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9C,CAAC;IACD,IAAA,gBAAW,EAAC,mBAAmB,EAAE,YAAY,CAAC,CAAC;AACnD,CAAC;AAED,SAAgB,IAAI;IAChB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;IAC1B,MAAM,YAAY,GAAG,IAAA,WAAI,EAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,eAAe,EAAE,KAAK,EAAE,UAAU,EAAE,qBAAqB,CAAC,CAAC;IAEzH,IAAI,CAAC,IAAA,eAAU,EAAC,YAAY,CAAC,EAAE,CAAC;QAC5B,OAAO,CAAC,KAAK,CAAC,mDAAmD,YAAY,EAAE,CAAC,CAAC;QACjF,OAAO,CAAC,KAAK,CAAC,mCAAmC,CAAC,CAAC;QACnD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;IAED,MAAM,mBAAmB,GAAG,IAAA,WAAI,EAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,cAAc,CAAC,CAAC;IACrF,IAAI,CAAC,IAAA,eAAU,EAAC,mBAAmB,CAAC,EAAE,CAAC;QACnC,OAAO,CAAC,KAAK,CAAC,gEAAgE,mBAAmB,EAAE,CAAC,CAAC;QACrG,OAAO,CAAC,KAAK,CAAC,mCAAmC,CAAC,CAAC;QACnD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;IAED,MAAM,OAAO,GAAG,IAAA,YAAO,GAAE,CAAC;IAC1B,MAAM,YAAY,GAAG,IAAA,WAAI,EAAC,OAAO,EAAE,YAAY,CAAC,CAAC;IACjD,MAAM,UAAU,GAAG,IAAA,WAAI,EAAC,YAAY,EAAE,sBAAsB,CAAC,CAAC;IAC9D,MAAM,kBAAkB,GAAG,IAAA,WAAI,EAAC,OAAO,EAAE,SAAS,EAAE,eAAe,CAAC,CAAC;IAErE,IAAI,IAAA,eAAU,EAAC,UAAU,CAAC,EAAE,CAAC;QACzB,OAAO,CAAC,KAAK,CAAC,2EAA2E,CAAC,CAAC;QAC3F,OAAO,CAAC,KAAK,CAAC,qEAAqE,CAAC,CAAC;QACrF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;IAED,oBAAoB,CAAC,GAAG,EAAE,mBAAmB,CAAC,CAAC;IAE/C,IAAI,QAAQ,GAAmB,EAAE,CAAC;IAClC,IAAI,IAAA,eAAU,EAAC,kBAAkB,CAAC,EAAE,CAAC;QACjC,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,IAAA,iBAAY,EAAC,kBAAkB,EAAE,MAAM,CAAC,CAAmB,CAAC;IACtF,CAAC;IAED,sFAAsF;IACtF,MAAM,MAAM,GAAkB,EAAE,aAAa,EAAE,QAAQ,CAAC,KAAK,IAAI,IAAI,EAAE,CAAC;IACxE,IAAI,CAAC,IAAA,eAAU,EAAC,YAAY,CAAC,EAAE,CAAC;QAC5B,IAAA,cAAS,EAAC,YAAY,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACjD,CAAC;IACD,IAAA,kBAAa,EAAC,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAElE,MAAM,WAAW,GAAG,QAAQ,YAAY,EAAE,CAAC;IAC3C,QAAQ,CAAC,KAAK,GAAG;QACb,UAAU,EAAE;YACR;gBACI,OAAO,EAAE,2BAA2B;gBACpC,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC;aACrD;SACJ;KACJ,CAAC;IAEF,IAAA,cAAS,EAAC,IAAA,cAAO,EAAC,kBAAkB,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC5D,IAAA,kBAAa,EAAC,kBAAkB,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAE5E,OAAO,CAAC,GAAG,CAAC,0BAA0B,WAAW,EAAE,CAAC,CAAC;IACrD,OAAO,CAAC,GAAG,CAAC,kDAAkD,CAAC,CAAC;AACpE,CAAC;AAED,IAAI,OAAO,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;IAC1B,IAAI,EAAE,CAAC;AACX,CAAC","sourcesContent":["import { existsSync, readFileSync, writeFileSync, mkdirSync, symlinkSync, rmSync } from 'fs';\nimport { homedir } from 'os';\nimport { join, dirname } from 'path';\n\ninterface HookEntry {\n matcher: string;\n hooks: Array<{ type: string; command: string }>;\n}\n\ninterface ClaudeSettings {\n hooks?: {\n PreToolUse?: HookEntry[];\n };\n // webpieces-disable no-any-unknown -- opaque settings bag; arbitrary keys allowed\n [key: string]: unknown;\n}\n\ninterface DevHookBackup {\n previousHooks: ClaudeSettings['hooks'] | null;\n}\n\nfunction wireLocalRulesConfig(cwd: string, distRulesConfigPath: string): void {\n // Place the symlink in dist/packages/tooling/node_modules/ — one level above\n // the ai-hook-rules package output but still below the workspace root node_modules.\n // Node's require() resolution walks up from the requiring file and finds this\n // node_modules entry before reaching the workspace root's stale published package.\n // This directory is NOT wiped by any individual package build (each build only\n // cleans its own output subfolder), so it survives pnpm run build-all.\n const overrideDir = join(cwd, 'dist', 'packages', 'tooling', 'node_modules', '@webpieces');\n const overrideLink = join(overrideDir, 'rules-config');\n mkdirSync(overrideDir, { recursive: true });\n if (existsSync(overrideLink)) {\n rmSync(overrideLink, { recursive: true });\n }\n symlinkSync(distRulesConfigPath, overrideLink);\n}\n\nexport function main(): void {\n const cwd = process.cwd();\n const distHookPath = join(cwd, 'dist', 'packages', 'tooling', 'ai-hook-rules', 'src', 'adapters', 'claude-code-hook.js');\n\n if (!existsSync(distHookPath)) {\n console.error(`[wp-dev-hook-install] Local build not found at: ${distHookPath}`);\n console.error(' Run `pnpm run build-all` first.');\n process.exit(1);\n }\n\n const distRulesConfigPath = join(cwd, 'dist', 'packages', 'tooling', 'rules-config');\n if (!existsSync(distRulesConfigPath)) {\n console.error(`[wp-dev-hook-install] Local rules-config build not found at: ${distRulesConfigPath}`);\n console.error(' Run `pnpm run build-all` first.');\n process.exit(1);\n }\n\n const homeDir = homedir();\n const webpiecesDir = join(homeDir, '.webpieces');\n const backupPath = join(webpiecesDir, 'dev-hook-backup.json');\n const claudeSettingsPath = join(homeDir, '.claude', 'settings.json');\n\n if (existsSync(backupPath)) {\n console.error('[wp-dev-hook-install] Dev hook is already installed (backup file exists).');\n console.error(' Run `wp-dev-hook-uninstall` first to remove the current dev hook.');\n process.exit(1);\n }\n\n wireLocalRulesConfig(cwd, distRulesConfigPath);\n\n let settings: ClaudeSettings = {};\n if (existsSync(claudeSettingsPath)) {\n settings = JSON.parse(readFileSync(claudeSettingsPath, 'utf8')) as ClaudeSettings;\n }\n\n // Save whatever hooks exist now (may be undefined/null) so uninstall can restore them\n const backup: DevHookBackup = { previousHooks: settings.hooks ?? null };\n if (!existsSync(webpiecesDir)) {\n mkdirSync(webpiecesDir, { recursive: true });\n }\n writeFileSync(backupPath, JSON.stringify(backup, null, 4) + '\\n');\n\n const hookCommand = `node ${distHookPath}`;\n settings.hooks = {\n PreToolUse: [\n {\n matcher: 'Write|Edit|MultiEdit|Bash',\n hooks: [{ type: 'command', command: hookCommand }],\n },\n ],\n };\n\n mkdirSync(dirname(claudeSettingsPath), { recursive: true });\n writeFileSync(claudeSettingsPath, JSON.stringify(settings, null, 4) + '\\n');\n\n console.log(` Dev hook installed → ${hookCommand}`);\n console.log(' Run `wp-dev-hook-uninstall` when done testing.');\n}\n\nif (require.main === module) {\n main();\n}\n"]}
@@ -0,0 +1 @@
1
+ export declare function main(): void;
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.main = main;
4
+ const fs_1 = require("fs");
5
+ const os_1 = require("os");
6
+ const path_1 = require("path");
7
+ function main() {
8
+ const cwd = process.cwd();
9
+ const homeDir = (0, os_1.homedir)();
10
+ const backupPath = (0, path_1.join)(homeDir, '.webpieces', 'dev-hook-backup.json');
11
+ const claudeSettingsPath = (0, path_1.join)(homeDir, '.claude', 'settings.json');
12
+ if (!(0, fs_1.existsSync)(backupPath)) {
13
+ console.error('[wp-dev-hook-uninstall] No dev hook backup found — dev hook was not installed.');
14
+ process.exit(1);
15
+ }
16
+ const backup = JSON.parse((0, fs_1.readFileSync)(backupPath, 'utf8'));
17
+ let settings = {};
18
+ if ((0, fs_1.existsSync)(claudeSettingsPath)) {
19
+ settings = JSON.parse((0, fs_1.readFileSync)(claudeSettingsPath, 'utf8'));
20
+ }
21
+ if (backup.previousHooks === null) {
22
+ delete settings.hooks;
23
+ }
24
+ else {
25
+ settings.hooks = backup.previousHooks;
26
+ }
27
+ (0, fs_1.writeFileSync)(claudeSettingsPath, JSON.stringify(settings, null, 4) + '\n');
28
+ (0, fs_1.rmSync)(backupPath);
29
+ // Remove the symlink created during install
30
+ const overrideDir = (0, path_1.join)(cwd, 'dist', 'packages', 'tooling', 'node_modules', '@webpieces');
31
+ const overrideLink = (0, path_1.join)(overrideDir, 'rules-config');
32
+ if ((0, fs_1.existsSync)(overrideLink)) {
33
+ (0, fs_1.rmSync)(overrideLink, { recursive: true });
34
+ }
35
+ if ((0, fs_1.existsSync)(overrideDir) && (0, fs_1.readdirSync)(overrideDir).length === 0) {
36
+ (0, fs_1.rmSync)(overrideDir, { recursive: true });
37
+ }
38
+ console.log(' Dev hook removed. Previous hook configuration restored.');
39
+ }
40
+ if (require.main === module) {
41
+ main();
42
+ }
43
+ //# sourceMappingURL=dev-hook-uninstall.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev-hook-uninstall.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/dev-hook-uninstall.ts"],"names":[],"mappings":";;AAqBA,oBAsCC;AA3DD,2BAAkF;AAClF,2BAA6B;AAC7B,+BAA4B;AAmB5B,SAAgB,IAAI;IAChB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;IAC1B,MAAM,OAAO,GAAG,IAAA,YAAO,GAAE,CAAC;IAC1B,MAAM,UAAU,GAAG,IAAA,WAAI,EAAC,OAAO,EAAE,YAAY,EAAE,sBAAsB,CAAC,CAAC;IACvE,MAAM,kBAAkB,GAAG,IAAA,WAAI,EAAC,OAAO,EAAE,SAAS,EAAE,eAAe,CAAC,CAAC;IAErE,IAAI,CAAC,IAAA,eAAU,EAAC,UAAU,CAAC,EAAE,CAAC;QAC1B,OAAO,CAAC,KAAK,CAAC,gFAAgF,CAAC,CAAC;QAChG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAA,iBAAY,EAAC,UAAU,EAAE,MAAM,CAAC,CAAkB,CAAC;IAE7E,IAAI,QAAQ,GAAmB,EAAE,CAAC;IAClC,IAAI,IAAA,eAAU,EAAC,kBAAkB,CAAC,EAAE,CAAC;QACjC,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,IAAA,iBAAY,EAAC,kBAAkB,EAAE,MAAM,CAAC,CAAmB,CAAC;IACtF,CAAC;IAED,IAAI,MAAM,CAAC,aAAa,KAAK,IAAI,EAAE,CAAC;QAChC,OAAO,QAAQ,CAAC,KAAK,CAAC;IAC1B,CAAC;SAAM,CAAC;QACJ,QAAQ,CAAC,KAAK,GAAG,MAAM,CAAC,aAAa,CAAC;IAC1C,CAAC;IAED,IAAA,kBAAa,EAAC,kBAAkB,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAC5E,IAAA,WAAM,EAAC,UAAU,CAAC,CAAC;IAEnB,4CAA4C;IAC5C,MAAM,WAAW,GAAG,IAAA,WAAI,EAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,cAAc,EAAE,YAAY,CAAC,CAAC;IAC3F,MAAM,YAAY,GAAG,IAAA,WAAI,EAAC,WAAW,EAAE,cAAc,CAAC,CAAC;IACvD,IAAI,IAAA,eAAU,EAAC,YAAY,CAAC,EAAE,CAAC;QAC3B,IAAA,WAAM,EAAC,YAAY,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9C,CAAC;IACD,IAAI,IAAA,eAAU,EAAC,WAAW,CAAC,IAAI,IAAA,gBAAW,EAAC,WAAW,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACnE,IAAA,WAAM,EAAC,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7C,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,2DAA2D,CAAC,CAAC;AAC7E,CAAC;AAED,IAAI,OAAO,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;IAC1B,IAAI,EAAE,CAAC;AACX,CAAC","sourcesContent":["import { existsSync, readFileSync, writeFileSync, rmSync, readdirSync } from 'fs';\nimport { homedir } from 'os';\nimport { join } from 'path';\n\ninterface HookEntry {\n matcher: string;\n hooks: Array<{ type: string; command: string }>;\n}\n\ninterface ClaudeSettings {\n hooks?: {\n PreToolUse?: HookEntry[];\n };\n // webpieces-disable no-any-unknown -- opaque settings bag; arbitrary keys allowed\n [key: string]: unknown;\n}\n\ninterface DevHookBackup {\n previousHooks: ClaudeSettings['hooks'] | null;\n}\n\nexport function main(): void {\n const cwd = process.cwd();\n const homeDir = homedir();\n const backupPath = join(homeDir, '.webpieces', 'dev-hook-backup.json');\n const claudeSettingsPath = join(homeDir, '.claude', 'settings.json');\n\n if (!existsSync(backupPath)) {\n console.error('[wp-dev-hook-uninstall] No dev hook backup found — dev hook was not installed.');\n process.exit(1);\n }\n\n const backup = JSON.parse(readFileSync(backupPath, 'utf8')) as DevHookBackup;\n\n let settings: ClaudeSettings = {};\n if (existsSync(claudeSettingsPath)) {\n settings = JSON.parse(readFileSync(claudeSettingsPath, 'utf8')) as ClaudeSettings;\n }\n\n if (backup.previousHooks === null) {\n delete settings.hooks;\n } else {\n settings.hooks = backup.previousHooks;\n }\n\n writeFileSync(claudeSettingsPath, JSON.stringify(settings, null, 4) + '\\n');\n rmSync(backupPath);\n\n // Remove the symlink created during install\n const overrideDir = join(cwd, 'dist', 'packages', 'tooling', 'node_modules', '@webpieces');\n const overrideLink = join(overrideDir, 'rules-config');\n if (existsSync(overrideLink)) {\n rmSync(overrideLink, { recursive: true });\n }\n if (existsSync(overrideDir) && readdirSync(overrideDir).length === 0) {\n rmSync(overrideDir, { recursive: true });\n }\n\n console.log(' Dev hook removed. Previous hook configuration restored.');\n}\n\nif (require.main === module) {\n main();\n}\n"]}
@@ -1 +1 @@
1
- export declare function main(): void;
1
+ export declare function main(): Promise<void>;
@@ -2,59 +2,84 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.main = main;
4
4
  const fs_1 = require("fs");
5
+ const readline_1 = require("readline");
5
6
  const os_1 = require("os");
6
7
  const path_1 = require("path");
7
- function main() {
8
+ function prompt(question) {
9
+ return new Promise((resolve) => {
10
+ const rl = (0, readline_1.createInterface)({ input: process.stdin, output: process.stdout });
11
+ rl.question(question, (answer) => {
12
+ rl.close();
13
+ resolve(answer.trim().toLowerCase());
14
+ });
15
+ });
16
+ }
17
+ function isWired(settings) {
18
+ return (settings.hooks?.PreToolUse ?? []).some((e) => e.hooks.some((h) => h.command.includes('global-hook.js')));
19
+ }
20
+ function installHook(settings, shimSource, globalHookDest, claudeSettingsPath) {
21
+ (0, fs_1.mkdirSync)((0, path_1.dirname)(globalHookDest), { recursive: true });
22
+ (0, fs_1.copyFileSync)(shimSource, globalHookDest);
23
+ const hookCommand = `node ${globalHookDest}`;
24
+ if (!settings.hooks)
25
+ settings.hooks = {};
26
+ if (!Array.isArray(settings.hooks.PreToolUse))
27
+ settings.hooks.PreToolUse = [];
28
+ settings.hooks.PreToolUse.push({
29
+ matcher: 'Write|Edit|MultiEdit|Bash',
30
+ hooks: [{ type: 'command', command: hookCommand }],
31
+ });
32
+ (0, fs_1.mkdirSync)((0, path_1.dirname)(claudeSettingsPath), { recursive: true });
33
+ (0, fs_1.writeFileSync)(claudeSettingsPath, JSON.stringify(settings, null, 4) + '\n');
34
+ console.log(` Installed global hook → ${globalHookDest}`);
35
+ console.log(` Wired into ~/.claude/settings.json`);
36
+ console.log('');
37
+ console.log('✅ Global webpieces hook installed.');
38
+ console.log(' The global hook delegates to each repo\'s ./node_modules/.bin/wp-ai-hook automatically.');
39
+ }
40
+ function uninstallHook(settings, globalHookDest, claudeSettingsPath) {
41
+ if (settings.hooks?.PreToolUse) {
42
+ settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter((e) => !e.hooks.some((h) => h.command.includes('global-hook.js')));
43
+ }
44
+ (0, fs_1.writeFileSync)(claudeSettingsPath, JSON.stringify(settings, null, 4) + '\n');
45
+ if ((0, fs_1.existsSync)(globalHookDest)) {
46
+ (0, fs_1.rmSync)(globalHookDest);
47
+ }
48
+ console.log('✅ Global webpieces hook removed.');
49
+ }
50
+ async function main() {
8
51
  const homeDir = (0, os_1.homedir)();
9
- const webpiecesDir = (0, path_1.join)(homeDir, '.webpieces');
10
- const globalHookDest = (0, path_1.join)(webpiecesDir, 'global-hook.js');
52
+ const globalHookDest = (0, path_1.join)(homeDir, '.webpieces', 'global-hook.js');
11
53
  const claudeSettingsPath = (0, path_1.join)(homeDir, '.claude', 'settings.json');
12
- // Find compiled global-hook.js relative to this compiled file (src/bin/ → src/adapters/)
13
54
  const shimSource = (0, path_1.join)(__dirname, '..', 'adapters', 'global-hook.js');
14
55
  if (!(0, fs_1.existsSync)(shimSource)) {
15
56
  console.error(`[wp-setup-global-ai-hooks] Cannot find compiled hook at: ${shimSource}`);
16
- console.error(' Make sure the package is built first.');
17
57
  process.exit(1);
18
58
  }
19
- // Create ~/.webpieces/ if needed
20
- if (!(0, fs_1.existsSync)(webpiecesDir)) {
21
- (0, fs_1.mkdirSync)(webpiecesDir, { recursive: true });
22
- }
23
- (0, fs_1.copyFileSync)(shimSource, globalHookDest);
24
- console.log(` Installed global hook → ${globalHookDest}`);
25
- // Wire into ~/.claude/settings.json using absolute path (~ is not expanded by Claude Code)
26
- const hookCommand = `node ${globalHookDest}`;
27
59
  let settings = {};
28
60
  if ((0, fs_1.existsSync)(claudeSettingsPath)) {
29
61
  settings = JSON.parse((0, fs_1.readFileSync)(claudeSettingsPath, 'utf8'));
30
62
  }
31
- if (!settings.hooks)
32
- settings.hooks = {};
33
- if (!Array.isArray(settings.hooks.PreToolUse))
34
- settings.hooks.PreToolUse = [];
35
- const alreadyWired = settings.hooks.PreToolUse.some((e) => e.hooks.some((h) => h.command.includes('global-hook.js')));
36
- if (alreadyWired) {
37
- console.log(' ~/.claude/settings.json already has the global hook — skipping.');
63
+ if (isWired(settings)) {
64
+ const answer = await prompt('Global hook is already installed. Uninstall? [y/N]: ');
65
+ if (answer === 'y') {
66
+ uninstallHook(settings, globalHookDest, claudeSettingsPath);
67
+ }
68
+ else {
69
+ console.log(' No changes made.');
70
+ }
38
71
  }
39
72
  else {
40
- settings.hooks.PreToolUse.push({
41
- matcher: 'Write|Edit|MultiEdit|Bash',
42
- hooks: [{ type: 'command', command: hookCommand }],
43
- });
44
- (0, fs_1.mkdirSync)((0, path_1.dirname)(claudeSettingsPath), { recursive: true });
45
- (0, fs_1.writeFileSync)(claudeSettingsPath, JSON.stringify(settings, null, 4) + '\n');
46
- console.log(` Wired global hook into ~/.claude/settings.json`);
73
+ const answer = await prompt('Install global webpieces hook into ~/.claude/settings.json? [Y/n]: ');
74
+ if (answer !== 'n') {
75
+ installHook(settings, shimSource, globalHookDest, claudeSettingsPath);
76
+ }
77
+ else {
78
+ console.log(' No changes made.');
79
+ }
47
80
  }
48
- console.log('');
49
- console.log('✅ Global webpieces hook installed.');
50
- console.log(' IMPORTANT: Remove any per-project hook entries from .claude/settings.json files.');
51
- console.log(' The global hook delegates to each repo\'s ./node_modules/.bin/wp-ai-hook automatically.');
52
- console.log('');
53
- console.log(' If a project lacks webpieces, the hook will ask AI to warn you and offer:');
54
- console.log(' A) Install webpieces in that project');
55
- console.log(' B) Write a .skiphooks file to bypass temporarily or forever');
56
81
  }
57
82
  if (require.main === module) {
58
- main();
83
+ void main();
59
84
  }
60
85
  //# sourceMappingURL=global-setup.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"global-setup.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/global-setup.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, readFileSync, writeFileSync, copyFileSync, mkdirSync } from 'fs';\nimport { homedir } from 'os';\nimport { join, dirname } from 'path';\n\ninterface HookEntry {\n matcher: string;\n hooks: Array<{ type: string; command: string }>;\n}\n\ninterface ClaudeSettings {\n hooks?: {\n PreToolUse?: HookEntry[];\n };\n // webpieces-disable no-any-unknown -- opaque settings bag; arbitrary keys allowed\n [key: string]: unknown;\n}\n\nexport function main(): void {\n const homeDir = homedir();\n const webpiecesDir = join(homeDir, '.webpieces');\n const globalHookDest = join(webpiecesDir, 'global-hook.js');\n const claudeSettingsPath = join(homeDir, '.claude', 'settings.json');\n\n // Find compiled global-hook.js relative to this compiled file (src/bin/ → src/adapters/)\n const shimSource = join(__dirname, '..', 'adapters', 'global-hook.js');\n\n if (!existsSync(shimSource)) {\n console.error(`[wp-setup-global-ai-hooks] Cannot find compiled hook at: ${shimSource}`);\n console.error(' Make sure the package is built first.');\n process.exit(1);\n }\n\n // Create ~/.webpieces/ if needed\n if (!existsSync(webpiecesDir)) {\n mkdirSync(webpiecesDir, { recursive: true });\n }\n\n copyFileSync(shimSource, globalHookDest);\n console.log(` Installed global hook → ${globalHookDest}`);\n\n // Wire into ~/.claude/settings.json using absolute path (~ is not expanded by Claude Code)\n const hookCommand = `node ${globalHookDest}`;\n\n let settings: ClaudeSettings = {};\n if (existsSync(claudeSettingsPath)) {\n settings = JSON.parse(readFileSync(claudeSettingsPath, 'utf8')) as ClaudeSettings;\n }\n\n if (!settings.hooks) settings.hooks = {};\n if (!Array.isArray(settings.hooks.PreToolUse)) settings.hooks.PreToolUse = [];\n\n const alreadyWired = settings.hooks.PreToolUse.some((e) =>\n e.hooks.some((h) => h.command.includes('global-hook.js')),\n );\n\n if (alreadyWired) {\n console.log(' ~/.claude/settings.json already has the global hook — skipping.');\n } else {\n settings.hooks.PreToolUse.push({\n matcher: 'Write|Edit|MultiEdit|Bash',\n hooks: [{ type: 'command', command: hookCommand }],\n });\n mkdirSync(dirname(claudeSettingsPath), { recursive: true });\n writeFileSync(claudeSettingsPath, JSON.stringify(settings, null, 4) + '\\n');\n console.log(` Wired global hook into ~/.claude/settings.json`);\n }\n\n console.log('');\n console.log('✅ Global webpieces hook installed.');\n console.log(' IMPORTANT: Remove any per-project hook entries from .claude/settings.json files.');\n console.log(' The global hook delegates to each repo\\'s ./node_modules/.bin/wp-ai-hook automatically.');\n console.log('');\n console.log(' If a project lacks webpieces, the hook will ask AI to warn you and offer:');\n console.log(' A) Install webpieces in that project');\n console.log(' B) Write a .skiphooks file to bypass temporarily or forever');\n}\n\nif (require.main === module) {\n main();\n}\n"]}
1
+ {"version":3,"file":"global-setup.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/global-setup.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, readFileSync, writeFileSync, copyFileSync, mkdirSync, rmSync } from 'fs';\nimport { createInterface } from 'readline';\nimport { homedir } from 'os';\nimport { join, dirname } from 'path';\n\ninterface HookCommand {\n type: string;\n command: string;\n}\n\ninterface HookEntry {\n matcher: string;\n hooks: Array<HookCommand>;\n}\n\ninterface ClaudeSettings {\n hooks?: {\n PreToolUse?: HookEntry[];\n };\n // webpieces-disable no-any-unknown -- opaque settings bag; arbitrary keys allowed\n [key: string]: unknown;\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) => {\n rl.close();\n resolve(answer.trim().toLowerCase());\n });\n });\n}\n\nfunction isWired(settings: ClaudeSettings): boolean {\n return (settings.hooks?.PreToolUse ?? []).some((e: HookEntry) =>\n e.hooks.some((h: HookCommand) => h.command.includes('global-hook.js')),\n );\n}\n\nfunction installHook(settings: ClaudeSettings, shimSource: string, globalHookDest: string, claudeSettingsPath: string): void {\n mkdirSync(dirname(globalHookDest), { recursive: true });\n copyFileSync(shimSource, globalHookDest);\n\n const hookCommand = `node ${globalHookDest}`;\n if (!settings.hooks) settings.hooks = {};\n if (!Array.isArray(settings.hooks.PreToolUse)) settings.hooks.PreToolUse = [];\n settings.hooks.PreToolUse.push({\n matcher: 'Write|Edit|MultiEdit|Bash',\n hooks: [{ type: 'command', command: hookCommand }],\n });\n mkdirSync(dirname(claudeSettingsPath), { recursive: true });\n writeFileSync(claudeSettingsPath, JSON.stringify(settings, null, 4) + '\\n');\n\n console.log(` Installed global hook → ${globalHookDest}`);\n console.log(` Wired into ~/.claude/settings.json`);\n console.log('');\n console.log('✅ Global webpieces hook installed.');\n console.log(' The global hook delegates to each repo\\'s ./node_modules/.bin/wp-ai-hook automatically.');\n}\n\nfunction uninstallHook(settings: ClaudeSettings, globalHookDest: string, claudeSettingsPath: string): void {\n if (settings.hooks?.PreToolUse) {\n settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter((e: HookEntry) =>\n !e.hooks.some((h: HookCommand) => h.command.includes('global-hook.js')),\n );\n }\n writeFileSync(claudeSettingsPath, JSON.stringify(settings, null, 4) + '\\n');\n if (existsSync(globalHookDest)) {\n rmSync(globalHookDest);\n }\n console.log(' Global webpieces hook removed.');\n}\n\nexport async function main(): Promise<void> {\n const homeDir = homedir();\n const globalHookDest = join(homeDir, '.webpieces', 'global-hook.js');\n const claudeSettingsPath = join(homeDir, '.claude', 'settings.json');\n const shimSource = join(__dirname, '..', 'adapters', 'global-hook.js');\n\n if (!existsSync(shimSource)) {\n console.error(`[wp-setup-global-ai-hooks] Cannot find compiled hook at: ${shimSource}`);\n process.exit(1);\n }\n\n let settings: ClaudeSettings = {};\n if (existsSync(claudeSettingsPath)) {\n settings = JSON.parse(readFileSync(claudeSettingsPath, 'utf8')) as ClaudeSettings;\n }\n\n if (isWired(settings)) {\n const answer = await prompt('Global hook is already installed. Uninstall? [y/N]: ');\n if (answer === 'y') {\n uninstallHook(settings, globalHookDest, claudeSettingsPath);\n } else {\n console.log(' No changes made.');\n }\n } else {\n const answer = await prompt('Install global webpieces hook into ~/.claude/settings.json? [Y/n]: ');\n if (answer !== 'n') {\n installHook(settings, shimSource, globalHookDest, claudeSettingsPath);\n } else {\n console.log(' No changes made.');\n }\n }\n}\n\nif (require.main === module) {\n void main();\n}\n"]}
@@ -2,24 +2,17 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const child_process_1 = require("child_process");
4
4
  const types_1 = require("../types");
5
- const FIX_HINT_STALE_MAIN = [
6
- 'Local main is behind origin/main.',
7
- 'Run: git checkout main && git pull origin main',
8
- 'Then switch back to main and create your branch.',
9
- ];
10
- const FIX_HINT_NON_MAIN = [
11
- 'You should only branch off main, not off a feature branch!',
12
- 'Run: git checkout main && git pull origin main',
13
- 'Then create your branch from main.',
14
- 'If you truly need to branch off a non-main branch, a human must approve that — it is highly unusual.',
5
+ const FIX_HINT = [
6
+ "Run 'git checkout main && git pull origin main', then create your branch from main",
7
+ "If you truly need a sub-branch (requires human approval), name it using the convention in webpieces.config.json 'branch-creation-guard.subBranchNaming'",
15
8
  ];
16
9
  const branchCreationGuard = {
17
10
  name: 'branch-creation-guard',
18
11
  description: 'Block new-branch creation when main is stale, or when not on main.',
19
12
  scope: 'bash',
20
13
  files: [],
21
- defaultOptions: {},
22
- fixHint: [...FIX_HINT_STALE_MAIN, ...FIX_HINT_NON_MAIN],
14
+ defaultOptions: { subBranchNaming: 'feature/<ticket>/<short-description>' },
15
+ fixHint: FIX_HINT,
23
16
  check(ctx) {
24
17
  const requestedName = extractBranchName(ctx.command);
25
18
  if (!requestedName)
@@ -31,7 +24,7 @@ const branchCreationGuard = {
31
24
  if (currentBranch === 'main') {
32
25
  return checkMainIsUpToDate(ctx, requestedName);
33
26
  }
34
- return [new types_1.Violation(1, truncate(ctx.command), `You are on '${currentBranch}', not main. You should only branch off main! Switch to main first: git checkout main && git pull origin main. If you truly need to branch off a non-main branch, a human must approve that — it is highly unusual.`)];
27
+ return [new types_1.Violation(1, truncate(ctx.command), `You are on '${currentBranch}', not main. Branches must be created from main.`)];
35
28
  },
36
29
  };
37
30
  function checkMainIsUpToDate(ctx, requestedName) {
@@ -1 +1 @@
1
- {"version":3,"file":"branch-creation-guard.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/branch-creation-guard.ts"],"names":[],"mappings":";;AAAA,iDAAyC;AAEzC,oCAA0C;AAE1C,MAAM,mBAAmB,GAAsB;IAC3C,mCAAmC;IACnC,gDAAgD;IAChD,kDAAkD;CACrD,CAAC;AAEF,MAAM,iBAAiB,GAAsB;IACzC,4DAA4D;IAC5D,gDAAgD;IAChD,oCAAoC;IACpC,sGAAsG;CACzG,CAAC;AAEF,MAAM,mBAAmB,GAAa;IAClC,IAAI,EAAE,uBAAuB;IAC7B,WAAW,EAAE,oEAAoE;IACjF,KAAK,EAAE,MAAM;IACb,KAAK,EAAE,EAAE;IACT,cAAc,EAAE,EAAE;IAClB,OAAO,EAAE,CAAC,GAAG,mBAAmB,EAAE,GAAG,iBAAiB,CAAC;IAEvD,KAAK,CAAC,GAAgB;QAClB,MAAM,aAAa,GAAG,iBAAiB,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACrD,IAAI,CAAC,aAAa;YAAE,OAAO,EAAE,CAAC;QAE9B,MAAM,aAAa,GAAG,IAAA,wBAAQ,EAAC,iCAAiC,EAAE;YAC9D,GAAG,EAAE,GAAG,CAAC,aAAa;YACtB,QAAQ,EAAE,MAAM;SACnB,CAAC,CAAC,IAAI,EAAE,CAAC;QAEV,IAAI,aAAa,KAAK,MAAM,EAAE,CAAC;YAC3B,OAAO,mBAAmB,CAAC,GAAG,EAAE,aAAa,CAAC,CAAC;QACnD,CAAC;QAED,OAAO,CAAC,IAAI,iBAAC,CACT,CAAC,EACD,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,EACrB,eAAe,aAAa,qNAAqN,CACpP,CAAC,CAAC;IACP,CAAC;CACJ,CAAC;AAEF,SAAS,mBAAmB,CAAC,GAAgB,EAAE,aAAqB;IAChE,IAAA,wBAAQ,EAAC,+BAA+B,EAAE;QACtC,GAAG,EAAE,GAAG,CAAC,aAAa;QACtB,QAAQ,EAAE,MAAM;KACnB,CAAC,CAAC;IACH,MAAM,QAAQ,GAAG,IAAA,wBAAQ,EAAC,wCAAwC,EAAE;QAChE,GAAG,EAAE,GAAG,CAAC,aAAa;QACtB,QAAQ,EAAE,MAAM;KACnB,CAAC,CAAC,IAAI,EAAE,CAAC;IACV,MAAM,KAAK,GAAG,QAAQ,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IACrC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QACZ,OAAO,CAAC,IAAI,iBAAC,CACT,CAAC,EACD,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,EACrB,iBAAiB,KAAK,gGAAgG,aAAa,IAAI,CAC1I,CAAC,CAAC;IACP,CAAC;IACD,OAAO,EAAE,CAAC;AACd,CAAC;AAED,MAAM,eAAe,GAAa;IAC9B,mCAAmC;IACnC,iCAAiC;IACjC,8CAA8C;CACjD,CAAC;AAEF,SAAS,iBAAiB,CAAC,OAAe;IACtC,KAAK,MAAM,OAAO,IAAI,eAAe,EAAE,CAAC;QACpC,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAChC,IAAI,CAAC;YAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IACvB,CAAC;IACD,OAAO,IAAI,CAAC;AAChB,CAAC;AAED,SAAS,QAAQ,CAAC,CAAS;IACvB,MAAM,GAAG,GAAG,GAAG,CAAC;IAChB,OAAO,CAAC,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,GAAG,CAAC;AACvD,CAAC;AAED,kBAAe,mBAAmB,CAAC","sourcesContent":["import { execSync } from 'child_process';\nimport type { BashRule, BashContext, Violation } from '../types';\nimport { Violation as V } from '../types';\n\nconst FIX_HINT_STALE_MAIN: readonly string[] = [\n 'Local main is behind origin/main.',\n 'Run: git checkout main && git pull origin main',\n 'Then switch back to main and create your branch.',\n];\n\nconst FIX_HINT_NON_MAIN: readonly string[] = [\n 'You should only branch off main, not off a feature branch!',\n 'Run: git checkout main && git pull origin main',\n 'Then create your branch from main.',\n 'If you truly need to branch off a non-main branch, a human must approve that it is highly unusual.',\n];\n\nconst branchCreationGuard: BashRule = {\n name: 'branch-creation-guard',\n description: 'Block new-branch creation when main is stale, or when not on main.',\n scope: 'bash',\n files: [],\n defaultOptions: {},\n fixHint: [...FIX_HINT_STALE_MAIN, ...FIX_HINT_NON_MAIN],\n\n check(ctx: BashContext): readonly Violation[] {\n const requestedName = extractBranchName(ctx.command);\n if (!requestedName) return [];\n\n const currentBranch = execSync('git rev-parse --abbrev-ref HEAD', {\n cwd: ctx.workspaceRoot,\n encoding: 'utf8',\n }).trim();\n\n if (currentBranch === 'main') {\n return checkMainIsUpToDate(ctx, requestedName);\n }\n\n return [new V(\n 1,\n truncate(ctx.command),\n `You are on '${currentBranch}', not main. You should only branch off main! Switch to main first: git checkout main && git pull origin main. If you truly need to branch off a non-main branch, a human must approve that — it is highly unusual.`,\n )];\n },\n};\n\nfunction checkMainIsUpToDate(ctx: BashContext, requestedName: string): readonly Violation[] {\n execSync('git fetch origin main --quiet', {\n cwd: ctx.workspaceRoot,\n encoding: 'utf8',\n });\n const countStr = execSync('git rev-list HEAD..origin/main --count', {\n cwd: ctx.workspaceRoot,\n encoding: 'utf8',\n }).trim();\n const count = parseInt(countStr, 10);\n if (count > 0) {\n return [new V(\n 1,\n truncate(ctx.command),\n `Local main is ${count} commit(s) behind origin/main. Run 'git pull origin main' first, then retry creating branch '${requestedName}'.`,\n )];\n }\n return [];\n}\n\nconst BRANCH_PATTERNS: RegExp[] = [\n /git\\s+checkout\\s+-[bB]\\s+([^\\s]+)/,\n /git\\s+switch\\s+-[cC]\\s+([^\\s]+)/,\n /git\\s+branch\\s+(?!-[dDmMrRla])([^\\s-][^\\s]*)/,\n];\n\nfunction extractBranchName(command: string): string | null {\n for (const pattern of BRANCH_PATTERNS) {\n const m = pattern.exec(command);\n if (m) return m[1];\n }\n return null;\n}\n\nfunction truncate(s: string): string {\n const MAX = 120;\n return s.length <= MAX ? s : s.slice(0, MAX) + '…';\n}\n\nexport default branchCreationGuard;\n"]}
1
+ {"version":3,"file":"branch-creation-guard.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/branch-creation-guard.ts"],"names":[],"mappings":";;AAAA,iDAAyC;AAEzC,oCAA0C;AAE1C,MAAM,QAAQ,GAAsB;IAChC,oFAAoF;IACpF,yJAAyJ;CAC5J,CAAC;AAEF,MAAM,mBAAmB,GAAa;IAClC,IAAI,EAAE,uBAAuB;IAC7B,WAAW,EAAE,oEAAoE;IACjF,KAAK,EAAE,MAAM;IACb,KAAK,EAAE,EAAE;IACT,cAAc,EAAE,EAAE,eAAe,EAAE,sCAAsC,EAAE;IAC3E,OAAO,EAAE,QAAQ;IAEjB,KAAK,CAAC,GAAgB;QAClB,MAAM,aAAa,GAAG,iBAAiB,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACrD,IAAI,CAAC,aAAa;YAAE,OAAO,EAAE,CAAC;QAE9B,MAAM,aAAa,GAAG,IAAA,wBAAQ,EAAC,iCAAiC,EAAE;YAC9D,GAAG,EAAE,GAAG,CAAC,aAAa;YACtB,QAAQ,EAAE,MAAM;SACnB,CAAC,CAAC,IAAI,EAAE,CAAC;QAEV,IAAI,aAAa,KAAK,MAAM,EAAE,CAAC;YAC3B,OAAO,mBAAmB,CAAC,GAAG,EAAE,aAAa,CAAC,CAAC;QACnD,CAAC;QAED,OAAO,CAAC,IAAI,iBAAC,CACT,CAAC,EACD,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,EACrB,eAAe,aAAa,kDAAkD,CACjF,CAAC,CAAC;IACP,CAAC;CACJ,CAAC;AAEF,SAAS,mBAAmB,CAAC,GAAgB,EAAE,aAAqB;IAChE,IAAA,wBAAQ,EAAC,+BAA+B,EAAE;QACtC,GAAG,EAAE,GAAG,CAAC,aAAa;QACtB,QAAQ,EAAE,MAAM;KACnB,CAAC,CAAC;IACH,MAAM,QAAQ,GAAG,IAAA,wBAAQ,EAAC,wCAAwC,EAAE;QAChE,GAAG,EAAE,GAAG,CAAC,aAAa;QACtB,QAAQ,EAAE,MAAM;KACnB,CAAC,CAAC,IAAI,EAAE,CAAC;IACV,MAAM,KAAK,GAAG,QAAQ,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IACrC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QACZ,OAAO,CAAC,IAAI,iBAAC,CACT,CAAC,EACD,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,EACrB,iBAAiB,KAAK,gGAAgG,aAAa,IAAI,CAC1I,CAAC,CAAC;IACP,CAAC;IACD,OAAO,EAAE,CAAC;AACd,CAAC;AAED,MAAM,eAAe,GAAa;IAC9B,mCAAmC;IACnC,iCAAiC;IACjC,8CAA8C;CACjD,CAAC;AAEF,SAAS,iBAAiB,CAAC,OAAe;IACtC,KAAK,MAAM,OAAO,IAAI,eAAe,EAAE,CAAC;QACpC,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAChC,IAAI,CAAC;YAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IACvB,CAAC;IACD,OAAO,IAAI,CAAC;AAChB,CAAC;AAED,SAAS,QAAQ,CAAC,CAAS;IACvB,MAAM,GAAG,GAAG,GAAG,CAAC;IAChB,OAAO,CAAC,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,GAAG,CAAC;AACvD,CAAC;AAED,kBAAe,mBAAmB,CAAC","sourcesContent":["import { execSync } from 'child_process';\nimport type { BashRule, BashContext, Violation } from '../types';\nimport { Violation as V } from '../types';\n\nconst FIX_HINT: readonly string[] = [\n \"Run 'git checkout main && git pull origin main', then create your branch from main\",\n \"If you truly need a sub-branch (requires human approval), name it using the convention in webpieces.config.json 'branch-creation-guard.subBranchNaming'\",\n];\n\nconst branchCreationGuard: BashRule = {\n name: 'branch-creation-guard',\n description: 'Block new-branch creation when main is stale, or when not on main.',\n scope: 'bash',\n files: [],\n defaultOptions: { subBranchNaming: 'feature/<ticket>/<short-description>' },\n fixHint: FIX_HINT,\n\n check(ctx: BashContext): readonly Violation[] {\n const requestedName = extractBranchName(ctx.command);\n if (!requestedName) return [];\n\n const currentBranch = execSync('git rev-parse --abbrev-ref HEAD', {\n cwd: ctx.workspaceRoot,\n encoding: 'utf8',\n }).trim();\n\n if (currentBranch === 'main') {\n return checkMainIsUpToDate(ctx, requestedName);\n }\n\n return [new V(\n 1,\n truncate(ctx.command),\n `You are on '${currentBranch}', not main. Branches must be created from main.`,\n )];\n },\n};\n\nfunction checkMainIsUpToDate(ctx: BashContext, requestedName: string): readonly Violation[] {\n execSync('git fetch origin main --quiet', {\n cwd: ctx.workspaceRoot,\n encoding: 'utf8',\n });\n const countStr = execSync('git rev-list HEAD..origin/main --count', {\n cwd: ctx.workspaceRoot,\n encoding: 'utf8',\n }).trim();\n const count = parseInt(countStr, 10);\n if (count > 0) {\n return [new V(\n 1,\n truncate(ctx.command),\n `Local main is ${count} commit(s) behind origin/main. Run 'git pull origin main' first, then retry creating branch '${requestedName}'.`,\n )];\n }\n return [];\n}\n\nconst BRANCH_PATTERNS: RegExp[] = [\n /git\\s+checkout\\s+-[bB]\\s+([^\\s]+)/,\n /git\\s+switch\\s+-[cC]\\s+([^\\s]+)/,\n /git\\s+branch\\s+(?!-[dDmMrRla])([^\\s-][^\\s]*)/,\n];\n\nfunction extractBranchName(command: string): string | null {\n for (const pattern of BRANCH_PATTERNS) {\n const m = pattern.exec(command);\n if (m) return m[1];\n }\n return null;\n}\n\nfunction truncate(s: string): string {\n const MAX = 120;\n return s.length <= MAX ? s : s.slice(0, MAX) + '…';\n}\n\nexport default branchCreationGuard;\n"]}
@@ -3,13 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const child_process_1 = require("child_process");
4
4
  const types_1 = require("../types");
5
5
  const FIX_HINT = [
6
- 'Do not merge or rebase from main directly on a feature branch.',
7
- 'This breaks the 3-point fork-point system (A=fork point, B=feature HEAD, C=main HEAD).',
8
- '',
9
- 'Use the squash-update process instead:',
10
- ' ./scripts/git-updateFromMain.sh',
11
- '',
12
- 'See docs/git-workflow.md for the full process.',
6
+ "Run './scripts/git-updateFromMain.sh' to squash-update from main. This preserves the 3-point fork-point system (fork-point=A, feature-HEAD=B, main-HEAD=C) needed for clean PR diffs. See docs/git-workflow.md for details.",
13
7
  ];
14
8
  const WRONG_UPDATE_PATTERNS = [
15
9
  /git\s+merge\s+(origin\/main|main)\b/,
@@ -27,6 +21,10 @@ const noDirectMainUpdate = {
27
21
  const matched = WRONG_UPDATE_PATTERNS.some((p) => p.test(ctx.command));
28
22
  if (!matched)
29
23
  return [];
24
+ // Allow 'git checkout main && git pull origin main' — switching to main first is the recommended workflow
25
+ if (/git\s+(?:checkout|switch)\s+main\b/.test(ctx.command)) {
26
+ return [];
27
+ }
30
28
  const currentBranch = (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD', {
31
29
  cwd: ctx.workspaceRoot,
32
30
  encoding: 'utf8',
@@ -1 +1 @@
1
- {"version":3,"file":"no-direct-main-update.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/no-direct-main-update.ts"],"names":[],"mappings":";;AAAA,iDAAyC;AAEzC,oCAA0C;AAE1C,MAAM,QAAQ,GAAsB;IAChC,gEAAgE;IAChE,wFAAwF;IACxF,EAAE;IACF,wCAAwC;IACxC,mCAAmC;IACnC,EAAE;IACF,gDAAgD;CACnD,CAAC;AAEF,MAAM,qBAAqB,GAAa;IACpC,qCAAqC;IACrC,sCAAsC;IACtC,8BAA8B;CACjC,CAAC;AAEF,MAAM,kBAAkB,GAAa;IACjC,IAAI,EAAE,uBAAuB;IAC7B,WAAW,EAAE,0GAA0G;IACvH,KAAK,EAAE,MAAM;IACb,KAAK,EAAE,EAAE;IACT,cAAc,EAAE,EAAE;IAClB,OAAO,EAAE,QAAQ;IAEjB,KAAK,CAAC,GAAgB;QAClB,MAAM,OAAO,GAAG,qBAAqB,CAAC,IAAI,CAAC,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC;QAC/E,IAAI,CAAC,OAAO;YAAE,OAAO,EAAE,CAAC;QAExB,MAAM,aAAa,GAAG,IAAA,wBAAQ,EAAC,iCAAiC,EAAE;YAC9D,GAAG,EAAE,GAAG,CAAC,aAAa;YACtB,QAAQ,EAAE,MAAM;SACnB,CAAC,CAAC,IAAI,EAAE,CAAC;QAEV,IAAI,aAAa,KAAK,MAAM,EAAE,CAAC;YAC3B,OAAO,EAAE,CAAC;QACd,CAAC;QAED,OAAO,CAAC,IAAI,iBAAC,CACT,CAAC,EACD,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,EACrB;gBACI,4CAA4C,aAAa,eAAe;gBACxE,4CAA4C;gBAC5C,wCAAwC;gBACxC,mCAAmC;gBACnC,uCAAuC;aAC1C,CAAC,IAAI,CAAC,IAAI,CAAC,CACf,CAAC,CAAC;IACP,CAAC;CACJ,CAAC;AAEF,SAAS,QAAQ,CAAC,CAAS;IACvB,MAAM,GAAG,GAAG,GAAG,CAAC;IAChB,OAAO,CAAC,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,GAAG,CAAC;AACvD,CAAC;AAED,kBAAe,kBAAkB,CAAC","sourcesContent":["import { execSync } from 'child_process';\nimport type { BashRule, BashContext, Violation } from '../types';\nimport { Violation as V } from '../types';\n\nconst FIX_HINT: readonly string[] = [\n 'Do not merge or rebase from main directly on a feature branch.',\n 'This breaks the 3-point fork-point system (A=fork point, B=feature HEAD, C=main HEAD).',\n '',\n 'Use the squash-update process instead:',\n ' ./scripts/git-updateFromMain.sh',\n '',\n 'See docs/git-workflow.md for the full process.',\n];\n\nconst WRONG_UPDATE_PATTERNS: RegExp[] = [\n /git\\s+merge\\s+(origin\\/main|main)\\b/,\n /git\\s+rebase\\s+(origin\\/main|main)\\b/,\n /git\\s+pull\\s+origin\\s+main\\b/,\n];\n\nconst noDirectMainUpdate: BashRule = {\n name: 'no-direct-main-update',\n description: 'Block direct git merge/rebase/pull from main on feature branches. Use the squash-update process instead.',\n scope: 'bash',\n files: [],\n defaultOptions: {},\n fixHint: FIX_HINT,\n\n check(ctx: BashContext): readonly Violation[] {\n const matched = WRONG_UPDATE_PATTERNS.some((p: RegExp) => p.test(ctx.command));\n if (!matched) return [];\n\n const currentBranch = execSync('git rev-parse --abbrev-ref HEAD', {\n cwd: ctx.workspaceRoot,\n encoding: 'utf8',\n }).trim();\n\n if (currentBranch === 'main') {\n return [];\n }\n\n return [new V(\n 1,\n truncate(ctx.command),\n [\n `Direct merge/rebase from main on branch '${currentBranch}' is blocked.`,\n 'This breaks the 3-point fork-point system.',\n 'Use the squash-update process instead:',\n ' ./scripts/git-updateFromMain.sh',\n 'See docs/git-workflow.md for details.',\n ].join('\\n'),\n )];\n },\n};\n\nfunction truncate(s: string): string {\n const MAX = 120;\n return s.length <= MAX ? s : s.slice(0, MAX) + '…';\n}\n\nexport default noDirectMainUpdate;\n"]}
1
+ {"version":3,"file":"no-direct-main-update.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/no-direct-main-update.ts"],"names":[],"mappings":";;AAAA,iDAAyC;AAEzC,oCAA0C;AAE1C,MAAM,QAAQ,GAAsB;IAChC,6NAA6N;CAChO,CAAC;AAEF,MAAM,qBAAqB,GAAa;IACpC,qCAAqC;IACrC,sCAAsC;IACtC,8BAA8B;CACjC,CAAC;AAEF,MAAM,kBAAkB,GAAa;IACjC,IAAI,EAAE,uBAAuB;IAC7B,WAAW,EAAE,0GAA0G;IACvH,KAAK,EAAE,MAAM;IACb,KAAK,EAAE,EAAE;IACT,cAAc,EAAE,EAAE;IAClB,OAAO,EAAE,QAAQ;IAEjB,KAAK,CAAC,GAAgB;QAClB,MAAM,OAAO,GAAG,qBAAqB,CAAC,IAAI,CAAC,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC;QAC/E,IAAI,CAAC,OAAO;YAAE,OAAO,EAAE,CAAC;QAExB,0GAA0G;QAC1G,IAAI,oCAAoC,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;YACzD,OAAO,EAAE,CAAC;QACd,CAAC;QAED,MAAM,aAAa,GAAG,IAAA,wBAAQ,EAAC,iCAAiC,EAAE;YAC9D,GAAG,EAAE,GAAG,CAAC,aAAa;YACtB,QAAQ,EAAE,MAAM;SACnB,CAAC,CAAC,IAAI,EAAE,CAAC;QAEV,IAAI,aAAa,KAAK,MAAM,EAAE,CAAC;YAC3B,OAAO,EAAE,CAAC;QACd,CAAC;QAED,OAAO,CAAC,IAAI,iBAAC,CACT,CAAC,EACD,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,EACrB;gBACI,4CAA4C,aAAa,eAAe;gBACxE,4CAA4C;gBAC5C,wCAAwC;gBACxC,mCAAmC;gBACnC,uCAAuC;aAC1C,CAAC,IAAI,CAAC,IAAI,CAAC,CACf,CAAC,CAAC;IACP,CAAC;CACJ,CAAC;AAEF,SAAS,QAAQ,CAAC,CAAS;IACvB,MAAM,GAAG,GAAG,GAAG,CAAC;IAChB,OAAO,CAAC,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,GAAG,CAAC;AACvD,CAAC;AAED,kBAAe,kBAAkB,CAAC","sourcesContent":["import { execSync } from 'child_process';\nimport type { BashRule, BashContext, Violation } from '../types';\nimport { Violation as V } from '../types';\n\nconst FIX_HINT: readonly string[] = [\n \"Run './scripts/git-updateFromMain.sh' to squash-update from main. This preserves the 3-point fork-point system (fork-point=A, feature-HEAD=B, main-HEAD=C) needed for clean PR diffs. See docs/git-workflow.md for details.\",\n];\n\nconst WRONG_UPDATE_PATTERNS: RegExp[] = [\n /git\\s+merge\\s+(origin\\/main|main)\\b/,\n /git\\s+rebase\\s+(origin\\/main|main)\\b/,\n /git\\s+pull\\s+origin\\s+main\\b/,\n];\n\nconst noDirectMainUpdate: BashRule = {\n name: 'no-direct-main-update',\n description: 'Block direct git merge/rebase/pull from main on feature branches. Use the squash-update process instead.',\n scope: 'bash',\n files: [],\n defaultOptions: {},\n fixHint: FIX_HINT,\n\n check(ctx: BashContext): readonly Violation[] {\n const matched = WRONG_UPDATE_PATTERNS.some((p: RegExp) => p.test(ctx.command));\n if (!matched) return [];\n\n // Allow 'git checkout main && git pull origin main' — switching to main first is the recommended workflow\n if (/git\\s+(?:checkout|switch)\\s+main\\b/.test(ctx.command)) {\n return [];\n }\n\n const currentBranch = execSync('git rev-parse --abbrev-ref HEAD', {\n cwd: ctx.workspaceRoot,\n encoding: 'utf8',\n }).trim();\n\n if (currentBranch === 'main') {\n return [];\n }\n\n return [new V(\n 1,\n truncate(ctx.command),\n [\n `Direct merge/rebase from main on branch '${currentBranch}' is blocked.`,\n 'This breaks the 3-point fork-point system.',\n 'Use the squash-update process instead:',\n ' ./scripts/git-updateFromMain.sh',\n 'See docs/git-workflow.md for details.',\n ].join('\\n'),\n )];\n },\n};\n\nfunction truncate(s: string): string {\n const MAX = 120;\n return s.length <= MAX ? s : s.slice(0, MAX) + '…';\n}\n\nexport default noDirectMainUpdate;\n"]}
@@ -2,11 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const types_1 = require("../types");
4
4
  const FIX_HINT = [
5
- 'Shell substitutions trigger Claude Code "simple_expansion" permission prompts that interrupt the user.',
6
- 'Instead:',
7
- ' Build payload files with Write, then: node script.js < /path/to/payload',
8
- ' • Use Read, Grep, or Glob instead of piping shell output through $(...)',
9
- ' • Write a small script file with Write and execute it: bash /path/to/script.sh',
5
+ 'Build a payload file with the Write tool and pass it as stdin: node script.js < /path/to/payload.json',
6
+ 'Use the Read, Grep, or Glob tools directly instead of capturing output with $(...)',
7
+ 'Write a small script file with the Write tool and execute it directly: bash /path/to/script.sh',
10
8
  ];
11
9
  const noShellSubstitutionRule = {
12
10
  name: 'no-shell-substitution',
@@ -1 +1 @@
1
- {"version":3,"file":"no-shell-substitution.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/no-shell-substitution.ts"],"names":[],"mappings":";;AACA,oCAA0C;AAE1C,MAAM,QAAQ,GAAsB;IAChC,wGAAwG;IACxG,UAAU;IACV,6EAA6E;IAC7E,2EAA2E;IAC3E,kFAAkF;CACrF,CAAC;AAEF,MAAM,uBAAuB,GAAa;IACtC,IAAI,EAAE,uBAAuB;IAC7B,WAAW,EAAE,gFAAgF;IAC7F,KAAK,EAAE,MAAM;IACb,KAAK,EAAE,EAAE;IACT,cAAc,EAAE,EAAE;IAClB,OAAO,EAAE,QAAQ;IAEjB,KAAK,CAAC,GAAgB;QAClB,MAAM,OAAO,GAAG,iBAAiB,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAC/C,MAAM,UAAU,GAAgB,EAAE,CAAC;QAEnC,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;YACvB,UAAU,CAAC,IAAI,CAAC,IAAI,iBAAC,CACjB,CAAC,EACD,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,EACrB,iDAAiD,CACpD,CAAC,CAAC;QACP,CAAC;QACD,IAAI,oBAAoB,CAAC,OAAO,CAAC,EAAE,CAAC;YAChC,UAAU,CAAC,IAAI,CAAC,IAAI,iBAAC,CACjB,CAAC,EACD,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,EACrB,iDAAiD,CACpD,CAAC,CAAC;QACP,CAAC;QACD,IAAI,8BAA8B,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,mBAAmB,CAAC,OAAO,CAAC,EAAE,CAAC;YAC/E,UAAU,CAAC,IAAI,CAAC,IAAI,iBAAC,CACjB,CAAC,EACD,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,EACrB,yDAAyD,CAC5D,CAAC,CAAC;QACP,CAAC;QACD,OAAO,UAAU,CAAC;IACtB,CAAC;CACJ,CAAC;AAEF,SAAS,iBAAiB,CAAC,GAAW;IAClC,OAAO,GAAG,CAAC,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;AACzC,CAAC;AAED,SAAS,oBAAoB,CAAC,GAAW;IACrC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QACrC,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;IACxE,CAAC;IACD,OAAO,KAAK,CAAC;AACjB,CAAC;AAED,SAAS,mBAAmB,CAAC,GAAW;IACpC,MAAM,EAAE,GAAG,sCAAsC,CAAC;IAClD,OAAO,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACxB,CAAC;AAED,SAAS,QAAQ,CAAC,CAAS;IACvB,MAAM,GAAG,GAAG,GAAG,CAAC;IAChB,IAAI,CAAC,CAAC,MAAM,IAAI,GAAG;QAAE,OAAO,CAAC,CAAC;IAC9B,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,GAAG,CAAC;AACjC,CAAC;AAED,kBAAe,uBAAuB,CAAC","sourcesContent":["import type { BashRule, BashContext, Violation } from '../types';\nimport { Violation as V } from '../types';\n\nconst FIX_HINT: readonly string[] = [\n 'Shell substitutions trigger Claude Code \"simple_expansion\" permission prompts that interrupt the user.',\n 'Instead:',\n ' • Build payload files with Write, then: node script.js < /path/to/payload',\n 'Use Read, Grep, or Glob instead of piping shell output through $(...)',\n 'Write a small script file with Write and execute it: bash /path/to/script.sh',\n];\n\nconst noShellSubstitutionRule: BashRule = {\n name: 'no-shell-substitution',\n description: 'Reject Bash commands containing shell substitutions ($(...), backticks, $VAR).',\n scope: 'bash',\n files: [],\n defaultOptions: {},\n fixHint: FIX_HINT,\n\n check(ctx: BashContext): readonly Violation[] {\n const scanned = stripSingleQuoted(ctx.command);\n const violations: Violation[] = [];\n\n if (/\\$\\(/.test(scanned)) {\n violations.push(new V(\n 1,\n truncate(ctx.command),\n 'Command contains `$(...)` command substitution.',\n ));\n }\n if (hasUnescapedBacktick(scanned)) {\n violations.push(new V(\n 1,\n truncate(ctx.command),\n 'Command contains backtick command substitution.',\n ));\n }\n if (/\\$\\{[A-Za-z_][A-Za-z0-9_]*\\}/.test(scanned) || hasBareVarExpansion(scanned)) {\n violations.push(new V(\n 1,\n truncate(ctx.command),\n 'Command contains `$VAR` or `${VAR}` variable expansion.',\n ));\n }\n return violations;\n },\n};\n\nfunction stripSingleQuoted(cmd: string): string {\n return cmd.replace(/'[^']*'/g, \"''\");\n}\n\nfunction hasUnescapedBacktick(cmd: string): boolean {\n for (let i = 0; i < cmd.length; i += 1) {\n if (cmd[i] === '`' && (i === 0 || cmd[i - 1] !== '\\\\')) return true;\n }\n return false;\n}\n\nfunction hasBareVarExpansion(cmd: string): boolean {\n const re = /(^|[^\\\\])\\$([A-Za-z_][A-Za-z0-9_]*)/g;\n return re.test(cmd);\n}\n\nfunction truncate(s: string): string {\n const MAX = 120;\n if (s.length <= MAX) return s;\n return s.slice(0, MAX) + '…';\n}\n\nexport default noShellSubstitutionRule;\n"]}
1
+ {"version":3,"file":"no-shell-substitution.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/no-shell-substitution.ts"],"names":[],"mappings":";;AACA,oCAA0C;AAE1C,MAAM,QAAQ,GAAsB;IAChC,uGAAuG;IACvG,oFAAoF;IACpF,gGAAgG;CACnG,CAAC;AAEF,MAAM,uBAAuB,GAAa;IACtC,IAAI,EAAE,uBAAuB;IAC7B,WAAW,EAAE,gFAAgF;IAC7F,KAAK,EAAE,MAAM;IACb,KAAK,EAAE,EAAE;IACT,cAAc,EAAE,EAAE;IAClB,OAAO,EAAE,QAAQ;IAEjB,KAAK,CAAC,GAAgB;QAClB,MAAM,OAAO,GAAG,iBAAiB,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAC/C,MAAM,UAAU,GAAgB,EAAE,CAAC;QAEnC,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;YACvB,UAAU,CAAC,IAAI,CAAC,IAAI,iBAAC,CACjB,CAAC,EACD,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,EACrB,iDAAiD,CACpD,CAAC,CAAC;QACP,CAAC;QACD,IAAI,oBAAoB,CAAC,OAAO,CAAC,EAAE,CAAC;YAChC,UAAU,CAAC,IAAI,CAAC,IAAI,iBAAC,CACjB,CAAC,EACD,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,EACrB,iDAAiD,CACpD,CAAC,CAAC;QACP,CAAC;QACD,IAAI,8BAA8B,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,mBAAmB,CAAC,OAAO,CAAC,EAAE,CAAC;YAC/E,UAAU,CAAC,IAAI,CAAC,IAAI,iBAAC,CACjB,CAAC,EACD,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,EACrB,yDAAyD,CAC5D,CAAC,CAAC;QACP,CAAC;QACD,OAAO,UAAU,CAAC;IACtB,CAAC;CACJ,CAAC;AAEF,SAAS,iBAAiB,CAAC,GAAW;IAClC,OAAO,GAAG,CAAC,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;AACzC,CAAC;AAED,SAAS,oBAAoB,CAAC,GAAW;IACrC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QACrC,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;IACxE,CAAC;IACD,OAAO,KAAK,CAAC;AACjB,CAAC;AAED,SAAS,mBAAmB,CAAC,GAAW;IACpC,MAAM,EAAE,GAAG,sCAAsC,CAAC;IAClD,OAAO,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACxB,CAAC;AAED,SAAS,QAAQ,CAAC,CAAS;IACvB,MAAM,GAAG,GAAG,GAAG,CAAC;IAChB,IAAI,CAAC,CAAC,MAAM,IAAI,GAAG;QAAE,OAAO,CAAC,CAAC;IAC9B,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,GAAG,CAAC;AACjC,CAAC;AAED,kBAAe,uBAAuB,CAAC","sourcesContent":["import type { BashRule, BashContext, Violation } from '../types';\nimport { Violation as V } from '../types';\n\nconst FIX_HINT: readonly string[] = [\n 'Build a payload file with the Write tool and pass it as stdin: node script.js < /path/to/payload.json',\n 'Use the Read, Grep, or Glob tools directly instead of capturing output with $(...)',\n 'Write a small script file with the Write tool and execute it directly: bash /path/to/script.sh',\n];\n\nconst noShellSubstitutionRule: BashRule = {\n name: 'no-shell-substitution',\n description: 'Reject Bash commands containing shell substitutions ($(...), backticks, $VAR).',\n scope: 'bash',\n files: [],\n defaultOptions: {},\n fixHint: FIX_HINT,\n\n check(ctx: BashContext): readonly Violation[] {\n const scanned = stripSingleQuoted(ctx.command);\n const violations: Violation[] = [];\n\n if (/\\$\\(/.test(scanned)) {\n violations.push(new V(\n 1,\n truncate(ctx.command),\n 'Command contains `$(...)` command substitution.',\n ));\n }\n if (hasUnescapedBacktick(scanned)) {\n violations.push(new V(\n 1,\n truncate(ctx.command),\n 'Command contains backtick command substitution.',\n ));\n }\n if (/\\$\\{[A-Za-z_][A-Za-z0-9_]*\\}/.test(scanned) || hasBareVarExpansion(scanned)) {\n violations.push(new V(\n 1,\n truncate(ctx.command),\n 'Command contains `$VAR` or `${VAR}` variable expansion.',\n ));\n }\n return violations;\n },\n};\n\nfunction stripSingleQuoted(cmd: string): string {\n return cmd.replace(/'[^']*'/g, \"''\");\n}\n\nfunction hasUnescapedBacktick(cmd: string): boolean {\n for (let i = 0; i < cmd.length; i += 1) {\n if (cmd[i] === '`' && (i === 0 || cmd[i - 1] !== '\\\\')) return true;\n }\n return false;\n}\n\nfunction hasBareVarExpansion(cmd: string): boolean {\n const re = /(^|[^\\\\])\\$([A-Za-z_][A-Za-z0-9_]*)/g;\n return re.test(cmd);\n}\n\nfunction truncate(s: string): string {\n const MAX = 120;\n if (s.length <= MAX) return s;\n return s.slice(0, MAX) + '…';\n}\n\nexport default noShellSubstitutionRule;\n"]}
@@ -62,7 +62,7 @@ function runBashInternal(command, cwd) {
62
62
  return new types_1.BlockedResult(report);
63
63
  }
64
64
  function checkConfigSync(rules, config) {
65
- const unconfigured = rules.filter((r) => !config.rules.has(r.name)).map((r) => r.name);
65
+ const unconfigured = rules.filter((r) => !config.userConfiguredRuleNames.has(r.name)).map((r) => r.name);
66
66
  if (unconfigured.length === 0)
67
67
  return null;
68
68
  const lines = [
@@ -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';\n\nimport { buildContexts, buildBashContext } from './build-context';\nimport { loadConfig } from './load-config';\nimport { loadRules, globMatches } from './load-rules';\nimport { toError } from './to-error';\nimport { formatReport } from './report';\nimport {\n ToolKind, NormalizedToolInput, BlockedResult,\n Rule, EditRule, FileRule, BashRule, Violation, RuleGroup,\n EditContext, FileContext, BashContext,\n ResolvedConfig, ResolvedRuleConfig, RuleOptions,\n} from './types';\n\nexport function run(\n toolKind: ToolKind,\n input: NormalizedToolInput,\n cwd: string,\n): BlockedResult | null {\n return runInternal(toolKind, input, cwd);\n}\n\nfunction runInternal(\n toolKind: ToolKind,\n input: NormalizedToolInput,\n cwd: string,\n): BlockedResult | null {\n const config = loadConfig(cwd);\n if (!config.configPath) {\n return new BlockedResult(\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 );\n }\n\n const workspaceRoot = path.dirname(config.configPath);\n const rules = loadRules(config, workspaceRoot);\n if (rules.length === 0) return null;\n\n const outOfSync = checkConfigSync(rules, config);\n if (outOfSync) return outOfSync;\n\n const contexts = buildContexts(toolKind, input, workspaceRoot);\n const relativePath = path.relative(workspaceRoot, input.filePath);\n\n const editGroups = runEditRules(rules, contexts.editContexts, config);\n const fileGroups = runFileRules(rules, contexts.fileContext, config);\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): BlockedResult | null {\n return runBashInternal(command, cwd);\n}\n\nfunction runBashInternal(command: string, cwd: string): BlockedResult | null {\n const config = loadConfig(cwd);\n if (!config.configPath) {\n return new BlockedResult(\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 );\n }\n\n const workspaceRoot = path.dirname(config.configPath);\n const rules = loadRules(config, workspaceRoot);\n if (rules.length === 0) return null;\n\n const outOfSync = checkConfigSync(rules, config);\n if (outOfSync) return outOfSync;\n\n const ctx = buildBashContext(command, workspaceRoot);\n const groups = runBashRules(rules, ctx, config);\n if (groups.length === 0) return null;\n\n const report = formatReport('<bash>', groups);\n return new BlockedResult(report);\n}\n\nfunction checkConfigSync(rules: readonly Rule[], config: ResolvedConfig): BlockedResult | null {\n const unconfigured = rules.filter((r: Rule) => !config.rules.has(r.name)).map((r: Rule) => r.name);\n if (unconfigured.length === 0) return null;\n\n const lines = [\n 'webpieces.config.json is out of sync — new rules have been added that are not yet configured.',\n '',\n `Unconfigured rules: ${unconfigured.join(', ')}`,\n '',\n 'Ask the human: for each rule above, do you want it ON or OFF (and any specific options)?',\n 'Then run `./node_modules/.bin/wp-setup-ai-hooks --sync` to add missing rules (defaults to OFF),',\n 'or edit webpieces.config.json manually to add an entry for each rule.',\n 'Do not proceed until webpieces.config.json is updated.',\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 as EditRule | FileRule | BashRule).check(ctx as never);\n } catch (err: unknown) {\n const error = toError(err);\n return [new Violation(0, '', `Rule '${rule.name}' crashed: ${error.message}`)];\n }\n}\n\nfunction runBashRules(\n rules: readonly Rule[],\n bashContext: BashContext,\n config: ResolvedConfig,\n): readonly RuleGroup[] {\n const groups: RuleGroup[] = [];\n for (const rule of rules) {\n if (rule.scope !== 'bash') continue;\n const ruleConfig = config.rules.get(rule.name);\n if (!ruleConfig || ruleConfig.isOff) continue;\n bashContext.options = mergeOptions(rule.defaultOptions, ruleConfig);\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 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 mergeOptions(defaultOptions: RuleOptions, ruleConfig: ResolvedRuleConfig): RuleOptions {\n // webpieces-disable no-any-unknown -- building an options bag from opaque RuleOptions\n const out: Record<string, unknown> = {};\n for (const key of Object.keys(defaultOptions)) out[key] = defaultOptions[key];\n for (const key of Object.keys(ruleConfig.options)) {\n // 'mode' is the framework-level on/off switch, not a rule option.\n if (key === 'mode') continue;\n out[key] = ruleConfig.options[key];\n }\n return out;\n}\n\nfunction runEditRules(\n rules: readonly Rule[],\n editContexts: readonly EditContext[],\n config: ResolvedConfig,\n): readonly RuleGroup[] {\n const groups: RuleGroup[] = [];\n for (const rule of rules) {\n if (rule.scope !== 'edit') continue;\n const ruleConfig = config.rules.get(rule.name);\n if (!ruleConfig || ruleConfig.isOff) continue;\n const allViolations: Violation[] = [];\n for (const ctx of editContexts) {\n if (!ruleMatchesFile(rule, ctx.relativePath)) continue;\n ctx.options = mergeOptions(rule.defaultOptions, ruleConfig);\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(\n rules: readonly Rule[],\n fileContext: FileContext,\n config: ResolvedConfig,\n): readonly RuleGroup[] {\n const groups: RuleGroup[] = [];\n for (const rule of rules) {\n if (rule.scope !== 'file') continue;\n const ruleConfig = config.rules.get(rule.name);\n if (!ruleConfig || ruleConfig.isOff) continue;\n if (!ruleMatchesFile(rule, fileContext.relativePath)) continue;\n fileContext.options = mergeOptions(rule.defaultOptions, ruleConfig);\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';\n\nimport { buildContexts, buildBashContext } from './build-context';\nimport { loadConfig } from './load-config';\nimport { loadRules, globMatches } from './load-rules';\nimport { toError } from './to-error';\nimport { formatReport } from './report';\nimport {\n ToolKind, NormalizedToolInput, BlockedResult,\n Rule, EditRule, FileRule, BashRule, Violation, RuleGroup,\n EditContext, FileContext, BashContext,\n ResolvedConfig, ResolvedRuleConfig, RuleOptions,\n} from './types';\n\nexport function run(\n toolKind: ToolKind,\n input: NormalizedToolInput,\n cwd: string,\n): BlockedResult | null {\n return runInternal(toolKind, input, cwd);\n}\n\nfunction runInternal(\n toolKind: ToolKind,\n input: NormalizedToolInput,\n cwd: string,\n): BlockedResult | null {\n const config = loadConfig(cwd);\n if (!config.configPath) {\n return new BlockedResult(\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 );\n }\n\n const workspaceRoot = path.dirname(config.configPath);\n const rules = loadRules(config, workspaceRoot);\n if (rules.length === 0) return null;\n\n const outOfSync = checkConfigSync(rules, config);\n if (outOfSync) return outOfSync;\n\n const contexts = buildContexts(toolKind, input, workspaceRoot);\n const relativePath = path.relative(workspaceRoot, input.filePath);\n\n const editGroups = runEditRules(rules, contexts.editContexts, config);\n const fileGroups = runFileRules(rules, contexts.fileContext, config);\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): BlockedResult | null {\n return runBashInternal(command, cwd);\n}\n\nfunction runBashInternal(command: string, cwd: string): BlockedResult | null {\n const config = loadConfig(cwd);\n if (!config.configPath) {\n return new BlockedResult(\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 );\n }\n\n const workspaceRoot = path.dirname(config.configPath);\n const rules = loadRules(config, workspaceRoot);\n if (rules.length === 0) return null;\n\n const outOfSync = checkConfigSync(rules, config);\n if (outOfSync) return outOfSync;\n\n const ctx = buildBashContext(command, workspaceRoot);\n const groups = runBashRules(rules, ctx, config);\n if (groups.length === 0) return null;\n\n const report = formatReport('<bash>', groups);\n return new BlockedResult(report);\n}\n\nfunction checkConfigSync(rules: readonly Rule[], config: ResolvedConfig): BlockedResult | null {\n const unconfigured = rules.filter((r: Rule) => !config.userConfiguredRuleNames.has(r.name)).map((r: Rule) => r.name);\n if (unconfigured.length === 0) return null;\n\n const lines = [\n 'webpieces.config.json is out of sync — new rules have been added that are not yet configured.',\n '',\n `Unconfigured rules: ${unconfigured.join(', ')}`,\n '',\n 'Ask the human: for each rule above, do you want it ON or OFF (and any specific options)?',\n 'Then run `./node_modules/.bin/wp-setup-ai-hooks --sync` to add missing rules (defaults to OFF),',\n 'or edit webpieces.config.json manually to add an entry for each rule.',\n 'Do not proceed until webpieces.config.json is updated.',\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 as EditRule | FileRule | BashRule).check(ctx as never);\n } catch (err: unknown) {\n const error = toError(err);\n return [new Violation(0, '', `Rule '${rule.name}' crashed: ${error.message}`)];\n }\n}\n\nfunction runBashRules(\n rules: readonly Rule[],\n bashContext: BashContext,\n config: ResolvedConfig,\n): readonly RuleGroup[] {\n const groups: RuleGroup[] = [];\n for (const rule of rules) {\n if (rule.scope !== 'bash') continue;\n const ruleConfig = config.rules.get(rule.name);\n if (!ruleConfig || ruleConfig.isOff) continue;\n bashContext.options = mergeOptions(rule.defaultOptions, ruleConfig);\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 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 mergeOptions(defaultOptions: RuleOptions, ruleConfig: ResolvedRuleConfig): RuleOptions {\n // webpieces-disable no-any-unknown -- building an options bag from opaque RuleOptions\n const out: Record<string, unknown> = {};\n for (const key of Object.keys(defaultOptions)) out[key] = defaultOptions[key];\n for (const key of Object.keys(ruleConfig.options)) {\n // 'mode' is the framework-level on/off switch, not a rule option.\n if (key === 'mode') continue;\n out[key] = ruleConfig.options[key];\n }\n return out;\n}\n\nfunction runEditRules(\n rules: readonly Rule[],\n editContexts: readonly EditContext[],\n config: ResolvedConfig,\n): readonly RuleGroup[] {\n const groups: RuleGroup[] = [];\n for (const rule of rules) {\n if (rule.scope !== 'edit') continue;\n const ruleConfig = config.rules.get(rule.name);\n if (!ruleConfig || ruleConfig.isOff) continue;\n const allViolations: Violation[] = [];\n for (const ctx of editContexts) {\n if (!ruleMatchesFile(rule, ctx.relativePath)) continue;\n ctx.options = mergeOptions(rule.defaultOptions, ruleConfig);\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(\n rules: readonly Rule[],\n fileContext: FileContext,\n config: ResolvedConfig,\n): readonly RuleGroup[] {\n const groups: RuleGroup[] = [];\n for (const rule of rules) {\n if (rule.scope !== 'file') continue;\n const ruleConfig = config.rules.get(rule.name);\n if (!ruleConfig || ruleConfig.isOff) continue;\n if (!ruleMatchesFile(rule, fileContext.relativePath)) continue;\n fileContext.options = mergeOptions(rule.defaultOptions, ruleConfig);\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"]}