@webpieces/ai-hook-rules 0.3.189 → 0.3.190

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.189",
3
+ "version": "0.3.190",
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",
@@ -32,7 +32,7 @@
32
32
  "directory": "packages/tooling/ai-hook-rules"
33
33
  },
34
34
  "dependencies": {
35
- "@webpieces/rules-config": "0.3.189"
35
+ "@webpieces/rules-config": "0.3.190"
36
36
  },
37
37
  "publishConfig": {
38
38
  "access": "public"
package/src/bin/setup.js CHANGED
@@ -7,6 +7,15 @@ const path = tslib_1.__importStar(require("path"));
7
7
  const index_1 = require("../core/rules/index");
8
8
  const HOOK_COMMAND = './node_modules/.bin/wp-ai-hook';
9
9
  const CONFIG_FILENAME = 'webpieces.config.json';
10
+ // ignoreModifiedUntilEpoch is required on every rule (0 = active). A freshly seeded rule is OFF.
11
+ function seedRule() {
12
+ return { mode: 'OFF', ignoreModifiedUntilEpoch: 0 };
13
+ }
14
+ // pr-gate is a required top-level block; seed it OFF (opt-out) with a ready-to-use buildCommand so
15
+ // flipping it ON is a one-line edit.
16
+ function seedPrGate() {
17
+ return { mode: 'OFF', buildCommand: 'pnpm nx affected --target=ci --base=origin/main', gates: [] };
18
+ }
10
19
  function settingsAlreadyHasHook(settingsPath) {
11
20
  if (!fs.existsSync(settingsPath))
12
21
  return false;
@@ -49,9 +58,9 @@ function seedConfig(projectRoot) {
49
58
  }
50
59
  const rules = {};
51
60
  for (const name of index_1.builtInRuleNames) {
52
- rules[name] = { mode: 'OFF' };
61
+ rules[name] = seedRule();
53
62
  }
54
- const config = { rules, rulesDir: [] };
63
+ const config = { rules, rulesDir: [], 'pr-gate': seedPrGate() };
55
64
  fs.writeFileSync(configPath, JSON.stringify(config, null, 4) + '\n');
56
65
  console.log(` [ai-hook-rules] Created ${CONFIG_FILENAME} with all rules set to OFF.`);
57
66
  console.log(' Review and enable the rules you want by changing "mode" to "ON" or "MODIFIED_CODE" etc.');
@@ -67,15 +76,22 @@ function syncConfig(projectRoot) {
67
76
  const added = [];
68
77
  for (const name of index_1.builtInRuleNames) {
69
78
  if (!Object.prototype.hasOwnProperty.call(config.rules, name)) {
70
- config.rules[name] = { mode: 'OFF' };
79
+ config.rules[name] = seedRule();
71
80
  added.push(name);
72
81
  }
73
82
  }
74
- if (added.length === 0) {
83
+ let addedPrGate = false;
84
+ if (!Object.prototype.hasOwnProperty.call(config, 'pr-gate')) {
85
+ config['pr-gate'] = seedPrGate();
86
+ addedPrGate = true;
87
+ }
88
+ if (added.length === 0 && !addedPrGate) {
75
89
  console.log(` [ai-hook-rules] ${CONFIG_FILENAME} is already up to date — no new rules to add.`);
76
90
  return;
77
91
  }
78
92
  fs.writeFileSync(configPath, JSON.stringify(config, null, 4) + '\n');
93
+ if (addedPrGate)
94
+ console.log(` [ai-hook-rules] Added the required "pr-gate" block (mode OFF) to ${CONFIG_FILENAME}.`);
79
95
  console.log(` [ai-hook-rules] Added ${added.length} new rule(s) to ${CONFIG_FILENAME} (set to OFF):`);
80
96
  for (const name of added) {
81
97
  console.log(` - ${name}`);
@@ -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';\n\nimport { builtInRuleNames } from '../core/rules/index';\n\nconst HOOK_COMMAND = './node_modules/.bin/wp-ai-hook';\nconst CONFIG_FILENAME = 'webpieces.config.json';\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 allows arbitrary key access\n [key: string]: unknown;\n}\n\ninterface RulesConfig {\n rules: Record<string, object>;\n rulesDir?: string[];\n}\n\nfunction settingsAlreadyHasHook(settingsPath: string): boolean {\n if (!fs.existsSync(settingsPath)) return false;\n const content = fs.readFileSync(settingsPath, 'utf8');\n return content.includes('wp-ai-hook');\n}\n\nfunction wireSettings(projectRoot: string): void {\n const claudeDir = path.join(projectRoot, '.claude');\n if (!fs.existsSync(claudeDir)) {\n console.log(' [ai-hook-rules] No .claude/ directory found — add the hook manually:');\n console.log(' In .claude/settings.json under hooks.PreToolUse, add:');\n console.log(` { \"matcher\": \"Write|Edit|MultiEdit|Bash\", \"hooks\": [{ \"type\": \"command\", \"command\": \"${HOOK_COMMAND}\" }] }`);\n return;\n }\n\n const settingsPath = path.join(claudeDir, 'settings.json');\n\n if (settingsAlreadyHasHook(settingsPath)) {\n console.log(' [ai-hook-rules] .claude/settings.json already has the hook — skipping.');\n return;\n }\n\n let settings: ClaudeSettings = {};\n if (fs.existsSync(settingsPath)) {\n settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')) as ClaudeSettings;\n }\n\n if (!settings.hooks) settings.hooks = {};\n if (!Array.isArray(settings.hooks.PreToolUse)) settings.hooks.PreToolUse = [];\n\n settings.hooks.PreToolUse.push({\n matcher: 'Write|Edit|MultiEdit|Bash',\n hooks: [{ type: 'command', command: HOOK_COMMAND }],\n });\n\n fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 4) + '\\n');\n console.log(` [ai-hook-rules] Wired ${HOOK_COMMAND} into .claude/settings.json`);\n}\n\nfunction seedConfig(projectRoot: string): void {\n const configPath = path.join(projectRoot, CONFIG_FILENAME);\n if (fs.existsSync(configPath)) {\n console.log(` [ai-hook-rules] ${CONFIG_FILENAME} already exists — run with --sync to add missing rules.`);\n return;\n }\n\n const rules: Record<string, object> = {};\n for (const name of builtInRuleNames) {\n rules[name] = { mode: 'OFF' };\n }\n\n const config: RulesConfig = { rules, rulesDir: [] };\n fs.writeFileSync(configPath, JSON.stringify(config, null, 4) + '\\n');\n console.log(` [ai-hook-rules] Created ${CONFIG_FILENAME} with all rules set to OFF.`);\n console.log(' Review and enable the rules you want by changing \"mode\" to \"ON\" or \"MODIFIED_CODE\" etc.');\n}\n\nfunction syncConfig(projectRoot: string): void {\n const configPath = path.join(projectRoot, CONFIG_FILENAME);\n\n let config: RulesConfig = { rules: {}, rulesDir: [] };\n if (fs.existsSync(configPath)) {\n config = JSON.parse(fs.readFileSync(configPath, 'utf8')) as RulesConfig;\n }\n if (!config.rules) config.rules = {};\n\n const added: string[] = [];\n for (const name of builtInRuleNames) {\n if (!Object.prototype.hasOwnProperty.call(config.rules, name)) {\n config.rules[name] = { mode: 'OFF' };\n added.push(name);\n }\n }\n\n if (added.length === 0) {\n console.log(` [ai-hook-rules] ${CONFIG_FILENAME} is already up to date — no new rules to add.`);\n return;\n }\n\n fs.writeFileSync(configPath, JSON.stringify(config, null, 4) + '\\n');\n console.log(` [ai-hook-rules] Added ${added.length} new rule(s) to ${CONFIG_FILENAME} (set to OFF):`);\n for (const name of added) {\n console.log(` - ${name}`);\n }\n console.log(' Review each new rule and set \"mode\" to ON/MODIFIED_CODE/etc. as desired.');\n}\n\nexport function main(): void {\n const args = process.argv.slice(2);\n const isSync = args.includes('--sync');\n\n const projectRoot = process.cwd();\n\n if (isSync) {\n syncConfig(projectRoot);\n } else {\n seedConfig(projectRoot);\n console.log('');\n console.log(' To install the global Claude Code hook (one-time, per machine):');\n console.log(' ./node_modules/.bin/wp-setup-global-ai-hooks');\n }\n}\n\nif (require.main === module) {\n 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';\n\nimport { builtInRuleNames } from '../core/rules/index';\n\nconst HOOK_COMMAND = './node_modules/.bin/wp-ai-hook';\nconst CONFIG_FILENAME = 'webpieces.config.json';\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 allows arbitrary key access\n [key: string]: unknown;\n}\n\ninterface RulesConfig {\n rules: Record<string, object>;\n rulesDir?: string[];\n 'pr-gate'?: object;\n}\n\n// ignoreModifiedUntilEpoch is required on every rule (0 = active). A freshly seeded rule is OFF.\nfunction seedRule(): object {\n return { mode: 'OFF', ignoreModifiedUntilEpoch: 0 };\n}\n\n// pr-gate is a required top-level block; seed it OFF (opt-out) with a ready-to-use buildCommand so\n// flipping it ON is a one-line edit.\nfunction seedPrGate(): object {\n return { mode: 'OFF', buildCommand: 'pnpm nx affected --target=ci --base=origin/main', gates: [] };\n}\n\nfunction settingsAlreadyHasHook(settingsPath: string): boolean {\n if (!fs.existsSync(settingsPath)) return false;\n const content = fs.readFileSync(settingsPath, 'utf8');\n return content.includes('wp-ai-hook');\n}\n\nfunction wireSettings(projectRoot: string): void {\n const claudeDir = path.join(projectRoot, '.claude');\n if (!fs.existsSync(claudeDir)) {\n console.log(' [ai-hook-rules] No .claude/ directory found — add the hook manually:');\n console.log(' In .claude/settings.json under hooks.PreToolUse, add:');\n console.log(` { \"matcher\": \"Write|Edit|MultiEdit|Bash\", \"hooks\": [{ \"type\": \"command\", \"command\": \"${HOOK_COMMAND}\" }] }`);\n return;\n }\n\n const settingsPath = path.join(claudeDir, 'settings.json');\n\n if (settingsAlreadyHasHook(settingsPath)) {\n console.log(' [ai-hook-rules] .claude/settings.json already has the hook — skipping.');\n return;\n }\n\n let settings: ClaudeSettings = {};\n if (fs.existsSync(settingsPath)) {\n settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')) as ClaudeSettings;\n }\n\n if (!settings.hooks) settings.hooks = {};\n if (!Array.isArray(settings.hooks.PreToolUse)) settings.hooks.PreToolUse = [];\n\n settings.hooks.PreToolUse.push({\n matcher: 'Write|Edit|MultiEdit|Bash',\n hooks: [{ type: 'command', command: HOOK_COMMAND }],\n });\n\n fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 4) + '\\n');\n console.log(` [ai-hook-rules] Wired ${HOOK_COMMAND} into .claude/settings.json`);\n}\n\nfunction seedConfig(projectRoot: string): void {\n const configPath = path.join(projectRoot, CONFIG_FILENAME);\n if (fs.existsSync(configPath)) {\n console.log(` [ai-hook-rules] ${CONFIG_FILENAME} already exists — run with --sync to add missing rules.`);\n return;\n }\n\n const rules: Record<string, object> = {};\n for (const name of builtInRuleNames) {\n rules[name] = seedRule();\n }\n\n const config: RulesConfig = { rules, rulesDir: [], 'pr-gate': seedPrGate() };\n fs.writeFileSync(configPath, JSON.stringify(config, null, 4) + '\\n');\n console.log(` [ai-hook-rules] Created ${CONFIG_FILENAME} with all rules set to OFF.`);\n console.log(' Review and enable the rules you want by changing \"mode\" to \"ON\" or \"MODIFIED_CODE\" etc.');\n}\n\nfunction syncConfig(projectRoot: string): void {\n const configPath = path.join(projectRoot, CONFIG_FILENAME);\n\n let config: RulesConfig = { rules: {}, rulesDir: [] };\n if (fs.existsSync(configPath)) {\n config = JSON.parse(fs.readFileSync(configPath, 'utf8')) as RulesConfig;\n }\n if (!config.rules) config.rules = {};\n\n const added: string[] = [];\n for (const name of builtInRuleNames) {\n if (!Object.prototype.hasOwnProperty.call(config.rules, name)) {\n config.rules[name] = seedRule();\n added.push(name);\n }\n }\n\n let addedPrGate = false;\n if (!Object.prototype.hasOwnProperty.call(config, 'pr-gate')) {\n config['pr-gate'] = seedPrGate();\n addedPrGate = true;\n }\n\n if (added.length === 0 && !addedPrGate) {\n console.log(` [ai-hook-rules] ${CONFIG_FILENAME} is already up to date — no new rules to add.`);\n return;\n }\n\n fs.writeFileSync(configPath, JSON.stringify(config, null, 4) + '\\n');\n if (addedPrGate) console.log(` [ai-hook-rules] Added the required \"pr-gate\" block (mode OFF) to ${CONFIG_FILENAME}.`);\n console.log(` [ai-hook-rules] Added ${added.length} new rule(s) to ${CONFIG_FILENAME} (set to OFF):`);\n for (const name of added) {\n console.log(` - ${name}`);\n }\n console.log(' Review each new rule and set \"mode\" to ON/MODIFIED_CODE/etc. as desired.');\n}\n\nexport function main(): void {\n const args = process.argv.slice(2);\n const isSync = args.includes('--sync');\n\n const projectRoot = process.cwd();\n\n if (isSync) {\n syncConfig(projectRoot);\n } else {\n seedConfig(projectRoot);\n console.log('');\n console.log(' To install the global Claude Code hook (one-time, per machine):');\n console.log(' ./node_modules/.bin/wp-setup-global-ai-hooks');\n }\n}\n\nif (require.main === module) {\n main();\n}\n"]}
@@ -1 +1 @@
1
- export { loadConfig, findConfigFile, CONFIG_FILENAME } from '@webpieces/rules-config';
1
+ export { findConfigFile, CONFIG_FILENAME } from '@webpieces/rules-config';
@@ -1,10 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CONFIG_FILENAME = exports.findConfigFile = exports.loadConfig = void 0;
3
+ exports.CONFIG_FILENAME = exports.findConfigFile = void 0;
4
4
  // Re-export the shared workspace config loader so ai-hooks and the Nx
5
5
  // validate-code executor use the same webpieces.config.json.
6
6
  var rules_config_1 = require("@webpieces/rules-config");
7
- Object.defineProperty(exports, "loadConfig", { enumerable: true, get: function () { return rules_config_1.loadConfig; } });
8
7
  Object.defineProperty(exports, "findConfigFile", { enumerable: true, get: function () { return rules_config_1.findConfigFile; } });
9
8
  Object.defineProperty(exports, "CONFIG_FILENAME", { enumerable: true, get: function () { return rules_config_1.CONFIG_FILENAME; } });
10
9
  //# sourceMappingURL=load-config.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"load-config.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/load-config.ts"],"names":[],"mappings":";;;AAAA,sEAAsE;AACtE,6DAA6D;AAC7D,wDAAsF;AAA7E,0GAAA,UAAU,OAAA;AAAE,8GAAA,cAAc,OAAA;AAAE,+GAAA,eAAe,OAAA","sourcesContent":["// Re-export the shared workspace config loader so ai-hooks and the Nx\n// validate-code executor use the same webpieces.config.json.\nexport { loadConfig, findConfigFile, CONFIG_FILENAME } from '@webpieces/rules-config';\n"]}
1
+ {"version":3,"file":"load-config.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/load-config.ts"],"names":[],"mappings":";;;AAAA,sEAAsE;AACtE,6DAA6D;AAC7D,wDAA0E;AAAjE,8GAAA,cAAc,OAAA;AAAE,+GAAA,eAAe,OAAA","sourcesContent":["// Re-export the shared workspace config loader so ai-hooks and the Nx\n// validate-code executor use the same webpieces.config.json.\nexport { findConfigFile, CONFIG_FILENAME } from '@webpieces/rules-config';\n"]}
@@ -3,10 +3,13 @@ import type { BashContext, Violation } from '../types';
3
3
  import { BashRuleBase } from '../rule-base';
4
4
  export declare class BranchCreationGuardRule extends BashRuleBase<BranchCreationGuardConfig> {
5
5
  constructor(config: BranchCreationGuardConfig);
6
- readonly description = "Block new-branch creation when main is stale, or when not on main.";
6
+ readonly description = "Block new-branch creation when main is stale, or when branching off a non-main branch.";
7
7
  readonly defaultOptions: {
8
8
  subBranchNaming: string;
9
+ branchFormat: string;
9
10
  };
10
- readonly fixHint: readonly string[];
11
+ private get branchFormat();
12
+ private get subBranchNaming();
13
+ get fixHint(): readonly string[];
11
14
  check(ctx: BashContext): readonly Violation[];
12
15
  }
@@ -4,10 +4,11 @@ exports.BranchCreationGuardRule = void 0;
4
4
  const child_process_1 = require("child_process");
5
5
  const types_1 = require("../types");
6
6
  const rule_base_1 = require("../rule-base");
7
- const FIX_HINT = [
8
- "Run 'git checkout main && git pull origin main', then create your branch from main",
9
- "If you truly need a sub-branch (requires human approval), name it using the convention in webpieces.config.json 'branch-creation-guard.subBranchNaming'",
10
- ];
7
+ // Defaults used when the rule has no explicit value in webpieces.config.json.
8
+ // branchFormat is a human sentence telling the AI how to name a branch created off main; it is
9
+ // intentionally NOT the sub-branch convention (sub-branches are a separate, human-approved path).
10
+ const DEFAULT_BRANCH_FORMAT = 'Name it {whoami}/<short-feature-description> — lowercase, no version numbers, no sub/ prefix (e.g. dean/upgrade-webpieces)';
11
+ const DEFAULT_SUB_BRANCH_NAMING = 'feature/<ticket>/<short-description>';
11
12
  const BRANCH_PATTERNS = [
12
13
  /git\s+checkout\s+-[bB]\s+([^\s]+)/,
13
14
  /git\s+switch\s+-[cC]\s+([^\s]+)/,
@@ -42,9 +43,34 @@ function checkMainIsUpToDate(ctx, requestedName) {
42
43
  }
43
44
  class BranchCreationGuardRule extends rule_base_1.BashRuleBase {
44
45
  constructor(config) { super(config, 'branch-creation-guard'); }
45
- description = 'Block new-branch creation when main is stale, or when not on main.';
46
- defaultOptions = { subBranchNaming: 'feature/<ticket>/<short-description>' };
47
- fixHint = FIX_HINT;
46
+ description = 'Block new-branch creation when main is stale, or when branching off a non-main branch.';
47
+ defaultOptions = {
48
+ subBranchNaming: DEFAULT_SUB_BRANCH_NAMING,
49
+ branchFormat: DEFAULT_BRANCH_FORMAT,
50
+ };
51
+ get branchFormat() {
52
+ return this.config.branchFormat ?? DEFAULT_BRANCH_FORMAT;
53
+ }
54
+ get subBranchNaming() {
55
+ return this.config.subBranchNaming ?? DEFAULT_SUB_BRANCH_NAMING;
56
+ }
57
+ // Mode-aware fix hints. Branches off main follow branchFormat — never the sub-branch
58
+ // convention. The sub-branch affordance only appears under mode 'ON'; 'ON_NO_SUBBRANCHES'
59
+ // hard-blocks it and points instead at the ignoreModifiedUntilEpoch escape hatch.
60
+ get fixHint() {
61
+ const hints = [
62
+ "Run 'git checkout main && git pull origin main', then create your branch FROM main",
63
+ `Name a branch off main per branch-creation-guard.branchFormat: ${this.branchFormat}`,
64
+ ];
65
+ if (this.config.mode === 'ON_NO_SUBBRANCHES') {
66
+ hints.push('Sub-branches (branching off another feature branch) are disabled. To temporarily allow one, set ' +
67
+ "branch-creation-guard.ignoreModifiedUntilEpoch to a future epoch in webpieces.config.json");
68
+ }
69
+ else {
70
+ hints.push(`If you truly need a stacked sub-branch (requires human approval), name it per branch-creation-guard.subBranchNaming: ${this.subBranchNaming}`);
71
+ }
72
+ return hints;
73
+ }
48
74
  check(ctx) {
49
75
  const requestedName = extractBranchName(ctx.command);
50
76
  if (!requestedName)
@@ -56,7 +82,18 @@ class BranchCreationGuardRule extends rule_base_1.BashRuleBase {
56
82
  if (currentBranch === 'main') {
57
83
  return checkMainIsUpToDate(ctx, requestedName);
58
84
  }
59
- return [new types_1.Violation(1, truncate(ctx.command), `You are on '${currentBranch}', not main. Branches must be created from main.`)];
85
+ // Not on main: creating this branch would stack it on a feature branch (a sub-branch).
86
+ if (this.config.mode === 'ON_NO_SUBBRANCHES') {
87
+ return [new types_1.Violation(1, truncate(ctx.command), `You are on '${currentBranch}', not main. You need to run ` +
88
+ `git checkout main && git pull && git checkout -b ${requestedName} ` +
89
+ `instead of branching from this branch!!! ${this.branchFormat}. ` +
90
+ `You can temporarily turn this off if you truly need a sub-branch by setting ` +
91
+ `branch-creation-guard.ignoreModifiedUntilEpoch (a future epoch) in webpieces.config.json.`)];
92
+ }
93
+ return [new types_1.Violation(1, truncate(ctx.command), `You are on '${currentBranch}', not main. Branches must be created from main: ` +
94
+ `git checkout main && git pull && git checkout -b ${requestedName}. ${this.branchFormat}. ` +
95
+ `If you truly need a stacked sub-branch (requires human approval), name it per ` +
96
+ `branch-creation-guard.subBranchNaming ('${this.subBranchNaming}').`)];
60
97
  }
61
98
  }
62
99
  exports.BranchCreationGuardRule = BranchCreationGuardRule;
@@ -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;AAKzC,oCAA0C;AAC1C,4CAA4C;AAE5C,MAAM,QAAQ,GAAsB;IAChC,oFAAoF;IACpF,yJAAyJ;CAC5J,CAAC;AAEF,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,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,MAAa,uBAAwB,SAAQ,wBAAuC;IAChF,YAAY,MAAiC,IAAI,KAAK,CAAC,MAAM,EAAE,uBAAuB,CAAC,CAAC,CAAC,CAAC;IAEjF,WAAW,GAAG,oEAAoE,CAAC;IAC1E,cAAc,GAAG,EAAE,eAAe,EAAE,sCAAsC,EAAE,CAAC;IACtF,OAAO,GAAG,QAAQ,CAAC;IAE5B,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;AA1BD,0DA0BC","sourcesContent":["import { execSync } from 'child_process';\n\nimport { BranchCreationGuardConfig } from '@webpieces/rules-config';\n\nimport type { BashContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { BashRuleBase } from '../rule-base';\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 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\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\nexport class BranchCreationGuardRule extends BashRuleBase<BranchCreationGuardConfig> {\n constructor(config: BranchCreationGuardConfig) { super(config, 'branch-creation-guard'); }\n\n readonly description = 'Block new-branch creation when main is stale, or when not on main.';\n override readonly defaultOptions = { subBranchNaming: 'feature/<ticket>/<short-description>' };\n readonly 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"]}
1
+ {"version":3,"file":"branch-creation-guard.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/branch-creation-guard.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync } from 'child_process';\n\nimport { BranchCreationGuardConfig } from '@webpieces/rules-config';\n\nimport type { BashContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { BashRuleBase } from '../rule-base';\n\n// Defaults used when the rule has no explicit value in webpieces.config.json.\n// branchFormat is a human sentence telling the AI how to name a branch created off main; it is\n// intentionally NOT the sub-branch convention (sub-branches are a separate, human-approved path).\nconst DEFAULT_BRANCH_FORMAT =\n 'Name it {whoami}/<short-feature-description> lowercase, no version numbers, no sub/ prefix (e.g. dean/upgrade-webpieces)';\nconst DEFAULT_SUB_BRANCH_NAMING = 'feature/<ticket>/<short-description>';\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\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\nexport class BranchCreationGuardRule extends BashRuleBase<BranchCreationGuardConfig> {\n constructor(config: BranchCreationGuardConfig) { super(config, 'branch-creation-guard'); }\n\n readonly description = 'Block new-branch creation when main is stale, or when branching off a non-main branch.';\n override readonly defaultOptions = {\n subBranchNaming: DEFAULT_SUB_BRANCH_NAMING,\n branchFormat: DEFAULT_BRANCH_FORMAT,\n };\n\n private get branchFormat(): string {\n return this.config.branchFormat ?? DEFAULT_BRANCH_FORMAT;\n }\n\n private get subBranchNaming(): string {\n return this.config.subBranchNaming ?? DEFAULT_SUB_BRANCH_NAMING;\n }\n\n // Mode-aware fix hints. Branches off main follow branchFormat — never the sub-branch\n // convention. The sub-branch affordance only appears under mode 'ON'; 'ON_NO_SUBBRANCHES'\n // hard-blocks it and points instead at the ignoreModifiedUntilEpoch escape hatch.\n get fixHint(): readonly string[] {\n const hints = [\n \"Run 'git checkout main && git pull origin main', then create your branch FROM main\",\n `Name a branch off main per branch-creation-guard.branchFormat: ${this.branchFormat}`,\n ];\n if (this.config.mode === 'ON_NO_SUBBRANCHES') {\n hints.push(\n 'Sub-branches (branching off another feature branch) are disabled. To temporarily allow one, set ' +\n \"branch-creation-guard.ignoreModifiedUntilEpoch to a future epoch in webpieces.config.json\",\n );\n } else {\n hints.push(\n `If you truly need a stacked sub-branch (requires human approval), name it per branch-creation-guard.subBranchNaming: ${this.subBranchNaming}`,\n );\n }\n return hints;\n }\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 // Not on main: creating this branch would stack it on a feature branch (a sub-branch).\n if (this.config.mode === 'ON_NO_SUBBRANCHES') {\n return [new V(\n 1,\n truncate(ctx.command),\n `You are on '${currentBranch}', not main. You need to run ` +\n `git checkout main && git pull && git checkout -b ${requestedName} ` +\n `instead of branching from this branch!!! ${this.branchFormat}. ` +\n `You can temporarily turn this off if you truly need a sub-branch by setting ` +\n `branch-creation-guard.ignoreModifiedUntilEpoch (a future epoch) in webpieces.config.json.`,\n )];\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 `git checkout main && git pull && git checkout -b ${requestedName}. ${this.branchFormat}. ` +\n `If you truly need a stacked sub-branch (requires human approval), name it per ` +\n `branch-creation-guard.subBranchNaming ('${this.subBranchNaming}').`,\n )];\n }\n}\n"]}
@@ -17,18 +17,18 @@ function run(toolKind, input, cwd) {
17
17
  return runInternal(toolKind, input, cwd);
18
18
  }
19
19
  function runInternal(toolKind, input, cwd) {
20
- const loaded = (0, rules_config_1.loadWebpiecesRulesConfig)(cwd);
21
- if (!loaded)
20
+ const loaded = (0, rules_config_1.loadAndValidate)(cwd);
21
+ if (loaded.configPath === null)
22
22
  return new types_1.BlockedResult(CONFIG_MISSING_REPORT);
23
23
  const workspaceRoot = path.dirname(loaded.configPath);
24
24
  // Always allow edits to webpieces.config.json — it's the fix target when out of sync
25
25
  if (path.resolve(input.filePath) === path.resolve(loaded.configPath)) {
26
26
  return null;
27
27
  }
28
- const rules = (0, load_rules_1.loadRules)(loaded.config, workspaceRoot);
28
+ const rules = (0, load_rules_1.loadRules)(loaded.rulesConfig, workspaceRoot);
29
29
  if (rules.length === 0)
30
30
  return null;
31
- const outOfSync = checkConfigSync(rules, loaded.config);
31
+ const outOfSync = checkConfigSync(rules, loaded.rulesConfig);
32
32
  if (outOfSync)
33
33
  return outOfSync;
34
34
  const contexts = (0, build_context_1.buildContexts)(toolKind, input, workspaceRoot);
@@ -45,14 +45,14 @@ function runBash(command, cwd) {
45
45
  return runBashInternal(command, cwd);
46
46
  }
47
47
  function runBashInternal(command, cwd) {
48
- const loaded = (0, rules_config_1.loadWebpiecesRulesConfig)(cwd);
49
- if (!loaded)
48
+ const loaded = (0, rules_config_1.loadAndValidate)(cwd);
49
+ if (loaded.configPath === null)
50
50
  return new types_1.BlockedResult(CONFIG_MISSING_REPORT);
51
51
  const workspaceRoot = path.dirname(loaded.configPath);
52
- const rules = (0, load_rules_1.loadRules)(loaded.config, workspaceRoot);
52
+ const rules = (0, load_rules_1.loadRules)(loaded.rulesConfig, workspaceRoot);
53
53
  if (rules.length === 0)
54
54
  return null;
55
- const outOfSync = checkConfigSync(rules, loaded.config);
55
+ const outOfSync = checkConfigSync(rules, loaded.rulesConfig);
56
56
  if (outOfSync)
57
57
  return outOfSync;
58
58
  const ctx = (0, build_context_1.buildBashContext)(command, workspaceRoot);
@@ -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 { loadWebpiecesRulesConfig, WebpiecesRulesConfig } from '@webpieces/rules-config';\n\nimport { buildContexts, buildBashContext } from './build-context';\nimport { loadRules, globMatches } from './load-rules';\nimport { toError } from './to-error';\nimport { formatReport } from './report';\nimport {\n ToolKind, NormalizedToolInput, BlockedResult,\n Rule, Violation, RuleGroup,\n EditContext, FileContext, BashContext,\n} from './types';\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): 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 loaded = loadWebpiecesRulesConfig(cwd);\n if (!loaded) 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 rules = loadRules(loaded.config, workspaceRoot);\n if (rules.length === 0) return null;\n\n const outOfSync = checkConfigSync(rules, loaded.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);\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): BlockedResult | null {\n return runBashInternal(command, cwd);\n}\n\nfunction runBashInternal(command: string, cwd: string): BlockedResult | null {\n const loaded = loadWebpiecesRulesConfig(cwd);\n if (!loaded) return new BlockedResult(CONFIG_MISSING_REPORT);\n\n const workspaceRoot = path.dirname(loaded.configPath);\n const rules = loadRules(loaded.config, workspaceRoot);\n if (rules.length === 0) return null;\n\n const outOfSync = checkConfigSync(rules, loaded.config);\n if (outOfSync) return outOfSync;\n\n const ctx = buildBashContext(command, workspaceRoot);\n const groups = runBashRules(rules, ctx);\n if (groups.length === 0) return null;\n\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, MODIFIED_CODE, or 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';\n\nimport { loadAndValidate, WebpiecesRulesConfig } from '@webpieces/rules-config';\n\nimport { buildContexts, buildBashContext } from './build-context';\nimport { loadRules, globMatches } from './load-rules';\nimport { toError } from './to-error';\nimport { formatReport } from './report';\nimport {\n ToolKind, NormalizedToolInput, BlockedResult,\n Rule, Violation, RuleGroup,\n EditContext, FileContext, BashContext,\n} from './types';\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): 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 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 rules = loadRules(loaded.rulesConfig, workspaceRoot);\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 const relativePath = path.relative(workspaceRoot, input.filePath);\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): BlockedResult | null {\n return runBashInternal(command, cwd);\n}\n\nfunction runBashInternal(command: string, cwd: string): 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 const rules = loadRules(loaded.rulesConfig, workspaceRoot);\n if (rules.length === 0) return null;\n\n const outOfSync = checkConfigSync(rules, loaded.rulesConfig);\n if (outOfSync) return outOfSync;\n\n const ctx = buildBashContext(command, workspaceRoot);\n const groups = runBashRules(rules, ctx);\n if (groups.length === 0) return null;\n\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, MODIFIED_CODE, or 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"]}